fix(session): type turn/end error as one structured failure

TurnEndReasonMap.error now carries a single `error: LlmFailure` field:
an LlmError keeps its structured facts, any other error flattens to
errorChain text under the UNKNOWN code. Consumers read message/code
directly instead of defending against an unknown union — this also fixes
errorChain() rendering structured failures as '[object Object]' in the
TUI and ACP error paths. Document the turn-stopping contract: a
concludesTurn result never short-circuits already-submitted next-step
work (same-step additionalContexts or racing steering still runs), data
decides.
This commit is contained in:
_Kerman
2026-08-03 16:33:23 +08:00
parent 8b975edf44
commit a597763393
30 changed files with 81 additions and 54 deletions

View File

@@ -340,7 +340,7 @@ describe('SQLite session search', () => {
{ type: 'user/message', seq: 2, time: 12, data: createUserMessage({
content: [{ type: 'text', text: 'needle summary' }], source: { kind: 'plugin', plugin: 'test' },
}), surfaceOp: { op: 'replace', start: 0, end: 0 }, sourceEventSeqs: [0] },
{ type: 'turn/end', seq: 3, time: 13, data: { turn: 1, step: 1, reason: { kind: 'error', error: 'needle failure' } } },
{ type: 'turn/end', seq: 3, time: 13, data: { turn: 1, step: 1, reason: { kind: 'error', error: { message: 'needle failure', code: 'UNKNOWN' } } } },
]
ctx.sessions.create(SessionId('a'), { seed: events, meta: { cwd: '/a', parentSession: parent, createdAt: 20 } })
ctx.sessions.create(SessionId('b'), { seed: messageEvents('needle peer', 12), meta: { createdAt: 20 } })

View File

@@ -44,7 +44,7 @@ export function extractSessionEventText(event: SessionEvent): string {
function turnEndText(reason: SessionEvent<'turn/end'>['data']['reason']): string {
switch (reason.kind) {
case 'error':
return joinText(['error', reason.error instanceof Error ? reason.error.message : String(reason.error)])
return joinText(['error', reason.error.message])
case 'aborted':
return 'aborted'
case 'max-tokens':

View File

@@ -119,8 +119,8 @@ describe('session-query semantic extraction', () => {
it('extracts meaningful turn outcomes and skips structural or unknown events', () => {
const reasons: Array<[SessionEvent<'turn/end'>['data']['reason'], string]> = [
[{ kind: 'error', error: new Error('boom') }, 'error\nboom'],
[{ kind: 'error', error: 'provider boom' }, 'error\nprovider boom'],
[{ kind: 'error', error: { message: 'boom', code: 'UNKNOWN' } }, 'error\nboom'],
[{ kind: 'error', error: { message: 'provider boom', code: 'UNKNOWN' } }, 'error\nprovider boom'],
[{ kind: 'aborted', reason: { kind: 'user' } }, 'aborted'],
[{ kind: 'aborted', reason: { kind: 'disposed' } }, 'aborted'],
[{ kind: 'max-tokens' }, 'max-tokens'],