bookworm-smart-assistant/hooks/session-heartbeat.js

96 lines
3.5 KiB
JavaScript
Raw Normal View History

#!/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);
})();