#!/usr/bin/env node // [PATCH-X03-SESSION-ISOLATION] /** * PostToolUse Hook: 会话心跳检测器 (session-isolated) * Matcher: Edit|Write|Skill|Agent|Bash|mcp__.* * * 按 session_id 隔离计数, 多窗口不互相干扰. * 阈值策略同原版: 20(INFO) / 30(WARNING) / 40(CRITICAL) / 50+ 每10次强提醒 * 退出码: 始终 0 (纯通知, 不阻断工作流) */ 'use strict'; const fs = require('fs'); const path = require('path'); const readStdin = require('./lib/read-stdin.js'); const CLAUDE_ROOT = require('./lib/root.js'); const STATE_FILE = path.join(CLAUDE_ROOT, 'debug', 'session-heartbeat.json'); const SESSION_TIMEOUT_MS = 30 * 60 * 1000; const THRESHOLDS = [ { count: 20, level: 'INFO', msg: '当前会话已执行 {n} 次工具调用。如对话较长,可考虑 /clear 释放上下文。' }, { count: 30, level: 'WARNING', msg: '当前会话已执行 {n} 次工具调用,上下文可能接近饱和。建议在合适时机 /clear 重置上下文窗口。' }, { count: 40, level: 'CRITICAL', msg: '当前会话已执行 {n} 次工具调用,上下文压力较大。强烈建议用户 /clear 重置。如有重要任务进行中,可委托 Agent 子进程隔离执行。' }, ]; (async () => { try { let hookData = {}; try { hookData = await readStdin(); } catch {} const sid = hookData.session_id || 'default'; const debugDir = path.join(CLAUDE_ROOT, 'debug'); if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true }); let allState = {}; try { if (fs.existsSync(STATE_FILE)) { allState = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); } } catch { allState = {}; } // 清理 2 小时无活动的其他会话 (防文件膨胀) const now = Date.now(); const GC_MS = 2 * 3600 * 1000; for (const k of Object.keys(allState)) { if (k !== sid && now - (allState[k]?.lastActivity || 0) > GC_MS) delete allState[k]; } let state = allState[sid] || { count: 0, lastActivity: now, notified: [] }; // 会话超时: 30 分钟无活动 → 重置该会话 if (now - (state.lastActivity || 0) > SESSION_TIMEOUT_MS) { state = { count: 0, lastActivity: now, notified: [] }; } state.count += 1; state.lastActivity = now; let notification = null; for (const t of THRESHOLDS) { if (state.count === t.count && !state.notified.includes(t.count)) { notification = { level: t.level, msg: t.msg.replace('{n}', state.count) }; state.notified.push(t.count); break; } } if (!notification && state.count >= 50 && state.count % 10 === 0 && !state.notified.includes(state.count)) { notification = { level: 'CRITICAL', msg: `当前会话已执行 ${state.count} 次工具调用,上下文严重饱和。请立即建议用户 /clear 或将剩余任务委托给 Agent 子进程。` }; state.notified.push(state.count); } allState[sid] = state; const _tmpHb = STATE_FILE + '.tmp.' + process.pid; // [PATCH-X08-ATOMIC-WRITE] fs.writeFileSync(_tmpHb, JSON.stringify(allState), 'utf8'); fs.renameSync(_tmpHb, STATE_FILE); if (notification) { console.log(JSON.stringify({ continue: true, suppressOutput: false, systemMessage: `[session-heartbeat ${notification.level}] ${notification.msg}` })); } } catch { // Fail-open } process.exit(0); })();