Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/dispatcher/EventDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
25 changes: 25 additions & 0 deletions src/dispatcher/sessionError.test.ts
Original file line number Diff line number Diff line change
@@ -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' })])
})
13 changes: 13 additions & 0 deletions testing/fixtures/session-error/usage-limit-1.18.31.json
Original file line number Diff line number Diff line change
@@ -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\"}}"
}
}
}
Loading