103 lines
3.5 KiB
JavaScript
103 lines
3.5 KiB
JavaScript
|
|
#!/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);
|
|||
|
|
})();
|