Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface OctoPart {
input?: unknown
output?: unknown
error?: string
metadata?: Record<string, unknown>
title?: string
}
[key: string]: unknown
Expand Down Expand Up @@ -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<string, string[]>
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -176,7 +180,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))
Expand All @@ -194,6 +201,30 @@ 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 = toolResultBody(state)
if (!state || originalText === undefined) return part
if (trimEnd(coreBody) === trimEnd(originalText)) return part
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(
id: string,
sessionID: string,
Expand Down
51 changes: 51 additions & 0 deletions tests/messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,57 @@ 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("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)
Expand Down