Skip to content

Commit f0752f0

Browse files
committed
fix(opencode): publish overflow error when recovery is abandoned, not attempted
A provider 413 with auto-compaction on published Session.Event.Error and then recovered: compact, retry, succeed. Every consumer of that channel treats it as the run's outcome — CLI run exits 1, TUI shows an error toast, the desktop app fires an OS notification, and orchestrators tailing --format json mark the run failed — so a run that produced a complete, correct result was reported as a failure. The publish at processor.halt's auto-compact branch was the outlier on three counts: it was the only publisher that does not set message.error (the durable record disagreed with the event), the only one not followed by idle or a throw, and nothing consumed it (compaction is triggered by the "compact" return value). Recoverable provider errors elsewhere are silent: retried 429s never publish, and the proactive overflow check sets needsCompaction without an event. Move the announcement to where failure is actually decided: compaction's result === "compact" branch ("too large to compact even after stripping"), which already writes the durable error but published nothing — its error event previously arrived only as a side effect of the halt re-entering during summary generation. Net contract: session.error with a sessionID fires iff the outcome is affected. Tests pin both directions: 413 -> compact -> retry exits 0 with no error event; 413 -> compaction also overflows exits 1 with exactly one error event carrying the clearer compaction message. Both fail without the source change (verified by stashing it). The recovered-413 fixture is from #123 by Magnus, which fixed the same symptom consumer-side in run.ts; this supersedes it at the producer so run.ts stays identical to upstream and stream consumers need no clear-on-later-success logic.
1 parent 7f37345 commit f0752f0

3 files changed

Lines changed: 70 additions & 1 deletion

File tree

packages/opencode/src/session/compaction.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,13 +402,20 @@ const layer = Layer.effect(
402402
})
403403

404404
if (result === "compact") {
405+
// Recovery abandoned: this is where overflow becomes an outcome, so
406+
// the error event fires here rather than on the recoverable 413 in
407+
// processor.halt.
405408
processor.message.error = new SessionV1.ContextOverflowError({
406409
message: replay
407410
? "Conversation history too large to compact - exceeds model context limit"
408411
: "Session too large to compact - context exceeds model limit even after stripping media",
409412
}).toObject()
410413
processor.message.finish = "error"
411414
yield* session.updateMessage(processor.message)
415+
yield* events.publish(Session.Event.Error, {
416+
sessionID: input.sessionID,
417+
error: processor.message.error,
418+
})
412419
return "stop"
413420
}
414421

packages/opencode/src/session/processor.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,8 +638,13 @@ const layer = Layer.effect(
638638
yield* status.set(ctx.sessionID, { type: "idle" })
639639
return
640640
}
641+
// Recovery attempt, not an outcome: auto-compaction will retry, so
642+
// nothing is published here. The durable message carries no error
643+
// either; announcing one on the error channel made every consumer
644+
// (CLI exit code, TUI toast, orchestrators) fail runs that recover.
645+
// If compaction cannot shrink the session, compaction.ts publishes
646+
// the terminal error.
641647
ctx.needsCompaction = true
642-
yield* events.publish(Session.Event.Error, { sessionID: ctx.sessionID, error })
643648
return
644649
}
645650
ctx.assistantMessage.error = error

packages/opencode/test/cli/run/run-process.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,63 @@ describe("opencode run (non-interactive subprocess)", () => {
241241
60_000,
242242
)
243243

244+
cliIt.concurrent(
245+
"recovers from a provider size error via compaction without emitting an error event",
246+
({ llm, opencode }) =>
247+
Effect.gen(function* () {
248+
yield* llm.error(413, {
249+
error: { type: "request_too_large", message: "Request exceeds the maximum size" },
250+
})
251+
yield* llm.text("compacted history")
252+
yield* llm.text("recovered output")
253+
254+
const result = yield* opencode.run("recover after overflow", {
255+
format: "json",
256+
env: { OPENCODE_DISABLE_AUTOCOMPACT: "0" },
257+
})
258+
259+
opencode.expectExit(result, 0)
260+
const events = opencode.parseJsonEvents(result.stdout)
261+
expect(events.some((event) => event.type === "error")).toBe(false)
262+
expect(
263+
events.some(
264+
(event) =>
265+
event.type === "text" &&
266+
typeof event.part === "object" &&
267+
event.part !== null &&
268+
"text" in event.part &&
269+
event.part.text === "recovered output",
270+
),
271+
).toBe(true)
272+
}),
273+
60_000,
274+
)
275+
276+
cliIt.concurrent(
277+
"exits nonzero with an error event when compaction cannot shrink the session",
278+
({ llm, opencode }) =>
279+
Effect.gen(function* () {
280+
yield* llm.error(413, {
281+
error: { type: "request_too_large", message: "Request exceeds the maximum size" },
282+
})
283+
yield* llm.error(413, {
284+
error: { type: "request_too_large", message: "Request exceeds the maximum size" },
285+
})
286+
287+
const result = yield* opencode.run("overflow beyond recovery", {
288+
format: "json",
289+
env: { OPENCODE_DISABLE_AUTOCOMPACT: "0" },
290+
})
291+
292+
opencode.expectExit(result, 1)
293+
const events = opencode.parseJsonEvents(result.stdout)
294+
const errors = events.filter((event) => event.type === "error")
295+
expect(errors.length).toBe(1)
296+
expect(JSON.stringify(errors[0])).toContain("too large to compact")
297+
}),
298+
60_000,
299+
)
300+
244301
cliIt.concurrent(
245302
"rejects requested permissions by default and allows them with the dangerous flag",
246303
({ home, llm, opencode }) =>

0 commit comments

Comments
 (0)