Files
brain/tools/observer-v4-signals.mjs
T

75 lines
2.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Pure reader of router-gate v4 runtime signals, scoped to one episode's
* [startMs, endMs] turn window. Feeds observer-transcript-parser (new
* episode.v4_signals block + task_cost.judge_spend_usd). No exec/exit.
*
* Sources (all in ~/.claude/runtime/, per-session):
* - rationalization-flags-<sess>.jsonl ({ts, kind, evidence})
* - llm-judge-verdicts-<sess>.jsonl ({ts, tool, verdict})
* - safe-baseline-actions-<sess>.jsonl ({ts, tool, action})
* - llm-judge-budget-<sess>.json ({calls})
*/
import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
function rtDir(override) {
return override || join(homedir(), '.claude', 'runtime');
}
function readJsonl(path) {
if (!existsSync(path)) return [];
try {
return readFileSync(path, 'utf-8').split('\n').filter(Boolean).map((l) => {
try { return JSON.parse(l); } catch { return null; }
}).filter(Boolean);
} catch { return []; }
}
function inWindow(rec, startMs, endMs) {
const ms = Date.parse(rec && rec.ts);
return Number.isFinite(ms) && ms >= startMs && ms <= endMs;
}
// Severity order: hard_block > soft_flag > allow.
const ACTION_RANK = { allow: 0, soft_flag: 1, hard_block: 2 };
function worstAction(records) {
let best = null;
for (const r of records) {
const a = r && r.action;
if (a == null) continue;
if (best === null || (ACTION_RANK[a] ?? -1) > (ACTION_RANK[best] ?? -1)) best = a;
}
return best;
}
export function extractV4Signals(sessionId, { startMs, endMs, baseDir } = {}) {
const dir = rtDir(baseDir);
const sess = sessionId || 'unknown';
const flags = readJsonl(join(dir, `rationalization-flags-${sess}.jsonl`))
.filter((r) => inWindow(r, startMs, endMs));
const verdicts = readJsonl(join(dir, `llm-judge-verdicts-${sess}.jsonl`))
.filter((r) => inWindow(r, startMs, endMs));
const judge_verdict = verdicts.length ? (verdicts[verdicts.length - 1].verdict ?? null) : null;
const actions = readJsonl(join(dir, `safe-baseline-actions-${sess}.jsonl`))
.filter((r) => inWindow(r, startMs, endMs));
const safe_baseline_action = worstAction(actions);
let judge_calls = 0;
const bp = join(dir, `llm-judge-budget-${sess}.json`);
if (existsSync(bp)) {
try { judge_calls = Number(JSON.parse(readFileSync(bp, 'utf-8')).calls) || 0; } catch { /* keep 0 */ }
}
return {
rationalization_flag_count: flags.length,
judge_verdict,
safe_baseline_action,
judge_calls,
};
}