fix: add context monitor cost warning opt-out

This commit is contained in:
Affaan Mustafa
2026-05-17 01:43:50 -04:00
committed by Affaan Mustafa
parent 471dee27ec
commit b47dfa95a3
5 changed files with 140 additions and 23 deletions

View File

@@ -476,6 +476,15 @@ export ECC_SESSION_START_MAX_CHARS=4000
# Disable SessionStart additional context entirely for low-context/local-model setups # Disable SessionStart additional context entirely for low-context/local-model setups
export ECC_SESSION_START_CONTEXT=off 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 | | `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 | | `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 | | `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: 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) | | `/compact` | At logical task breakpoints (research done, milestone complete) |
| `/cost` | Monitor token spending during session | | `/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 ### 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. 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.

View File

@@ -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. | | `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. | | `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. | | `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 ### 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). | | `/compact` | At logical task breakpoints (after planning, after debugging, before switching focus). |
| `/cost` | Check token spending for the current session. | | `/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 ### 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. 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.

View File

@@ -110,6 +110,15 @@ export ECC_SESSION_START_MAX_CHARS=4000
# Disable SessionStart additional context entirely # Disable SessionStart additional context entirely
export ECC_SESSION_START_CONTEXT=off 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: Profiles:

View File

@@ -24,6 +24,20 @@ const LOOP_THRESHOLD = 3;
const STALE_SECONDS = 60; const STALE_SECONDS = 60;
const DEBOUNCE_CALLS = 5; 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. * Get debounce state file path.
* @param {string} sessionId * @param {string} sessionId
@@ -84,7 +98,7 @@ function detectLoop(recentTools) {
* Evaluate all warning conditions against bridge data. * Evaluate all warning conditions against bridge data.
* Returns array of {severity, type, message} sorted by severity desc. * Returns array of {severity, type, message} sorted by severity desc.
*/ */
function evaluateConditions(bridge) { function evaluateConditions(bridge, options = {}) {
const warnings = []; const warnings = [];
const remaining = bridge.context_remaining_pct; const remaining = bridge.context_remaining_pct;
@@ -109,25 +123,27 @@ function evaluateConditions(bridge) {
} }
// Cost warnings // Cost warnings
const cost = bridge.total_cost_usd || 0; if (options.costWarnings !== false) {
if (cost > COST_CRITICAL_USD) { const cost = bridge.total_cost_usd || 0;
warnings.push({ if (cost > COST_CRITICAL_USD) {
severity: 3, warnings.push({
type: 'cost', severity: 3,
message: `COST CRITICAL: Session cost is $${cost.toFixed(2)}. ` + 'Stop and inform the user about high cost before continuing.' 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({ } else if (cost > COST_WARNING_USD) {
severity: 2, warnings.push({
type: 'cost', severity: 2,
message: `COST WARNING: Session cost is $${cost.toFixed(2)}. ` + 'Review whether the current approach justifies the expense.' 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({ } else if (cost > COST_NOTICE_USD) {
severity: 1, warnings.push({
type: 'cost', severity: 1,
message: `COST NOTICE: Session cost is $${cost.toFixed(2)}. ` + 'Consider whether the current approach is efficient.' type: 'cost',
}); message: `COST NOTICE: Session cost is $${cost.toFixed(2)}. ` + 'Consider whether the current approach is efficient.'
});
}
} }
// File scope warning // File scope warning
@@ -185,7 +201,7 @@ function run(rawInput) {
// If bridge is stale, null out context data (still check cost/scope/loop) // If bridge is stale, null out context data (still check cost/scope/loop)
const evalBridge = isStale ? { ...bridge, context_remaining_pct: null } : bridge; 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; if (warnings.length === 0) return rawInput;
// Debounce logic // Debounce logic
@@ -239,4 +255,4 @@ if (require.main === module) {
}); });
} }
module.exports = { run, evaluateConditions, detectLoop, severityLabel }; module.exports = { run, evaluateConditions, detectLoop, severityLabel, costWarningsEnabled };

View File

@@ -5,8 +5,12 @@
*/ */
const assert = require('assert'); 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 // Test helper
function test(name, fn) { 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() { function runTests() {
console.log('\n=== Testing ecc-context-monitor.js ===\n'); console.log('\n=== Testing ecc-context-monitor.js ===\n');
@@ -113,6 +129,53 @@ function runTests() {
passed++; passed++;
else failed++; 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 // evaluateConditions — scope warnings
console.log('\nevaluateConditions (scope):'); console.log('\nevaluateConditions (scope):');