bookworm-smart-assistant/hooks/memory-persistence-trigger.js

158 lines
4.8 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: Pre-Compaction 记忆持久化触发器
* @version 1.0.0
* @file memory-persistence-trigger.js
* @matcher Bash|Edit|Write|Read|Glob|Grep|Skill|Agent
*
* 功能: 追踪工具调用次数,每 30 次提醒 Claude 持久化重要记忆
* 触发条件: count 达到 30 的倍数时30、60、90...
* 冷却机制: 触发后 5 分钟内不重复触发
*
* stdin: { "session_id": "...", "tool_name": "...", "tool_input": {...}, "tool_output": "..." }
* stdout: { "hookSpecificOutput": { "additionalContext": "..." } } (仅在触发时)
* 退出码: 0 (始终放行fail-open)
*
* 性能目标: <5ms (仅读写一个小 JSON 文件 + 计数器判断)
*/
const fs = require('fs');
const path = require('path');
const readStdin = require('./lib/read-stdin.js');
// ─── 路径解析 ────────────────────────────────────────
let debugDir;
try {
const { PATHS } = require('../scripts/paths.config.js');
debugDir = PATHS.debugDir;
} catch {
debugDir = path.resolve(__dirname, '..', 'debug');
}
const COUNTER_FILE = path.join(debugDir, 'tool-call-counter.json');
const TRIGGER_INTERVAL = 30; // 每 30 次触发
const COOLDOWN_MS = 5 * 60 * 1000; // 5 分钟冷却
// ─── 计数器文件读写 ──────────────────────────────────
/**
* 读取计数器状态
* @returns {{ count: number, lastTrigger: string, sessionId: string }}
*/
function readCounter() {
try {
if (fs.existsSync(COUNTER_FILE)) {
return JSON.parse(fs.readFileSync(COUNTER_FILE, 'utf8'));
}
} catch {}
return { count: 0, lastTrigger: '', sessionId: '' };
}
/**
* 写入计数器状态 (原子写入: temp+rename 防止竞态截断)
* @param {{ count: number, lastTrigger: string, sessionId: string }} state
*/
function writeCounter(state) {
try {
if (!fs.existsSync(debugDir)) {
fs.mkdirSync(debugDir, { recursive: true });
}
var tmp = COUNTER_FILE + '.tmp.' + process.pid;
fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n');
fs.renameSync(tmp, COUNTER_FILE);
} catch {}
}
/**
* 检查是否在冷却期内
* @param {string} lastTrigger - ISO 时间字符串
* @returns {boolean}
*/
function isInCooldown(lastTrigger) {
if (!lastTrigger) return false;
try {
const elapsed = Date.now() - new Date(lastTrigger).getTime();
return elapsed < COOLDOWN_MS;
} catch {
return false;
}
}
/**
* 构建 additionalContext 提醒文本
* @param {number} count - 当前工具调用次数
* @returns {string}
*/
function buildReminder(count) {
return `[MEMORY_PERSISTENCE_TRIGGER] 你已进行了较多工具调用(${count}次),上下文可能即将被压缩。
如果本次会话有重要决策、关键发现或架构变更,请考虑写入记忆文件。
写入规则:
- 稳定结论写入 MEMORY.md 对应章节
- 项目细节写入 memory/ 下的专题文件
- 章节标题加 #tag 标签便于检索 (如 ## 部署记录 #deploy #服务器)
- 常用标签: #bookworm #deploy #agenttin #wan22 #ai #安全 #架构 #debug #进度
- 禁止写入: API Key、密码、token、SSH 密钥等敏感信息
- 禁止写入: 会话临时状态、未验证的猜测
检索命令: node ~/.claude/scripts/memory-search.js "<关键词>" [--tag <标签>]`;
}
// ─── 主流程 ──────────────────────────────────────────
function main() {
readStdin({ maxSize: 128 * 1024 }).then(input => {
const sessionId = input.session_id || 'unknown';
// 读取当前计数器状态
const state = readCounter();
// 新会话检测: sessionId 变化则重置计数器
if (state.sessionId && state.sessionId !== sessionId) {
state.count = 0;
state.lastTrigger = '';
}
state.sessionId = sessionId;
// 递增计数器
state.count++;
// 判断是否需要触发提醒
const shouldTrigger = (state.count % TRIGGER_INTERVAL === 0)
&& !isInCooldown(state.lastTrigger);
if (shouldTrigger) {
// 更新触发时间
state.lastTrigger = new Date().toISOString();
writeCounter(state);
// 输出 additionalContext 提醒
const output = {
hookSpecificOutput: {
additionalContext: buildReminder(state.count)
}
};
process.stdout.write(JSON.stringify(output));
} else {
// 仅更新计数器,无输出
writeCounter(state);
}
process.exit(0);
}).catch(() => process.exit(0));
}
// 模块导出 (供测试使用)
if (typeof module !== 'undefined') {
module.exports = {
readCounter,
writeCounter,
isInCooldown,
buildReminder,
COUNTER_FILE,
TRIGGER_INTERVAL,
COOLDOWN_MS,
};
}
if (require.main === module) {
main();
}