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