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

187 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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