From 004a627cdf73076209be07940b2b62a7f3c55e61 Mon Sep 17 00:00:00 2001 From: 5258MF <5258MF@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:29:52 +0800 Subject: [PATCH 1/2] fix: apply kernel truncation to surviving tool parts --- src/messages.ts | 26 +++++++++++++++++++++++++- tests/messages.test.ts | 17 +++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/messages.ts b/src/messages.ts index 180d801..c260870 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -176,7 +176,10 @@ export function reassemble( const callAlive = outCoreById.has(ids[0]!) const resultAlive = outCoreById.has(ids[1]!) if (!(callAlive && resultAlive)) continue - parts.push(p) + // Honor kernel body mutations (emergency truncation of large + // tool-results): if the result core's text differs from the original + // output, replace it — otherwise truncation never reaches the model. + parts.push(applyToolBody(p, outCoreById.get(ids[1]!))) continue } const survived = ids.some((id) => outCoreById.has(id)) @@ -194,6 +197,27 @@ export function reassemble( return result } +function trimEnd(s: string): string { + return s.replace(/\s+$/, "") +} + +export function applyToolBody(part: OctoPart, resultCore: CoreMessage | undefined): OctoPart { + const coreBody = resultCore?.text ?? "" + if (!coreBody) return part + // Compare the original output against the result core's text — kernel + // truncation rewrites that text, and dropping the rewrite would leave the + // full output in the request. + const state = part.state + const originalText = + state?.status === "completed" + ? typeof state.output === "string" + ? state.output + : safeStringify(state.output) + : `Error: ${state?.error ?? ""}` + if (trimEnd(coreBody) === trimEnd(originalText)) return part + return { ...part, state: { status: "completed", ...state, output: coreBody } } +} + export function makeNudgeMessage( id: string, sessionID: string, diff --git a/tests/messages.test.ts b/tests/messages.test.ts index f7fd2f8..e6047ad 100644 --- a/tests/messages.test.ts +++ b/tests/messages.test.ts @@ -110,6 +110,23 @@ test("compress + reassembly replaces covered messages with synthetic user summar assert.ok(hasU2, "recent uncompressed message preserved") }) +test("reassemble: kernel-truncated tool body replaces part output", () => { + const msgs = [toolMsg("a2", "s1", "bash", "call_1", "ORIGINAL_LONG_OUTPUT")] + const { cores, partIdToCoreIds } = octoToCoreMessages(msgs) + const truncated = cores.map((c) => (c.id === "a2#x0" ? { ...c, text: "TRUNCATED_BODY" } : c)) + const out = reassemble(truncated, msgs, partIdToCoreIds, "s1") + assert.equal(out.length, 1) + assert.equal(out[0]!.parts[0]!.state!.output, "TRUNCATED_BODY", "truncated body applied to part output") +}) + +test("reassemble: trailing whitespace difference is not treated as a rewrite", () => { + const msgs = [toolMsg("a2", "s1", "bash", "call_1", "done")] + const { cores, partIdToCoreIds } = octoToCoreMessages(msgs) + const padded = cores.map((c) => (c.id === "a2#x0" ? { ...c, text: "done\n\n " } : c)) + const out = reassemble(padded, msgs, partIdToCoreIds, "s1") + assert.equal(out[0]!.parts[0]!.state!.output, "done", "original output kept") +}) + test("makeNudgeMessage produces a valid user message", () => { const msgs = [userMsg("u1", "s1", "hi")] const n = makeNudgeMessage("bili_nudge_0", "s1", "please compress", msgs) From 567cffa584151d98adb4f7e5979c731fbdcbb515 Mon Sep 17 00:00:00 2001 From: 5258MF <5258MF@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:35:01 +0800 Subject: [PATCH 2/2] fix: preserve tool error result state after truncation --- src/messages.ts | 35 +++++++++++++++++++++-------------- tests/messages.test.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/messages.ts b/src/messages.ts index c260870..34ae637 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -14,6 +14,7 @@ export interface OctoPart { input?: unknown output?: unknown error?: string + metadata?: Record title?: string } [key: string]: unknown @@ -43,6 +44,15 @@ function safeStringify(value: unknown): string { } } +function toolResultBody(state: OctoPart["state"]): string | undefined { + if (state?.status === "completed") { + return typeof state.output === "string" ? state.output : safeStringify(state.output) + } + if (state?.status !== "error") return undefined + const interruptedOutput = state.metadata?.interrupted === true ? state.metadata.output : undefined + return typeof interruptedOutput === "string" ? interruptedOutput : `Error: ${state.error ?? ""}` +} + export interface ConversionResult { cores: CoreMessage[] partIdToCoreIds: Map @@ -82,19 +92,13 @@ export function octoToCoreMessages(msgs: OctoMessage[]): ConversionResult { const ids = [callId] if (part.state?.status === "completed" || part.state?.status === "error") { const resultId = `${msg.info.id}#x${partIdx}` - const outText = - part.state.status === "completed" - ? typeof part.state.output === "string" - ? part.state.output - : safeStringify(part.state.output) - : `Error: ${part.state.error ?? ""}` cores.push({ id: resultId, role: "tool", contentType: "tool-result", toolName, toolCallId, - text: outText, + text: toolResultBody(part.state) ?? "", }) ids.push(resultId) } @@ -208,14 +212,17 @@ export function applyToolBody(part: OctoPart, resultCore: CoreMessage | undefine // truncation rewrites that text, and dropping the rewrite would leave the // full output in the request. const state = part.state - const originalText = - state?.status === "completed" - ? typeof state.output === "string" - ? state.output - : safeStringify(state.output) - : `Error: ${state?.error ?? ""}` + const originalText = toolResultBody(state) + if (!state || originalText === undefined) return part if (trimEnd(coreBody) === trimEnd(originalText)) return part - return { ...part, state: { status: "completed", ...state, output: coreBody } } + if (state.status === "completed") { + return { ...part, state: { ...state, output: coreBody } } + } + if (state.metadata?.interrupted === true && typeof state.metadata.output === "string") { + return { ...part, state: { ...state, metadata: { ...state.metadata, output: coreBody } } } + } + const error = coreBody.startsWith("Error: ") ? coreBody.slice("Error: ".length) : coreBody + return { ...part, state: { ...state, error } } } export function makeNudgeMessage( diff --git a/tests/messages.test.ts b/tests/messages.test.ts index e6047ad..43f6821 100644 --- a/tests/messages.test.ts +++ b/tests/messages.test.ts @@ -127,6 +127,40 @@ test("reassemble: trailing whitespace difference is not treated as a rewrite", ( assert.equal(out[0]!.parts[0]!.state!.output, "done", "original output kept") }) +test("reassemble: kernel-truncated tool error replaces error without changing status", () => { + const msg = toolMsg("a2", "s1", "bash", "call_1", "unused") + msg.parts[0]!.state = { status: "error", input: { a: 1 }, error: "ORIGINAL_LONG_ERROR", title: "bash" } + const { cores, partIdToCoreIds } = octoToCoreMessages([msg]) + assert.equal(cores.find((c) => c.id === "a2#x0")!.text, "Error: ORIGINAL_LONG_ERROR", "error projected for kernel") + const truncated = cores.map((c) => (c.id === "a2#x0" ? { ...c, text: "Error: TRUNCATED_ERROR" } : c)) + const out = reassemble(truncated, [msg], partIdToCoreIds, "s1") + const state = out[0]!.parts[0]!.state! + assert.equal(state.status, "error", "error status preserved") + assert.equal(state.error, "TRUNCATED_ERROR", "adapter prefix removed before writing error") + assert.equal(state.output, undefined, "completed output field not introduced") +}) + +test("reassemble: interrupted tool output is projected and rewritten in metadata", () => { + const msg = toolMsg("a2", "s1", "bash", "call_1", "unused") + msg.parts[0]!.state = { + status: "error", + input: { a: 1 }, + error: "Tool execution aborted", + metadata: { interrupted: true, output: "ORIGINAL_PARTIAL_OUTPUT", exitCode: 130 }, + title: "bash", + } + const { cores, partIdToCoreIds } = octoToCoreMessages([msg]) + assert.equal(cores.find((c) => c.id === "a2#x0")!.text, "ORIGINAL_PARTIAL_OUTPUT", "partial output projected for kernel") + const truncated = cores.map((c) => (c.id === "a2#x0" ? { ...c, text: "TRUNCATED_PARTIAL_OUTPUT" } : c)) + const out = reassemble(truncated, [msg], partIdToCoreIds, "s1") + const state = out[0]!.parts[0]!.state! + assert.equal(state.status, "error", "interrupted status preserved") + assert.equal(state.error, "Tool execution aborted", "original interruption error preserved") + assert.equal(state.metadata?.interrupted, true, "interrupted marker preserved") + assert.equal(state.metadata?.output, "TRUNCATED_PARTIAL_OUTPUT", "truncated partial output written where OpenCode reads it") + assert.equal(state.metadata?.exitCode, 130, "other metadata preserved") +}) + test("makeNudgeMessage produces a valid user message", () => { const msgs = [userMsg("u1", "s1", "hi")] const n = makeNudgeMessage("bili_nudge_0", "s1", "please compress", msgs)