Render error cause chains at every diagnostic seam
A TUI run against an unreachable endpoint failed with only 'fetch failed':
undici wraps transport failures in a bare TypeError whose diagnosis lives
on .cause, and every diagnostic seam rendered only error.message. The
readline front door additionally rendered failed turns as pure silence.
- dsh-llm: new errorChain(value) renders the full cause chain and
AggregateError members with circular/hostile-coercion containment.
- llm-deepseek: pre-response transport failures throw LlmError('NETWORK')
naming the endpoint and chaining the fetch TypeError; aborts keep their
DOMException so the loop still classifies them as cancellation.
- agent-loop: durable turn/end error messages and logger warnings render
through errorChain; local renderThrown copies removed.
- ui-stdio: failure turn/end reasons now render ([turn failed <code>],
[turn aborted], [turn rejected], output-token-limit); startup-failure
logs use errorChain.
- ui-tui: agent/error notices and the startup-failure line use errorChain.
This commit is contained in:
@@ -227,6 +227,41 @@ describe('createStdioChat rendering', () => {
|
||||
expect(out.text()).toContain('\n> ')
|
||||
})
|
||||
|
||||
it('renders failure turn/end reasons so a failed turn is not silent', async () => {
|
||||
const { ctx, out } = await setup()
|
||||
const session = makeSession('main')
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/end', seq: 1, time: 0,
|
||||
data: { turn: 1, reason: { kind: 'error', step: 1, message: 'fetch failed: connect ECONNREFUSED', code: 'NETWORK' } },
|
||||
} as SessionEvent)
|
||||
expect(out.text()).toContain('[turn failed NETWORK] fetch failed: connect ECONNREFUSED')
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/end', seq: 2, time: 0,
|
||||
data: { turn: 2, reason: { kind: 'error', step: 1, message: 'uncoded failure' } },
|
||||
} as SessionEvent)
|
||||
expect(out.text()).toContain('[turn failed] uncoded failure')
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/end', seq: 3, time: 0, data: { turn: 3, reason: { kind: 'aborted', reason: 'user cancelled' } },
|
||||
} as SessionEvent)
|
||||
expect(out.text()).toContain('[turn aborted] user cancelled')
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/end', seq: 4, time: 0, data: { turn: 4, reason: { kind: 'aborted' } },
|
||||
} as SessionEvent)
|
||||
expect(out.text()).toContain('[turn aborted]\n> ')
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/end', seq: 5, time: 0, data: { turn: 5, reason: { kind: 'rejected', reason: 'policy veto' } },
|
||||
} as SessionEvent)
|
||||
expect(out.text()).toContain('[turn rejected] policy veto')
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/end', seq: 6, time: 0, data: { turn: 6, reason: { kind: 'max-tokens' } },
|
||||
} as SessionEvent)
|
||||
expect(out.text()).toContain('[turn hit the output-token limit]')
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/end', seq: 7, time: 0, data: { turn: 7, reason: { kind: 'interrupted' } },
|
||||
} as SessionEvent)
|
||||
expect(out.text()).toContain('[turn interrupted by a previous process exit]')
|
||||
})
|
||||
|
||||
it('uses the session id as the label for a non-target session', async () => {
|
||||
const { ctx, out } = await setup()
|
||||
// No target exists, so the event's durable identity is the label.
|
||||
@@ -814,7 +849,7 @@ describe('createStdioChat input', () => {
|
||||
await new Promise(r => setImmediate(r))
|
||||
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
'ui-stdio: main agent failed to start; dropped queued stdin (1 line(s)): <unrenderable thrown value>',
|
||||
'ui-stdio: main agent failed to start; dropped queued stdin (1 line(s)): <unrenderable value>',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -902,7 +937,7 @@ describe('createStdioChat EOF exit', () => {
|
||||
await flushExit()
|
||||
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
'ui-stdio: main agent failed to start; dropped queued stdin (1 line(s)): <unrenderable thrown value>',
|
||||
'ui-stdio: main agent failed to start; dropped queued stdin (1 line(s)): <unrenderable value>',
|
||||
)
|
||||
expect(exit).toHaveBeenCalledWith(0)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user