From c1e7fb58c528d52fd4a0918d4a3d7319d49bae21 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Sat, 19 Sep 2026 01:58:56 -0700 Subject: [PATCH] fix(dispatcher): a session error reports the provider's text from error.data.message Agent Code #1018: an orchestration child's usage-limit failure reached the parent as "OpenCode session error". OpenCode 1.18 publishes session.error as { sessionID, error: { name, data: { message, statusCode, ... } } }, the shape of the assistant row's error in its database, and the lookup read only message and error.message. It now reads error.data.message first, then error.message, then error.name, the same order the Terminal runtime uses. The fail-first test uses a real error from an OpenCode 1.18.31 database row. Co-Authored-By: Claude Opus 5 (1M context) --- src/dispatcher/EventDispatcher.ts | 9 ++++++- src/dispatcher/sessionError.test.ts | 25 +++++++++++++++++++ .../session-error/usage-limit-1.18.31.json | 13 ++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/dispatcher/sessionError.test.ts create mode 100644 testing/fixtures/session-error/usage-limit-1.18.31.json diff --git a/src/dispatcher/EventDispatcher.ts b/src/dispatcher/EventDispatcher.ts index a67f784..385f9db 100644 --- a/src/dispatcher/EventDispatcher.ts +++ b/src/dispatcher/EventDispatcher.ts @@ -290,7 +290,14 @@ export class EventDispatcher { this.semantic.publish({ type: 'api_error', turnId: this.turns.getActiveTurnId(), - message: getString(payload, ['message', 'error.message']) ?? 'OpenCode session error', + // `error.data.message` first (Agent Code #1018): OpenCode 1.18 + // publishes session.error as { sessionID, error: { name, data: { + // message, statusCode, ... } } }, the same shape as the assistant + // row's error in its database. The old lookup found nothing there, + // so every failure, a usage limit included, read "OpenCode session + // error". The Terminal runtime's LiveStateProjector already reads it + // this way. + message: getString(payload, ['message', 'error.data.message', 'error.message', 'error.name']) ?? 'OpenCode session error', error: payload, source: 'opencode-sse', ts: Date.now(), diff --git a/src/dispatcher/sessionError.test.ts b/src/dispatcher/sessionError.test.ts new file mode 100644 index 0000000..d15e2b6 --- /dev/null +++ b/src/dispatcher/sessionError.test.ts @@ -0,0 +1,25 @@ +import { readFileSync } from 'node:fs' +import { expect, it } from 'vitest' + +import { CommittedChannel } from '../channels/CommittedChannel.js' +import { ScreenChannel } from '../channels/ScreenChannel.js' +import { SemanticChannel } from '../channels/SemanticChannel.js' +import { EventDispatcher } from './EventDispatcher.js' + +// Agent Code #1018: an orchestration child's usage-limit failure reached the +// parent as "OpenCode session error". The error is the REAL one from an +// OpenCode 1.18.31 database row (testing/fixtures/session-error), and +// session.error carries it as `error` beside the sessionID. +const recorded = JSON.parse(readFileSync(new URL('../../testing/fixtures/session-error/usage-limit-1.18.31.json', import.meta.url), 'utf8')) as { + sessionID: string + error: { name: string; data: { message: string } } +} + +it('reports the provider\'s own text for a session error, not the generic fallback', () => { + const semantic = new SemanticChannel() + const errors: Array<{ message: string }> = [] + semantic.on('api_error', event => errors.push(event as { message: string })) + const dispatcher = new EventDispatcher({ semantic, screen: new ScreenChannel(), committed: new CommittedChannel(), sessionID: recorded.sessionID }) + dispatcher.dispatch({ type: 'session.error', properties: { sessionID: recorded.sessionID, error: recorded.error } }) + expect(errors).toEqual([expect.objectContaining({ message: 'Usage limit reached for 5 hour. Your limit will reset at 2026-09-19 14:14:24' })]) +}) diff --git a/testing/fixtures/session-error/usage-limit-1.18.31.json b/testing/fixtures/session-error/usage-limit-1.18.31.json new file mode 100644 index 0000000..2d39fdd --- /dev/null +++ b/testing/fixtures/session-error/usage-limit-1.18.31.json @@ -0,0 +1,13 @@ +{ + "provenance": "Agent Code testing/fixtures/orchestration-api-error/opencode-structured-usage-limit-terminal.json (OpenCode 1.18.31 database, 2026-09-19): the assistant row error of a zai usage-limit failure. session.error publishes {sessionID, error: assistantMessage.error}, so this is the payload's error field verbatim (response headers dropped).", + "sessionID": "ses_f47fca718ffePibDCd10YM7Jc8", + "error": { + "name": "APIError", + "data": { + "message": "Usage limit reached for 5 hour. Your limit will reset at 2026-09-19 14:14:24", + "statusCode": 429, + "isRetryable": true, + "responseBody": "{\"error\":{\"code\":\"1308\",\"message\":\"Usage limit reached for 5 hour. Your limit will reset at 2026-09-19 14:14:24\"}}" + } + } +} \ No newline at end of file