From 3eb57dcb014b7f4b55ff4090dbce1e4ee0ca93be Mon Sep 17 00:00:00 2001 From: ranxianglei Date: Sat, 22 Aug 2026 19:28:35 +0800 Subject: [PATCH] fix(wire): hoist leading system/developer prefix out of the openai fold space The openai codec was the last one folding the system prompt into the message id space (anthropic keeps it in the top-level system field, responses collects it as instructions). Two failure modes: 1. A compress range covering pos 0 removed the model's system prompt from the rebuilt wire entirely (observed: glm-5.3 wire systemLen 45151 -> 0 after the first compression). 2. Restart replay: prime-fold reconstructs system content from host state (runtime injections differ across restarts), shifting every span fingerprint covering pos 0, so the guard rejected the replay and blocks showed as none until the first live request. openaiToCore now returns { msgs, systemText } with the contiguous leading system/developer prefix hoisted out; mid-conversation system traffic stays in the fold space unchanged. mirrorOpenaiToCore needs no change: the mirror system message folds through the same hoist, so mirror and live spaces converge regardless of systemText. --- src/wire/openai.ts | 18 +++- tests/openai-system-hoist.test.ts | 115 ++++++++++++++++++++++ tests/wire-bili-message-roundtrip.test.ts | 42 +++++--- 3 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 tests/openai-system-hoist.test.ts diff --git a/src/wire/openai.ts b/src/wire/openai.ts index 2e856dd..40e35d4 100644 --- a/src/wire/openai.ts +++ b/src/wire/openai.ts @@ -36,15 +36,29 @@ export type OpenAIRequestBody = { [key: string]: unknown; }; -type Flat = { msgs: BiliMessage[] }; +type Flat = { msgs: BiliMessage[]; systemText: string }; +/** Hoist the contiguous leading system/developer prefix out of the fold + * space (openai-chat variant of the responses codec's systemParts and the + * anthropic codec's top-level `system` field). The system prompt is host + * runtime state: its content varies across restarts (injected reminders, + * host-composed instructions), so keeping it inside the id space made every + * downstream fingerprint spanning it unstable, and a compress range that + * covered it removed the model's system prompt from the rebuilt wire + * entirely. Mid-conversation system messages (rare, host-synthetic) stay in + * the fold space unchanged. */ export function openaiToCore(body: OpenAIRequestBody): Flat { const msgs: BiliMessage[] = []; + const systemParts: string[] = []; const clusters = new ClusterCounter(); for (const m of body.messages) { switch (m.role) { case "system": case "developer": { + if (msgs.length === 0) { + systemParts.push(stringContent(m.content)); + break; + } const base = deriveMessageId(m.role, "text", stringContent(m.content)); msgs.push({ id: clusters.next(base), role: "system", contentType: "text", text: stringContent(m.content), originalRole: m.role }); break; @@ -134,7 +148,7 @@ export function openaiToCore(body: OpenAIRequestBody): Flat { } } } - return { msgs }; + return { msgs, systemText: systemParts.join("\n\n") }; } export function coreToOpenai(messages: BiliMessage[]): OpenAIMessage[] { diff --git a/tests/openai-system-hoist.test.ts b/tests/openai-system-hoist.test.ts new file mode 100644 index 0000000..0c678f3 --- /dev/null +++ b/tests/openai-system-hoist.test.ts @@ -0,0 +1,115 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + openaiToCore, + coreToOpenai, + injectOpenaiSystem, +} from "../src/wire/openai.js"; +import { mirrorOpenaiToCore } from "../src/wire/mirror.js"; +import type { MirrorMessage } from "../src/wire/mirror.js"; + +const conv: Array> = [ + { role: "user", content: "hello" }, + { role: "assistant", content: "hi there" }, + { role: "user", content: "what is 2+2" }, + { role: "assistant", content: "4" }, +]; + +test("openaiToCore hoists the contiguous leading system/developer prefix out of the fold space", () => { + const body = { + model: "m", + messages: [ + { role: "system", content: "you are terse" }, + { role: "developer", content: "second system part" }, + ...conv, + ], + }; + const { msgs, systemText } = openaiToCore(body as never); + assert.equal(systemText, "you are terse\n\nsecond system part"); + assert.ok(msgs.length > 0); + assert.notEqual(msgs[0].role, "system"); + assert.ok( + msgs.every((m) => m.role !== "system"), + "no system pieces remain in the fold space", + ); + // conversation ids unchanged vs a body with no system prefix at all + const bare = openaiToCore({ model: "m", messages: conv } as never); + assert.deepEqual( + msgs.map((m) => m.id), + bare.msgs.map((m) => m.id), + ); +}); + +test("openaiToCore with no system prefix returns empty systemText", () => { + const { systemText } = openaiToCore({ model: "m", messages: conv } as never); + assert.equal(systemText, ""); +}); + +test("mid-conversation system messages stay in the fold space", () => { + const { msgs } = openaiToCore({ + model: "m", + messages: [ + ...conv.slice(0, 2), + { role: "system", content: "mid-stream system" }, + ...conv.slice(2), + ], + } as never); + assert.equal( + msgs.filter((m) => m.role === "system").length, + 1, + "mid-stream system kept as a fold piece", + ); +}); + +test("system content changes never shift conversation ids (restart regression)", () => { + // Live wire carries the host system prompt; after restart the prime-fold + // reconstruction differs. Ids must not depend on system content, or every + // span fingerprint covering pos 0 breaks and restart replay is rejected. + const a = openaiToCore({ + model: "m", + messages: [{ role: "system", content: "LIVE system, 45k chars of host state" }, ...conv], + } as never); + const b = openaiToCore({ + model: "m", + messages: [{ role: "system", content: "RECONSTRUCTED system, shorter" }, ...conv], + } as never); + assert.deepEqual( + a.msgs.map((m) => m.id), + b.msgs.map((m) => m.id), + ); +}); + +test("mirrorOpenaiToCore converges with the live space regardless of systemText", () => { + const t = (text: string): MirrorMessage => ({ role: "user", blocks: [{ type: "text", text }] }); + const a = (text: string): MirrorMessage => ({ role: "assistant", blocks: [{ type: "text", text }] }); + const view: MirrorMessage[] = [ + t("hello"), + a("hi there"), + t("what is 2+2"), + a("4"), + ]; + const live = openaiToCore({ model: "m", messages: conv } as never); + const mirrorA = mirrorOpenaiToCore(view, "live 45k system"); + const mirrorB = mirrorOpenaiToCore(view, ""); + assert.deepEqual( + mirrorA.map((m) => m.id), + live.msgs.map((m) => m.id), + ); + assert.deepEqual( + mirrorB.map((m) => m.id), + live.msgs.map((m) => m.id), + ); +}); + +test("coreToOpenai output has no head system; injectOpenaiSystem re-injects it", () => { + const { msgs, systemText } = openaiToCore({ + model: "m", + messages: [{ role: "system", content: "you are terse" }, ...conv], + } as never); + const rebuilt = coreToOpenai(msgs); + assert.notEqual(rebuilt[0]?.role, "system"); + const withSystem = injectOpenaiSystem(rebuilt, [systemText, "compress prompt"]); + assert.equal(withSystem[0]?.role, "system"); + assert.match(String(withSystem[0]?.content), /you are terse/); + assert.match(String(withSystem[0]?.content), /compress prompt/); +}); diff --git a/tests/wire-bili-message-roundtrip.test.ts b/tests/wire-bili-message-roundtrip.test.ts index db4d5aa..36b9833 100644 --- a/tests/wire-bili-message-roundtrip.test.ts +++ b/tests/wire-bili-message-roundtrip.test.ts @@ -92,24 +92,38 @@ test("anthropic: tool_result without is_error stays clean", () => { assert.equal(tr.is_error, undefined, "no spurious is_error"); }); -// 4. OpenAI developer role round-trips (was collapsed to system). -test("openai: developer role is restored", () => { - const body: OpenAIRequestBody = { messages: [{ role: "developer", content: "you are a dev" }] }; - const { msgs } = openaiToCore(body); - assert.equal(msgs[0]?.role, "system", "kernel sees system"); - assert.equal(msgs[0]?.originalRole, "developer", "originalRole sidecar stored"); - const rebuilt = coreToOpenai(msgs); - assert.equal(rebuilt[0]?.role, "developer", "developer role reconstructed"); - assert.equal(rebuilt[0]?.content, "you are a dev"); +// 4. OpenAI leading system/developer prefix is hoisted OUT of the fold space +// (restart-regression fix: host system content is runtime-unstable, so it must +// never enter the id/fingerprint space); originalRole still round-trips for +// mid-conversation system traffic. +test("openai: leading developer/system prefix is hoisted, not folded", () => { + const body: OpenAIRequestBody = { + messages: [ + { role: "developer", content: "you are a dev" }, + { role: "system", content: "sys" }, + { role: "user", content: "hi" }, + ], + }; + const { msgs, systemText } = openaiToCore(body); + assert.equal(systemText, "you are a dev\n\nsys", "prefix returned separately"); + assert.ok(msgs.every((m) => m.role !== "system"), "no system piece in the fold space"); + assert.equal(msgs[0]?.role, "user"); }); -// 4b. Sanity: a plain system role is NOT promoted to developer. -test("openai: system role stays system", () => { - const body: OpenAIRequestBody = { messages: [{ role: "system", content: "sys" }] }; +// 4b. Mid-conversation system traffic keeps the role sidecar round-trip. +test("openai: mid-conversation developer role is restored", () => { + const body: OpenAIRequestBody = { + messages: [ + { role: "user", content: "hi" }, + { role: "developer", content: "you are a dev" }, + ], + }; const { msgs } = openaiToCore(body); - assert.equal(msgs[0]?.originalRole, "system"); + const dev = msgs.find((m) => m.role === "system")!; + assert.equal(dev.originalRole, "developer", "originalRole sidecar stored"); const rebuilt = coreToOpenai(msgs); - assert.equal(rebuilt[0]?.role, "system"); + assert.equal(rebuilt[1]?.role, "developer", "developer role reconstructed"); + assert.equal(rebuilt[1]?.content, "you are a dev"); }); // 5. OpenAI image_url round-trips (image was previously dropped entirely).