A turn that OpenCode stopped could end with nothing on screen: the session.error event was only turned into a sidebar badge, its message was dropped (the notification expected a different shape than OpenCode sends), and a send that was accepted but never answered looked the same as success. - The chat shows what OpenCode reported under the last message while that turn is the latest one, and names a user message an idle session has left unanswered for five seconds. - The last 20 session errors are kept in memory and listed in the status report (Ctrl/Cmd+Shift+L, also `__opencodeDebug.statusReport()`), next to rejected sends, the managed OpenCode process's last error and stderr tail, and the OpenCode and desktop log file locations. - The OpenCode health probe hits /global/health instead of a route that does not exist, and probe URLs resolve against the page for web runtimes.
60 lines
2.4 KiB
TypeScript
60 lines
2.4 KiB
TypeScript
/**
|
|
* Recent OpenCode session errors, kept in memory for diagnostics.
|
|
*
|
|
* OpenCode reports a failed turn as a `session.error` event. The message it
|
|
* carries is the only account of what went wrong, and it may arrive without
|
|
* an assistant message to attach itself to, so a turn can end with nothing
|
|
* on screen. This buffer keeps the last errors until someone asks for them,
|
|
* via the status report (Ctrl/Cmd+Shift+L) or `__opencodeDebug`. In-memory
|
|
* only: never persisted, never sent anywhere, dropped on reload.
|
|
*/
|
|
|
|
import type { EventSessionError } from '@opencode-ai/sdk/v2'
|
|
|
|
const MAX_RECORDED_SESSION_ERRORS = 20
|
|
const MAX_MESSAGE_LENGTH = 400
|
|
|
|
export type OpenCodeErrorSummary = {
|
|
name: string | null
|
|
message: string | null
|
|
}
|
|
|
|
export type SessionErrorRecord = OpenCodeErrorSummary & {
|
|
at: number
|
|
sessionId: string
|
|
directory: string | null
|
|
}
|
|
|
|
/**
|
|
* OpenCode error payloads are `{ name, data: { message, ... } }`; older or
|
|
* foreign shapes carry `message` at the top. Returns nulls for anything
|
|
* else so a caller can tell "no details" from a real message.
|
|
*/
|
|
export type OpenCodeSessionErrorPayload = EventSessionError['properties']['error']
|
|
|
|
export function summarizeOpenCodeError(error: OpenCodeSessionErrorPayload | { message?: string } | null | undefined): OpenCodeErrorSummary {
|
|
if (!error || typeof error !== 'object') return { name: null, message: null }
|
|
// SAFETY: the SDK union is `{ name, data: { message } }` per variant; a
|
|
// top-level `message` covers foreign shapes. Every field is checked before use.
|
|
const record = error as { name?: unknown; message?: unknown; data?: { message?: unknown } }
|
|
const name = typeof record.name === 'string' && record.name.trim() ? record.name.trim() : null
|
|
const dataMessage = typeof record.data?.message === 'string' ? record.data.message.trim() : ''
|
|
const topMessage = typeof record.message === 'string' ? record.message.trim() : ''
|
|
const message = dataMessage || topMessage || null
|
|
return { name, message: message ? message.slice(0, MAX_MESSAGE_LENGTH) : null }
|
|
}
|
|
|
|
const records: SessionErrorRecord[] = []
|
|
|
|
export function recordSessionError(record: Omit<SessionErrorRecord, 'at'>): void {
|
|
records.push({ ...record, at: Date.now() })
|
|
if (records.length > MAX_RECORDED_SESSION_ERRORS) {
|
|
records.splice(0, records.length - MAX_RECORDED_SESSION_ERRORS)
|
|
}
|
|
}
|
|
|
|
/** Newest first. */
|
|
export function getRecentSessionErrors(): SessionErrorRecord[] {
|
|
return [...records].reverse()
|
|
}
|