Skip to content

Commit b015618

Browse files
fix(ui): reset sawAssistantReply across autonomous turn boundaries
In #90122, annotateToolTurnOutcome was added to collapse non-terminal failed tool errors when the turn produced a reply. The backward pass resets sawAssistantReply only at user groups, assuming every turn starts with a user message. Agent-initiated turns (cron/scheduled/autonomous) have no user group between them, so a later turn reply leaks backward and stamps an earlier genuinely-failed turn as turnSucceeded=true, hiding its error banner. Fix: add a terminal-turn-boundary reset for assistant groups without reply text, scope tool errors by terminal turn outcome, and attribute transcript messages to agent runs. Related to #97849 Co-authored-by: SunnyShu0925 <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
1 parent 40d4e32 commit b015618

21 files changed

Lines changed: 777 additions & 516 deletions

ui/src/styles/chat/sidebar.css

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,8 @@
181181

182182
.chat-workspace-rail__section {
183183
display: flex;
184-
flex: 0 1 auto;
185184
flex-direction: column;
186185
min-height: 0;
187-
overflow-y: auto;
188186
padding-top: 8px;
189187
}
190188

ui/src/ui/app-chat.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3150,6 +3150,10 @@ describe("handleSendChat", () => {
31503150

31513151
await handleSendChat(host);
31523152

3153+
const sendParams = requireRecord(
3154+
mockArg(request, 0, 1, "chat.send params"),
3155+
"chat.send params",
3156+
);
31533157
expect(getChatAttachmentDataUrl(attachment)).toBeNull();
31543158
expect(getChatAttachmentPreviewUrl(attachment)).toBe("blob:brief");
31553159
expect(host.chatMessages).toStrictEqual([
@@ -3168,6 +3172,7 @@ describe("handleSendChat", () => {
31683172
},
31693173
],
31703174
timestamp: expect.any(Number),
3175+
__openclaw: { runId: sendParams.idempotencyKey },
31713176
},
31723177
]);
31733178
});

ui/src/ui/app-chat.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,7 @@ async function sendQueuedChatMessage(
11481148
host as unknown as ChatState,
11491149
message,
11501150
hasAttachments ? attachments : undefined,
1151+
runId,
11511152
startedAt,
11521153
);
11531154
if (ack.status === "ok") {

ui/src/ui/app-tool-stream.node.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,34 @@ describe("app-tool-stream fallback lifecycle handling", () => {
432432
vi.useRealTimers();
433433
});
434434

435+
it("uses mapped source run ownership for rendered tool messages", () => {
436+
const host = createHost();
437+
438+
handleAgentEvent(host, {
439+
runId: "run-client",
440+
seq: 1,
441+
stream: "tool",
442+
ts: Date.now(),
443+
sessionKey: "main",
444+
data: {
445+
phase: "result",
446+
name: "exec",
447+
toolCallId: "call-mapped",
448+
sourceRunId: "run-source",
449+
result: { content: [{ type: "text", text: "done" }] },
450+
},
451+
});
452+
453+
expect(host.toolStreamById.get("call-mapped")).toMatchObject({
454+
runId: "run-source",
455+
message: { runId: "run-source" },
456+
});
457+
expect(host.activityEntries?.[0]).toMatchObject({
458+
id: "run-source:call-mapped",
459+
runId: "run-source",
460+
});
461+
});
462+
435463
it("records tool activity summaries without storing raw argument values", () => {
436464
useToolStreamFakeTimers();
437465
const host = createHost();

ui/src/ui/app-tool-stream.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -844,11 +844,14 @@ export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPaylo
844844
}
845845

846846
const data = payload.data ?? {};
847+
// Mapped gateway runs keep the client id on the envelope for stream routing,
848+
// while tool/terminal outcome ownership follows the persisted source run.
849+
const sourceRunId = toTrimmedString(data.sourceRunId) ?? payload.runId;
847850
const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : "";
848851
if (!toolCallId) {
849852
return;
850853
}
851-
updateActivityFromToolEvent(host, { ...payload, data });
854+
updateActivityFromToolEvent(host, { ...payload, runId: sourceRunId, data });
852855
const name = typeof data.name === "string" ? data.name : "tool";
853856
const phase = typeof data.phase === "string" ? data.phase : "";
854857
const args = phase === "start" ? data.args : undefined;
@@ -882,7 +885,7 @@ export function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPaylo
882885
}
883886
entry = {
884887
toolCallId,
885-
runId: payload.runId,
888+
runId: sourceRunId,
886889
sessionKey,
887890
name,
888891
args,

0 commit comments

Comments
 (0)