From e5cfc696dae951717c44f614a2209b910670643b Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Thu, 28 May 2026 16:56:52 +1000 Subject: [PATCH] fix(anthropic): suppress stream-json log spam for system/* and user events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NDJSON event parser matched only `system/init` and treated every other system subtype as "unknown event type" — logged via console.error, once per event. During the 2026-05-28 OAuth-expiry 401 cascade this produced 2 spam lines per failed request (PI231 server log was ~50% noise). Also caught: claude echoes {type:'user'} events back in some stream-json modes (analogous to --replay-user-messages); these were also spammed as unknown. Both are now consumed silently — neither maps to an IR chunk. Authority: - empirical PI231 v2.1.104 transcripts 2026-05-28 (server log shows '[anthropic] unknown stream_json event type: system' and '[anthropic] unknown stream_json event type: user') - ADR 0009 Amendment 1 § "NDJSON event handling" — generic "future-proof for unknown events" intent; the table was incomplete Tests: 795 → 797 - 41e-5b: regression guard for system/ - 41e-5c: user (echo) event consumption Co-Authored-By: Claude Opus 4.7 --- lib/providers/anthropic.mjs | 15 +++++++++++++-- test-features.mjs | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) 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);