bookworm-smart-assistant/hooks/session-start-restore.js

126 lines
4.7 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* UserPromptSubmit Hook - 会话启动恢复器
* 1. 检测 handoff.json 存在时注入恢复上下文并归档
* 2. 每次会话首次 prompt 时重置 heartbeat 计数器解决 /clear 后计数不归零问题
*/
const fs = require('fs');
const path = require('path');
const CLAUDE_ROOT = require('./lib/root.js');
const readStdin = require('./lib/read-stdin.js');
const SESSION_STATE_DIR = path.join(CLAUDE_ROOT, 'session-state');
const HANDOFF_PATH = path.join(SESSION_STATE_DIR, 'handoff.json');
const RESTORE_MARKER = path.join(SESSION_STATE_DIR, '.session-restored');
const HEARTBEAT_FILE = path.join(CLAUDE_ROOT, 'debug', 'session-heartbeat.json');
(async () => {
try {
let hookData = {};
try { hookData = await readStdin(); } catch (_) {}
const sessionId = hookData.session_id || '';
let messages = [];
// === 1. 会话 ID 变化检测 → 重置 heartbeat ===
let lastSessionId = '';
try {
if (fs.existsSync(RESTORE_MARKER)) {
lastSessionId = fs.readFileSync(RESTORE_MARKER, 'utf8').trim();
}
} catch (_) {}
if (sessionId && sessionId !== lastSessionId) {
// 新会话: 仅重置当前 session 的 heartbeat (X03 keyed 结构) // [PATCH-X05-KEYED-RESET]
if (fs.existsSync(HEARTBEAT_FILE)) {
try {
const hbAll = JSON.parse(fs.readFileSync(HEARTBEAT_FILE, 'utf8'));
hbAll[sessionId] = { count: 0, lastActivity: Date.now(), notified: [] };
const tmp = HEARTBEAT_FILE + '.tmp.' + process.pid;
fs.writeFileSync(tmp, JSON.stringify(hbAll), 'utf8');
fs.renameSync(tmp, HEARTBEAT_FILE);
} catch {
// 损坏: 初始化为新 keyed 结构
const init = {}; init[sessionId] = { count: 0, lastActivity: Date.now(), notified: [] };
fs.writeFileSync(HEARTBEAT_FILE, JSON.stringify(init), 'utf8');
}
}
// 记录当前 session_id
if (!fs.existsSync(SESSION_STATE_DIR)) {
fs.mkdirSync(SESSION_STATE_DIR, { recursive: true });
}
fs.writeFileSync(RESTORE_MARKER, sessionId, 'utf8');
// === 1b. Auto Git Pull新会话拉取最新代码===
try {
const sync = require('../scripts/auto-git-sync.js');
const pullMsgs = sync.pullLatest();
if (pullMsgs && pullMsgs.length > 0) {
messages.push(pullMsgs.join('\n'));
}
} catch (_) {} // fail-open
}
// === 2. Handoff 恢复 ===
if (fs.existsSync(HANDOFF_PATH)) {
let handoff = {};
try {
handoff = JSON.parse(fs.readFileSync(HANDOFF_PATH, 'utf8'));
} catch (_) {
fs.unlinkSync(HANDOFF_PATH);
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
return process.exit(0);
}
// 检查是否过期24 小时)
const age = Date.now() - new Date(handoff.timestamp || 0).getTime();
if (age > 24 * 60 * 60 * 1000) {
const archiveName = `handoff-expired-${Date.now()}.json`;
fs.renameSync(HANDOFF_PATH, path.join(SESSION_STATE_DIR, archiveName));
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
return process.exit(0);
}
// 归档并注入恢复上下文
const archiveName = `handoff-${Date.now()}.json`;
fs.renameSync(HANDOFF_PATH, path.join(SESSION_STATE_DIR, archiveName));
// [PATCH-X10-ARCHIVE-CLEANUP]
try {
const archives = fs.readdirSync(SESSION_STATE_DIR)
.filter(f => /^handoff-(\d+|expired-\d+)\.json$/.test(f))
.sort();
if (archives.length > 20) {
for (const old of archives.slice(0, archives.length - 20)) {
try { fs.unlinkSync(path.join(SESSION_STATE_DIR, old)); } catch {}
}
}
} catch {}
messages.push([
'[SESSION_RESTORE] 检测到上次会话的 handoff 记录:',
`- 时间: ${handoff.timestamp}`,
`- 工作目录: ${handoff.working_directory || 'unknown'}`,
`- 工具调用数: ${handoff.tool_call_count || 'unknown'}`,
`- 摘要: ${handoff.conversation_summary || '无'}`,
'',
'请检查 memory 和 task 列表以恢复上下文。'
].join('\n'));
}
if (messages.length > 0) {
console.log(JSON.stringify({
continue: true,
suppressOutput: false,
systemMessage: messages.join('\n')
}));
} else {
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
}
} catch {
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
}
process.exit(0);
})();