bookworm-smart-assistant/scripts/patches/patch-p0v2-stop-parallel.js

270 lines
11 KiB
JavaScript

#!/usr/bin/env node
/**
* P0v2 补丁 — 2026-04-19 performance-expert 优化方案
*
* 问题: stop-dispatcher.js 14 个子模块串行执行 → P50=4.2s / P99=12.1s / Max=29.8s
* (近 10 天 hook-slow.log 34 次慢事件, 累计 169.8s 用户等待)
*
* 修复: 改为三批编排 (Batch1 轻量并行 → Batch2 学习管线 → Batch3 尾部串行)
* - Batch1 并行 (800ms): route-auditor + session-pin + session-memory + dedup-errors + constitution
* - Batch2 并行 (2-15s): implicit→fwl 链式 + daily-health + skill-effectiveness
* - Batch3 串行: sentinel → auto-cleanup → log-rotator → auto-backup → auto-git-push
*
* 关键依赖保持:
* - implicit-feedback 必须先写 route-feedback.jsonl, fusion-weight-learner 才能读到 → Batch2 内链式
* - consistency-sentinel append evolution-log 必须早于 auto-cleanup truncate → Batch3 串行前段
* - auto-backup 必须早于 auto-git-push (备份快照先于远端) → Batch3 串行尾段
*
* 超时策略 (分档):
* - I/O 轻量: 500-800ms
* - 学习/巡检: 1500-2000ms
* - 重 IO (fwl/cleanup/backup/push): 3000-15000ms
* - env 覆盖: BWR_HOOK_TIMEOUT_MS (默认 1500)
*
* fail-open 语义:
* - 任一阶段 reject/timeout 不阻断后续 batch
* - 记录 hook-timeout.log (独立于 hook-slow.log 避免淹没)
* - process.exit(0) 始终
*
* 幂等: sentinel = 'P0V2_PARALLEL_BATCHES'
* 回滚: 还原 .bak.p0v2.{timestamp} 到 stop-dispatcher.js
*/
'use strict';
const fs = require('fs');
const path = require('path');
const TARGET = path.join(__dirname, '..', '..', 'hooks', 'stop-dispatcher.js');
const SENTINEL = 'P0V2_PARALLEL_BATCHES';
const NEW_RUNALL = `async function runAll(input) {
// ${SENTINEL} (2026-04-19 performance-expert)
// 14 子模块串行 (P99=12.1s) → 三批编排 (Batch1 轻量并行 / Batch2 学习管线 / Batch3 尾部串行)
// fail-open: 任一阶段 reject/timeout 不阻断后续 batch; 记录 debug/hook-timeout.log
const { runStageWithTimeout: race } = require('./lib/run-stage.js');
const _perf_start = Date.now();
const _stageRecords = [];
const _record = (rs) => { for (const r of rs) if (r && r.status === 'fulfilled') _stageRecords.push(r.value); };
// ─── Batch 1 · 轻量 I/O 并行 (500-800ms) ───
// 无互相依赖: 各自读写不同文件 (route-feedback.jsonl / pinned-sessions.json / session-* / hook-errors.log / constitution-report)
_record(await Promise.allSettled([
race('route-auditor', () => {
const ra = require('./route-auditor.js');
if (ra.runAudit) ra.runAudit();
}, 800),
race('session-pin', () => {
const sp = require('../scripts/session-pin.js');
if (sp.pinSession) sp.pinSession(input);
}, 500),
race('session-memory', () => {
const sm = require('../scripts/session-memory.js');
if (sm.cleanExpiredSessions) sm.cleanExpiredSessions();
}, 800),
race('dedup-errors', () => deduplicateHookErrors(), 500),
race('constitution', () => {
const csr = require('./constitution-session-report.js');
if (csr.runReport) csr.runReport();
}, 800),
]));
// ─── Batch 2 · 学习管线 + 巡检 并行 (2-15s) ───
// implicit-feedback → fusion-weight-learner 链式 (读后写依赖)
// daily-health / skill-effectiveness 各自独立冷却, 与学习管线并发无冲突
_record(await Promise.allSettled([
race('implicit→fwl', async () => {
try {
const imf = require('../scripts/implicit-feedback.js');
const fn = imf.generateImplicitFeedback || imf.inferAndWrite;
if (fn) fn({ days: 1 });
} catch {}
try {
const fwl = require('../scripts/fusion-weight-learner.js');
if (fwl.atomicWeightUpdate) {
fwl.atomicWeightUpdate();
} else {
if (fwl.bootstrapWeights) fwl.bootstrapWeights();
if (fwl.learnFusionWeights) fwl.learnFusionWeights();
if (fwl.applyImplicitWeights) fwl.applyImplicitWeights();
}
} catch {}
}, 15000),
race('daily-health', () => {
const dhs = require('../scripts/daily-health-snapshot.js');
if (dhs.main) dhs.main();
}, 2000),
race('skill-effectiveness', () => {
const se = require('../scripts/skill-effectiveness.js');
if (!se.analyze) return;
const reportFile = path.join(require('./lib/root.js'), 'debug', 'skill-effectiveness-report.json');
const lastRun = fs.existsSync(reportFile) ? fs.statSync(reportFile).mtimeMs : 0;
if (Date.now() - lastRun > 23 * 60 * 60 * 1000) se.analyze({ save: true });
}, 1500),
]));
// ─── Batch 3 · 尾部串行 (明确依赖顺序) ───
// sentinel append evolution-log → cleanup 才能 truncate
// auto-backup → auto-git-push (快照先于远端推送)
_stageRecords.push(await race('consistency-sentinel', () => runConsistencySentinel(), 1000));
_stageRecords.push(await race('auto-cleanup', () => {
const ac = require('../scripts/auto-cleanup.js');
if (ac.main) ac.main({ execute: true, ifStale: 86400 });
}, 5000));
_stageRecords.push(await race('log-rotator', () => {
const lr = require('./log-rotator.js');
if (lr.runRotation) lr.runRotation();
}, 800));
_stageRecords.push(await race('auto-backup', () => {
const backup = require('../scripts/auto-backup.js');
backup();
}, 3000));
_stageRecords.push(await race('auto-git-push', () => {
const sync = require('../scripts/auto-git-sync.js');
sync.pushChanges();
}, 5000));
// ─── 慢 Stop 总耗时监控 + W3 告警 (保持原语义) ───
try {
const _total = Date.now() - _perf_start;
if (_total > 2000) {
const debugDir = path.join(require('./lib/root.js'), 'debug');
const errLog = path.join(debugDir, 'hook-slow.log');
fs.appendFileSync(errLog, JSON.stringify({
ts: new Date().toISOString(),
hook: 'stop-dispatcher',
totalMs: _total,
stages: _stageRecords.map(r => ({ n: r.name, ok: r.ok, ms: r.ms })),
}) + '\\n');
try {
const alertSentinel = path.join(debugDir, '.slow-alert.sentinel');
const now = Date.now();
let lastAlert = 0;
try {
if (fs.existsSync(alertSentinel)) lastAlert = fs.statSync(alertSentinel).mtimeMs || 0;
} catch {}
if (now - lastAlert > 60000) {
process.stderr.write('[stop-dispatcher] 慢 Stop 事件 (' + _total + 'ms) — 详见 debug/hook-slow.log\\n');
try {
const _tmp = alertSentinel + '.tmp.' + process.pid;
fs.writeFileSync(_tmp, String(now));
fs.renameSync(_tmp, alertSentinel);
} catch {}
}
} catch {}
}
} catch {}
process.exit(0);
}
// consistency-sentinel 拆出为独立函数 (原 L171-232, P0v2 重构)
function runConsistencySentinel() {
try {
const crypto = require('crypto');
const sRoot = require('./lib/root.js');
const evoLog = path.join(sRoot, 'evolution-log.jsonl');
const findings = [];
try {
const ffPath = path.join(sRoot, 'feature-flags.json');
const sigPath = path.join(sRoot, 'feature-flags.json.sig');
if (fs.existsSync(ffPath) && fs.existsSync(sigPath)) {
const actual = crypto.createHash('sha256').update(fs.readFileSync(ffPath)).digest('hex');
const stored = fs.readFileSync(sigPath, 'utf8').trim();
if (actual !== stored) {
const tmp = sigPath + '.tmp.' + process.pid;
fs.writeFileSync(tmp, actual);
fs.renameSync(tmp, sigPath);
findings.push({ id: 'sig-drift', fix: 'auto', detail: 'feature-flags.sig 重签 ' + actual.slice(0, 8) });
}
}
} catch {}
try {
const statsPath = path.join(sRoot, 'stats-compiled.json');
const skillsDir = path.join(sRoot, 'skills');
if (fs.existsSync(statsPath) && fs.existsSync(skillsDir)) {
const stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
const actual = fs.readdirSync(skillsDir, { withFileTypes: true })
.filter(d => d.isDirectory() && !d.name.startsWith('_') && !d.name.startsWith('.'))
.filter(d => fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md')))
.length;
const declared = stats && stats.summary && stats.summary.skills;
if (declared && declared !== actual) {
findings.push({ id: 'skills-drift', fix: 'todo', detail: 'stats=' + declared + ' actual=' + actual + ', 需跑 generate-stats.js' });
}
}
} catch {}
if (findings.length > 0 && fs.existsSync(evoLog)) {
const evoContent = fs.readFileSync(evoLog, 'utf8');
const lines = evoContent.trim().split('\\n');
let maxSeq = 0;
for (const line of lines) {
try { const e = JSON.parse(line); if (e.seq > maxSeq) maxSeq = e.seq; } catch {}
}
const fixed = findings.filter(f => f.fix === 'auto').length;
const entry = {
seq: maxSeq + 1,
ts: new Date().toISOString().slice(0, 10),
version: 'v6.5.1',
scope: 'consistency-sentinel',
trigger: 'stop-dispatcher',
summary: 'T1 自动巡检: ' + findings.map(f => f.id + '(' + f.fix + ')').join(', ') + ' | ' + findings.map(f => f.detail).join(' | '),
fix_count: fixed,
tags: ['learning-loop', 'auto-sentinel']
};
const needsNewline = evoContent.length > 0 && !evoContent.endsWith('\\n');
fs.appendFileSync(evoLog, (needsNewline ? '\\n' : '') + JSON.stringify(entry) + '\\n');
}
} catch {}
}
`;
function main() {
if (!fs.existsSync(TARGET)) {
console.error('[patch-p0v2] 目标文件不存在: ' + TARGET);
process.exit(1);
}
const before = fs.readFileSync(TARGET, 'utf8');
if (before.includes(SENTINEL)) {
console.log('[patch-p0v2] 已打过补丁 (sentinel=' + SENTINEL + '),跳过');
process.exit(0);
}
// 备份原文件 (.bak.p0v2.{timestamp})
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = TARGET + '.bak.p0v2.' + ts;
fs.copyFileSync(TARGET, backupPath);
console.log('[patch-p0v2] 备份原文件: ' + path.basename(backupPath));
// 锚点匹配: 从 'function runAll(input) {' 到文件结尾 (覆盖整个 runAll + 末尾闭合)
const anchor = 'function runAll(input) {';
const idx = before.indexOf(anchor);
if (idx < 0) {
console.error('[patch-p0v2] 未找到锚点 "function runAll(input) {"');
process.exit(1);
}
// 新文件 = 头部 (require 段 + deduplicateHookErrors 函数) + 新 runAll + runConsistencySentinel
const header = before.slice(0, idx);
const after = header + NEW_RUNALL;
// 写入 (atomic: tmp + rename)
const tmp = TARGET + '.tmp.p0v2.' + process.pid;
fs.writeFileSync(tmp, after);
fs.renameSync(tmp, TARGET);
console.log('[patch-p0v2] 补丁已应用');
console.log('[patch-p0v2] - 新增 runStageWithTimeout (hooks/lib/run-stage.js)');
console.log('[patch-p0v2] - 14 子模块 → 3 批编排');
console.log('[patch-p0v2] - 分档超时 (500ms-15s, env BWR_HOOK_TIMEOUT_MS)');
console.log('[patch-p0v2] - per-module 耗时写入 hook-slow.log.stages');
console.log('[patch-p0v2] - 独立 hook-timeout.log 避免告警淹没');
console.log('[patch-p0v2] 回滚: mv "' + backupPath + '" "' + TARGET + '"');
process.exit(0);
}
main();