diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index fa439e4eff..e61d1d1057 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -402,6 +402,9 @@ const layer = Layer.effect( }) if (result === "compact") { + // Recovery abandoned: this is where overflow becomes an outcome, so + // the error event fires here rather than on the recoverable 413 in + // processor.halt. processor.message.error = new SessionV1.ContextOverflowError({ message: replay ? "Conversation history too large to compact - exceeds model context limit" @@ -409,6 +412,10 @@ const layer = Layer.effect( }).toObject() processor.message.finish = "error" yield* session.updateMessage(processor.message) + yield* events.publish(Session.Event.Error, { + sessionID: input.sessionID, + error: processor.message.error, + }) return "stop" } diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index af31be7e15..1bdc820b59 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -638,8 +638,13 @@ const layer = Layer.effect( yield* status.set(ctx.sessionID, { type: "idle" }) return } + // Recovery attempt, not an outcome: auto-compaction will retry, so + // nothing is published here. The durable message carries no error + // either; announcing one on the error channel made every consumer + // (CLI exit code, TUI toast, orchestrators) fail runs that recover. + // If compaction cannot shrink the session, compaction.ts publishes + // the terminal error. ctx.needsCompaction = true - yield* events.publish(Session.Event.Error, { sessionID: ctx.sessionID, error }) return } ctx.assistantMessage.error = error diff --git a/packages/opencode/test/cli/run/run-process.test.ts b/packages/opencode/test/cli/run/run-process.test.ts index bd5847e272..b3697922fe 100644 --- a/packages/opencode/test/cli/run/run-process.test.ts +++ b/packages/opencode/test/cli/run/run-process.test.ts @@ -241,6 +241,63 @@ describe("opencode run (non-interactive subprocess)", () => { 60_000, ) + cliIt.concurrent( + "recovers from a provider size error via compaction without emitting an error event", + ({ llm, opencode }) => + Effect.gen(function* () { + yield* llm.error(413, { + error: { type: "request_too_large", message: "Request exceeds the maximum size" }, + }) + yield* llm.text("compacted history") + yield* llm.text("recovered output") + + const result = yield* opencode.run("recover after overflow", { + format: "json", + env: { OPENCODE_DISABLE_AUTOCOMPACT: "0" }, + }) + + opencode.expectExit(result, 0) + const events = opencode.parseJsonEvents(result.stdout) + expect(events.some((event) => event.type === "error")).toBe(false) + expect( + events.some( + (event) => + event.type === "text" && + typeof event.part === "object" && + event.part !== null && + "text" in event.part && + event.part.text === "recovered output", + ), + ).toBe(true) + }), + 60_000, + ) + + cliIt.concurrent( + "exits nonzero with an error event when compaction cannot shrink the session", + ({ llm, opencode }) => + Effect.gen(function* () { + yield* llm.error(413, { + error: { type: "request_too_large", message: "Request exceeds the maximum size" }, + }) + yield* llm.error(413, { + error: { type: "request_too_large", message: "Request exceeds the maximum size" }, + }) + + const result = yield* opencode.run("overflow beyond recovery", { + format: "json", + env: { OPENCODE_DISABLE_AUTOCOMPACT: "0" }, + }) + + opencode.expectExit(result, 1) + const events = opencode.parseJsonEvents(result.stdout) + const errors = events.filter((event) => event.type === "error") + expect(errors.length).toBe(1) + expect(JSON.stringify(errors[0])).toContain("too large to compact") + }), + 60_000, + ) + cliIt.concurrent( "rejects requested permissions by default and allows them with the dangerous flag", ({ home, llm, opencode }) =>