From 1acf8024a5e4ca847ed18fd08bcd2db66533bec6 Mon Sep 17 00:00:00 2001 From: "vellum-apollo-bot[bot]" <206299977+vellum-apollo-bot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 19:28:07 +0000 Subject: [PATCH] refactor(chat): stop post-turn reconciliation; trust the anchor invariant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../domains/chat/hooks/use-stream-event-handler.ts | 4 ++-- .../utils/stream-handlers/message-handlers.test.ts | 14 ++++---------- .../chat/utils/stream-handlers/message-handlers.ts | 4 ---- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/apps/web/src/domains/chat/hooks/use-stream-event-handler.ts b/apps/web/src/domains/chat/hooks/use-stream-event-handler.ts index 8fb0ced6a1f..b19de8b1be3 100644 --- a/apps/web/src/domains/chat/hooks/use-stream-event-handler.ts +++ b/apps/web/src/domains/chat/hooks/use-stream-event-handler.ts @@ -347,10 +347,10 @@ export function useStreamEventHandler( handleAssistantTextDelta(event, ctx); break; case "assistant_activity_state": - handleAssistantActivityState(event, epoch, ctx); + handleAssistantActivityState(event, ctx); break; case "message_complete": - handleMessageComplete(event, epoch, ctx); + handleMessageComplete(event, ctx); break; case "generation_handoff": handleGenerationHandoff(event, ctx); diff --git a/apps/web/src/domains/chat/utils/stream-handlers/message-handlers.test.ts b/apps/web/src/domains/chat/utils/stream-handlers/message-handlers.test.ts index eb971421d7b..9946e52d776 100644 --- a/apps/web/src/domains/chat/utils/stream-handlers/message-handlers.test.ts +++ b/apps/web/src/domains/chat/utils/stream-handlers/message-handlers.test.ts @@ -56,14 +56,13 @@ describe("handleAssistantActivityState", () => { reason: "thinking_delta", conversationId: "conv-1", }, - 1, ctx, ); expect(ctx.turnActions.onActivityThinking).not.toHaveBeenCalled(); expect(ctx.turnActions.completeTurn).not.toHaveBeenCalled(); }); - it("updates version and handles idle phase", () => { + it("updates version and handles idle phase without starting reconcile", () => { const ctx = makeCtx(); handleAssistantActivityState( { @@ -74,14 +73,13 @@ describe("handleAssistantActivityState", () => { reason: "message_complete", conversationId: "conv-1", }, - 1, ctx, ); expect(ctx.lastActivityVersionRef.current.get("conv-1")).toBe(1); expect(ctx.setMessages).toHaveBeenCalled(); expect(ctx.turnActions.completeTurn).toHaveBeenCalled(); expect(ctx.clearProcessingKey).toHaveBeenCalledWith("conv-1"); - expect(ctx.startReconciliationLoop).toHaveBeenCalledWith(1); + expect(ctx.startReconciliationLoop).not.toHaveBeenCalled(); }); it("calls onActivityThinking for thinking phase", () => { @@ -95,7 +93,6 @@ describe("handleAssistantActivityState", () => { reason: "tool_result_received", conversationId: "conv-1", }, - 1, ctx, ); expect(ctx.lastActivityVersionRef.current.get("conv-1")).toBe(2); @@ -116,7 +113,6 @@ describe("handleAssistantActivityState", () => { statusText: "Processing bash results", conversationId: "conv-1", }, - 1, ctx, ); expect(ctx.turnActions.onActivityThinking).toHaveBeenCalledWith("Processing bash results"); @@ -132,7 +128,6 @@ describe("handleAssistantActivityState", () => { anchor: "assistant_turn", reason: "first_text_delta", }, - 1, ctx, ); expect(ctx.lastActivityVersionRef.current.get( @@ -144,17 +139,16 @@ describe("handleAssistantActivityState", () => { }); describe("handleMessageComplete", () => { - it("finalizes message and starts reconciliation", () => { + it("finalizes message and completes turn without starting reconcile", () => { const ctx = makeCtx(); handleMessageComplete( { type: "message_complete", messageId: "msg-1", content: "Done" }, - 1, ctx, ); expect(ctx.setMessages).toHaveBeenCalled(); expect(ctx.turnActions.completeTurn).toHaveBeenCalled(); expect(ctx.clearProcessingKey).toHaveBeenCalledWith("conv-1"); - expect(ctx.startReconciliationLoop).toHaveBeenCalledWith(1); + expect(ctx.startReconciliationLoop).not.toHaveBeenCalled(); }); }); diff --git a/apps/web/src/domains/chat/utils/stream-handlers/message-handlers.ts b/apps/web/src/domains/chat/utils/stream-handlers/message-handlers.ts index 094536bed0c..51bd98a6286 100644 --- a/apps/web/src/domains/chat/utils/stream-handlers/message-handlers.ts +++ b/apps/web/src/domains/chat/utils/stream-handlers/message-handlers.ts @@ -29,7 +29,6 @@ export function handleAssistantTextDelta( export function handleAssistantActivityState( event: AssistantActivityStateEvent, - epoch: number, ctx: StreamHandlerContext, ): void { const convId = @@ -82,12 +81,10 @@ export function handleAssistantActivityState( activityVersion: event.activityVersion, turnPhaseBefore, }); - ctx.startReconciliationLoop(epoch); } export function handleMessageComplete( event: MessageCompleteEvent, - epoch: number, ctx: StreamHandlerContext, ): void { ctx.setMessages((prev) => finalizeMessageComplete(prev, event)); @@ -104,7 +101,6 @@ export function handleMessageComplete( hasContent: !!event.content, hasAttachments: !!event.attachments?.length, }); - ctx.startReconciliationLoop(epoch); } export function handleGenerationHandoff(