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.
This commit is contained in:
Mohamed Boudra
2026-05-25 17:11:44 +08:00
committed by GitHub
parent 132e572d6e
commit a91dfb63c6
2 changed files with 79 additions and 8 deletions
@@ -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<OpenCodeEvent, { type: "session.error" }>,
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<OpenCodeEvent, { type: "session.status" }>,
state: OpenCodeEventTranslationState,
@@ -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"}}',
},
]);
});
});