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

103 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* PostToolUse Hook: 会话心跳检测器
* Matcher: Edit|Write|Skill|Agent|Bash|mcp__.*
*
* 累计当前会话的工具调用次数,在达到阈值时通过 systemMessage
* 提醒 Claude 建议用户 /clear 重置上下文,防止上下文爆窗。
*
* 阈值策略 (渐进式提醒,避免通知轰炸):
* 20 次 — 轻提醒 (INFO)
* 30 次 — 中提醒 (WARNING)
* 40 次 — 强提醒 (CRITICAL)
* 50+ 次 — 每 10 次重复强提醒
*
* 会话判定: 超过 30 分钟无工具调用 → 视为新会话,计数器归零。
*
* 退出码: 始终 0 (纯通知,不阻断工作流)
* Fail-open: 任何异常 → exit(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; // 30 分钟无活动 = 新会话
// 阈值与提醒级别
const THRESHOLDS = [
{ count: 20, level: 'INFO', emoji: '', msg: '当前会话已执行 {n} 次工具调用。如对话较长,可考虑 /clear 释放上下文。' },
{ count: 30, level: 'WARNING', emoji: '', msg: '当前会话已执行 {n} 次工具调用,上下文可能接近饱和。建议在合适时机 /clear 重置上下文窗口。' },
{ count: 40, level: 'CRITICAL', emoji: '', msg: '当前会话已执行 {n} 次工具调用,上下文压力较大。强烈建议用户 /clear 重置。如有重要任务进行中,可委托 Agent 子进程隔离执行。' },
];
(async () => {
try {
await readStdin(); // 消费 stdin不需要具体内容
const debugDir = path.join(CLAUDE_ROOT, 'debug');
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
// 读取或初始化状态
let state = { count: 0, lastActivity: Date.now(), notified: [] };
try {
if (fs.existsSync(STATE_FILE)) {
state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
}
} catch { /* 损坏则重置 */ }
const now = Date.now();
// 会话超时检测: 超过 30 分钟无活动 → 新会话
if (now - (state.lastActivity || 0) > SESSION_TIMEOUT_MS) {
state = { count: 0, lastActivity: now, notified: [] };
}
// 累加计数
state.count += 1;
state.lastActivity = now;
// 检查是否命中阈值
let notification = null;
// 固定阈值 (20/30/40)
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;
}
}
// 50+ 每 10 次重复强提醒
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);
}
// 持久化状态
fs.writeFileSync(STATE_FILE, JSON.stringify(state), 'utf8');
// 输出通知
if (notification) {
console.log(JSON.stringify({
continue: true,
suppressOutput: false,
systemMessage: `[session-heartbeat ${notification.level}] ${notification.msg}`
}));
}
} catch {
// Fail-open: 任何异常不阻断工作流
}
process.exit(0);
})();