bookworm-smart-assistant/scripts/context-tracker.js

187 lines
6.2 KiB
JavaScript
Raw Normal View History

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