Files
get-shit-done/hooks/gsd-context-monitor.js
vinicius-tersi 7542d364b4 feat: context window monitor hook with agent-side WARNING/CRITICAL alerts
Adds PostToolUse hook that reads context metrics from statusline bridge file and injects alerts into agent conversation when context is low.

Features:
- Two-tier alerts: WARNING (<=35% remaining) and CRITICAL (<=25%)
- Smart debounce: 5 tool uses between warnings, severity escalation bypasses
- Silent fail: never blocks tool execution
- Security: session_id sanitized to prevent path traversal

Ref #212
2026-02-20 14:40:08 -06:00

123 lines
4.2 KiB
JavaScript

#!/usr/bin/env node
// Context Monitor - PostToolUse hook
// Reads context metrics from the statusline bridge file and injects
// warnings when context usage is high. This makes the AGENT aware of
// context limits (the statusline only shows the user).
//
// How it works:
// 1. The statusline hook writes metrics to /tmp/claude-ctx-{session_id}.json
// 2. This hook reads those metrics after each tool use
// 3. When remaining context drops below thresholds, it injects a warning
// as additionalContext, which the agent sees in its conversation
//
// Thresholds:
// WARNING (remaining <= 35%): Agent should wrap up current task
// CRITICAL (remaining <= 25%): Agent should stop immediately and save state
//
// Debounce: 5 tool uses between warnings to avoid spam
// Severity escalation bypasses debounce (WARNING -> CRITICAL fires immediately)
const fs = require('fs');
const os = require('os');
const path = require('path');
const WARNING_THRESHOLD = 35; // remaining_percentage <= 35%
const CRITICAL_THRESHOLD = 25; // remaining_percentage <= 25%
const STALE_SECONDS = 60; // ignore metrics older than 60s
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
try {
const data = JSON.parse(input);
const sessionId = data.session_id;
if (!sessionId) {
process.exit(0);
}
const tmpDir = os.tmpdir();
const metricsPath = path.join(tmpDir, `claude-ctx-${sessionId}.json`);
// If no metrics file, this is a subagent or fresh session -- exit silently
if (!fs.existsSync(metricsPath)) {
process.exit(0);
}
const metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
const now = Math.floor(Date.now() / 1000);
// Ignore stale metrics
if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) {
process.exit(0);
}
const remaining = metrics.remaining_percentage;
const usedPct = metrics.used_pct;
// No warning needed
if (remaining > WARNING_THRESHOLD) {
process.exit(0);
}
// Debounce: check if we warned recently
const warnPath = path.join(tmpDir, `claude-ctx-${sessionId}-warned.json`);
let warnData = { callsSinceWarn: 0, lastLevel: null };
let firstWarn = true;
if (fs.existsSync(warnPath)) {
try {
warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8'));
firstWarn = false;
} catch (e) {
// Corrupted file, reset
}
}
warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
const isCritical = remaining <= CRITICAL_THRESHOLD;
const currentLevel = isCritical ? 'critical' : 'warning';
// Emit immediately on first warning, then debounce subsequent ones
// Severity escalation (WARNING -> CRITICAL) bypasses debounce
const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
// Update counter and exit without warning
fs.writeFileSync(warnPath, JSON.stringify(warnData));
process.exit(0);
}
// Reset debounce counter
warnData.callsSinceWarn = 0;
warnData.lastLevel = currentLevel;
fs.writeFileSync(warnPath, JSON.stringify(warnData));
// Build warning message
let message;
if (isCritical) {
message = `CONTEXT MONITOR CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
'STOP new work immediately. Save state NOW and inform the user that context is nearly exhausted. ' +
'If using GSD, run /gsd:pause-work to save execution state.';
} else {
message = `CONTEXT MONITOR WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
'Begin wrapping up current task. Do not start new complex work. ' +
'If using GSD, consider /gsd:pause-work to save state.';
}
const output = {
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: message
}
};
process.stdout.write(JSON.stringify(output));
} catch (e) {
// Silent fail -- never block tool execution
process.exit(0);
}
});