#!/usr/bin/env node /** * Auto-Compact Event Recorder * * Records observable metrics when an auto-compact (context window compression) * occurs in Claude Code sessions. This helps build empirical understanding of * what triggers compaction since we cannot directly measure Claude's internal * context window consumption. * * Usage: * node scripts/record-auto-compact.js [options] * * Options: * --tokens / Token count from system reminder (e.g., 89195/200000) * --messages Message count from system reminder * --note "message" Optional note about what was happening * --auto Automatic detection (no prompts) * * This script should be run IMMEDIATELY AFTER noticing a compact event: * - Session summary message appears * - Context seems to reset * - Previous conversation details become unavailable * * The goal is to build a dataset correlating observable metrics with actual * compact events, which can later inform heuristic warnings. */ const fs = require('fs'); const path = require('path'); const readline = require('readline'); const SESSION_STATE_PATH = path.join(__dirname, '../.claude/session-state.json'); // Parse command line arguments function parseArgs() { const args = process.argv.slice(2); const options = { tokens: null, budget: null, messages: null, note: null, auto: false }; for (let i = 0; i < args.length; i++) { switch (args[i]) { case '--tokens': const [current, budget] = args[++i].split('/').map(s => parseInt(s.trim())); options.tokens = current; options.budget = budget; break; case '--messages': options.messages = parseInt(args[++i]); break; case '--note': options.note = args[++i]; break; case '--auto': options.auto = true; break; case '--help': console.log(` Auto-Compact Event Recorder - Tractatus Framework This tool records observable metrics when Claude Code auto-compaction occurs. Run IMMEDIATELY after noticing a compact event to capture accurate data. Usage: node scripts/record-auto-compact.js [options] Options: --tokens / Token usage (e.g., 89195/200000) --messages Message count --note "message" What were you doing when compact occurred --auto Non-interactive mode --help Show this help Examples: # Interactive mode (prompts for missing values) node scripts/record-auto-compact.js --tokens 150000/200000 # Fully specified node scripts/record-auto-compact.js --tokens 145000/200000 --messages 45 --note "Large file reads" # Automatic mode (use current session state) node scripts/record-auto-compact.js --auto `); process.exit(0); } } return options; } // Load session state function loadSessionState() { if (!fs.existsSync(SESSION_STATE_PATH)) { console.error(`Error: Session state not found at ${SESSION_STATE_PATH}`); console.error('Run: node scripts/session-init.js'); process.exit(1); } return JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8')); } // Save session state function saveSessionState(state) { fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(state, null, 2)); } // Prompt user for input async function promptFor(question, defaultValue = null) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `; rl.question(prompt, (answer) => { rl.close(); resolve(answer.trim() || defaultValue); }); }); } // Calculate session duration function calculateDuration(startTime) { const start = new Date(startTime); const now = new Date(); const diffMs = now - start; const hours = Math.floor(diffMs / (1000 * 60 * 60)); const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); return { hours, minutes, totalMinutes: Math.floor(diffMs / (1000 * 60)) }; } // Main recording function async function recordCompactEvent(options) { console.log('\n╔════════════════════════════════════════════════════════════════╗'); console.log('║ Auto-Compact Event Recorder ║'); console.log('╚════════════════════════════════════════════════════════════════╝\n'); // Load current session state const sessionState = loadSessionState(); console.log(`Session ID: ${sessionState.session_id}`); console.log(`Started: ${new Date(sessionState.started).toLocaleString()}\n`); // Initialize auto_compact_events array if not present if (!sessionState.auto_compact_events) { sessionState.auto_compact_events = []; } // Calculate session duration const duration = calculateDuration(sessionState.started); console.log(`Session Duration: ${duration.hours}h ${duration.minutes}m\n`); // Gather metrics (prompt if not provided) let tokens = options.tokens; let budget = options.budget; let messages = options.messages; let note = options.note; if (!options.auto) { if (tokens === null) { const tokenInput = await promptFor('Token usage (format: current/budget)', `${sessionState.token_estimate}/${sessionState.staleness_thresholds.tokens || 200000}`); [tokens, budget] = tokenInput.split('/').map(s => parseInt(s.trim())); } if (messages === null) { const messageInput = await promptFor('Message count', sessionState.message_count); messages = parseInt(messageInput); } if (note === null) { note = await promptFor('What were you doing? (optional)', null); } } else { // Auto mode: use session state tokens = sessionState.token_estimate || 0; budget = 200000; // Default budget messages = sessionState.message_count || 0; note = 'Auto-detected compact'; } // Get pressure score from last monitoring activity const lastPressure = sessionState.last_framework_activity?.ContextPressureMonitor; const pressureLevel = lastPressure?.last_level || 'UNKNOWN'; const pressureScore = lastPressure?.last_score || null; // Build compact event record const compactEvent = { timestamp: new Date().toISOString(), session_id: sessionState.session_id, metrics: { tokens_at_compact: tokens, token_budget: budget, token_percentage: budget > 0 ? ((tokens / budget) * 100).toFixed(1) : null, messages: messages, action_count: sessionState.action_count || 0, session_duration_minutes: duration.totalMinutes, pressure_level: pressureLevel, pressure_score: pressureScore }, framework_activity: { cross_reference_validations: sessionState.framework_components?.CrossReferenceValidator?.validations_performed || 0, bash_command_validations: sessionState.framework_components?.BashCommandValidator?.validations_performed || 0, bash_blocks_issued: sessionState.framework_components?.BashCommandValidator?.blocks_issued || 0 }, note: note || null }; // Add to history sessionState.auto_compact_events.push(compactEvent); // Save updated state saveSessionState(sessionState); // Display summary console.log('✅ Compact event recorded!\n'); console.log('Metrics Captured:'); console.log(` Tokens: ${compactEvent.metrics.tokens_at_compact} / ${compactEvent.metrics.token_budget} (${compactEvent.metrics.token_percentage}%)`); console.log(` Messages: ${compactEvent.metrics.messages}`); console.log(` Actions: ${compactEvent.metrics.action_count}`); console.log(` Duration: ${duration.hours}h ${duration.minutes}m`); console.log(` Pressure Level: ${compactEvent.metrics.pressure_level}`); if (compactEvent.metrics.pressure_score !== null) { console.log(` Pressure Score: ${(compactEvent.metrics.pressure_score * 100).toFixed(1)}%`); } console.log(`\nTotal Compacts: ${sessionState.auto_compact_events.length}\n`); // Show compact frequency if (sessionState.auto_compact_events.length > 1) { const allTokens = sessionState.auto_compact_events.map(e => e.metrics.tokens_at_compact); const avgTokens = allTokens.reduce((a, b) => a + b, 0) / allTokens.length; const allMessages = sessionState.auto_compact_events.map(e => e.metrics.messages); const avgMessages = allMessages.reduce((a, b) => a + b, 0) / allMessages.length; console.log('Historical Averages:'); console.log(` Avg Tokens at Compact: ${avgTokens.toFixed(0)}`); console.log(` Avg Messages at Compact: ${avgMessages.toFixed(1)}`); console.log(); } console.log('Data saved to:', SESSION_STATE_PATH); console.log('\nRun: node scripts/check-session-pressure.js --tokens /'); console.log(' to see compact statistics and predictions\n'); } // Run if called directly if (require.main === module) { const options = parseArgs(); recordCompactEvent(options).catch(err => { console.error('Error:', err.message); process.exit(1); }); } module.exports = { recordCompactEvent, loadSessionState };