fix(web): replayed tool call after resume reuses its card (duplicate React key)
A suspended agent.propose_options / approval call is replayed on resume with the same toolCallId; foldEvents pushed a second item with the same key, which also inflated the tool call meter. Also use 「」 in the report footnote. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a99c77ce4e
commit
57346d438c
3 files changed
+40
-2
No files matched your search
@@ -789,7 +789,7 @@ export function FinalReport({
|
|||||||
) : null}
|
) : null}
|
||||||
<p className="mt-2 text-meta text-muted-foreground">
|
<p className="mt-2 text-meta text-muted-foreground">
|
||||||
取自下载计划、下载任务和整理记录(生成于 {formatDateTime(report.generatedAt)}
|
取自下载计划、下载任务和整理记录(生成于 {formatDateTime(report.generatedAt)}
|
||||||
)。"已提交"指已进入下载器,整理成功才算入库。
|
)。「已提交」指已进入下载器,整理成功才算入库。
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import type { AgentRunEvent } from "@umt/contracts";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { authDenial, isDenied, pairAudit, runPlanIds } from "./agent-conversation.tsx";
|
import { authDenial, isDenied, pairAudit, runPlanIds } from "./agent-conversation.tsx";
|
||||||
import { contextFromPath, contextPrefix, splitContextGoal } from "./agent-store.ts";
|
import { contextFromPath, contextPrefix, splitContextGoal } from "./agent-store.ts";
|
||||||
import type { StreamItem, ToolCallView } from "./use-agent-stream.ts";
|
import { foldEvents, type StreamItem, type ToolCallView } from "./use-agent-stream.ts";
|
||||||
|
|
||||||
const call = (p: Partial<ToolCallView>): ToolCallView => ({
|
const call = (p: Partial<ToolCallView>): ToolCallView => ({
|
||||||
toolCallId: "t1",
|
toolCallId: "t1",
|
||||||
@@ -81,3 +82,32 @@ describe("侧栏上下文", () => {
|
|||||||
expect(splitContextGoal(goal)).toEqual({ context: expect.stringContaining("taskId="), text: "排查这个任务" });
|
expect(splitContextGoal(goal)).toEqual({ context: expect.stringContaining("taskId="), text: "排查这个任务" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("事件折叠", () => {
|
||||||
|
const ev = (seq: number, body: Record<string, unknown>) =>
|
||||||
|
({ runId: "r1", seq, at: "2026-09-28T00:00:00.000Z", ...body }) as unknown as AgentRunEvent;
|
||||||
|
it("挂起后恢复重放同一 toolCallId:只保留一张工具卡,状态以重放为准", () => {
|
||||||
|
const start = {
|
||||||
|
type: "tool_call_start",
|
||||||
|
toolCallId: "tc1",
|
||||||
|
capabilityId: "agent.propose_options",
|
||||||
|
inputSummary: "{}",
|
||||||
|
};
|
||||||
|
const items = foldEvents([
|
||||||
|
ev(1, { ...start, recovery: false }),
|
||||||
|
ev(2, { ...start, recovery: true }),
|
||||||
|
ev(3, {
|
||||||
|
type: "tool_call_end",
|
||||||
|
toolCallId: "tc1",
|
||||||
|
capabilityId: "agent.propose_options",
|
||||||
|
outputSummary: "ok",
|
||||||
|
isError: false,
|
||||||
|
recovery: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const tools = items.filter((i) => i.kind === "tool");
|
||||||
|
expect(tools).toHaveLength(1);
|
||||||
|
expect(new Set(items.map((i) => i.key)).size).toBe(items.length);
|
||||||
|
expect(tools[0]?.kind === "tool" && tools[0].call).toMatchObject({ status: "succeeded", recovery: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -106,6 +106,14 @@ export function foldEvents(events: AgentRunEvent[]): StreamItem[] {
|
|||||||
else items.push({ kind: "thinking", key: `th${e.seq}`, text: e.delta, at: e.at });
|
else items.push({ kind: "thinking", key: `th${e.seq}`, text: e.delta, at: e.at });
|
||||||
break;
|
break;
|
||||||
case "tool_call_start": {
|
case "tool_call_start": {
|
||||||
|
// 挂起的调用(方案选择 / 审批 / 等子会话)恢复时按同一 toolCallId 重放:沿用原卡片,不重复出卡
|
||||||
|
const prev = tools.get(e.toolCallId);
|
||||||
|
if (prev) {
|
||||||
|
Object.assign(prev, { status: "running", outputSummary: null, reason: null, endedAt: null });
|
||||||
|
prev.recovery = e.recovery;
|
||||||
|
if (e.inputSummary) prev.inputSummary = e.inputSummary;
|
||||||
|
break;
|
||||||
|
}
|
||||||
const call: ToolCallView = {
|
const call: ToolCallView = {
|
||||||
toolCallId: e.toolCallId,
|
toolCallId: e.toolCallId,
|
||||||
capabilityId: e.capabilityId,
|
capabilityId: e.capabilityId,
|
||||||
|
|||||||
Reference in new issue
Block a user