#!/usr/bin/env node /** * Brain-retro batch reviewer (one-off, not part of canonical procedure). * * Reads docs/observer/episodes-YYYY-MM.jsonl, filters episodes in period and * without outcome_reviewed, samples N (or all), calls reviewViaDirectApi on * each (Opus 4.7 via ProxyAPI), and writes review.* fields + outcome_reviewed * + outcome_reviewed_source = "direct_api_batch" back into the JSONL file * (in-place line replacement, preserves forward-only forward fields). * * Usage: * node tools/brain-retro-batch-reviewer.mjs [limit] [concurrency] * * Example: * node tools/brain-retro-batch-reviewer.mjs docs/observer/episodes-2026-05.jsonl 2026-05-24T13:18:00Z 30 5 */ import { readFileSync, writeFileSync } from 'fs'; import { reviewViaDirectApi } from './brain-retro-opus-reviewer.mjs'; const [, , filePath, cutoff, limitStr = '30', concStr = '5'] = process.argv; if (!filePath || !cutoff) { console.error('usage: [limit=30] [concurrency=5]'); process.exit(1); } const limit = parseInt(limitStr, 10); const concurrency = parseInt(concStr, 10); const raw = readFileSync(filePath, 'utf-8'); const lines = raw.split('\n'); const lineCount = lines.length; const targets = []; // { idx, episode } for (let i = 0; i < lineCount; i++) { const line = lines[i]; if (!line.trim()) continue; let ep; try { ep = JSON.parse(line); } catch { continue; } if (ep.observer_error) continue; if (!ep.timestamps?.started_at) continue; if (ep.timestamps.started_at < cutoff) continue; if (ep.outcome_reviewed) continue; targets.push({ idx: i, episode: ep }); } const total = targets.length; const slice = targets.slice(0, limit); console.error(`[batch-reviewer] total in period unreviewed: ${total}, processing first ${slice.length} with concurrency ${concurrency}`); let done = 0; let errors = 0; const startTs = Date.now(); async function reviewOne({ idx, episode }) { try { const review = await reviewViaDirectApi(episode); if (review && !review.reviewer_error) { episode.review = review; episode.outcome_reviewed = review.outcome_reviewed ?? null; episode.outcome_reviewed_source = 'direct_api_batch'; lines[idx] = JSON.stringify(episode); done++; } else { errors++; console.error(`[batch-reviewer] ${idx}: null/error from API`); } } catch (e) { errors++; console.error(`[batch-reviewer] ${idx}: ${e.message}`); } } async function runBatched() { for (let i = 0; i < slice.length; i += concurrency) { const batch = slice.slice(i, i + concurrency); await Promise.all(batch.map(reviewOne)); const elapsed = ((Date.now() - startTs) / 1000).toFixed(1); console.error(`[batch-reviewer] progress ${done + errors}/${slice.length} (${elapsed}s)`); } } await runBatched(); // Write file back. Note: we re-serialize EVERY line we mutated, but other lines // are kept verbatim (no re-serialization that could alter ordering/escaping). writeFileSync(filePath, lines.join('\n'), 'utf-8'); const elapsed = ((Date.now() - startTs) / 1000).toFixed(1); console.error(`[batch-reviewer] done: ${done} reviewed, ${errors} errors, ${elapsed}s wall-clock`); process.exit(0);