diff --git a/scripts/hooks/ecc-metrics-bridge.js b/scripts/hooks/ecc-metrics-bridge.js index ebb6e394..ccc6307e 100644 --- a/scripts/hooks/ecc-metrics-bridge.js +++ b/scripts/hooks/ecc-metrics-bridge.js @@ -19,6 +19,7 @@ const MAX_STDIN = 1024 * 1024; const MAX_FILES_TRACKED = 200; const RECENT_TOOLS_SIZE = 5; const HASH_INPUT_LIMIT = 2048; +const costWarningSignatures = new Map(); function toNumber(value) { const n = Number(value); @@ -76,6 +77,13 @@ function extractFilePaths(toolName, toolInput) { return paths; } +function writeCostWarningOnce(kind, costsPath, signature, message) { + const key = `${kind}:${costsPath}`; + if (costWarningSignatures.get(key) === signature) return; + costWarningSignatures.set(key, signature); + process.stderr.write(message); +} + /** * Read cumulative cost for a session from costs.jsonl. * @@ -117,8 +125,15 @@ function readSessionCost(sessionId) { } // One aggregated breadcrumb per call rather than one per bad row, so a // log-flooded costs.jsonl stays diagnosable without overwhelming stderr. + // Suppress repeats for the same malformed-line count; this hook runs after + // every tool invocation, so a persistent bad row should not spam stderr. if (malformed > 0) { - process.stderr.write(`[ecc-metrics-bridge] skipped ${malformed} malformed line(s) in ${costsPath}\n`); + writeCostWarningOnce( + 'malformed', + costsPath, + String(malformed), + `[ecc-metrics-bridge] skipped ${malformed} malformed line(s) in ${costsPath}\n` + ); } return { totalCost, totalIn, totalOut }; } catch (err) { @@ -127,7 +142,12 @@ function readSessionCost(sessionId) { // (permission, EISDIR, malformed read) deserves a breadcrumb because // the bridge will silently report zero cost otherwise. if (err && err.code !== 'ENOENT') { - process.stderr.write(`[ecc-metrics-bridge] failing open after ${err.name || 'error'} reading ${costsPath}: ${err.message || String(err)}\n`); + writeCostWarningOnce( + 'read-error', + costsPath, + err.code || err.name || 'error', + `[ecc-metrics-bridge] failing open after ${err.name || 'error'} reading ${costsPath}: ${err.message || String(err)}\n` + ); } return { totalCost: 0, totalIn: 0, totalOut: 0 }; } diff --git a/tests/hooks/ecc-metrics-bridge.test.js b/tests/hooks/ecc-metrics-bridge.test.js index 241022ff..d1b40b72 100644 --- a/tests/hooks/ecc-metrics-bridge.test.js +++ b/tests/hooks/ecc-metrics-bridge.test.js @@ -226,11 +226,13 @@ function runTests() { else failed++; if ( - test('readSessionCost writes a stderr breadcrumb when malformed lines are skipped', () => { + test('readSessionCost writes one stderr breadcrumb when malformed lines persist across calls', () => { // Reviewer (coderabbitai) asked for diagnosability when the inner // catch silently skips malformed JSON rows. Verify the aggregated // "skipped N malformed line(s)" breadcrumb appears on stderr while - // the function still recovers the last valid matching row. + // the function still recovers the last valid matching row. Because + // this hook runs after every tool invocation, the same bad rows should + // not emit the same warning on every call. const tmpHome = makeTempHome(); const originalHome = process.env.HOME; const originalUserProfile = process.env.USERPROFILE; @@ -257,8 +259,11 @@ function runTests() { ); const result = readSessionCost('S1'); assert.strictEqual(result.totalCost, 0.7, 'last valid row should still win'); - assert.ok(/skipped 2 malformed line\(s\)/.test(captured), - `expected aggregated malformed-line breadcrumb on stderr, got: ${captured}`); + const secondResult = readSessionCost('S1'); + assert.deepStrictEqual(secondResult, result); + const matches = captured.match(/skipped 2 malformed line\(s\)/g) || []; + assert.strictEqual(matches.length, 1, + `expected one aggregated malformed-line breadcrumb on stderr, got: ${captured}`); } finally { process.stderr.write = originalStderrWrite; if (originalHome === undefined) delete process.env.HOME;