From 4c2659666b0d689079128e15a299a01f6f9617ce Mon Sep 17 00:00:00 2001 From: lojasetetoco <299766123+lojasetetoco@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:53:34 -0300 Subject: [PATCH 01/11] fix(hooks): update cost-tracker pricing table and filter harness noise from session summaries cost-tracker.js: RATE_TABLE priced all Opus models at the legacy $15/$75 tier and routed Fable/Mythos 5 to Sonnet rates, overstating Opus 5 sessions ~3x and understating Fable ~3.3x in costs.jsonl. Adds fable ($10/$50) and current opus ($5/$25) tiers, keeps Opus 4.0/4.1/3 on the legacy tier, updates haiku to 4.5 pricing ($1/$5). session-end.js: extractSessionSummary included local-command echoes (, , ), system reminders, tool_result carrier turns and isMeta entries in the Tasks list, so SessionStart reloaded noise instead of user asks. Adds a noise filter. Both test suites pass (10/10 cost-tracker, 1/1 session-end). Co-Authored-By: Claude Fable 5 --- scripts/hooks/cost-tracker.js | 13 ++++++++++--- scripts/hooks/session-end.js | 6 +++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/scripts/hooks/cost-tracker.js b/scripts/hooks/cost-tracker.js index 3de1eaaec..fa3677e4f 100755 --- a/scripts/hooks/cost-tracker.js +++ b/scripts/hooks/cost-tracker.js @@ -70,15 +70,22 @@ function readHarnessCost(sessionId, maxAgeSeconds) { // Approximate per-1M-token billing rates (USD). // Cache creation: 1.25x input rate. Cache read: 0.1x input rate. +// Current-generation list prices: Fable/Mythos 5 $10/$50, Opus 5 and +// Opus 4.5-4.8 $5/$25, Sonnet 5/4.6 $3/$15, Haiku 4.5 $1/$5. Opus 4.0/4.1 +// and Opus 3 stay on the legacy $15/$75 tier. const RATE_TABLE = { - haiku: { in: 0.80, out: 4.0, cacheWrite: 1.00, cacheRead: 0.08 }, - sonnet: { in: 3.00, out: 15.0, cacheWrite: 3.75, cacheRead: 0.30 }, - opus: { in: 15.00, out: 75.0, cacheWrite: 18.75, cacheRead: 1.50 } + haiku: { in: 1.00, out: 5.0, cacheWrite: 1.25, cacheRead: 0.10 }, + sonnet: { in: 3.00, out: 15.0, cacheWrite: 3.75, cacheRead: 0.30 }, + opus: { in: 5.00, out: 25.0, cacheWrite: 6.25, cacheRead: 0.50 }, + opusLegacy: { in: 15.00, out: 75.0, cacheWrite: 18.75, cacheRead: 1.50 }, + fable: { in: 10.00, out: 50.0, cacheWrite: 12.50, cacheRead: 1.00 } }; function getRates(model) { const m = String(model || '').toLowerCase(); + if (m.includes('fable') || m.includes('mythos')) return RATE_TABLE.fable; if (m.includes('haiku')) return RATE_TABLE.haiku; + if (m.includes('opus-4-1') || m.includes('opus-4-0') || m.includes('3-opus')) return RATE_TABLE.opusLegacy; if (m.includes('opus')) return RATE_TABLE.opus; return RATE_TABLE.sonnet; } diff --git a/scripts/hooks/session-end.js b/scripts/hooks/session-end.js index c224371aa..60ba74720 100644 --- a/scripts/hooks/session-end.js +++ b/scripts/hooks/session-end.js @@ -43,9 +43,13 @@ function extractSessionSummary(transcriptPath) { if (entry.type === 'user' || entry.role === 'user' || entry.message?.role === 'user') { // Support both direct content and nested message.content (Claude Code JSONL format) const rawContent = entry.message?.content ?? entry.content; + // Skip tool_result carrier turns — they are not user asks. + const isToolResult = Array.isArray(rawContent) && rawContent.some(c => c && c.type === 'tool_result'); const text = typeof rawContent === 'string' ? rawContent : Array.isArray(rawContent) ? rawContent.map(c => (c && c.text) || '').join(' ') : ''; const cleaned = stripAnsi(text).trim(); - if (cleaned) { + // Skip harness noise: local command echoes, caveats, system reminders. + const isNoise = /^<(local-command-caveat|local-command-stdout|command-name|command-message|command-args|system-reminder|task-notification)/i.test(cleaned); + if (cleaned && !isToolResult && !isNoise && !entry.isMeta) { userMessages.push(cleaned.slice(0, 200)); } } From 08c1c4073cf608fe5d00d36149d9f50470322053 Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Thu, 13 Aug 2026 01:18:08 +0000 Subject: [PATCH 02/11] fix(lib): remove unused cost-estimate.js duplicate rate table cost-estimate.js carries its own copy of the stale Opus/Haiku/Sonnet rate table already reported in #2574, but grepping every .js/.json/.md file outside node_modules turns up zero callers besides its own test. It was added in 940135e alongside the statusline observability hooks and never wired into any of them. The maintainer's comment on #2656 named two acceptable outcomes: remove the unused duplicate, or share one rate source with the live tracker. cost-tracker.js's own fix (#2574) has not landed yet, so sharing its table now would import numbers that are still wrong. Removing the dead file is the smaller, immediately-correct step. Fixes #2656 --- scripts/lib/cost-estimate.js | 32 --------- tests/lib/cost-estimate.test.js | 114 -------------------------------- 2 files changed, 146 deletions(-) delete mode 100644 scripts/lib/cost-estimate.js delete mode 100644 tests/lib/cost-estimate.test.js diff --git a/scripts/lib/cost-estimate.js b/scripts/lib/cost-estimate.js deleted file mode 100644 index a1651a8c9..000000000 --- a/scripts/lib/cost-estimate.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -/** - * Shared cost estimation for ECC hooks. - * - * Approximate per-1M-token blended rates (conservative defaults). - */ - -const RATE_TABLE = { - haiku: { in: 0.8, out: 4.0 }, - sonnet: { in: 3.0, out: 15.0 }, - opus: { in: 15.0, out: 75.0 } -}; - -/** - * Estimate USD cost from token counts. - * @param {string} model - Model name (may contain "haiku", "sonnet", or "opus") - * @param {number} inputTokens - * @param {number} outputTokens - * @returns {number} Estimated cost in USD (rounded to 6 decimal places) - */ -function estimateCost(model, inputTokens, outputTokens) { - const normalized = String(model || '').toLowerCase(); - let rates = RATE_TABLE.sonnet; - if (normalized.includes('haiku')) rates = RATE_TABLE.haiku; - if (normalized.includes('opus')) rates = RATE_TABLE.opus; - - const cost = (inputTokens / 1_000_000) * rates.in + (outputTokens / 1_000_000) * rates.out; - return Math.round(cost * 1e6) / 1e6; -} - -module.exports = { estimateCost, RATE_TABLE }; diff --git a/tests/lib/cost-estimate.test.js b/tests/lib/cost-estimate.test.js deleted file mode 100644 index bcb5906bc..000000000 --- a/tests/lib/cost-estimate.test.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Tests for scripts/lib/cost-estimate.js - * - * Run with: node tests/lib/cost-estimate.test.js - */ - -const assert = require('assert'); - -const { estimateCost, RATE_TABLE } = require('../../scripts/lib/cost-estimate'); - -// Test helper -function test(name, fn) { - try { - fn(); - console.log(` \u2713 ${name}`); - return true; - } catch (err) { - console.log(` \u2717 ${name}`); - console.log(` Error: ${err.message}`); - return false; - } -} - -function runTests() { - console.log('\n=== Testing cost-estimate.js ===\n'); - - let passed = 0; - let failed = 0; - - // RATE_TABLE structure - console.log('RATE_TABLE:'); - - if ( - test('RATE_TABLE has haiku, sonnet, opus keys', () => { - assert.ok(RATE_TABLE.haiku, 'Missing haiku'); - assert.ok(RATE_TABLE.sonnet, 'Missing sonnet'); - assert.ok(RATE_TABLE.opus, 'Missing opus'); - assert.strictEqual(typeof RATE_TABLE.haiku.in, 'number'); - assert.strictEqual(typeof RATE_TABLE.haiku.out, 'number'); - assert.strictEqual(typeof RATE_TABLE.sonnet.in, 'number'); - assert.strictEqual(typeof RATE_TABLE.sonnet.out, 'number'); - assert.strictEqual(typeof RATE_TABLE.opus.in, 'number'); - assert.strictEqual(typeof RATE_TABLE.opus.out, 'number'); - }) - ) - passed++; - else failed++; - - // estimateCost tests - console.log('\nestimateCost:'); - - if ( - test('opus 1M/1M tokens returns 90', () => { - const cost = estimateCost('opus', 1_000_000, 1_000_000); - assert.strictEqual(cost, 90); - }) - ) - passed++; - else failed++; - - if ( - test('sonnet 1M/1M tokens returns 18', () => { - const cost = estimateCost('sonnet', 1_000_000, 1_000_000); - assert.strictEqual(cost, 18); - }) - ) - passed++; - else failed++; - - if ( - test('haiku 1M/1M tokens returns 4.8', () => { - const cost = estimateCost('haiku', 1_000_000, 1_000_000); - assert.strictEqual(cost, 4.8); - }) - ) - passed++; - else failed++; - - if ( - test('null model with 0 tokens returns 0', () => { - const cost = estimateCost(null, 0, 0); - assert.strictEqual(cost, 0); - }) - ) - passed++; - else failed++; - - if ( - test('full model name claude-opus-4-6 uses opus rates', () => { - const cost = estimateCost('claude-opus-4-6', 500, 200); - // (500 / 1_000_000) * 15 + (200 / 1_000_000) * 75 = 0.0075 + 0.015 = 0.0225 - const expected = Math.round(0.0225 * 1e6) / 1e6; - assert.strictEqual(cost, expected); - }) - ) - passed++; - else failed++; - - if ( - test('unknown model falls back to sonnet rates', () => { - const cost = estimateCost('unknown-model', 1_000_000, 1_000_000); - assert.strictEqual(cost, 18); - }) - ) - passed++; - else failed++; - - // Summary - console.log(`\nResults: ${passed} passed, ${failed} failed\n`); - return { passed, failed }; -} - -const { failed } = runTests(); -process.exit(failed > 0 ? 1 : 0); From 08092276f9fe3cea4e7b7e5a802a0e521a78f98f Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Fri, 28 Aug 2026 16:22:55 -0400 Subject: [PATCH 03/11] fix(hooks): price Sonnet 5 at the published $2/$10 rate --- scripts/hooks/cost-tracker.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/hooks/cost-tracker.js b/scripts/hooks/cost-tracker.js index fa3677e4f..e42dc97a1 100755 --- a/scripts/hooks/cost-tracker.js +++ b/scripts/hooks/cost-tracker.js @@ -71,11 +71,12 @@ function readHarnessCost(sessionId, maxAgeSeconds) { // Approximate per-1M-token billing rates (USD). // Cache creation: 1.25x input rate. Cache read: 0.1x input rate. // Current-generation list prices: Fable/Mythos 5 $10/$50, Opus 5 and -// Opus 4.5-4.8 $5/$25, Sonnet 5/4.6 $3/$15, Haiku 4.5 $1/$5. Opus 4.0/4.1 -// and Opus 3 stay on the legacy $15/$75 tier. +// Opus 4.5-4.8 $5/$25, Sonnet 5 $2/$10, Sonnet 4.6 $3/$15, and Haiku 4.5 +// $1/$5. Opus 4.0/4.1 and Opus 3 stay on the legacy $15/$75 tier. const RATE_TABLE = { haiku: { in: 1.00, out: 5.0, cacheWrite: 1.25, cacheRead: 0.10 }, sonnet: { in: 3.00, out: 15.0, cacheWrite: 3.75, cacheRead: 0.30 }, + sonnet5: { in: 2.00, out: 10.0, cacheWrite: 2.50, cacheRead: 0.20 }, opus: { in: 5.00, out: 25.0, cacheWrite: 6.25, cacheRead: 0.50 }, opusLegacy: { in: 15.00, out: 75.0, cacheWrite: 18.75, cacheRead: 1.50 }, fable: { in: 10.00, out: 50.0, cacheWrite: 12.50, cacheRead: 1.00 } @@ -85,11 +86,16 @@ function getRates(model) { const m = String(model || '').toLowerCase(); if (m.includes('fable') || m.includes('mythos')) return RATE_TABLE.fable; if (m.includes('haiku')) return RATE_TABLE.haiku; + if (isSonnet5(m)) return RATE_TABLE.sonnet5; if (m.includes('opus-4-1') || m.includes('opus-4-0') || m.includes('3-opus')) return RATE_TABLE.opusLegacy; if (m.includes('opus')) return RATE_TABLE.opus; return RATE_TABLE.sonnet; } +function isSonnet5(model) { + return /(?:^|[^a-z0-9])sonnet-5(?:[^a-z0-9]|$)/.test(model); +} + function toNumber(v) { const n = Number(v); return Number.isFinite(n) ? n : 0; From f5d0b295cb4a9d4a1197497c388e1ae2fc68bc28 Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Sun, 23 Aug 2026 15:45:24 +0000 Subject: [PATCH 04/11] test(hooks): expand Sonnet 5 cost-tracker coverage for cache and model matching - Add cache write/read token pricing test for Sonnet 5. - Add dated Sonnet 5 ID and claude-sonnet-50 near-miss regression tests. - Keep Sonnet 4.6 standard rate distinction intact. Co-Authored-By: Paperclip --- tests/hooks/cost-tracker.test.js | 142 ++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/tests/hooks/cost-tracker.test.js b/tests/hooks/cost-tracker.test.js index 8f652d7a1..521e7ecd2 100644 --- a/tests/hooks/cost-tracker.test.js +++ b/tests/hooks/cost-tracker.test.js @@ -297,7 +297,147 @@ function runTests() { } }) ? passed++ : failed++); - // 9. Ignores stale harness-cost cache and falls back to transcript estimate + // 9. Prices Sonnet 5 at the documented $2/$10 rate. + (test('prices Sonnet 5 at $12 per 1M input + 1M output tokens', () => { + const tmpHome = makeTempDir(); + const transcriptPath = path.join(tmpHome, 'session.jsonl'); + writeTranscript(transcriptPath, [ + { + type: 'assistant', + message: { + id: 'msg_sonnet5', + model: 'claude-sonnet-5', + usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + }, + }, + ]); + + const result = runScript( + { session_id: 'sonnet5-session', transcript_path: transcriptPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.strictEqual(row.estimated_cost_usd, 12, 'Expected Sonnet 5 1M/1M to cost $12.00'); + + fs.rmSync(tmpHome, { recursive: true, force: true }); + }) ? passed++ : failed++); + + // 9b. Sonnet 5 cache write/read tokens use the correct rates. + (test('prices Sonnet 5 cache tokens at the documented rates', () => { + const tmpHome = makeTempDir(); + const transcriptPath = path.join(tmpHome, 'session.jsonl'); + writeTranscript(transcriptPath, [ + { + type: 'assistant', + message: { + id: 'msg_sonnet5_cache', + model: 'claude-sonnet-5', + usage: { + input_tokens: 1_000_000, + output_tokens: 1_000_000, + cache_creation_input_tokens: 1_000_000, + cache_read_input_tokens: 1_000_000, + }, + }, + }, + ]); + + const result = runScript( + { session_id: 'sonnet5-cache-session', transcript_path: transcriptPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.strictEqual(row.estimated_cost_usd, 14.7, 'Expected Sonnet 5 1M input + 1M output + 1M cache write + 1M cache read to cost $14.70'); + + fs.rmSync(tmpHome, { recursive: true, force: true }); + }) ? passed++ : failed++); + + // 10. Sonnet 4.6 keeps the existing $3/$15 rate and is not mistaken for Sonnet 5. + (test('prices Sonnet 4.6 at $18 per 1M input + 1M output tokens', () => { + const tmpHome = makeTempDir(); + const transcriptPath = path.join(tmpHome, 'session.jsonl'); + writeTranscript(transcriptPath, [ + { + type: 'assistant', + message: { + id: 'msg_sonnet46', + model: 'claude-sonnet-4-6', + usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + }, + }, + ]); + + const result = runScript( + { session_id: 'sonnet46-session', transcript_path: transcriptPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.strictEqual(row.estimated_cost_usd, 18, 'Expected Sonnet 4.6 1M/1M to remain $18.00'); + + fs.rmSync(tmpHome, { recursive: true, force: true }); + }) ? passed++ : failed++); + + // 10b. Dated Sonnet 5 IDs and near-misses are matched correctly. + (test('prices dated Sonnet 5 IDs at $12 and rejects claude-sonnet-50 near-miss', () => { + const tmpHome = makeTempDir(); + const transcriptPath = path.join(tmpHome, 'session.jsonl'); + writeTranscript(transcriptPath, [ + { + type: 'assistant', + message: { + id: 'msg_sonnet5_dated', + model: 'claude-sonnet-5-20261001', + usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + }, + }, + ]); + + const result = runScript( + { session_id: 'sonnet5-dated-session', transcript_path: transcriptPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.strictEqual(row.estimated_cost_usd, 12, 'Expected dated Sonnet 5 1M/1M to cost $12.00'); + + // Near-miss `claude-sonnet-50` must fall through to the standard Sonnet rate. + const nearMissPath = path.join(tmpHome, 'near-miss.jsonl'); + writeTranscript(nearMissPath, [ + { + type: 'assistant', + message: { + id: 'msg_sonnet50', + model: 'claude-sonnet-50', + usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + }, + }, + ]); + + const nearResult = runScript( + { session_id: 'sonnet50-near-miss-session', transcript_path: nearMissPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(nearResult.code, 0, `Expected exit code 0, got ${nearResult.code}`); + + const lines = fs.readFileSync(metricsFile, 'utf8').trim().split('\n'); + const nearRow = JSON.parse(lines[lines.length - 1]); + assert.strictEqual(nearRow.estimated_cost_usd, 18, 'Expected claude-sonnet-50 near-miss to fall back to $18.00 Sonnet rate'); + + fs.rmSync(tmpHome, { recursive: true, force: true }); + }) ? passed++ : failed++); + + // 11. Ignores stale harness-cost cache and falls back to transcript estimate (test('ignores stale harness-cost cache (>300s) and uses transcript estimate', () => { const tmpHome = makeTempDir(); const sessionId = 'harness-stale-' + Date.now(); From 5ee14cb2af6a4263fa5345cd4994edc090b1447d Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Sun, 23 Aug 2026 15:54:00 +0000 Subject: [PATCH 05/11] test(hooks): use unique session IDs in Sonnet 5 pricing tests Prevent stale /tmp/harness-cost cache files from affecting Sonnet 5, dated, near-miss, and cache-rate pricing tests by using Date.now() in each session ID. Co-Authored-By: Paperclip --- tests/hooks/cost-tracker.test.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/hooks/cost-tracker.test.js b/tests/hooks/cost-tracker.test.js index 521e7ecd2..f35334663 100644 --- a/tests/hooks/cost-tracker.test.js +++ b/tests/hooks/cost-tracker.test.js @@ -300,6 +300,7 @@ function runTests() { // 9. Prices Sonnet 5 at the documented $2/$10 rate. (test('prices Sonnet 5 at $12 per 1M input + 1M output tokens', () => { const tmpHome = makeTempDir(); + const sessionId = 'sonnet5-' + Date.now(); const transcriptPath = path.join(tmpHome, 'session.jsonl'); writeTranscript(transcriptPath, [ { @@ -313,7 +314,7 @@ function runTests() { ]); const result = runScript( - { session_id: 'sonnet5-session', transcript_path: transcriptPath }, + { session_id: sessionId, transcript_path: transcriptPath }, withTempHome(tmpHome) ); assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); @@ -328,6 +329,7 @@ function runTests() { // 9b. Sonnet 5 cache write/read tokens use the correct rates. (test('prices Sonnet 5 cache tokens at the documented rates', () => { const tmpHome = makeTempDir(); + const sessionId = 'sonnet5-cache-' + Date.now(); const transcriptPath = path.join(tmpHome, 'session.jsonl'); writeTranscript(transcriptPath, [ { @@ -346,7 +348,7 @@ function runTests() { ]); const result = runScript( - { session_id: 'sonnet5-cache-session', transcript_path: transcriptPath }, + { session_id: sessionId, transcript_path: transcriptPath }, withTempHome(tmpHome) ); assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); @@ -361,6 +363,7 @@ function runTests() { // 10. Sonnet 4.6 keeps the existing $3/$15 rate and is not mistaken for Sonnet 5. (test('prices Sonnet 4.6 at $18 per 1M input + 1M output tokens', () => { const tmpHome = makeTempDir(); + const sessionId = 'sonnet46-' + Date.now(); const transcriptPath = path.join(tmpHome, 'session.jsonl'); writeTranscript(transcriptPath, [ { @@ -374,7 +377,7 @@ function runTests() { ]); const result = runScript( - { session_id: 'sonnet46-session', transcript_path: transcriptPath }, + { session_id: sessionId, transcript_path: transcriptPath }, withTempHome(tmpHome) ); assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); @@ -389,6 +392,7 @@ function runTests() { // 10b. Dated Sonnet 5 IDs and near-misses are matched correctly. (test('prices dated Sonnet 5 IDs at $12 and rejects claude-sonnet-50 near-miss', () => { const tmpHome = makeTempDir(); + const sessionId = 'sonnet5-dated-' + Date.now(); const transcriptPath = path.join(tmpHome, 'session.jsonl'); writeTranscript(transcriptPath, [ { @@ -402,7 +406,7 @@ function runTests() { ]); const result = runScript( - { session_id: 'sonnet5-dated-session', transcript_path: transcriptPath }, + { session_id: sessionId, transcript_path: transcriptPath }, withTempHome(tmpHome) ); assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); @@ -425,7 +429,7 @@ function runTests() { ]); const nearResult = runScript( - { session_id: 'sonnet50-near-miss-session', transcript_path: nearMissPath }, + { session_id: 'sonnet50-near-miss-' + Date.now(), transcript_path: nearMissPath }, withTempHome(tmpHome) ); assert.strictEqual(nearResult.code, 0, `Expected exit code 0, got ${nearResult.code}`); From 616716f370929ef891dc01a812bd8a1458c683ab Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Mon, 24 Aug 2026 06:10:07 +0000 Subject: [PATCH 06/11] test: isolate cost tracker cache fixtures --- tests/hooks/cost-tracker.test.js | 156 +++++++++++++++++++------------ 1 file changed, 95 insertions(+), 61 deletions(-) diff --git a/tests/hooks/cost-tracker.test.js b/tests/hooks/cost-tracker.test.js index f35334663..907bfa74a 100644 --- a/tests/hooks/cost-tracker.test.js +++ b/tests/hooks/cost-tracker.test.js @@ -54,6 +54,15 @@ function runScript(input, envOverrides = {}) { return { code: result.status || 0, stdout: result.stdout || '', stderr: result.stderr || '' }; } +function removeHarnessCostCache(sessionId) { + const cachePath = path.join(os.tmpdir(), `harness-cost-${sessionId}.json`); + try { + fs.unlinkSync(cachePath); + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } +} + function runTests() { console.log('\n=== Testing cost-tracker.js ===\n'); @@ -300,7 +309,7 @@ function runTests() { // 9. Prices Sonnet 5 at the documented $2/$10 rate. (test('prices Sonnet 5 at $12 per 1M input + 1M output tokens', () => { const tmpHome = makeTempDir(); - const sessionId = 'sonnet5-' + Date.now(); + const sessionId = `sonnet5-${process.pid}-${Date.now()}`; const transcriptPath = path.join(tmpHome, 'session.jsonl'); writeTranscript(transcriptPath, [ { @@ -313,23 +322,33 @@ function runTests() { }, ]); - const result = runScript( - { session_id: sessionId, transcript_path: transcriptPath }, - withTempHome(tmpHome) + fs.writeFileSync( + path.join(os.tmpdir(), `harness-cost-${sessionId}.json`), + JSON.stringify({ ts: Math.floor(Date.now() / 1000), cost_usd: 999 }), + 'utf8' ); - assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); - const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); - const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); - assert.strictEqual(row.estimated_cost_usd, 12, 'Expected Sonnet 5 1M/1M to cost $12.00'); + try { + removeHarnessCostCache(sessionId); + const result = runScript( + { session_id: sessionId, transcript_path: transcriptPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); - fs.rmSync(tmpHome, { recursive: true, force: true }); + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.strictEqual(row.estimated_cost_usd, 12, 'Expected Sonnet 5 1M/1M to cost $12.00'); + } finally { + removeHarnessCostCache(sessionId); + fs.rmSync(tmpHome, { recursive: true, force: true }); + } }) ? passed++ : failed++); // 9b. Sonnet 5 cache write/read tokens use the correct rates. (test('prices Sonnet 5 cache tokens at the documented rates', () => { const tmpHome = makeTempDir(); - const sessionId = 'sonnet5-cache-' + Date.now(); + const sessionId = `sonnet5-cache-${process.pid}-${Date.now()}`; const transcriptPath = path.join(tmpHome, 'session.jsonl'); writeTranscript(transcriptPath, [ { @@ -347,23 +366,27 @@ function runTests() { }, ]); - const result = runScript( - { session_id: sessionId, transcript_path: transcriptPath }, - withTempHome(tmpHome) - ); - assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + try { + removeHarnessCostCache(sessionId); + const result = runScript( + { session_id: sessionId, transcript_path: transcriptPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); - const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); - const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); - assert.strictEqual(row.estimated_cost_usd, 14.7, 'Expected Sonnet 5 1M input + 1M output + 1M cache write + 1M cache read to cost $14.70'); - - fs.rmSync(tmpHome, { recursive: true, force: true }); + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.strictEqual(row.estimated_cost_usd, 14.7, 'Expected Sonnet 5 1M input + 1M output + 1M cache write + 1M cache read to cost $14.70'); + } finally { + removeHarnessCostCache(sessionId); + fs.rmSync(tmpHome, { recursive: true, force: true }); + } }) ? passed++ : failed++); // 10. Sonnet 4.6 keeps the existing $3/$15 rate and is not mistaken for Sonnet 5. (test('prices Sonnet 4.6 at $18 per 1M input + 1M output tokens', () => { const tmpHome = makeTempDir(); - const sessionId = 'sonnet46-' + Date.now(); + const sessionId = `sonnet46-${process.pid}-${Date.now()}`; const transcriptPath = path.join(tmpHome, 'session.jsonl'); writeTranscript(transcriptPath, [ { @@ -376,23 +399,28 @@ function runTests() { }, ]); - const result = runScript( - { session_id: sessionId, transcript_path: transcriptPath }, - withTempHome(tmpHome) - ); - assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + try { + removeHarnessCostCache(sessionId); + const result = runScript( + { session_id: sessionId, transcript_path: transcriptPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); - const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); - const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); - assert.strictEqual(row.estimated_cost_usd, 18, 'Expected Sonnet 4.6 1M/1M to remain $18.00'); - - fs.rmSync(tmpHome, { recursive: true, force: true }); + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.strictEqual(row.estimated_cost_usd, 18, 'Expected Sonnet 4.6 1M/1M to remain $18.00'); + } finally { + removeHarnessCostCache(sessionId); + fs.rmSync(tmpHome, { recursive: true, force: true }); + } }) ? passed++ : failed++); // 10b. Dated Sonnet 5 IDs and near-misses are matched correctly. (test('prices dated Sonnet 5 IDs at $12 and rejects claude-sonnet-50 near-miss', () => { const tmpHome = makeTempDir(); - const sessionId = 'sonnet5-dated-' + Date.now(); + const sessionId = `sonnet5-dated-${process.pid}-${Date.now()}`; + const nearMissSessionId = `sonnet50-near-miss-${process.pid}-${Date.now()}`; const transcriptPath = path.join(tmpHome, 'session.jsonl'); writeTranscript(transcriptPath, [ { @@ -405,40 +433,46 @@ function runTests() { }, ]); - const result = runScript( - { session_id: sessionId, transcript_path: transcriptPath }, - withTempHome(tmpHome) - ); - assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + try { + removeHarnessCostCache(sessionId); + removeHarnessCostCache(nearMissSessionId); + const result = runScript( + { session_id: sessionId, transcript_path: transcriptPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); - const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); - const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); - assert.strictEqual(row.estimated_cost_usd, 12, 'Expected dated Sonnet 5 1M/1M to cost $12.00'); + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.strictEqual(row.estimated_cost_usd, 12, 'Expected dated Sonnet 5 1M/1M to cost $12.00'); - // Near-miss `claude-sonnet-50` must fall through to the standard Sonnet rate. - const nearMissPath = path.join(tmpHome, 'near-miss.jsonl'); - writeTranscript(nearMissPath, [ - { - type: 'assistant', - message: { - id: 'msg_sonnet50', - model: 'claude-sonnet-50', - usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + // Near-miss `claude-sonnet-50` must fall through to the standard Sonnet rate. + const nearMissPath = path.join(tmpHome, 'near-miss.jsonl'); + writeTranscript(nearMissPath, [ + { + type: 'assistant', + message: { + id: 'msg_sonnet50', + model: 'claude-sonnet-50', + usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + }, }, - }, - ]); + ]); - const nearResult = runScript( - { session_id: 'sonnet50-near-miss-' + Date.now(), transcript_path: nearMissPath }, - withTempHome(tmpHome) - ); - assert.strictEqual(nearResult.code, 0, `Expected exit code 0, got ${nearResult.code}`); + const nearResult = runScript( + { session_id: nearMissSessionId, transcript_path: nearMissPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(nearResult.code, 0, `Expected exit code 0, got ${nearResult.code}`); - const lines = fs.readFileSync(metricsFile, 'utf8').trim().split('\n'); - const nearRow = JSON.parse(lines[lines.length - 1]); - assert.strictEqual(nearRow.estimated_cost_usd, 18, 'Expected claude-sonnet-50 near-miss to fall back to $18.00 Sonnet rate'); - - fs.rmSync(tmpHome, { recursive: true, force: true }); + const lines = fs.readFileSync(metricsFile, 'utf8').trim().split('\n'); + const nearRow = JSON.parse(lines[lines.length - 1]); + assert.strictEqual(nearRow.estimated_cost_usd, 18, 'Expected claude-sonnet-50 near-miss to fall back to $18.00 Sonnet rate'); + } finally { + removeHarnessCostCache(sessionId); + removeHarnessCostCache(nearMissSessionId); + fs.rmSync(tmpHome, { recursive: true, force: true }); + } }) ? passed++ : failed++); // 11. Ignores stale harness-cost cache and falls back to transcript estimate From ab8cbf6505a823eed11168711678a368deeb8add Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Mon, 24 Aug 2026 06:41:04 +0000 Subject: [PATCH 07/11] test: cover Sonnet 5 cache rate splits --- tests/hooks/cost-tracker.test.js | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/hooks/cost-tracker.test.js b/tests/hooks/cost-tracker.test.js index 907bfa74a..af6ee8671 100644 --- a/tests/hooks/cost-tracker.test.js +++ b/tests/hooks/cost-tracker.test.js @@ -63,6 +63,40 @@ function removeHarnessCostCache(sessionId) { } } +function assertSonnet5CacheCost(cacheUsage, expectedCost, description) { + const tmpHome = makeTempDir(); + const sessionId = `sonnet5-${description}-${process.pid}-${Date.now()}`; + const transcriptPath = path.join(tmpHome, 'session.jsonl'); + writeTranscript(transcriptPath, [{ + type: 'assistant', + message: { + id: `msg_sonnet5_${description}`, + model: 'claude-sonnet-5', + usage: { + input_tokens: 1_000_000, + output_tokens: 1_000_000, + ...cacheUsage, + }, + }, + }]); + + try { + removeHarnessCostCache(sessionId); + const result = runScript( + { session_id: sessionId, transcript_path: transcriptPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.strictEqual(row.estimated_cost_usd, expectedCost, description); + } finally { + removeHarnessCostCache(sessionId); + fs.rmSync(tmpHome, { recursive: true, force: true }); + } +} + function runTests() { console.log('\n=== Testing cost-tracker.js ===\n'); @@ -383,6 +417,23 @@ function runTests() { } }) ? passed++ : failed++); + // 9c. Cache write/read rates are independently covered. + (test('prices Sonnet 5 cache writes at $2.50 per 1M tokens', () => { + assertSonnet5CacheCost( + { cache_creation_input_tokens: 1_000_000 }, + 14.5, + 'cache-write' + ); + }) ? passed++ : failed++); + + (test('prices Sonnet 5 cache reads at $0.20 per 1M tokens', () => { + assertSonnet5CacheCost( + { cache_read_input_tokens: 1_000_000 }, + 12.2, + 'cache-read' + ); + }) ? passed++ : failed++); + // 10. Sonnet 4.6 keeps the existing $3/$15 rate and is not mistaken for Sonnet 5. (test('prices Sonnet 4.6 at $18 per 1M input + 1M output tokens', () => { const tmpHome = makeTempDir(); From 64f0acf60e77079ede823860953c8046bfb3b363 Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Mon, 24 Aug 2026 09:00:51 +0000 Subject: [PATCH 08/11] test: split Sonnet model pricing cases --- tests/hooks/cost-tracker.test.js | 63 ++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/tests/hooks/cost-tracker.test.js b/tests/hooks/cost-tracker.test.js index af6ee8671..20054d782 100644 --- a/tests/hooks/cost-tracker.test.js +++ b/tests/hooks/cost-tracker.test.js @@ -467,11 +467,10 @@ function runTests() { } }) ? passed++ : failed++); - // 10b. Dated Sonnet 5 IDs and near-misses are matched correctly. - (test('prices dated Sonnet 5 IDs at $12 and rejects claude-sonnet-50 near-miss', () => { + // 10b. Dated Sonnet 5 IDs are matched correctly. + (test('prices dated Sonnet 5 IDs at $12', () => { const tmpHome = makeTempDir(); const sessionId = `sonnet5-dated-${process.pid}-${Date.now()}`; - const nearMissSessionId = `sonnet50-near-miss-${process.pid}-${Date.now()}`; const transcriptPath = path.join(tmpHome, 'session.jsonl'); writeTranscript(transcriptPath, [ { @@ -486,7 +485,6 @@ function runTests() { try { removeHarnessCostCache(sessionId); - removeHarnessCostCache(nearMissSessionId); const result = runScript( { session_id: sessionId, transcript_path: transcriptPath }, withTempHome(tmpHome) @@ -496,32 +494,41 @@ function runTests() { const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); assert.strictEqual(row.estimated_cost_usd, 12, 'Expected dated Sonnet 5 1M/1M to cost $12.00'); - - // Near-miss `claude-sonnet-50` must fall through to the standard Sonnet rate. - const nearMissPath = path.join(tmpHome, 'near-miss.jsonl'); - writeTranscript(nearMissPath, [ - { - type: 'assistant', - message: { - id: 'msg_sonnet50', - model: 'claude-sonnet-50', - usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 }, - }, - }, - ]); - - const nearResult = runScript( - { session_id: nearMissSessionId, transcript_path: nearMissPath }, - withTempHome(tmpHome) - ); - assert.strictEqual(nearResult.code, 0, `Expected exit code 0, got ${nearResult.code}`); - - const lines = fs.readFileSync(metricsFile, 'utf8').trim().split('\n'); - const nearRow = JSON.parse(lines[lines.length - 1]); - assert.strictEqual(nearRow.estimated_cost_usd, 18, 'Expected claude-sonnet-50 near-miss to fall back to $18.00 Sonnet rate'); } finally { removeHarnessCostCache(sessionId); - removeHarnessCostCache(nearMissSessionId); + fs.rmSync(tmpHome, { recursive: true, force: true }); + } + }) ? passed++ : failed++); + + // 10c. Near-miss Sonnet 5 IDs fall back to standard Sonnet rates. + (test('rejects claude-sonnet-50 as a Sonnet 5 near-miss', () => { + const tmpHome = makeTempDir(); + const sessionId = `sonnet50-near-miss-${process.pid}-${Date.now()}`; + const transcriptPath = path.join(tmpHome, 'session.jsonl'); + writeTranscript(transcriptPath, [ + { + type: 'assistant', + message: { + id: 'msg_sonnet50', + model: 'claude-sonnet-50', + usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + }, + }, + ]); + + try { + removeHarnessCostCache(sessionId); + const result = runScript( + { session_id: sessionId, transcript_path: transcriptPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); + const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()); + assert.strictEqual(row.estimated_cost_usd, 18, 'Expected claude-sonnet-50 near-miss to fall back to $18.00 Sonnet rate'); + } finally { + removeHarnessCostCache(sessionId); fs.rmSync(tmpHome, { recursive: true, force: true }); } }) ? passed++ : failed++); From 77c358dd3fbf7a5e67cbb93b469fe3de110cc0db Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:26:13 -0400 Subject: [PATCH 09/11] fix(costs): retain dated Opus 4 legacy pricing --- scripts/hooks/cost-tracker.js | 6 ++++- tests/hooks/cost-tracker.test.js | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/scripts/hooks/cost-tracker.js b/scripts/hooks/cost-tracker.js index e42dc97a1..9f8b07e87 100755 --- a/scripts/hooks/cost-tracker.js +++ b/scripts/hooks/cost-tracker.js @@ -82,12 +82,16 @@ const RATE_TABLE = { fable: { in: 10.00, out: 50.0, cacheWrite: 12.50, cacheRead: 1.00 } }; +// Opus 4.0's dated snapshot omits the minor segment, so an `opus-4-0` +// substring check alone misses `claude-opus-4-20250514`. +const LEGACY_OPUS_RE = /3-opus|opus-4-0(?!\d)|opus-4-1(?!\d)|opus-4[-@]\d{8}/; + function getRates(model) { const m = String(model || '').toLowerCase(); if (m.includes('fable') || m.includes('mythos')) return RATE_TABLE.fable; if (m.includes('haiku')) return RATE_TABLE.haiku; if (isSonnet5(m)) return RATE_TABLE.sonnet5; - if (m.includes('opus-4-1') || m.includes('opus-4-0') || m.includes('3-opus')) return RATE_TABLE.opusLegacy; + if (LEGACY_OPUS_RE.test(m)) return RATE_TABLE.opusLegacy; if (m.includes('opus')) return RATE_TABLE.opus; return RATE_TABLE.sonnet; } diff --git a/tests/hooks/cost-tracker.test.js b/tests/hooks/cost-tracker.test.js index 20054d782..78e72f68c 100644 --- a/tests/hooks/cost-tracker.test.js +++ b/tests/hooks/cost-tracker.test.js @@ -533,6 +533,51 @@ function runTests() { } }) ? passed++ : failed++); + // 10d. Opus 4.0's dated ID has no explicit minor segment. It must retain + // the legacy $15/$75 rate while Opus 4.5 uses the current $5/$25 rate. + (test('distinguishes the dated Opus 4.0 snapshot from current Opus 4.x', () => { + const priceModel = model => { + const tmpHome = makeTempDir(); + const sessionId = `opus-rate-${process.pid}-${Date.now()}-${model}`; + const transcriptPath = path.join(tmpHome, 'session.jsonl'); + writeTranscript(transcriptPath, [ + { + type: 'assistant', + message: { + id: `msg_${model}`, + model, + usage: { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + }, + }, + ]); + + try { + removeHarnessCostCache(sessionId); + const result = runScript( + { session_id: sessionId, transcript_path: transcriptPath }, + withTempHome(tmpHome) + ); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl'); + return JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim()).estimated_cost_usd; + } finally { + removeHarnessCostCache(sessionId); + fs.rmSync(tmpHome, { recursive: true, force: true }); + } + }; + + assert.strictEqual( + priceModel('claude-opus-4-20250514'), + 90, + 'Expected dated Opus 4.0 to retain the legacy $15/$75 rate' + ); + assert.strictEqual( + priceModel('claude-opus-4-5-20251101'), + 30, + 'Expected Opus 4.5 to use the current $5/$25 rate' + ); + }) ? passed++ : failed++); + // 11. Ignores stale harness-cost cache and falls back to transcript estimate (test('ignores stale harness-cost cache (>300s) and uses transcript estimate', () => { const tmpHome = makeTempDir(); From 1d19789c7576ce5e43d53bb156bf2be67751b941 Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:28:12 -0400 Subject: [PATCH 10/11] fix(hooks): preserve metadata-marked human prompts --- scripts/hooks/session-end.js | 5 ++++- tests/hooks/hooks.test.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/scripts/hooks/session-end.js b/scripts/hooks/session-end.js index 60ba74720..cb4ba7ac1 100644 --- a/scripts/hooks/session-end.js +++ b/scripts/hooks/session-end.js @@ -49,7 +49,10 @@ function extractSessionSummary(transcriptPath) { const cleaned = stripAnsi(text).trim(); // Skip harness noise: local command echoes, caveats, system reminders. const isNoise = /^<(local-command-caveat|local-command-stdout|command-name|command-message|command-args|system-reminder|task-notification)/i.test(cleaned); - if (cleaned && !isToolResult && !isNoise && !entry.isMeta) { + // `isMeta` is also used for genuine channel- and plugin-originated + // human prompts. Exclude known structured noise above instead of + // discarding every metadata-marked user turn. + if (cleaned && !isToolResult && !isNoise) { userMessages.push(cleaned.slice(0, 200)); } } diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index 49d6f1e23..7a5a4e7c3 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -2118,6 +2118,41 @@ async function runTests() { passed++; else failed++; + if ( + await asyncTest('keeps isMeta human prompts while filtering structured transcript noise', async () => { + const testDir = createTestDir(); + const transcriptPath = path.join(testDir, 'transcript.jsonl'); + const lines = [ + JSON.stringify({ type: 'user', isMeta: true, content: 'Prompt delivered by a channel plugin' }), + JSON.stringify({ type: 'user', isMeta: true, content: 'internal harness context' }), + JSON.stringify({ + type: 'user', + message: { role: 'user', content: [{ type: 'tool_result', content: 'tool output' }] }, + }), + ]; + fs.writeFileSync(transcriptPath, lines.join('\n')); + + const result = await runScript( + path.join(scriptsDir, 'session-end.js'), + JSON.stringify({ transcript_path: transcriptPath }), + { HOME: testDir, USERPROFILE: testDir } + ); + assert.strictEqual(result.code, 0); + + const sessionsDir = getCanonicalSessionsDir(testDir); + const sessionFiles = fs.readdirSync(sessionsDir).filter(file => file.endsWith('.tmp')); + assert.strictEqual(sessionFiles.length, 1, 'Should create one session file'); + const content = fs.readFileSync(path.join(sessionsDir, sessionFiles[0]), 'utf8'); + assert.ok(content.includes('Prompt delivered by a channel plugin')); + assert.ok(!content.includes('internal harness context')); + assert.ok(!content.includes('tool output')); + assert.ok(content.includes('Total user messages: 1')); + cleanupTestDir(testDir); + }) + ) + passed++; + else failed++; + if ( await asyncTest('extracts tool names and file paths from transcript', async () => { const testDir = createTestDir(); From e51224697d4be22ac88458deb16d275f52d76276 Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:38:56 -0400 Subject: [PATCH 11/11] docs(costs): cite the pricing contract --- scripts/hooks/cost-tracker.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/hooks/cost-tracker.js b/scripts/hooks/cost-tracker.js index 9f8b07e87..981f67619 100755 --- a/scripts/hooks/cost-tracker.js +++ b/scripts/hooks/cost-tracker.js @@ -70,6 +70,7 @@ function readHarnessCost(sessionId, maxAgeSeconds) { // Approximate per-1M-token billing rates (USD). // Cache creation: 1.25x input rate. Cache read: 0.1x input rate. +// Source: https://platform.claude.com/docs/en/about-claude/pricing // Current-generation list prices: Fable/Mythos 5 $10/$50, Opus 5 and // Opus 4.5-4.8 $5/$25, Sonnet 5 $2/$10, Sonnet 4.6 $3/$15, and Haiku 4.5 // $1/$5. Opus 4.0/4.1 and Opus 3 stay on the legacy $15/$75 tier.