refactor(chat): stop post-turn reconciliation; trust the anchor invariant#32024
Conversation
…iant PR 2b.3 of the chat-state-slimming workstream. With the anchor invariant landed in 2b.1 (#32012) — assistant emits messageId = state.lastAssistantMessageId, client preserves the anchor via first-id-wins lock in appendTextDelta and role-based finalize in finalizeMessageComplete — the post-turn reconciliation loop that fired on every message_complete and activity_state idle is now redundant. The live client state is already correct; the refetch existed only to paper over id drift. Removes startReconciliationLoop calls from: - handleAssistantActivityState (idle phase) - handleMessageComplete Drops the now-unused epoch parameter from both handler signatures. The dispatcher still uses epoch for stale-epoch guards on the dispatch side, and reconciliation still fires on load / switch / SSE reopen / POST-resolve confirmation. cancelReconciliation calls remain since those still-spawned loops can be cancelled by text deltas and generation handoff.
There was a problem hiding this comment.
💡 Codex Review
Removing reconciliation starts from both handleAssistantActivityState(...idle) and handleMessageComplete leaves normal SSE-driven turns with no post-turn reconcile trigger. In useSendMessage, startReconciliationLoop runs only on the poll fallback path (when there is no matching active stream), so the common "active stream" path now never enters the loop after terminal events. That regresses the silent-stall rescue path (onPollReconciled) and can leave stale streaming/tool-call state or missed final server-side message fields whenever terminal SSE delivery is imperfect but the connection stays open.
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
PR 2b.3 of the chat-state-slimming workstream. The endgame piece.
What
With the anchor invariant landed in 2b.1 (#32012) — assistant emits
messageId = state.lastAssistantMessageId, client preserves the anchor via first-id-wins lock inappendTextDeltaand role-based finalize infinalizeMessageComplete— the post-turn reconciliation loop that fired on everymessage_completeandactivity_stateidle is now redundant. The live client state is already correct; the refetch existed only to paper over id drift.Removes
startReconciliationLoopcalls from:handleAssistantActivityState(idle phase)handleMessageCompleteDrops the now-unused
epochparameter from both handler signatures.What stays
Reconciliation still fires legitimately on:
chat-page.tsxtop-level wiringuse-event-stream.ts:361,408use-send-message.ts:429The dispatcher still uses
epochfor stale-epoch guards on the dispatch side, andcancelReconciliationcalls remain (inhandleAssistantTextDelta,handleGenerationHandoff,tool-call-handlers) since those still-spawned loops can be cancelled by text deltas and generation handoff.Diff
Tests
Updated 6 tests in
message-handlers.test.tsto drop theepocharg and flip thestartReconciliationLoopassertion fromtoHaveBeenCalledWith(1)tonot.toHaveBeenCalled()on the idle +message_completepaths. Renamed the affected tests to make the no-reconcile expectation explicit.use-assistant-lifecycle.ts/ai-page.tsx/ billing files as main)bun test src/domains/chatmatches main exactly: 1331 pass / 29 fail / 3 errors — zero new failures introduced. (The 29/3 baseline is cross-suite mock isolation flakiness; affected tests pass in isolation.)Closes the chat-state-slimming endgame for the streaming path. Next up: PR 2c (client rewrites reconcile to pure id-match + deletes
stableId).