From b7c3473055667f525c83a91073f5b043a3adbf37 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 12 Jul 2026 08:26:06 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8E=BB=E9=87=8D=E5=8A=A9=E6=89=8B?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E9=87=8D=E6=94=BE=E5=B9=B6=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E6=97=A0=E6=AD=A3=E6=96=87=E7=BB=88=E6=80=81=E5=B0=81=E5=8D=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/codex-stdio.ts | 41 +++-- src/common/events.ts | 30 ++++ src/mgr/result.ts | 35 ++++ .../cases/36-stdio-event-reconstruction.ts | 16 +- src/selftest/cases/80-assistant-final-seal.ts | 164 ++++++++++++++++++ src/selftest/fake-codex-app-server.ts | 26 +++ 6 files changed, 300 insertions(+), 12 deletions(-) create mode 100644 src/selftest/cases/80-assistant-final-seal.ts diff --git a/src/backend/codex-stdio.ts b/src/backend/codex-stdio.ts index 43b98b5..b8a330c 100644 --- a/src/backend/codex-stdio.ts +++ b/src/backend/codex-stdio.ts @@ -130,7 +130,7 @@ interface FinalAssistantMessage { text: string; messageIndex: number | null; messageCount: number | null; - source: string; + targetSource: string; } interface AssistantDeltaProgressItem { @@ -157,6 +157,7 @@ export class CodexStdioEventReducer { private assistantText = ""; private readonly assistantDeltaProgress = createAssistantDeltaProgressState(); private readonly completedAssistantMessages: CompletedAssistantMessage[] = []; + private readonly completedAssistantItemIds = new Set(); private readonly deferredTerminalEvents: BackendEvent[] = []; private readonly suppressedNotifications = createSuppressedNotificationSummary(); @@ -169,8 +170,14 @@ export class CodexStdioEventReducer { if (progress) events.push(progress); } if (normalized.completedAssistantMessage) { - this.completedAssistantMessages.push(normalized.completedAssistantMessage); - events.push(assistantMessageEventForCompleted(normalized.completedAssistantMessage, this.completedAssistantMessages.length)); + const itemId = normalized.completedAssistantMessage.itemId; + if (itemId && this.completedAssistantItemIds.has(itemId)) { + recordSuppressedNotification(this.suppressedNotifications, "item/completed:duplicate-agentMessage", "agentMessage"); + } else { + if (itemId) this.completedAssistantItemIds.add(itemId); + this.completedAssistantMessages.push(normalized.completedAssistantMessage); + events.push(assistantMessageEventForCompleted(normalized.completedAssistantMessage, this.completedAssistantMessages.length)); + } } if (normalized.terminalEvents) this.deferredTerminalEvents.push(...normalized.terminalEvents); return { @@ -183,9 +190,9 @@ export class CodexStdioEventReducer { finalize(terminal: { status: TerminalStatus; failureKind: FailureKind | null; message: string | null } | null): BackendEvent[] { const events = flushAssistantDeltaProgress(this.assistantDeltaProgress); + if (this.completedAssistantMessages.length === 0) events.push(...assistantMessageEventsForTurn(this.assistantText, false)); const finalAssistant = terminal?.status === "completed" ? finalAssistantMessageForTurn(this.completedAssistantMessages, this.assistantText) : null; - if (finalAssistant) events.push(assistantFinalResponseEvent(finalAssistant)); - else if (this.completedAssistantMessages.length === 0) events.push(...assistantMessageEventsForTurn(this.assistantText, false)); + if (finalAssistant) events.push(assistantFinalSealEvent(finalAssistant)); events.push(...suppressedNotificationEvents(this.suppressedNotifications)); events.push(...this.deferredTerminalEvents); if (terminal) events.push({ type: "terminal_status", payload: { terminalStatus: terminal.status, failureKind: terminal.failureKind, message: terminal.message } }); @@ -1297,6 +1304,7 @@ function assistantMessageEventForCompleted(message: CompletedAssistantMessage, m messageCount: null, replyAuthority: false, final: false, + durableText: true, }, }; } @@ -1313,6 +1321,7 @@ function assistantMessageEventsForTurn(assistantDeltaText: string, completed: bo messageCount: 1, replyAuthority: completed, final: completed, + durableText: true, }, }]; } @@ -1325,7 +1334,7 @@ function finalAssistantMessageForTurn(completedMessages: CompletedAssistantMessa text: latestCompleted.text, messageIndex: completedMessages.length, messageCount: completedMessages.length, - source: "completed-agent-message-final", + targetSource: "completed-agent-message", }; } if (assistantDeltaText.trim().length === 0) return null; @@ -1334,25 +1343,37 @@ function finalAssistantMessageForTurn(completedMessages: CompletedAssistantMessa text: assistantDeltaText, messageIndex: 1, messageCount: 1, - source: "agent-message-delta-final", + targetSource: "agent-message-delta-fallback", }; } -function assistantFinalResponseEvent(message: FinalAssistantMessage): BackendEvent { +function assistantFinalSealEvent(message: FinalAssistantMessage): BackendEvent { return { type: "assistant_message", payload: { - text: message.text, itemId: message.itemId, - source: message.source, + source: "assistant-message-final-seal", messageIndex: message.messageIndex, messageCount: message.messageCount, replyAuthority: true, final: true, + finalSeal: true, + replyRef: { + eventType: "assistant_message", + itemId: message.itemId, + messageIndex: message.messageIndex, + messageCount: message.messageCount, + source: message.targetSource, + textSha256: sha256Text(redactText(message.text)), + }, }, }; } +function sha256Text(value: string): string { + return createHash("sha256").update(value).digest("hex"); +} + function createAssistantDeltaProgressState(): AssistantDeltaProgressState { return new Map(); } diff --git a/src/common/events.ts b/src/common/events.ts index 6ed780e..6147db7 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -123,6 +123,8 @@ function nonEmptyText(value: unknown): string | null { function normalizeTextPayload(payload: JsonRecord): JsonRecord { const { text: _text, delta: _delta, content: _content, summary: _summary, textBytes: _textBytes, textTruncated: _textTruncated, outputBytes: _outputBytes, outputTruncated: _outputTruncated, ...rest } = payload; const value = typeof payload.text === "string" ? payload.text : typeof payload.delta === "string" ? payload.delta : typeof payload.content === "string" ? payload.content : ""; + if (payload.finalSeal === true) return normalizeAssistantFinalSeal(rest, value); + if (payload.durableText === true) return normalizeDurableAssistantText(rest, value); if (payload.replyAuthority === true || payload.final === true) { const text = redactText(value); const outputBytes = Buffer.byteLength(text, "utf8"); @@ -133,6 +135,34 @@ function normalizeTextPayload(payload: JsonRecord): JsonRecord { return { ...rest, text: summary.text, summary, textBytes: summary.textBytes, textTruncated: summary.textTruncated, outputBytes: summary.outputBytes, outputTruncated: summary.outputTruncated }; } +function normalizeDurableAssistantText(payload: JsonRecord, body: string): JsonRecord { + const source = typeof payload.source === "string" ? payload.source : ""; + if (source !== "completed-agent-message" && source !== "agent-message-delta-fallback") { + throw new AgentRunError("schema-invalid", "durable assistant text requires a completed-agent-message or agent-message-delta-fallback source", { httpStatus: 400 }); + } + const text = redactText(body); + const textBytes = Buffer.byteLength(text, "utf8"); + return { + ...payload, + text, + summary: { textChars: text.length, textBytes, outputBytes: textBytes, textTruncated: false, outputTruncated: false }, + textBytes, + textTruncated: false, + outputBytes: textBytes, + outputTruncated: false, + }; +} + +function normalizeAssistantFinalSeal(payload: JsonRecord, body: string): JsonRecord { + const replyRef = typeof payload.replyRef === "object" && payload.replyRef !== null && !Array.isArray(payload.replyRef) ? payload.replyRef as JsonRecord : null; + const textSha256 = replyRef && typeof replyRef.textSha256 === "string" ? replyRef.textSha256 : ""; + if (body.length > 0) throw new AgentRunError("schema-invalid", "assistant final seal must not carry text, content, or delta", { httpStatus: 400 }); + if (payload.replyAuthority !== true || payload.final !== true || replyRef?.eventType !== "assistant_message" || typeof replyRef.source !== "string" || !/^[0-9a-f]{64}$/u.test(textSha256)) { + throw new AgentRunError("schema-invalid", "assistant final seal requires final replyAuthority and an assistant_message replyRef with source and textSha256", { httpStatus: 400 }); + } + return { ...payload, replyRef: redactJson(replyRef), textBytes: 0, textTruncated: false, outputBytes: 0, outputTruncated: false }; +} + function normalizeToolCallPayload(payload: JsonRecord): JsonRecord { const redacted = redactJson(payload); if (isCommandExecutionToolCall(redacted)) { diff --git a/src/mgr/result.ts b/src/mgr/result.ts index 6fadedf..4f24ad1 100644 --- a/src/mgr/result.ts +++ b/src/mgr/result.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import type { AgentRunStore } from "./store.js"; import type { CommandRecord, FailureKind, JsonRecord, JsonValue, RunEvent, RunRecord, RunnerJobRecord, TerminalStatus } from "../common/types.js"; import { boundedTextSummary, outputBytesFromPayload, outputTruncatedFromPayload } from "../common/output.js"; @@ -794,6 +795,8 @@ function resultFailureDetails(events: RunEvent[], terminal: TerminalStatus | nul function assistantReply(events: RunEvent[]): AssistantReplySummary { const assistantEvents = events.filter((event) => event.type === "assistant_message"); + const sealedReply = assistantReplyFromFinalSeal(assistantEvents); + if (sealedReply) return sealedReply; const latestAuthoritative = [...assistantEvents].reverse().find((event) => (event.payload.replyAuthority === true || event.payload.final === true) && textPayload(event.payload).length > 0); if (latestAuthoritative) return assistantReplySummary(latestAuthoritative); const latestAssistant = [...assistantEvents].reverse().find((event) => textPayload(event.payload).length > 0); @@ -801,6 +804,34 @@ function assistantReply(events: RunEvent[]): AssistantReplySummary { return { text: "", seq: null, source: null, final: false, replyAuthority: false, textTruncated: false, outputTruncated: false }; } +function assistantReplyFromFinalSeal(events: RunEvent[]): AssistantReplySummary | null { + const seals = [...events].reverse().filter((event) => event.payload.finalSeal === true && event.payload.replyAuthority === true); + for (const seal of seals) { + const replyRef = jsonRecordValue(seal.payload.replyRef); + if (!replyRef || replyRef.eventType !== "assistant_message") continue; + const target = [...events].reverse().find((event) => event.seq < seal.seq && assistantReplyRefMatches(event, replyRef)); + if (!target) continue; + const targetSummary = assistantReplySummary(target); + return { + ...targetSummary, + seq: seal.seq, + source: typeof seal.payload.source === "string" ? seal.payload.source : "assistant-message-final-seal", + final: true, + replyAuthority: true, + }; + } + return null; +} + +function assistantReplyRefMatches(event: RunEvent, replyRef: JsonRecord): boolean { + if (event.payload.finalSeal === true || textPayload(event.payload).length === 0) return false; + if (stringJsonValue(event.payload.source) !== stringJsonValue(replyRef.source)) return false; + if ((stringJsonValue(event.payload.itemId) ?? null) !== (stringJsonValue(replyRef.itemId) ?? null)) return false; + if ((numberJsonValue(event.payload.messageIndex) ?? null) !== (numberJsonValue(replyRef.messageIndex) ?? null)) return false; + const textSha256 = stringJsonValue(replyRef.textSha256); + return textSha256 !== null && textSha256 === sha256Text(textPayload(event.payload)); +} + function assistantReplySummary(event: RunEvent): AssistantReplySummary { return { text: textPayload(event.payload), @@ -821,6 +852,10 @@ function textPayload(payload: JsonRecord): string { return ""; } +function sha256Text(value: string): string { + return createHash("sha256").update(value).digest("hex"); +} + function artifactSummary(events: RunEvent[]): JsonRecord { let commandOutputEvents = 0; let diffEvents = 0; diff --git a/src/selftest/cases/36-stdio-event-reconstruction.ts b/src/selftest/cases/36-stdio-event-reconstruction.ts index cb29d8d..9b71133 100644 --- a/src/selftest/cases/36-stdio-event-reconstruction.ts +++ b/src/selftest/cases/36-stdio-event-reconstruction.ts @@ -7,6 +7,7 @@ import { closeAgentRunKafkaProducer, publishAgentRunKafkaMessage, setAgentRunKafkaProducerFactoryForSelfTest, + sha256Hex, type AgentRunKafkaConfig, } from "../../common/kafka-events.js"; import type { JsonRecord } from "../../common/types.js"; @@ -53,12 +54,23 @@ const selfTest: SelfTestCase = async (context) => { assert.equal((record.value.reconstruction as JsonRecord).originalOutboxSeq, null); assert.equal(((record.value.reconstruction as JsonRecord).identityDerivation as JsonRecord).reversible, true); } - const finalAssistant = reconstructed.records.find((record) => { + const finalSeal = reconstructed.records.find((record) => { const event = record.value.event as JsonRecord; const payload = event.payload as JsonRecord; return event.type === "assistant_message" && payload.replyAuthority === true; }); - assert.equal(((finalAssistant?.value.event as JsonRecord).payload as JsonRecord).text, "stdio reconstruction fixture answer"); + const finalSealPayload = (finalSeal?.value.event as JsonRecord).payload as JsonRecord; + assert.equal(finalSealPayload.finalSeal, true); + assert.equal(Object.hasOwn(finalSealPayload, "text"), false); + const replyRef = finalSealPayload.replyRef as JsonRecord; + const finalAssistant = reconstructed.records.find((record) => { + const event = record.value.event as JsonRecord; + const payload = event.payload as JsonRecord; + return event.type === "assistant_message" && payload.itemId === replyRef.itemId && payload.source === replyRef.source; + }); + const finalAssistantText = String(((finalAssistant?.value.event as JsonRecord).payload as JsonRecord).text ?? ""); + assert.equal(finalAssistantText, "stdio reconstruction fixture answer"); + assert.equal(replyRef.textSha256, sha256Hex(finalAssistantText)); assert.equal((reconstructed.records.at(-1)?.value.event as JsonRecord).type, "terminal_status"); let readGroupId = ""; diff --git a/src/selftest/cases/80-assistant-final-seal.ts b/src/selftest/cases/80-assistant-final-seal.ts new file mode 100644 index 0000000..0cfae8c --- /dev/null +++ b/src/selftest/cases/80-assistant-final-seal.ts @@ -0,0 +1,164 @@ +import assert from "node:assert/strict"; +import type { JsonRecord } from "../../common/types.js"; +import { ManagerClient } from "../../mgr/client.js"; +import { startManagerServer } from "../../mgr/server.js"; +import { MemoryAgentRunStore } from "../../mgr/store.js"; +import { runOnce } from "../../runner/run-once.js"; +import { createRunWithCommand, type SelfTestCase, type SelfTestContext } from "../harness.js"; + +interface EventRecord { + seq: number; + type: string; + payload: JsonRecord; +} + +const selfTest: SelfTestCase = async (context) => { + const server = await startManagerServer({ + port: 0, + host: "127.0.0.1", + sourceCommit: "self-test", + store: new MemoryAgentRunStore(), + runnerJobDefaults: { + namespace: "agentrun-v02", + managerUrl: "http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080", + runnerApiKeySecretRef: { name: "agentrun-v02-api-key", key: "HWLAB_API_KEY" }, + image: "registry.invalid/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111", + serviceAccountName: "agentrun-v02-runner", + jobNamePrefix: "agentrun-v02-runner", + lane: "v0.2", + }, + }); + try { + const client = new ManagerClient(server.baseUrl); + await assertHistoryReplaySealedOnce(client, context, server.baseUrl); + await assertDeltaFallbackUsesBodylessSeal(client, context, server.baseUrl); + return { + name: "assistant-final-seal", + tests: [ + "assistant-final-seal-history-replay", + "assistant-final-seal-distinct-identical-text", + "assistant-final-seal-late-final", + "assistant-final-seal-manager-result-pointer", + "assistant-final-seal-delta-fallback", + ], + }; + } finally { + await new Promise((resolve) => server.server.close(() => resolve())); + } +}; + +async function assertHistoryReplaySealedOnce(client: ManagerClient, context: SelfTestContext, managerUrl: string): Promise { + const lateFinalText = `Late unique final answer:${"x".repeat(5_000)} Authorization: Bearer REDACTED`; + const rawLateFinalSecret = "final-secret-token-value"; + const command = await createRunWithCommand(client, context, "history replay final seal", "selftest-assistant-history-replay-final-seal", 15_000); + const run = await runOnce({ + managerUrl, + runId: command.runId, + codexCommand: context.fakeCodexCommand, + codexArgs: context.fakeCodexArgs, + codexHome: context.codexHome, + env: { + CODEX_HOME: context.codexHome, + AGENTRUN_FAKE_CODEX_MODE: "assistant-history-replay-final-seal", + AGENTRUN_CODEX_TERMINAL_NOTIFICATION_DRAIN_MS: "200", + }, + oneShot: true, + }) as JsonRecord; + assert.equal(run.terminalStatus, "completed"); + + const events = await listEvents(client, command.runId); + const completedMessages = events.filter((event) => event.type === "assistant_message" && event.payload.source === "completed-agent-message"); + assert.deepEqual(completedMessages.map((event) => event.payload.itemId), [ + "msg_replay_progress", + "msg_same_text_a", + "msg_same_text_b", + "msg_late_final_seal", + ]); + assert.equal(completedMessages.filter((event) => event.payload.text === "Legitimate identical assistant text.").length, 2, "different itemId values with identical text must both remain durable"); + for (const itemId of ["msg_replay_progress", "msg_same_text_a", "msg_same_text_b", "msg_late_final_seal"]) { + assert.equal(completedMessages.filter((event) => event.payload.itemId === itemId).length, 1, `item ${itemId} must be durable exactly once`); + } + const lateFinal = completedMessages.find((event) => event.payload.itemId === "msg_late_final_seal"); + assert.equal(lateFinal?.payload.text, lateFinalText, "the unique durable message body must remain untruncated for seal dereferencing"); + assert.equal(lateFinal?.payload.outputTruncated, false); + assert.equal(JSON.stringify(events).includes(rawLateFinalSecret), false, "secret-like final text must be redacted before becoming durable"); + + const seals = events.filter((event) => event.type === "assistant_message" && event.payload.finalSeal === true); + assert.equal(seals.length, 1, "one completed turn must have exactly one final seal"); + const seal = seals[0]!; + assert.equal(Object.hasOwn(seal.payload, "text"), false, "final seal must not duplicate the assistant body"); + assert.equal(Object.hasOwn(seal.payload, "content"), false, "final seal must not duplicate the assistant body through content"); + assert.equal(Object.hasOwn(seal.payload, "delta"), false, "final seal must not duplicate the assistant body through delta"); + assert.equal(seal.payload.source, "assistant-message-final-seal"); + assert.equal(seal.payload.replyAuthority, true); + assert.equal(seal.payload.final, true); + const replyRef = asRecord(seal.payload.replyRef); + assert.equal(replyRef.itemId, "msg_late_final_seal"); + assert.equal(replyRef.source, "completed-agent-message"); + assert.match(String(replyRef.textSha256), /^[0-9a-f]{64}$/u); + + const suppression = events.find((event) => event.type === "backend_status" && event.payload.phase === "codex-app-server-notifications-suppressed"); + assert.ok(suppression, "history replay suppression must remain visible without persisting duplicate bodies"); + const replayMethod = asRecords(suppression?.payload.methods).find((item) => item.method === "item/completed:duplicate-agentMessage"); + assert.equal(replayMethod?.count, 5); + + const toolEvents = events.filter((event) => event.type === "tool_call" && event.payload.itemId === "tool_before_history_replay"); + assert.equal(toolEvents.length, 2, "assistant replay suppression must neither replay nor drop the tool start/completion pair"); + assert.deepEqual(toolEvents.map((event) => event.payload.method), ["item/started", "item/completed"]); + + const turnCompleted = events.find((event) => event.type === "backend_status" && event.payload.phase === "turn/completed"); + assert.ok(turnCompleted && seal.seq < turnCompleted.seq, "bodyless final seal must precede the deferred turn terminal event"); + + const result = await client.get(`/api/v1/runs/${command.runId}/commands/${command.commandId}/result`) as JsonRecord; + assert.equal(result.reply, lateFinalText, "Manager result must dereference the untruncated final seal target instead of reading duplicate final text"); + assert.equal(JSON.stringify(result).includes(rawLateFinalSecret), false, "Manager result must not re-expose the redacted secret-like source text"); + assert.equal(result.finalResponseAuthority, "authoritative"); + assert.equal(result.finalAssistantSeq, seal.seq); + const finalResponse = asRecord(result.finalResponse); + assert.equal(finalResponse.source, "assistant-message-final-seal"); + assert.equal(finalResponse.replyAuthority, true); + assert.equal(finalResponse.final, true); +} + +async function assertDeltaFallbackUsesBodylessSeal(client: ManagerClient, context: SelfTestContext, managerUrl: string): Promise { + const command = await createRunWithCommand(client, context, "delta fallback final seal", "selftest-assistant-delta-fallback-final-seal", 15_000); + const run = await runOnce({ + managerUrl, + runId: command.runId, + codexCommand: context.fakeCodexCommand, + codexArgs: context.fakeCodexArgs, + codexHome: context.codexHome, + env: { CODEX_HOME: context.codexHome }, + oneShot: true, + }) as JsonRecord; + assert.equal(run.terminalStatus, "completed"); + + const events = await listEvents(client, command.runId); + const fallback = events.filter((event) => event.type === "assistant_message" && event.payload.source === "agent-message-delta-fallback"); + assert.equal(fallback.length, 1, "delta-only turns must persist one full fallback body before sealing it"); + assert.equal(fallback[0]?.payload.text, "fake codex stdio reply"); + const seal = events.find((event) => event.type === "assistant_message" && event.payload.finalSeal === true); + assert.ok(seal); + assert.equal(Object.hasOwn(seal.payload, "text"), false); + assert.equal(asRecord(seal.payload.replyRef).source, "agent-message-delta-fallback"); + + const result = await client.get(`/api/v1/runs/${command.runId}/commands/${command.commandId}/result`) as JsonRecord; + assert.equal(result.reply, "fake codex stdio reply"); + assert.equal(result.finalResponseAuthority, "authoritative"); + assert.equal(result.finalAssistantSeq, seal.seq); +} + +async function listEvents(client: ManagerClient, runId: string): Promise { + const response = await client.get(`/api/v1/runs/${runId}/events?afterSeq=0&limit=200`) as { items?: EventRecord[] }; + return response.items ?? []; +} + +function asRecord(value: unknown): JsonRecord { + return typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonRecord : {}; +} + +function asRecords(value: unknown): JsonRecord[] { + return Array.isArray(value) ? value.map(asRecord) : []; +} + +export default selfTest; diff --git a/src/selftest/fake-codex-app-server.ts b/src/selftest/fake-codex-app-server.ts index f03c111..43e9432 100644 --- a/src/selftest/fake-codex-app-server.ts +++ b/src/selftest/fake-codex-app-server.ts @@ -238,6 +238,32 @@ for await (const line of rl) { }, 450).unref?.(); continue; } + if (mode === "assistant-history-replay-final-seal") { + turnCounter += 1; + const turn = { id: `turn_selftest_${turnCounter}`, status: "completed" }; + const lateFinalText = `Late unique final answer:${"x".repeat(5_000)} Authorization: Bearer final-secret-token-value`; + respond(message.id, { turn }); + notify("turn/started", { turn }); + notify("item/started", { item: { id: "tool_before_history_replay", type: "commandExecution", command: "echo durable-once", status: "running" } }); + notify("item/completed", { item: { id: "tool_before_history_replay", type: "commandExecution", command: "echo durable-once", status: "completed", exitCode: 0 } }); + notify("item/completed", { item: { id: "msg_replay_progress", type: "agentMessage", text: "Progress before history replay." } }); + notify("item/completed", { item: { id: "msg_same_text_a", type: "agentMessage", text: "Legitimate identical assistant text." } }); + notify("item/completed", { item: { id: "msg_same_text_b", type: "agentMessage", text: "Legitimate identical assistant text." } }); + notify("item/completed", { item: { id: "msg_replay_progress", type: "agentMessage", text: "Progress before history replay." } }); + notify("turn/completed", { turn }); + setTimeout(() => { + notify("item/completed", { item: { id: "msg_replay_progress", type: "agentMessage", text: "Progress before history replay." } }); + notify("item/completed", { item: { id: "msg_same_text_a", type: "agentMessage", text: "Legitimate identical assistant text." } }); + notify("item/completed", { item: { id: "msg_same_text_b", type: "agentMessage", text: "Legitimate identical assistant text." } }); + }, 20).unref?.(); + setTimeout(() => { + notify("item/completed", { item: { id: "msg_late_final_seal", type: "agentMessage", text: lateFinalText } }); + }, 40).unref?.(); + setTimeout(() => { + notify("item/completed", { item: { id: "msg_late_final_seal", type: "agentMessage", text: lateFinalText } }); + }, 60).unref?.(); + continue; + } if (mode === "web-search-progress") { turnCounter += 1; const turn = { id: `turn_selftest_${turnCounter}`, status: "completed" };