Skip to content

πŸ› DB-owned message sequences β€” fix broken conversation ordering#32

Merged
Robdel12 merged 2 commits intomainfrom
rd/db-owned-sequences
Mar 19, 2026
Merged

πŸ› DB-owned message sequences β€” fix broken conversation ordering#32
Robdel12 merged 2 commits intomainfrom
rd/db-owned-sequences

Conversation

@Robdel12
Copy link
Owner

Summary

  • DB is the sole authority for sequence assignment. INSERT computes MAX(sequence)+1 β€” app code never assigns sequences.
  • Persist-first broadcasts. Row commands use a oneshot response channel to get the DB-assigned sequence back before broadcasting to clients. Zero divergence between in-memory and persisted state.
  • Removed all app-layer sequence computation. msg_counter/fetch_add removed from Claude connector, conditional assignment removed from transition.rs.
  • V029 migration fixes historical broken sequences (82/86 rows with sequence=0) using ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY rowid).

Why

Dogfooding revealed Codex direct sessions were missing user messages. Root cause: sequences were computed independently in 4 places (Claude connector, Codex connector, transition.rs, SessionHandle). Any restart, restore, or path disagreement could corrupt ordering permanently. Now there's one code path and one source of truth.

How it works

  1. Entry arrives with sequence: 0
  2. RowAppend/RowUpsert persist command includes oneshot::Sender<u64>
  3. DB computes sequence via COALESCE(MAX(sequence)+1, 0) on INSERT
  4. execute_command reads back assigned sequence and sends via oneshot
  5. Writer flushes immediately when any command has a response channel
  6. Handler awaits response, updates in-memory row, then broadcasts

Key files

File Change
persistence/mod.rs DB-computed sequences in SQL, readback via oneshot
persistence/commands.rs sequence_tx on RowAppend/RowUpsert, removed Clone
persistence/writer.rs Immediate flush when response channels present
session_command_handler.rs 3-pass persist→await→broadcast in transition path
connector-claude/src/lib.rs Removed msg_counter, all entries use sequence=0
connector-core/src/transition.rs Removed conditional sequence assignment
session.rs Added set_row_sequence(), row_by_id()
session_actor.rs Mock writer in tests, test outcomes not implementation
V029__fix_message_sequences.sql Fix historical broken sequences

Test plan

  • make rust-ci β€” fmt + clippy + 437 tests pass
  • Run server, open broken session β†’ user messages visible after migration
  • New session: send messages, verify SELECT sequence FROM messages increments
  • Pagination works, streaming smooth

Sequences were computed by app code in 4 places (Claude connector,
Codex connector, transition.rs, SessionHandle), each with different
strategies. This caused 82/86 messages to have sequence=0 in one
dogfooding session, breaking pagination entirely.

Fix: DB computes sequences via MAX(sequence)+1 on INSERT. App-layer
code no longer assigns sequences β€” entries arrive with sequence=0
and the DB assigns the authoritative value. RowUpsert preserves the
original sequence on conflict (no overwrite).

- Remove msg_counter and fetch_add from Claude connector
- Remove sequence assignment in transition RowCreated handler
- Update SQL for RowAppend/RowUpsert to use DB-computed sequences
- Add V029 migration to fix historical broken sequences via rowid
The previous commit made the DB compute sequences, but broadcasts
still used in-memory sequences. If a DB write failed silently, the
two could diverge. Now broadcasts wait for the DB-assigned sequence
before emitting to clients. One source of truth, zero divergence.

- Add oneshot response channel to RowAppend/RowUpsert persist commands
- execute_command reads back DB-assigned sequence and sends via channel
- Writer flushes immediately when any command has a response channel
- AddRowAndBroadcast/UpsertRowAndBroadcast await DB sequence before broadcast
- dispatch_transition_input uses 3-pass: persist β†’ await sequences β†’ broadcast
- Add set_row_sequence() and row_by_id() to SessionHandle
- Remove Clone from PersistCommand (was unused, blocked oneshot::Sender)
- Rewrite actor tests with mock writer that simulates DB sequence assignment
@Robdel12 Robdel12 changed the title DB-owned message sequences β€” fix broken conversation ordering πŸ› DB-owned message sequences β€” fix broken conversation ordering Mar 19, 2026
@Robdel12 Robdel12 enabled auto-merge (squash) March 19, 2026 05:56
@Robdel12 Robdel12 merged commit bfe94ff into main Mar 19, 2026
3 checks passed
@Robdel12 Robdel12 deleted the rd/db-owned-sequences branch March 19, 2026 05:57
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