From 2b68785e82ab7d9d17d6b5cc7b487d47e1382cac Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 24 Jun 2026 23:11:12 +0000 Subject: [PATCH] fix(agent): include provider payloads in append-only digests Responses-style providers serialize providerPayload history items instead of the visible message blocks when replaying native history. Include providerPayload in the append-only per-message digest so payload-only history rewrites stop the stable-prefix walk and re-sync the changed message before any later divergent tail. Add a regression where an assistant message keeps identical visible content and id but changes its openaiResponsesHistory providerPayload while a later message also diverges; syncMessages must preserve the prefix before the assistant and refresh the assistant payload. Fixes #3406 --- packages/agent/src/append-only-context.ts | 9 ++-- .../agent/test/append-only-context.test.ts | 43 +++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/agent/src/append-only-context.ts b/packages/agent/src/append-only-context.ts index 4085e28d8..3e5498272 100644 --- a/packages/agent/src/append-only-context.ts +++ b/packages/agent/src/append-only-context.ts @@ -285,16 +285,17 @@ export class AppendOnlyContextManager { } /** Deterministic digest over every field the provider may serialize — role, - * content, tool calls (both `toolCalls` and OpenAI-wire `tool_calls`), - * tool-result ids/names/error flags (both internal camelCase and wire - * snake_case), and assistant `id` — so an in-place rewrite of *any* of - * these fields is visible to {@link #longestStablePrefix}. */ + * content, provider-native replay payloads, tool calls (both `toolCalls` and + * OpenAI-wire `tool_calls`), tool-result ids/names/error flags (both internal + * camelCase and wire snake_case), and assistant `id` — so an in-place rewrite + * of *any* of these fields is visible to {@link #longestStablePrefix}. */ #messageDigest(msg: unknown): number { if (!msg || typeof msg !== "object") return 0; const m = msg as Record; const payload = JSON.stringify({ r: m.role ?? null, c: m.content ?? null, + pp: m.providerPayload ?? null, tc: m.toolCalls ?? m.tool_calls ?? null, tcid: m.toolCallId ?? m.tool_call_id ?? null, tn: m.toolName ?? m.name ?? null, diff --git a/packages/agent/test/append-only-context.test.ts b/packages/agent/test/append-only-context.test.ts index e11f407b7..5424ceb7c 100644 --- a/packages/agent/test/append-only-context.test.ts +++ b/packages/agent/test/append-only-context.test.ts @@ -572,6 +572,49 @@ describe("message sync", () => { expect((entries[2] as { content: unknown }).content).toBe("a1-pruned"); }); + it("detects providerPayload-only rewrites before preserving a later prefix (#3406)", () => { + const mgr = new AppendOnlyContextManager(); + mgr.build(makeContext(), BUILD_OPTS); + + const original0 = { role: "user", content: "q1" } as any; + const original1 = { + role: "assistant", + content: [{ type: "text", text: "same visible output" }], + id: "assistant-1", + providerPayload: { + type: "openaiResponsesHistory", + provider: "openai", + items: [{ type: "message", role: "assistant", content: [{ type: "output_text", text: "old native" }] }], + }, + } as any; + const original2 = { role: "user", content: "q2" } as any; + mgr.syncMessages([original0, original1, original2]); + + mgr.syncMessages([ + { role: "user", content: "q1" }, + { + role: "assistant", + content: [{ type: "text", text: "same visible output" }], + id: "assistant-1", + providerPayload: { + type: "openaiResponsesHistory", + provider: "openai", + items: [{ type: "message", role: "assistant", content: [{ type: "output_text", text: "new native" }] }], + }, + }, + { role: "user", content: "q2-rewritten" }, + ] as any); + + const entries = mgr.log.entries(); + expect(entries).toHaveLength(3); + expect(entries[0]).toBe(original0); + expect( + (entries[1] as { providerPayload?: { items?: Array<{ content?: Array<{ text?: string }> }> } }).providerPayload + ?.items?.[0]?.content?.[0]?.text, + ).toBe("new native"); + expect((entries[2] as { content: unknown }).content).toBe("q2-rewritten"); + }); + it("does not reuse a stable prefix longer than the current log after direct log clear (#3406)", () => { const mgr = new AppendOnlyContextManager(); mgr.build(makeContext(), BUILD_OPTS);