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