Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,9 @@ impl WorkspaceRegistry {
});
self.connection.execute(
"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>

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>

<= CAST(json_extract(excluded.value, '$.cursor') AS INTEGER)",
params![format!("journal_reducer.{reducer_id}"), value.to_string()],
)?;
Ok(())
Expand Down Expand Up @@ -2022,6 +2024,22 @@ mod tests {
);
}

#[test]
fn reducer_state_cursor_does_not_regress_on_late_write() {
let registry = WorkspaceRegistry::in_memory("reducer-cursor-monotonic").unwrap();
registry
.put_journal_reducer_state("agent_roster", 3, 10, r#"{"entries":{"new":{}}}"#)
.unwrap();
registry
.put_journal_reducer_state("agent_roster", 3, 9, r#"{"entries":{"old":{}}}"#)
.unwrap();

let (_, cursor, snapshot) =
registry.journal_reducer_state("agent_roster").unwrap().unwrap();
assert_eq!(cursor, 10);
assert!(snapshot.contains("new"));
}

#[test]
fn persistent_reader_observes_commits_on_an_independent_connection() {
let root = std::env::temp_dir().join(format!("cmux-journal-reader-{}", new_uuid_v4()));
Expand Down
Loading