fix(cli): forward Claude API error records with null service_tier (#1857) (#2087)

Claude Code writes failed API calls ("Prompt is too long", usage limits,
logged out) as synthetic assistant records whose usage carries
`service_tier: null`. The schema only accepted undefined, so the local
session scanner dropped every one and the web session just looked stalled.

- Accept null for the usage fields the Messages API types as nullable.
- Keep the top-level `error`/`isApiErrorMessage` on assistant records in
  local mode, and `error` in the SDK converter for remote mode.
- Render assistant records carrying `error` as the existing error event,
  so the all-zero synthetic usage no longer resets the context gauge.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SSU-WEI HUANG
2026-10-26 10:00:23 +08:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 6bf0a633bb
commit d3473f9ed4
6 changed files with 123 additions and 4 deletions
+37
View File
@@ -29,4 +29,41 @@ describe('normalizeAgentRecord — agent error payloads', () => {
}
})
})
it('normalizes Claude API error assistant records into error events without usage (#1857)', () => {
const normalized = normalizeAgentRecord(
'msg-2',
null,
1_700_000_000_000,
{
type: 'output',
data: {
type: 'assistant',
uuid: 'a1',
isSidechain: false,
isApiErrorMessage: true,
error: 'invalid_request',
message: {
model: '<synthetic>',
role: 'assistant',
content: [{ type: 'text', text: 'Prompt is too long' }],
usage: { input_tokens: 0, output_tokens: 0, service_tier: null }
}
}
}
)
expect(normalized).toEqual({
id: 'msg-2',
localId: null,
createdAt: 1_700_000_000_000,
role: 'event',
isSidechain: false,
parentToolUseId: null,
content: {
type: 'error',
message: 'Prompt is too long'
}
})
})
})
+23
View File
@@ -733,6 +733,29 @@ export function normalizeAgentRecord(
if (data.isCompactSummary) return null
if (!isClaudeChatVisibleMessage({ type: data.type, subtype: data.subtype })) return null
// Claude Code reports a failed API call ("Prompt is too long", usage
// limits, logged out) as a synthetic assistant message with a top-level
// `error`. Render it as an error, and keep its all-zero usage out of
// the context gauge.
if (data.type === 'assistant' && typeof data.error === 'string') {
const message = isObject(data.message) ? data.message : null
const text = Array.isArray(message?.content)
? message.content
.flatMap((block) => isObject(block) && typeof block.text === 'string' ? [block.text] : [])
.join('\n')
: asString(message?.content)
return {
id: messageId,
localId,
createdAt,
role: 'event',
content: { type: 'error', message: text?.trim() || data.error },
isSidechain: Boolean(data.isSidechain),
parentToolUseId: asString(data.parentToolUseId) ?? null,
meta
}
}
if (data.type === 'assistant') {
return normalizeAssistantOutput(messageId, localId, createdAt, data, meta)
}