import { type TreeSnapshot } from "./tree-delta.js"; /** One running job, as the journal records it. */ export interface JournalBrokerExecution { kind: "daemon-v1"; executionId: string; } export interface JournalRow { jobId: string; laneId: string; spec?: string; cwd: string; startedAt: number; /** The task's first line, cut short, so another process can list the job recognisably. */ label?: string; /** * The job-wide git status captured before the first lane ran, plus the caller's scope. Optional * because answer-mode jobs, non-git cwd values, old rows, and trees above the bounded path limit * deliberately carry none. */ startingTree?: JournalStartingTree; /** * D1 daemon-owned execution reference. Optional/additive so pre-broker rows remain valid. * The task, environment, command line and pid are deliberately NOT persisted here. */ brokerExecution?: JournalBrokerExecution; /** * The process and the journal instance that own the row. Absent on a row written before * 2026-09-17, which is read exactly as before: an orphan. * * ⚠ Why both exist. Two hosts run their own `llm-relay mcp` process at the same time (Claude * Desktop and Codex Desktop, measured), and both write this one file. Before the owner was * recorded, the second process to start read the first one's LIVE jobs as orphans and reported * them killed, and every write by either process erased the other's rows. The pid says whether * the owner can still be running; the instance token tells two journals inside one process apart, * which is how the suite simulates a restart without a second process. */ owner?: { pid: number; instance: string; }; } export interface JournalStartingTree { prefix: string; entries: [string, string][]; scope?: string[]; } export interface JournalFile { version: 1; jobs: JournalRow[]; } export declare const JOB_JOURNAL_VERSION = 1; /** * Where the journal lives. Cache-kind (re-fetchable: it describes live processes, and a lost row * costs one unreported death, never a credential or an operator's own edit), and XDG-aware through * the one policy in `state-paths.ts` rather than a fourteenth hand-rolled resolver. */ export declare function jobJournalPath(env?: NodeJS.ProcessEnv): string; /** * The journal seam. Injected so the suite proves the restart path without a real file, and so a * caller that wants no journal at all (a programmatic embed) can pass a no-op. */ export interface JobJournal { /** Record a job as running. */ note(row: JournalRow): void; /** Persist the starting git status for a running agent-mode job, when it fits the bound. */ noteStartingTree?(jobId: string, tree: TreeSnapshot, scope: readonly string[] | undefined): void; /** Persist the daemon-owned execution reference used by restart recovery and daemon cancellation. */ noteBrokerExecution?(jobId: string, execution: JournalBrokerExecution): void; /** * Atomically claim a dead-owner broker row for this MCP process. Exactly one replacement process * may collect a daemon execution after a host restart. */ claimBrokerOrphan?(jobId: string): JournalRow | undefined; /** Remove a job that reached a terminal state, or one that never started. */ clear(jobId: string): void; /** * Remove one startup orphan after its terminal adoption has been durably recorded. * The removal is conditional on the exact row identity observed at startup, so it cannot erase * a same-id row another process subsequently published. */ clearOrphan?(row: JournalRow): void; /** Rows present at STARTUP whose owner is gone — i.e. jobs a previous process died holding. */ orphans(): JournalRow[]; /** * The row for `jobId` if ANOTHER live process owns it right now, read from disk at call time. * This is how one host's server answers for a job another host's server is still running. */ foreign(jobId: string): JournalRow | undefined; /** Every row another live process owns now. */ foreignRows(): JournalRow[]; } /** A journal that records nothing. The default under vitest, and for an embedder that declines one. */ export declare const nullJobJournal: JobJournal; /** How a journal decides who owns a row. Injected so the suite can prove both branches. */ export interface JobJournalOptions { /** This process's pid. */ pid?: number; /** Does `pid` still name a running process? `process.kill(pid, 0)` by default. */ isAlive?: (pid: number) => boolean; /** * Optional diagnostic seam for a best-effort persistence failure. * Production leaves this unset; concurrency regressions use it to surface the exact swallowed * lock/read/write error instead of discovering only a missing row later. */ onPersistError?: (error: unknown) => void; } /** * The real journal: a small JSON file rewritten on every change, read ONCE at construction. * * ⚠ The read happens before any write, and the file is left in place afterwards. It is NOT deleted * on startup: a second host session starting while this one runs would otherwise erase the rows * still describing this one's live jobs. Rows are removed one at a time, by the process that owns * them, as their jobs end. * * Corrupt or wrong-version ⇒ no orphans, and the file is left alone. Reporting a job as killed on * the strength of an unparseable file would invent deaths, which is the same fail-safe direction * `lane-manifest.ts` takes on a corrupt manifest. */ export declare function createJobJournal(path?: string, options?: JobJournalOptions): JobJournal;