Files
brain/tools/mentor-journal-store.mjs
T

58 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
/**
* mentor-journal-store — персист журнала переговоров наставника (F-C2-4) + вердикта.
* Формат журнала = action-journal (jsonl-цепь + head-подпись [action-journal.mjs:83-119]);
* файлы mentor-journal-<sess>.jsonl/.head + mentor-verdict-<sess>.json. Atomic temp→rename
* (зеркало saveTaskId F-C1 / writeAtomic plan-lock). Node fs — НЕ Write-tool.
*/
import fsDefault from 'node:fs';
import { assertSafeSessionId } from './action-journal.mjs';
function paths(runtimeDir, sessionId) {
assertSafeSessionId(sessionId);
const sep = runtimeDir.endsWith('/') ? '' : '/';
return {
jsonl: `${runtimeDir}${sep}mentor-journal-${sessionId}.jsonl`,
head: `${runtimeDir}${sep}mentor-journal-${sessionId}.head`,
verdict: `${runtimeDir}${sep}mentor-verdict-${sessionId}.json`,
};
}
function writeAtomic(path, data, fsImpl) {
const tmp = `${path}.tmp`;
fsImpl.writeFileSync(tmp, data);
fsImpl.renameSync(tmp, path);
}
/** Загрузить цепь переговоров (F-C2-4: ИМЕННО это идёт в onPlanWrite.journalEntries). */
export function loadMentorJournal({ sessionId, runtimeDir, fsImpl = fsDefault }) {
const { jsonl, head } = paths(runtimeDir, sessionId);
let entries = [];
try {
entries = String(fsImpl.readFileSync(jsonl, 'utf8')).split('\n').filter(Boolean).map((l) => JSON.parse(l));
} catch (e) {
if (e && e.code === 'ENOENT') return { entries: [], headSig: null };
throw e;
}
let headSig = null;
try { headSig = String(fsImpl.readFileSync(head, 'utf8')).trim() || null; } catch { headSig = null; }
return { entries, headSig };
}
/** Персист полной цепи (выход onPlanWrite.journal = {entries, headSig}) — atomic. */
export function persistMentorJournal({ journal, sessionId, runtimeDir, fsImpl = fsDefault }) {
const { jsonl, head } = paths(runtimeDir, sessionId);
const lines = (journal && Array.isArray(journal.entries) ? journal.entries : []).map((e) => JSON.stringify(e)).join('\n');
writeAtomic(jsonl, lines ? lines + '\n' : '', fsImpl);
writeAtomic(head, String((journal && journal.headSig) ?? ''), fsImpl);
}
/** Персист/чтение последнего вердикта наставника (потребитель — freeze-gate в пути печати). */
export function persistMentorVerdict({ record, sessionId, runtimeDir, fsImpl = fsDefault }) {
writeAtomic(paths(runtimeDir, sessionId).verdict, JSON.stringify(record ?? null), fsImpl);
}
export function loadMentorVerdict({ sessionId, runtimeDir, fsImpl = fsDefault }) {
try { return JSON.parse(fsImpl.readFileSync(paths(runtimeDir, sessionId).verdict, 'utf8')); }
catch (e) { if (e && e.code === 'ENOENT') return null; throw e; }
}