From a91dfb63c64658e03d7d4539fdded6463aa7201a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 25 May 2026 17:11:44 +0800 Subject: [PATCH] Treat OpenCode abort as cancellation, not error (#1169) OpenCode emits session.error with MessageAbortedError when the user interrupts a running turn. Paseo was mapping every session.error to turn_failed, which set the agent lifecycle to error and appended a raw JSON system error message to the timeline. Now MessageAbortedError translates to turn_canceled (same as an explicit cancel), so the agent returns to idle with no error state. Real errors still produce turn_failed. --- .../server/agent/providers/opencode-agent.ts | 39 +++++++++++---- .../opencode/event-translator.test.ts | 48 +++++++++++++++++++ 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index 2ef9b9e2a..34b7edd8a 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -1691,14 +1691,7 @@ export function translateOpenCodeEvent( } break; case "session.error": - if (event.properties.sessionID === state.sessionId) { - resetOpenCodeTurnTrackingState(state); - events.push({ - type: "turn_failed", - provider: "opencode", - error: toDiagnosticErrorMessage(event.properties.error), - }); - } + appendOpenCodeSessionError(event, state, events); break; case "session.status": appendOpenCodeSessionStatus(event, state, events); @@ -2294,6 +2287,36 @@ function appendOpenCodeQuestionAsked( }); } +function appendOpenCodeSessionError( + event: Extract, + state: OpenCodeEventTranslationState, + events: AgentStreamEvent[], +): void { + if (event.properties.sessionID !== state.sessionId) { + return; + } + resetOpenCodeTurnTrackingState(state); + const error = event.properties.error; + if ( + typeof error === "object" && + error !== null && + "name" in error && + error.name === "MessageAbortedError" + ) { + events.push({ + type: "turn_canceled", + provider: "opencode", + reason: "interrupted", + }); + } else { + events.push({ + type: "turn_failed", + provider: "opencode", + error: toDiagnosticErrorMessage(error), + }); + } +} + function appendOpenCodeSessionStatus( event: Extract, state: OpenCodeEventTranslationState, diff --git a/packages/server/src/server/agent/providers/opencode/event-translator.test.ts b/packages/server/src/server/agent/providers/opencode/event-translator.test.ts index 548a40f44..5dd55251c 100644 --- a/packages/server/src/server/agent/providers/opencode/event-translator.test.ts +++ b/packages/server/src/server/agent/providers/opencode/event-translator.test.ts @@ -924,4 +924,52 @@ describe("translateOpenCodeEvent", () => { ]); expect(second).toEqual([]); }); + + it("translates session.error with MessageAbortedError as turn_canceled, not turn_failed", () => { + const state = createState(); + + const events = translateOpenCodeEvent( + { + type: "session.error", + properties: { + sessionID: "session-1", + error: { + name: "MessageAbortedError", + data: { message: "aborted" }, + }, + }, + }, + state, + ); + + expect(events).toEqual([ + { type: "turn_canceled", provider: "opencode", reason: "interrupted" }, + ]); + }); + + it("translates session.error with a real error as turn_failed", () => { + const state = createState(); + + const events = translateOpenCodeEvent( + { + type: "session.error", + properties: { + sessionID: "session-1", + error: { + name: "UnknownError", + data: { message: "something broke" }, + }, + }, + }, + state, + ); + + expect(events).toEqual([ + { + type: "turn_failed", + provider: "opencode", + error: '{"name":"UnknownError","data":{"message":"something broke"}}', + }, + ]); + }); });