#!/usr/bin/env node /** * 会话上下文追踪器 (v5.0) * * 维护最近 10 次技能调用的滑动窗口, * 计算候选技能的上下文相关分数。 * * 核心函数: * recordSkillUsage(skillName) → 记录技能使用到滑动窗口 * computeContextScore(candidateSkill, composableIndex) → 上下文分数 (0~1.0) */ const fs = require('fs'); const path = require('path'); const detectClaudeRoot = () => require('./paths.config.js').PATHS.root; const ROOT = detectClaudeRoot(); const STATE_FILE = path.join(ROOT, 'debug', 'context-state.json'); // v5.9: 多项目隔离 — 按 cwd 分离状态文件 let _isolator = null; try { _isolator = require('./project-isolator.js'); } catch {} function getStateFile(cwd) { if (_isolator && cwd) return _isolator.getIsolatedPath('context-state.json', cwd); return STATE_FILE; } const MAX_WINDOW = 10; const DECAY_FACTOR = 0.70; // v5.9: 降低衰减因子 (0.85→0.70),半衰期从 4.3 次降到 2.3 次 const ANTI_STICKY_THRESHOLD = 3; // 连续同技能超过此次数时降低同技能加成 /** * 加载上下文状态 * @param {string} [cwd] - 工作目录 (v5.9 多项目隔离) * @returns {{ recentSkills: string[], updatedAt: string }} */ function loadState(cwd) { try { const file = getStateFile(cwd); if (fs.existsSync(file)) { return JSON.parse(fs.readFileSync(file, 'utf8')); } } catch {} return { recentSkills: [], updatedAt: new Date().toISOString() }; } /** * 保存上下文状态 * @param {Object} state * @param {string} [cwd] - 工作目录 (v5.9 多项目隔离) */ function saveState(state, cwd) { const file = getStateFile(cwd); const dir = path.dirname(file); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); // P1-2: O_EXCL 文件锁 + 原子写入 (防并发竞态) const lockFile = file + '.lock'; try { const lockFd = fs.openSync(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY); fs.writeSync(lockFd, String(process.pid)); fs.closeSync(lockFd); } catch { try { const lockAge = Date.now() - fs.statSync(lockFile).mtimeMs; if (lockAge < 10000) return; // 未过期,跳过 fs.unlinkSync(lockFile); const lockFd2 = fs.openSync(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY); fs.writeSync(lockFd2, String(process.pid)); fs.closeSync(lockFd2); } catch { return; } } try { const tmpFile = file + '.tmp.' + process.pid; fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2) + '\n'); fs.renameSync(tmpFile, file); } finally { try { fs.unlinkSync(lockFile); } catch {} } } /** * 记录技能使用到滑动窗口 * @param {string} skillName - 技能名称 * @param {string} [cwd] - 工作目录 (v5.9 多项目隔离) */ function recordSkillUsage(skillName, cwd) { const state = loadState(cwd); state.recentSkills.push(skillName); // 保持窗口大小 if (state.recentSkills.length > MAX_WINDOW) { state.recentSkills = state.recentSkills.slice(-MAX_WINDOW); } state.updatedAt = new Date().toISOString(); saveState(state, cwd); } /** * 计算候选技能的上下文相关分数 * @param {string} candidateSkill - 候选技能名 * @param {Object} composableIndex - { skillName → { enhances, requires, conflicts } } * @param {Object} [preloadedState] - 预加载的状态 (避免重复 I/O,由调用方一次性加载) * @returns {number} 上下文分数 (0~1.0) */ function computeContextScore(candidateSkill, composableIndex, preloadedState) { const state = preloadedState || loadState(); const recent = state.recentSkills; if (recent.length === 0) return 0; let score = 0; // v5.9: 计算最近连续同技能次数 (反粘滞检测) let consecutiveSame = 0; for (let j = recent.length - 1; j >= 0; j--) { if (recent[j] === candidateSkill) consecutiveSame++; else break; } for (let i = 0; i < recent.length; i++) { const recentSkill = recent[recent.length - 1 - i]; // 最新的在前 const decay = Math.pow(DECAY_FACTOR, i); const comp = composableIndex[recentSkill] || {}; // 同技能重复使用: 反粘滞 — 连续 >= 3 次时降低加成 (0.3→0.1) if (recentSkill === candidateSkill) { const sameBoost = consecutiveSame >= ANTI_STICKY_THRESHOLD ? 0.1 : 0.3; score += sameBoost * decay; continue; } // composable enhances 关系 +0.5 if (comp.enhances && comp.enhances.includes(candidateSkill)) { score += 0.5 * decay; } // composable requires 关系 +0.4 if (comp.requires && comp.requires.includes(candidateSkill)) { score += 0.4 * decay; } // 反向: 候选技能 enhances 最近使用的技能 const candidateComp = composableIndex[candidateSkill] || {}; if (candidateComp.enhances && candidateComp.enhances.includes(recentSkill)) { score += 0.3 * decay; } } // 上限 1.0 // P2-2: sigmoid 归一化替代硬截断,保留高分区间区分度 return score <= 0 ? 0 : Math.round((2.0 / (1.0 + Math.exp(-score)) - 1.0) * 100) / 100; } /** * 从 skills-index.json 构建 composable 索引 * @param {Object} index - skills-index.json * @returns {Object} { skillName → composable } */ function buildComposableIndex(index) { const result = {}; for (const skill of (index.skills || [])) { if (skill.composable) { result[skill.name] = skill.composable; } } return result; } // 模块导出 if (typeof module !== 'undefined') { module.exports = { loadState, saveState, recordSkillUsage, computeContextScore, buildComposableIndex, MAX_WINDOW, DECAY_FACTOR, ANTI_STICKY_THRESHOLD, }; } // CLI 入口 if (require.main === module) { const state = loadState(); console.log('=== 上下文状态 ==='); console.log(`最近技能 (${state.recentSkills.length}/${MAX_WINDOW}):`); for (let i = 0; i < state.recentSkills.length; i++) { const decay = Math.pow(DECAY_FACTOR, state.recentSkills.length - 1 - i); console.log(` ${i + 1}. ${state.recentSkills[i]} (decay: ${decay.toFixed(3)})`); } console.log(`更新时间: ${state.updatedAt}`); }