Skip to content

Commit acf5a14

Browse files
committed
fix(webapp): a dead stream with a dangling tool call self-heals from the settled transcript
1 parent e4ab0a5 commit acf5a14

3 files changed

Lines changed: 36 additions & 4 deletions

File tree

apps/webapp/app/components/dashboard-agent/DashboardAgentChat.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import type { AgentPageContext } from "./page-context-types";
1919
import { retryAction } from "./retry-action";
2020
import {
2121
fetchChatTranscript,
22-
hasOpenInvestigation,
2322
pollSettledTranscript,
23+
transcriptLooksUnfinished,
2424
} from "./settled-transcript";
2525
import { useAgentMessageQuota } from "./useAgentMessageQuota";
2626
import { useTriggerUriResolver } from "./useTriggerUriResolver";
@@ -281,8 +281,9 @@ export function DashboardAgentChat({
281281

282282
onTurnSettled();
283283
// The terminal card is written to the chat row after the stream closes, so this
284-
// mounted panel would otherwise keep showing the last `in_progress` revision.
285-
if (!hasOpenInvestigation(messagesRef.current)) return;
284+
// mounted panel would otherwise keep showing the last `in_progress` revision — or,
285+
// if the stream died mid-tool, the tool call it never got an output for.
286+
if (!transcriptLooksUnfinished(messagesRef.current)) return;
286287
void pollSettledTranscript<UIMessage>({
287288
fetchTranscript: () => fetchChatTranscript(actionPath, chatId),
288289
apply: (merge) => setMessages((current) => merge(current)),

apps/webapp/app/components/dashboard-agent/settled-transcript.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
hasOpenInvestigation,
77
mergeSettledMessages,
88
pollSettledTranscript,
9+
transcriptLooksUnfinished,
910
} from "./settled-transcript";
1011

1112
/**
@@ -119,6 +120,27 @@ describe("reading the transcript endpoint", () => {
119120
});
120121
});
121122

123+
describe("deciding whether a settled turn is worth re-reading", () => {
124+
// The stream EOF'd while `get_report` was running: the part never gets an output.
125+
const DANGLING_TOOL = {
126+
id: "msg_dangling",
127+
role: "assistant",
128+
parts: [{ type: "tool-get_report", toolCallId: "call_1", state: "input-available" }],
129+
};
130+
131+
it("re-reads when the stream died mid-tool, not only when a card is open", () => {
132+
expect(transcriptLooksUnfinished([DANGLING_TOOL])).toBe(true);
133+
});
134+
135+
it("re-reads while a card is still open", () => {
136+
expect(transcriptLooksUnfinished([OPEN])).toBe(true);
137+
});
138+
139+
it("leaves a fully settled transcript alone", () => {
140+
expect(transcriptLooksUnfinished([OPEN, SETTLED])).toBe(false);
141+
});
142+
});
143+
122144
describe("an already-open panel when a turn is exhausted", () => {
123145
it("stops showing Working… without a reload or a reopen", async () => {
124146
// What the mounted panel holds when the stream closes: the card the model opened

apps/webapp/app/components/dashboard-agent/settled-transcript.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { liveInvestigation } from "./progress-line";
1+
import { inFlightToolName, liveInvestigation } from "./progress-line";
22

33
/**
44
* Re-reading the stored transcript once a turn settles.
@@ -29,6 +29,15 @@ export function hasOpenInvestigation(messages: ReadonlyArray<unknown>): boolean
2929
return liveInvestigation(messages as never) !== null;
3030
}
3131

32+
/**
33+
* Whether the transcript still reads as mid-turn. A stream that dies without
34+
* `turn-complete` leaves the tool part it was on dangling forever, so an open card is
35+
* not the only shape a re-read has to recover from.
36+
*/
37+
export function transcriptLooksUnfinished(messages: ReadonlyArray<unknown>): boolean {
38+
return hasOpenInvestigation(messages) || inFlightToolName(messages as never) !== null;
39+
}
40+
3241
/**
3342
* The settlement is written in `onTurnComplete`, which runs AFTER the client's stream
3443
* closes, so the first re-read can legitimately land before it. Retry a few times,

0 commit comments

Comments
 (0)