Skip to content

Commit 88760fb

Browse files
committed
fix(tui): keep surrendered finalized marker after late delta to block later final
For finalized surrendered runs, a late delta must not clear the surrender marker — otherwise a subsequent late final would slip through and produce a duplicate. Only in-flight surrendered runs clear the marker on delta (their final needs to render).
1 parent dd53763 commit 88760fb

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

src/tui/tui-event-handlers.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,6 +2028,51 @@ describe("tui-event-handlers: handleAgentEvent", () => {
20282028
expect(chatLog.finalizeAssistant).not.toHaveBeenCalled();
20292029
});
20302030

2031+
it("keeps surrendered finalized marker after late delta so later final is also suppressed (#96979 P2)", () => {
2032+
const { state, chatLog, handleChatEvent, handleSessionsChangedEvent } = createHandlersHarness(
2033+
{ state: { activeChatRunId: "run-done" } },
2034+
);
2035+
2036+
// First final: run is finalized with display.
2037+
handleChatEvent({
2038+
runId: "run-done",
2039+
sessionKey: state.currentSessionKey,
2040+
state: "final",
2041+
message: { content: [{ type: "text", text: "completed" }] },
2042+
});
2043+
expect(chatLog.finalizeAssistant).toHaveBeenCalledTimes(1);
2044+
chatLog.finalizeAssistant.mockClear();
2045+
2046+
// sessions.changed "new" surrenders the finalized run.
2047+
handleSessionsChangedEvent({
2048+
sessionKey: state.currentSessionKey,
2049+
reason: "new",
2050+
sessionId: state.currentSessionId ?? undefined,
2051+
updatedAt: 200,
2052+
} satisfies SessionChangedEvent);
2053+
2054+
chatLog.hasStreamingRun.mockReturnValue(false);
2055+
2056+
// Late delta for surrendered finalized run — suppressed.
2057+
handleChatEvent({
2058+
runId: "run-done",
2059+
sessionKey: state.currentSessionKey,
2060+
state: "delta",
2061+
message: { content: [{ type: "text", text: "stale chunk" }] },
2062+
});
2063+
expect(chatLog.updateAssistant).not.toHaveBeenCalled();
2064+
2065+
// Late displayable final — also suppressed because the surrender
2066+
// marker was NOT cleared by the delta (#96979 P2).
2067+
handleChatEvent({
2068+
runId: "run-done",
2069+
sessionKey: state.currentSessionKey,
2070+
state: "final",
2071+
message: { content: [{ type: "text", text: "completed" }] },
2072+
});
2073+
expect(chatLog.finalizeAssistant).not.toHaveBeenCalled();
2074+
});
2075+
20312076
it("suppresses late aborted for surrendered run (#96979)", () => {
20322077
const { state, chatLog, handleChatEvent, handleSessionsChangedEvent } = createHandlersHarness(
20332078
{

src/tui/tui-event-handlers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,12 @@ export function createEventHandlers(context: EventHandlerContext) {
625625
surrenderedToHistoryRunIds.delete(evt.runId);
626626
// Fall through to normal event handling below.
627627
} else if (evt.state === "delta") {
628-
surrenderedToHistoryRunIds.delete(evt.runId);
628+
// In-flight: remove surrender marker so a later final is
629+
// not permanently hidden. Finalized: keep the marker — a
630+
// late delta cannot re-enable a duplicate later final (#96979).
631+
if (source !== "finalized") {
632+
surrenderedToHistoryRunIds.delete(evt.runId);
633+
}
629634
return;
630635
} else if (evt.state === "final") {
631636
surrenderedToHistoryRunIds.delete(evt.runId);

0 commit comments

Comments
 (0)