/* * Task 8 — a watch over an append-only log, keyed on FILE OFFSET. * * A WATCH MUST NOT BE ABLE TO CONSUME. This is Task 7.1's rule at its second * caller, not a second fix: the read cursor records what the AGENT has taken * delivery of, and only the agent reading may advance it. A pusher typing into * a pane is a reader; a watch waiting for traffic is a reader; neither may * consume on the agent's behalf. * * So this keeps its OWN position — `lastLineSeen` — and never touches * `cursors/.json`. A watch that advanced the read cursor would reproduce * exactly the defect Task 7 removed, one component over: the message would be * marked consumed by something that only looked at it. * * WHY THIS DOES NOT VIOLATE 6.2 (8.5). "Events are derived from the record, * never parallel to it." The room and inbox JSONL logs ARE the record — they * are the authoritative store, not a projection of one. `land`'s events are * derived from a document write; these are derived from the log itself. Both * read the authoritative artifact and neither maintains a second source of * truth. The test for a violation is whether the stream could assert something * the record does not, and it cannot: every event here IS a line in the log, * identified by its offset in that log. * * Keyed on OFFSET rather than timestamp or id, because an offset is a property * of the log itself. Two lines can share a `ts`, an id can be rewritten, and a * clock can disagree with another clock — a line's position cannot. */ import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import { z } from "zod"; import { ROOT } from "../store.js"; export type LogKind = "room" | "inbox"; export type LogWatch = { id: string; agentId: string; kind: LogKind; target: string; createdAt: number; /** null until evaluated at least once — never-evaluated is an ERROR (8.3). */ lastEvaluatedAt: number | null; /** The offset this watch has REPORTED up to. Never the read cursor. */ lastLineSeen: number; }; export const logFileFor = (kind: LogKind, target: string): string => kind === "room" ? path.join(ROOT, "rooms", `${target}.jsonl`) : path.join(ROOT, "inbox", `${target}.jsonl`); /** Parsed lines of a log. Malformed lines are SKIPPED but still counted. */ export function readLog(file: string): { entries: unknown[]; total: number } { if (!existsSync(file)) return { entries: [], total: 0 }; const lines = readFileSync(file, "utf8").split("\n").filter((l) => l.trim()); const entries: unknown[] = []; for (const l of lines) { try { entries.push(JSON.parse(l)); } catch { // A malformed line still OCCUPIES an offset. Skipping it without counting // would shift every subsequent line's position by one and silently // re-report the whole tail. entries.push(null); } } return { entries, total: lines.length }; } export type LogEvent = { kind: LogKind; target: string; offset: number; line: unknown }; /** * Lines appended since `lastLineSeen`. * * A TRUNCATED LOG IS NOT A QUIET ONE. If the file is SHORTER than the watch's * position, something rewrote or pruned it — reporting "no new lines" would be * indistinguishable from a healthy quiet period. It is surfaced instead. */ export function newLines( watch: Pick, log: { entries: unknown[]; total: number }, ): { events: LogEvent[]; truncated: boolean; total: number } { if (log.total < watch.lastLineSeen) return { events: [], truncated: true, total: log.total }; const events: LogEvent[] = []; for (let i = watch.lastLineSeen; i < log.total; i++) { events.push({ kind: watch.kind, target: watch.target, offset: i, line: log.entries[i] ?? null }); } return { events, truncated: false, total: log.total }; } /** 8.3 — health, identical in shape to the record subscriptions, plus lastLineSeen. */ export function watchHealth(w: LogWatch): { level: "ok" | "error"; detail: string } { if (w.lastEvaluatedAt === null) return { level: "error", detail: `never evaluated — this watch has produced no evidence it is attached to anything. "No lines yet" and "never ran" are the same output, and only one of them is healthy.`, }; return { level: "ok", detail: `last evaluated ${new Date(w.lastEvaluatedAt).toISOString()}, reported up to line ${w.lastLineSeen}`, }; } export const logWatchSchema = { agentId: z.string().min(1), kind: z.enum(["room", "inbox"]), target: z.string().min(1), };