diff --git a/README.md b/README.md index 314be738..046237df 100644 --- a/README.md +++ b/README.md @@ -476,6 +476,15 @@ export ECC_SESSION_START_MAX_CHARS=4000 # Disable SessionStart additional context entirely for low-context/local-model setups export ECC_SESSION_START_CONTEXT=off + +# Keep context/scope/loop warnings but suppress API-rate cost estimates +export ECC_CONTEXT_MONITOR_COST_WARNINGS=off +``` + +Windows PowerShell: + +```powershell +[Environment]::SetEnvironmentVariable('ECC_CONTEXT_MONITOR_COST_WARNINGS', 'off', 'User') ``` --- @@ -1610,6 +1619,7 @@ Add to `~/.claude/settings.json`: | `model` | opus | **sonnet** | ~60% cost reduction; handles 80%+ of coding tasks | | `MAX_THINKING_TOKENS` | 31,999 | **10,000** | ~70% reduction in hidden thinking cost per request | | `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` | 95 | **50** | Compacts earlier — better quality in long sessions | +| `ECC_CONTEXT_MONITOR_COST_WARNINGS` | on | **off for subscription users** | Suppresses agent-facing API-rate estimate warnings while keeping context/scope/loop warnings | Switch to Opus only when you need deep architectural reasoning: ``` @@ -1626,6 +1636,8 @@ Switch to Opus only when you need deep architectural reasoning: | `/compact` | At logical task breakpoints (research done, milestone complete) | | `/cost` | Monitor token spending during session | +If you use a Claude subscription and the context monitor's API-rate estimates are not useful, set `ECC_CONTEXT_MONITOR_COST_WARNINGS=off`. This only suppresses the agent-facing cost warnings; it does not disable context exhaustion, scope, or loop warnings. + ### Strategic Compaction The `strategic-compact` skill (included in this plugin) suggests `/compact` at logical breakpoints instead of relying on auto-compaction at 95% context. See `skills/strategic-compact/SKILL.md` for the full decision guide. diff --git a/docs/token-optimization.md b/docs/token-optimization.md index ad5fd6ec..5ff087f8 100644 --- a/docs/token-optimization.md +++ b/docs/token-optimization.md @@ -29,6 +29,7 @@ Add to your `~/.claude/settings.json`: | `model` | opus | **sonnet** | Sonnet handles ~80% of coding tasks well. Switch to Opus with `/model opus` for complex reasoning. ~60% cost reduction. | | `MAX_THINKING_TOKENS` | 31,999 | **10,000** | Extended thinking reserves up to 31,999 output tokens per request for internal reasoning. Reducing this cuts hidden cost by ~70%. Set to `0` to disable for trivial tasks. | | `CLAUDE_CODE_SUBAGENT_MODEL` | _(inherits main)_ | **haiku** | Subagents (Task tool) run on this model. Haiku is ~80% cheaper and sufficient for exploration, file reading, and test running. | +| `ECC_CONTEXT_MONITOR_COST_WARNINGS` | on | **off for subscription users** | Suppresses agent-facing API-rate estimate warnings while keeping context exhaustion, scope, and loop warnings. | ### Community note on auto-compaction overrides @@ -71,6 +72,22 @@ Switch models mid-session: | `/compact` | At logical task breakpoints (after planning, after debugging, before switching focus). | | `/cost` | Check token spending for the current session. | +### API-rate cost estimate warnings + +ECC's context monitor can emit API-rate cost estimates from local hook telemetry. If you are on a Claude subscription and those estimates do not reflect your actual bill, disable only the agent-facing cost warnings: + +```bash +export ECC_CONTEXT_MONITOR_COST_WARNINGS=off +``` + +Windows PowerShell: + +```powershell +[Environment]::SetEnvironmentVariable('ECC_CONTEXT_MONITOR_COST_WARNINGS', 'off', 'User') +``` + +This does not disable context exhaustion warnings, scope warnings, loop warnings, `/cost`, or cost telemetry files. + ### Strategic compaction The `strategic-compact` skill (in `skills/strategic-compact/`) suggests `/compact` at logical intervals rather than relying on auto-compaction, which can trigger mid-task. See the skill's README for hook setup instructions. diff --git a/hooks/README.md b/hooks/README.md index db8b0e35..8df6e4f9 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -110,6 +110,15 @@ export ECC_SESSION_START_MAX_CHARS=4000 # Disable SessionStart additional context entirely export ECC_SESSION_START_CONTEXT=off + +# Keep context/scope/loop warnings but suppress API-rate cost estimates +export ECC_CONTEXT_MONITOR_COST_WARNINGS=off +``` + +Windows PowerShell: + +```powershell +[Environment]::SetEnvironmentVariable('ECC_CONTEXT_MONITOR_COST_WARNINGS', 'off', 'User') ``` Profiles: diff --git a/scripts/hooks/ecc-context-monitor.js b/scripts/hooks/ecc-context-monitor.js index a3c0be8b..8ddb36dc 100644 --- a/scripts/hooks/ecc-context-monitor.js +++ b/scripts/hooks/ecc-context-monitor.js @@ -24,6 +24,20 @@ const LOOP_THRESHOLD = 3; const STALE_SECONDS = 60; const DEBOUNCE_CALLS = 5; +function isEnabledEnv(value, defaultValue = true) { + if (value === undefined || value === null || String(value).trim() === '') { + return defaultValue; + } + const normalized = String(value).trim().toLowerCase(); + if (['0', 'false', 'no', 'off', 'disabled'].includes(normalized)) return false; + if (['1', 'true', 'yes', 'on', 'enabled'].includes(normalized)) return true; + return defaultValue; +} + +function costWarningsEnabled(env = process.env) { + return isEnabledEnv(env.ECC_CONTEXT_MONITOR_COST_WARNINGS, true); +} + /** * Get debounce state file path. * @param {string} sessionId @@ -84,7 +98,7 @@ function detectLoop(recentTools) { * Evaluate all warning conditions against bridge data. * Returns array of {severity, type, message} sorted by severity desc. */ -function evaluateConditions(bridge) { +function evaluateConditions(bridge, options = {}) { const warnings = []; const remaining = bridge.context_remaining_pct; @@ -109,25 +123,27 @@ function evaluateConditions(bridge) { } // Cost warnings - const cost = bridge.total_cost_usd || 0; - if (cost > COST_CRITICAL_USD) { - warnings.push({ - severity: 3, - type: 'cost', - message: `COST CRITICAL: Session cost is $${cost.toFixed(2)}. ` + 'Stop and inform the user about high cost before continuing.' - }); - } else if (cost > COST_WARNING_USD) { - warnings.push({ - severity: 2, - type: 'cost', - message: `COST WARNING: Session cost is $${cost.toFixed(2)}. ` + 'Review whether the current approach justifies the expense.' - }); - } else if (cost > COST_NOTICE_USD) { - warnings.push({ - severity: 1, - type: 'cost', - message: `COST NOTICE: Session cost is $${cost.toFixed(2)}. ` + 'Consider whether the current approach is efficient.' - }); + if (options.costWarnings !== false) { + const cost = bridge.total_cost_usd || 0; + if (cost > COST_CRITICAL_USD) { + warnings.push({ + severity: 3, + type: 'cost', + message: `COST CRITICAL: Session cost is $${cost.toFixed(2)}. ` + 'Stop and inform the user about high cost before continuing.' + }); + } else if (cost > COST_WARNING_USD) { + warnings.push({ + severity: 2, + type: 'cost', + message: `COST WARNING: Session cost is $${cost.toFixed(2)}. ` + 'Review whether the current approach justifies the expense.' + }); + } else if (cost > COST_NOTICE_USD) { + warnings.push({ + severity: 1, + type: 'cost', + message: `COST NOTICE: Session cost is $${cost.toFixed(2)}. ` + 'Consider whether the current approach is efficient.' + }); + } } // File scope warning @@ -185,7 +201,7 @@ function run(rawInput) { // If bridge is stale, null out context data (still check cost/scope/loop) const evalBridge = isStale ? { ...bridge, context_remaining_pct: null } : bridge; - const warnings = evaluateConditions(evalBridge); + const warnings = evaluateConditions(evalBridge, { costWarnings: costWarningsEnabled() }); if (warnings.length === 0) return rawInput; // Debounce logic @@ -239,4 +255,4 @@ if (require.main === module) { }); } -module.exports = { run, evaluateConditions, detectLoop, severityLabel }; +module.exports = { run, evaluateConditions, detectLoop, severityLabel, costWarningsEnabled }; diff --git a/tests/hooks/ecc-context-monitor.test.js b/tests/hooks/ecc-context-monitor.test.js index bbecc4ed..38ee8ef3 100644 --- a/tests/hooks/ecc-context-monitor.test.js +++ b/tests/hooks/ecc-context-monitor.test.js @@ -5,8 +5,12 @@ */ const assert = require('assert'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); -const { run, evaluateConditions, detectLoop, severityLabel } = require('../../scripts/hooks/ecc-context-monitor'); +const { run, evaluateConditions, detectLoop, severityLabel, costWarningsEnabled } = require('../../scripts/hooks/ecc-context-monitor'); +const { getBridgePath, writeBridgeAtomic } = require('../../scripts/lib/session-bridge'); // Test helper function test(name, fn) { @@ -21,6 +25,18 @@ function test(name, fn) { } } +function withEnv(name, value, fn) { + const original = process.env[name]; + try { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + return fn(); + } finally { + if (original === undefined) delete process.env[name]; + else process.env[name] = original; + } +} + function runTests() { console.log('\n=== Testing ecc-context-monitor.js ===\n'); @@ -113,6 +129,53 @@ function runTests() { passed++; else failed++; + if ( + test('cost warnings can be suppressed without hiding context warnings', () => { + const warnings = evaluateConditions({ total_cost_usd: 55, context_remaining_pct: 20 }, { costWarnings: false }); + assert.strictEqual(warnings.find(w => w.type === 'cost'), undefined); + const ctx = warnings.find(w => w.type === 'context'); + assert.ok(ctx, 'Expected context warning to remain enabled'); + assert.strictEqual(ctx.severity, 3); + }) + ) + passed++; + else failed++; + + if ( + test('ECC_CONTEXT_MONITOR_COST_WARNINGS=off disables only run-time cost warnings', () => { + const sessionId = `ctx-monitor-cost-off-${process.pid}-${Date.now()}`; + const input = JSON.stringify({ session_id: sessionId, tool_name: 'Bash' }); + const warnPath = path.join(os.tmpdir(), `ecc-ctx-warn-${sessionId}.json`); + try { + writeBridgeAtomic(sessionId, { + context_remaining_pct: 20, + total_cost_usd: 55, + last_timestamp: new Date().toISOString() + }); + const result = withEnv('ECC_CONTEXT_MONITOR_COST_WARNINGS', 'off', () => JSON.parse(run(input))); + const message = result.hookSpecificOutput.additionalContext; + assert.ok(message.includes('CONTEXT CRITICAL'), 'Expected context warning to remain'); + assert.ok(!message.includes('COST CRITICAL'), 'Expected cost warning to be suppressed'); + } finally { + fs.rmSync(getBridgePath(sessionId), { force: true }); + fs.rmSync(warnPath, { force: true }); + } + }) + ) + passed++; + else failed++; + + if ( + test('cost warning env defaults on and accepts false-like values', () => { + assert.strictEqual(withEnv('ECC_CONTEXT_MONITOR_COST_WARNINGS', undefined, () => costWarningsEnabled()), true); + assert.strictEqual(withEnv('ECC_CONTEXT_MONITOR_COST_WARNINGS', 'false', () => costWarningsEnabled()), false); + assert.strictEqual(withEnv('ECC_CONTEXT_MONITOR_COST_WARNINGS', '0', () => costWarningsEnabled()), false); + assert.strictEqual(withEnv('ECC_CONTEXT_MONITOR_COST_WARNINGS', 'yes', () => costWarningsEnabled()), true); + }) + ) + passed++; + else failed++; + // evaluateConditions — scope warnings console.log('\nevaluateConditions (scope):');