Files
brain/tools/router-pin-store.mjs
T
Дмитрий f22f8bd2ef feat(router): пининг совета роутера по goalHash (переписка шагов не зовёт LLM заново)
Новый router-pin-store: пин совета роутера по (task_id, goalHash) пер-сессионно.
on-plan-write пин-aware: пин-попадание по неизменной цели → совет переиспользуется,
classifyImpl НЕ зовётся; промах/смена цели → classify + сохранение пина. Проводка в
активный наставник-хук инъекцией реального стора с sessionId (инъекция-выкл по умолчанию,
старое поведение/тесты целы). Хвост спеки роутера §4 (пининг по goalHash), эпик роутер-реестр
этап 3, item 2. Граница не тронута (recommended_chain, цепочки, observer-stop-hook, owner-seal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 08:18:15 +03:00

48 lines
2.6 KiB
JavaScript

#!/usr/bin/env node
/**
* router-pin-store — пер-сессионный пининг совета роутера по (task_id, goalHash).
* Пока цель (тело «## Цель») не менялась, совет роутера переиспользуется — LLM-классификатор
* не зовётся заново на переписку шагов плана (спека роутер-реестр §4, пининг по goalHash).
* Ключ записи — task_id; запись несёт goalHash + классификацию. Чтение возвращает совет только
* при совпавшем goalHash, иначе null. Любой сбой I/O — мягкий (load→null, save→false). goalHash
* считается из текста цели тем же способом, что якорь цели судьи (canonicalJson + sha256).
*/
import { homedir } from 'node:os';
import { join } from 'node:path';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { canonicalJson } from './receipt-sign.mjs';
export function goalHashOf(goal) {
return createHash('sha256').update(canonicalJson(String(goal ?? ''))).digest('hex');
}
function baseOf(baseDir) { return baseDir || join(homedir(), '.claude', 'runtime'); }
function pinPath(sessionId, baseDir) { return join(baseOf(baseDir), `router-pin-${sessionId || 'unknown'}.json`); }
/** Совет роутера по (taskId, текущая цель) — только если сохранённый goalHash совпал; иначе null. */
export function loadRouterPin({ taskId, planGoal, sessionId, baseDir } = {}) {
try {
const p = pinPath(sessionId, baseDir);
if (!existsSync(p)) return null;
const obj = JSON.parse(readFileSync(p, 'utf8'));
const rec = obj && obj[taskId];
if (rec && rec.goalHash === goalHashOf(planGoal)) return rec.classification ?? null;
return null;
} catch { return null; }
}
/** Записать пин для taskId (перезапись своей записи). Best-effort: сбой → false, без throw. */
export function saveRouterPin({ taskId, planGoal, classification, sessionId, baseDir, nowMs = null } = {}) {
try {
const dir = baseOf(baseDir);
mkdirSync(dir, { recursive: true });
const p = pinPath(sessionId, baseDir);
let obj = existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : {};
if (!obj || typeof obj !== 'object') obj = {};
obj[taskId] = { goalHash: goalHashOf(planGoal), classification, ts: nowMs ?? Date.now() };
writeFileSync(p, JSON.stringify(obj));
return true;
} catch { return false; }
}