Skip to content

fix(tui): prevent reducer snapshot cursor regression - #11374

Closed
lawrencecchen wants to merge 3 commits into
feat-pr11068-snapshot-recoveryfrom
feat-pr11364-roster-cas
Closed

lawrencecchen wants to merge 3 commits into
feat-pr11068-snapshot-recoveryfrom
feat-pr11364-roster-cas

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #11364.

Adds a regression test and a SQLite monotonic cursor guard so late reducer writes cannot overwrite newer snapshots. Hosted focused tests are required; no local cargo tests were run.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes reducer snapshot persistence in the TUI journal so a late write with an older cursor can no longer overwrite a newer snapshot.

  • Adds a SQLite ON CONFLICT guard that only updates when the incoming cursor is equal to or newer than the stored cursor.
  • Adds a regression test covering the late-write scenario.

Written for commit 251a795. Summary will update on new commits.

Review in cubic

@vercel

vercel Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cmux166 Canceled Canceled Sep 1, 2026 3:20pm UTC
cmux41 Ready Ready Preview Sep 1, 2026 3:20pm UTC

@coderabbitai

coderabbitai Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: 7b9d739e-a486-4f52-8d4a-46e1c09771c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmux-tui/crates/cmux-tui-core/src/workspace_registry/session_journal.rs">

<violation number="1" location="cmux-tui/crates/cmux-tui-core/src/workspace_registry/session_journal.rs:935">
P1: When a fold and terminal retirement produce snapshots at the same cursor, `<=` still lets the late fold overwrite the newer retirement snapshot. Serialize snapshot mutation with persistence or add an ordering token; a nondecreasing cursor alone does not protect equal-cursor writes.</violation>

<violation number="2" location="cmux-tui/crates/cmux-tui-core/src/workspace_registry/session_journal.rs:935">
P1: The monotonic guard breaks the intentional reducer-state reset path. In `mux.rs:1172` the agent-roster loader writes `put_journal_reducer_state(AGENT_ROSTER_REDUCER_ID, VERSION, 0, &empty_snapshot)` specifically to clear a persisted snapshot when the journal has an unreplayable pruned gap (fail closed with an empty roster). Because the stored cursor is > 0, the new `WHERE old_cursor <= new_cursor` (0) evaluates false, the UPDATE is skipped, and the stale non-empty snapshot at the higher cursor stays persisted. `execute` still returns Ok (0 rows changed), so the "clearing the ... snapshot failed" handler never runs and the reset is silently lost. Exempt the explicit reset (new cursor == 0) from the guard, or route resets through a separate delete/clear that bypasses the monotonic check.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

"INSERT INTO meta(key, value) VALUES(?1, ?2)
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
ON CONFLICT(key) DO UPDATE SET value = excluded.value
WHERE COALESCE(CAST(json_extract(meta.value, '$.cursor') AS INTEGER), 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a fold and terminal retirement produce snapshots at the same cursor, <= still lets the late fold overwrite the newer retirement snapshot. Serialize snapshot mutation with persistence or add an ordering token; a nondecreasing cursor alone does not protect equal-cursor writes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmux-tui/crates/cmux-tui-core/src/workspace_registry/session_journal.rs, line 935:

<comment>When a fold and terminal retirement produce snapshots at the same cursor, `<=` still lets the late fold overwrite the newer retirement snapshot. Serialize snapshot mutation with persistence or add an ordering token; a nondecreasing cursor alone does not protect equal-cursor writes.</comment>

<file context>
@@ -931,7 +931,9 @@ impl WorkspaceRegistry {
             "INSERT INTO meta(key, value) VALUES(?1, ?2)
-             ON CONFLICT(key) DO UPDATE SET value = excluded.value",
+             ON CONFLICT(key) DO UPDATE SET value = excluded.value
+             WHERE COALESCE(CAST(json_extract(meta.value, '$.cursor') AS INTEGER), 0)
+                   <= CAST(json_extract(excluded.value, '$.cursor') AS INTEGER)",
             params![format!("journal_reducer.{reducer_id}"), value.to_string()],
</file context>

"INSERT INTO meta(key, value) VALUES(?1, ?2)
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
ON CONFLICT(key) DO UPDATE SET value = excluded.value
WHERE COALESCE(CAST(json_extract(meta.value, '$.cursor') AS INTEGER), 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The monotonic guard breaks the intentional reducer-state reset path. In mux.rs:1172 the agent-roster loader writes put_journal_reducer_state(AGENT_ROSTER_REDUCER_ID, VERSION, 0, &empty_snapshot) specifically to clear a persisted snapshot when the journal has an unreplayable pruned gap (fail closed with an empty roster). Because the stored cursor is > 0, the new WHERE old_cursor <= new_cursor (0) evaluates false, the UPDATE is skipped, and the stale non-empty snapshot at the higher cursor stays persisted. execute still returns Ok (0 rows changed), so the "clearing the ... snapshot failed" handler never runs and the reset is silently lost. Exempt the explicit reset (new cursor == 0) from the guard, or route resets through a separate delete/clear that bypasses the monotonic check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmux-tui/crates/cmux-tui-core/src/workspace_registry/session_journal.rs, line 935:

<comment>The monotonic guard breaks the intentional reducer-state reset path. In `mux.rs:1172` the agent-roster loader writes `put_journal_reducer_state(AGENT_ROSTER_REDUCER_ID, VERSION, 0, &empty_snapshot)` specifically to clear a persisted snapshot when the journal has an unreplayable pruned gap (fail closed with an empty roster). Because the stored cursor is > 0, the new `WHERE old_cursor <= new_cursor` (0) evaluates false, the UPDATE is skipped, and the stale non-empty snapshot at the higher cursor stays persisted. `execute` still returns Ok (0 rows changed), so the "clearing the ... snapshot failed" handler never runs and the reset is silently lost. Exempt the explicit reset (new cursor == 0) from the guard, or route resets through a separate delete/clear that bypasses the monotonic check.</comment>

<file context>
@@ -931,7 +931,9 @@ impl WorkspaceRegistry {
             "INSERT INTO meta(key, value) VALUES(?1, ?2)
-             ON CONFLICT(key) DO UPDATE SET value = excluded.value",
+             ON CONFLICT(key) DO UPDATE SET value = excluded.value
+             WHERE COALESCE(CAST(json_extract(meta.value, '$.cursor') AS INTEGER), 0)
+                   <= CAST(json_extract(excluded.value, '$.cursor') AS INTEGER)",
             params![format!("journal_reducer.{reducer_id}"), value.to_string()],
</file context>

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #11383. The successor explicitly includes this CAS snapshot work and fixes equal-cursor overwrite and cursor-zero reset behavior. No unique current-main change remains in this intermediate stack.

This branch was successfully deployed

2 active deployments
Preview – cmux166 — 251a795d Deployed Sep 1, 2026 by vercel[bot]
Preview – cmux41 — 251a795d Deployed Sep 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant