Files
bitvid/tools/fleet/bin/core/wake-log.mjs
T
2026-10-24 15:42:46 -04:00

86 lines
5.3 KiB
JavaScript

// One JSON line per wake attempt, in the fleet state dir (not the checkout).
// The line records what the pane looked like and what the nudge did. It does
// not record mail bodies. The file is capped so it cannot grow without bound.
import fs from 'node:fs';
import path from 'node:path';
import { writeAtomic } from '../lib.mjs';
export const WAKE_LOG_MAX = 400;
export function wakeLogPath(c) {
return path.join(c.stateDir, 'wake-log.jsonl');
}
export function appendWakeLog(c, entry) {
const file = wakeLogPath(c);
fs.mkdirSync(path.dirname(file), { recursive: true });
let lines = [];
try { lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean); } catch { /* new log */ }
lines.push(JSON.stringify(entry));
if (lines.length > WAKE_LOG_MAX) lines = lines.slice(lines.length - WAKE_LOG_MAX);
writeAtomic(file, `${lines.join('\n')}\n`);
}
export function priorWakes(c, area, file) {
return readRaw(c).filter((e) => e && e.kind !== 'stale' && e.area === area && e.file === file);
}
function readRaw(c) {
try {
return fs.readFileSync(wakeLogPath(c), 'utf8').split('\n').filter(Boolean).map((line) => {
try { return JSON.parse(line); } catch { return null; }
}).filter(Boolean);
} catch { return []; }
}
/** Plain words for the failure a later reader should tell the user. */
export function wakeFlag(entry) {
if (!entry) return 'unknown';
if (entry.kind === 'stale') {
const last = Array.isArray(entry.prior) ? entry.prior[entry.prior.length - 1] : null;
const age = entry.ageSec ?? '?';
if (!last) return `still unread after ${age}s; no earlier wake`;
if (last.outcome === 'deferred-enter' || (last.action === 'later-enter' && last.outcome !== 'submitted')) return `woke at ${last.t} (${last.trigger}), wake line still in the composer, still unread after ${age}s`;
if (last.outcome === 'swallowed') return `woke at ${last.t} (${last.trigger}), wake line still in the composer, still unread after ${age}s`;
if (last.outcome === 'timeout') return `woke at ${last.t} (${last.trigger}), pane stayed busy until the waiter stopped, still unread after ${age}s`;
if (last.outcome === 'deferred' || last.action === 'deferred') return `woke at ${last.t} (${last.trigger}), handed to a waiter because the pane was busy, still unread after ${age}s`;
if (last.action === 'skipped-busy') return `woke at ${last.t} (${last.trigger}), skipped because an interrupt hint was showing, still unread after ${age}s`;
if (last.action === 'no-window' || last.outcome === 'no-window') return `woke at ${last.t} (${last.trigger}), pane not found, still unread after ${age}s`;
if (last.outcome === 'submitted') return `woke at ${last.t} (${last.trigger}), line left the composer, still unread after ${age}s`;
return `woke at ${last.t} (${last.trigger}), outcome ${last.outcome}, still unread after ${age}s`;
}
if (entry.action === 'no-window' || entry.outcome === 'no-window') {
const names = Array.isArray(entry.windows) && entry.windows.length ? entry.windows.join(', ') : 'none';
const looked = entry.lookup || entry.window || 'unknown window';
const sock = entry.socket ? `socket ${entry.socket}${entry.socketExists === false ? ' missing' : ''}` : 'no recorded socket';
return `pane not found (looked up ${looked}; ${sock}; session windows: ${names})`;
}
if (entry.action === 'later-enter' && entry.outcome === 'submitted') {
return entry.enterAttempts ? 'a later Enter left the composer' : 'the wake line left the composer before the later Enter';
}
if (entry.action === 'later-enter' && entry.outcome === 'swallowed') return `a later Enter still left the wake line in the composer (${entry.enterAttempts} more)`;
if (entry.outcome === 'deferred-enter') return `wake line still in the composer after ${entry.enterAttempts} Enter; a later Enter is waiting`;
if (entry.outcome === 'waiter-running') return 'a waiter is already polling this pane';
if (entry.outcome === 'already-read') return 'mail was read before the pane went idle, so nothing was typed';
if (entry.outcome === 'timeout') return 'pane stayed busy until the waiter stopped';
if (entry.outcome === 'deferred') return 'handed to a waiter because an interrupt hint was showing';
if (entry.action === 'skipped-busy') return 'skipped: interrupt hint was showing, so the pane looked mid-turn';
if (entry.action === 'send-failed') return 'send-keys failed before Enter';
if (entry.trigger === 'deferred' && entry.outcome === 'swallowed') return `waiter typed it; wake line still on the cursor row after ${entry.enterAttempts} Enter`;
if (entry.outcome === 'swallowed') return `wake line still on the cursor row after ${entry.enterAttempts} Enter`;
if (entry.trigger === 'deferred' && entry.outcome === 'submitted') return 'waiter typed the line and it left the composer';
if (entry.outcome === 'submitted') {
if (entry.before && entry.before.idleSec != null && entry.before.idleSec < 3 && !entry.before.interrupt) {
return 'typed and the line left the composer while the pane was recently active';
}
return 'typed and the line left the composer';
}
return entry.outcome || entry.action || 'unknown';
}
export function readWakeLog(c, { limit = 40 } = {}) {
const all = readRaw(c);
const entries = all.slice(-Math.max(1, Number(limit) || 40)).map((entry) => ({ ...entry, flag: wakeFlag(entry) }));
return { path: wakeLogPath(c), entries };
}