diff --git a/lib/providers/anthropic.mjs b/lib/providers/anthropic.mjs index 8ef45a7..eef0711 100644 --- a/lib/providers/anthropic.mjs +++ b/lib/providers/anthropic.mjs @@ -703,8 +703,19 @@ export function parseStreamJsonLines(buffered) { export function anthropicStreamJsonEventToIR(event, isFirstDelta = false) { const t = event?.type; - // system/init — first event always; consumed (no yield) - if (t === 'system' && event.subtype === 'init') { + // system/* — first-event init plus any other system meta (api_retry, etc.) + // emitted during error paths. None map to IR chunks; all consumed silently. + // Originally we matched only `subtype === 'init'`, which caused noisy + // "unknown stream_json event type: system" logs during 401 cascades when + // claude emits other system subtypes. (Observed PI231 2026-05-28.) + if (t === 'system') { + return null; + } + + // user — claude echoes the user-message back as a confirmation event in + // some stream-json modes (analogous to --replay-user-messages). Always + // consumed; never an IR chunk. + if (t === 'user') { return null; } diff --git a/test-features.mjs b/test-features.mjs index a377fe2..c227f59 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -17581,6 +17581,26 @@ describe('Suite 41 — ADR 0009 Amendment 1: stream-json transport (Phase 6)', ( assert.equal(chunk, null, 'system/init must return null (consumed, no yield)'); }); + it('41e-5b: anthropicStreamJsonEventToIR — system/ → null (regression guard)', () => { + // 2026-05-28: 401 cascade transcripts on PI231 showed claude emitting + // system events with subtypes other than 'init' (e.g. api_retry). The + // original parser matched only subtype==='init' and routed everything + // else to the "unknown event type" log, spamming the server log. + for (const subtype of ['api_retry', 'shutdown', undefined]) { + const event = { type: 'system', subtype, note: 'whatever' }; + const chunk = anthropicStreamJsonEventToIR(event, false); + assert.equal(chunk, null, `system/${subtype} must return null (consumed, no IR yield)`); + } + }); + + it('41e-5c: anthropicStreamJsonEventToIR — user (echoed user-message) → null', () => { + // claude emits {type:'user'} echo events in some stream-json modes + // (analogous to --replay-user-messages). Must be consumed, never yielded. + const event = { type: 'user', message: { role: 'user', content: 'hi' } }; + const chunk = anthropicStreamJsonEventToIR(event, false); + assert.equal(chunk, null, 'user echo events must return null'); + }); + it('41e-6: anthropicStreamJsonEventToIR — rate_limit_event → null (consumed)', () => { const event = { type: 'rate_limit_event', rate_limit_info: { status: 'allowed' } }; const chunk = anthropicStreamJsonEventToIR(event, false);