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