UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

452 lines (451 loc) • 31 kB
import type { CorruptRecordRead, CorruptReplayTally } from "./corrupt-read-sink.js"; /** * Validate an engine-minted id (sessionId / tool-result ref) before using it as a path component. These are * `uuidv7` / an encoded tool-result ref — already filename-safe — but a strict guard is cheap * defense-in-depth: reject `.`/`..`/empty/separators so nothing can traverse out of the store dir. */ export declare function sanitizePathComponent(raw: string): string; /** * Map a model-influenced memory `scope` to a safe directory name (CC `sanitizePath` parity). Replace every * non-alphanumeric run with `-`; if the result is over 200 chars, append a short content hash so two long * scopes can't collide. The ORIGINAL scope is the partition key the engine passes — this only affects the * on-disk dir name, never the stored data. Never `path.join` a raw scope. */ export declare function sanitizeScope(scope: string): string; /** Resolve the data root (CC `getClaudeConfigHomeDir` analog): `$AGENT_DATA_DIR ?? ~/.ai-agent`, * NFC-normalized, canonicalized once the dir exists. Creates the dir (0o700) if absent. */ export declare function resolveDataRoot(explicit?: string): string; /** Ensure a directory exists with 0o700 perms (idempotent). */ /** * Atomically CREATE `target` carrying `content` — it appears in ONE step already fully written, no empty/partial * window. Fixes the create-then-write race: a reader/pruner can see a half-written * file, and a crash mid-write leaves a zero-byte target a retry would silently keep. Writes a temp in `target`'s * OWN dir (same FS → `link` is atomic) + fsync, then `linkSync`-publishes it; throws `EEXIST` if `target` exists. */ export declare function writeThenLink(target: string, content: string | Uint8Array): void; export declare function ensureDir(dir: string): void; /** * The 5-step atomic whole-file replace (§2.1). Skipping any of steps 3/5/6 is the classic corruption bug: * 1. open a temp under `root/tmp/` (same FS → rename is atomic), * 2. write the bytes, * 3. fsync the DATA (else ext4 "zero-length file after crash"), * 4. close → rename(tmp, target) (atomic intra-FS namespace swap), * 5. fsync the containing DIRECTORY (persists the rename itself). * On any failure the temp is unlinked so a crashed write leaves no scratch behind. */ export declare function atomicWriteFile(tmpDir: string, target: string, bytes: string): void; /** * Read a JSONL file and return every COMPLETE, parseable record — the universal torn-tail recovery (§2.2): * - the last segment, when no `\n` ends it, is judged as every line is (`@contract store.read_back_shape_judged`): * an object is served, an unparseable remnant is a crash's partial write and is discarded, * - any interior line that fails `JSON.parse` is skipped (never throws — `listRemoteAgentMetadata` * parity), so a single corrupt interior line never poisons the whole replay. * A missing file → `[]`. This is why JSONL beats one big JSON blob: a torn tail can never corrupt the store. * * SHAPE (`@contract store.read_back_shape_judged`): parseability is not the judgement. Every store on this * format writes ONE JSON OBJECT per line, so a line parsing to a number, string, boolean, `null` or array * takes the road a torn line takes — disclosed and skipped — because it is the same fact: these bytes are * not what this store wrote. `judge` (optional) is the consuming store's own floor on an object ("does it * carry my members at all?"); a record it rejects is skipped as `missing-required` on the same road, so * the count below covers every skip. * * `onCorrupt` (optional): one `record` disclosure per skipped record, then — iff any was skipped — one * `replay` tally. The unparseable unterminated tail stays out of both by design: dying mid-write is the * crash shape this format exists to absorb. An unterminated tail that parses to a non-object is not that * shape (no prefix of an object parses to one), so it is disclosed like any other foreign line. The callback * is invoked as given — a seat that must not throw swallow-guards on ITS side. */ export declare function readJsonlRecords<T>(path: string, onCorrupt?: (info: CorruptRecordRead | CorruptReplayTally) => void, judge?: JsonlRecordJudge): T[]; /** A consuming store's floor on a parsed JSONL object: `accepts` = one of its records; `what` names a * rejected one in the disclosure's words. */ export interface JsonlRecordJudge { accepts(record: object): boolean; what: string; } /** * An append-only JSONL log opened once with `O_APPEND` and held for the file's lifetime. One record = * one `JSON.stringify(...) + "\n"` written in a SINGLE `writeSync` call (never split across writes → the * only torn line possible is a crash tail, which {@link readJsonlRecords} drops). `fsync` is opt-in per * append (the checkpoint commit point ALWAYS fsyncs; session/memory cadence is the caller's `fsyncEvery`). */ /** * RB-144: THE canonical key for "one authority per physical location". Every file backend keeps a * module-level table so that N instances over one directory collapse into one CAS authority; getting the * KEY wrong reopens the defect the table exists to close, which this repo paid for four times (mailbox, * session store, then the agent/run/task-list stores still keying on a lexical `resolve()`). Point fixes * kept missing siblings, so the rule lives in ONE place they all call. * * Two normalizations, each for a demonstrated failure: * - PHYSICAL PATH — a symlinked data dir (`/var` → `/private/var` on macOS, a bind-mount, a linked * `~/.ai-agent`) otherwise yields two authorities for one directory, and both writers win the CAS. * - CASE FOLD — on a case-insensitive filesystem `Foo/` and `foo/` are the same directory. The mint * answers with the on-disk spelling, which folds the two only where the entry EXISTS; the fold here * also covers a not-yet-created tail, and folding a PATH would name a different directory on a * case-SENSITIVE volume (see the cc task-list store's lock target, which must not fold) — so it stays * at this call site, which keys, rather than inside the mint, which answers about paths. * * The key is used ONLY for table lookup — never for I/O, so the on-disk name stays verbatim. A path that * does not exist yet resolves through its deepest existing ancestor; a path with no resolvable ancestor at * all keys on its lexical form (a store must not fail to construct because canonicalization is unavailable). */ export declare function canonicalStoreKey(p: string): string; export declare class AppendLog { private fd; /** * RB-167 (2026-07-26): distinguishes "closed because the holder is done with it" from "closed for a * compaction swap". Only the first is permanent. * * Every store that compacts does close → rewrite → reopen, and the rewrite can fail (EACCES, ENOSPC, a * read-only mount, an immutable flag). The stores handled that in three different and individually * insufficient ways: three of them left the log CLOSED FOREVER, so one transient I/O error turned into * `log_closed` on every later write — and because these logs are now SHARED per directory, that bricks * every instance on it, not just the one that compacted. The fourth reopened in a `finally`, which only * covers the failure shapes where reopening still works and, worse, replaces the original error with the * reopen's own when it does not. * * The reopen therefore belongs here, lazily: a swap-closed log reopens on its next write, so a fault that * has cleared simply resolves itself, a fault that has not surfaces at the write (with its own error, the * original having already propagated from the compaction), and `close()` keeps meaning exactly what * RB-99 made it mean. */ private disposed; private readonly path; /** * RB-438 — the file size as of the end of THIS log's last successful append (or of the last time it * observed the file at a record boundary). `-1` = unknown, re-establish on the next append. * * It is the cheap arbiter for "did the file change under us since we last left it whole": equal ⇒ * nothing to check, different ⇒ read the last byte and repair if it is not a newline. See * {@link repairTail}. */ private boundary; constructor(path: string); private sizeOrUnknown; /** * RB-438 (2026-07-31, hardening probe R2) — re-establish the last-complete-record boundary before a write. * * The constructor's {@link repairJsonlTail} (and the RB-167 lazy reopen) only ever ran at OPEN time, and * the RB-66 loop below only repairs a SHORT write — not a `writeSync` that THROWS mid-loop (ENOSPC/EDQUOT/ * EIO after a partial `write(2)`, the classic full-volume shape) and not the `n <= 0` guard. Both leave * `written` bytes on disk with no terminating newline while the fd and this object stay live, and every * caller retries through the SAME AppendLog (`FileSessionStorage.appendEntry` throws before `recordEntry`, * so the in-memory leaf is unchanged and the CAS retry appends again). The retry's record was then GLUED * onto the remnant, so replay dropped BOTH — after that second append had returned and fsync'd. Measured: * records [1,2,3] + a torn remnant + appends 5,6 replayed as [1,2,3,6]. * * Cost on the healthy path is ONE `fstat` per append (the sizes match and we return); the tail read only * happens when the file actually moved off a boundary, and it reads the last segment, not the file. A * legitimate concurrent appender (another process under the boot-lock handoff) leaves the file * newline-terminated, so it is adopted, not truncated; a whole record it left without its newline is kept * and ended, and only a partial one is cut. */ private repairTail; /** * Append one record (`json + "\n"`). `fsync:true` makes the record durable before return. * * RB-66 (2026-07-25, red probe): `writeSync(fd, string)` issues ONE write(2) and may SHORT-WRITE — the * same defect class this file's own `writeThenLink` already documents and fixed ("silently SHORT-WRITES * "), never propagated to this shared primitive. A short write here leaves a torn * line with no trailing newline, so the NEXT append concatenates onto it and replay drops BOTH records. * Loop until every byte lands (write(2) returns how many it took) — the O_APPEND atomicity per call is * unchanged, and a genuine I/O error still throws. * * A write that fails leaves no bytes of its own: the file is cut back to where the write began, so a caller * that retries the record writes it once — a record that failed is never also half-present. */ append(record: unknown, fsync: boolean): void; /** * Cut the file back to `startedAt` after a write that threw, provided the bytes past it are exactly this * write's own (`written` of them): a peer that appended after us owns what follows, and its bytes are not * ours to cut. Best effort — the error the caller must see is the write's, so a rollback that cannot run * answers false and the next append's tail repair judges what is left. */ private rollBackFailedWrite; close(): void; /** * RB-167 — release the descriptor for a compaction SWAP. The next write reopens it lazily. * * Callers must use this rather than `close()` around a rewrite: `close()` means "this log is finished", * and a store that used it for a swap and then failed to rewrite left the log permanently unusable — * one transient I/O error becoming a permanent brick, shared by every instance on that directory. */ closeForSwap(): void; private releaseFd; } /** Bytes read per positioned read while a file's tail is walked back to its last newline. */ export declare const JSONL_TAIL_READ_BLOCK: number; /** * The longest last segment (in BYTES) a repair station will judge: V8's maximum string length in UTF-16 code units * (Node's `buffer.constants.MAX_STRING_LENGTH`). A segment of N bytes decodes to at most N code units, so one within * the bound can never make `toString` throw. One past it MAY still decode (multi-byte text has fewer units than * bytes) but is left unjudged either way: telling would mean reading and decoding up to three times the bound. */ export declare const JSONL_TAIL_SEGMENT_LIMIT: number; /** A file's last segment (the bytes after its last `\n`, or the whole file when it holds none) — or the fact that it is * too long to judge. `start` is the segment's offset; a file that ends on a newline has an empty segment at `size`. */ export type JsonlTailSegment = { unjudgeable: false; start: number; bytes: Buffer; } | { unjudgeable: true; }; /** * The last segment of an open file of `size` bytes, found by walking back from the end one block at a time to the * last newline: a read is bounded by the segment's length, never by the file's. The one reader of "what is the tail" * for every repair station; whether the segment is a record is {@link jsonlTextFormOf}'s to say. */ export declare function readTailSegment(fd: number, size: number, limit?: number): JsonlTailSegment; /** Why a {@link BootLock}/{@link acquireStoreDirLock} acquisition was refused, as a branchable code. */ export type FileStoreLockErrorCode = /** A LIVE process (this one or another) already holds the directory. Steady state: it ends when that * process ends. */ "store.dir_in_use" /** Another process is mid-RECLAIM of a lock left by a crashed owner. Transient by construction — the * claimant either finishes (and then owns the directory) or dies (and its gate is pruned in turn), so * the honest advice here is "retry", NOT the "stop the other process" that `dir_in_use` earns. */ | "store.dir_claiming" /** The lock is in a state only a human should resolve — no parseable owner, or buried under nested * reclaim gates from repeated crashes. Never pruned blindly; the message names what to remove. */ | "store.lock_unreadable"; /** * The structured refusal of a file-store directory fence. Carried as a class (not a bare `Error`) so a * host can branch on {@link code} — "someone else owns this data dir" is an operator-actionable * condition (stop the other process / pick another root / move to a SQL backend), and telling it apart * from a genuine I/O failure by string-matching a message is exactly the fragility the codes exist to * remove. The messages are unchanged from the pre-code era, so existing text assertions still hold. */ export declare class FileStoreLockError extends Error { readonly code: FileStoreLockErrorCode; /** The lock file whose acquisition was refused. */ readonly lockPath: string; /** The live owner's pid when the lock named one (absent for an unreadable lock or a lost race). */ readonly ownerPid?: number | undefined; readonly name = "FileStoreLockError"; constructor(code: FileStoreLockErrorCode, message: string, /** The lock file whose acquisition was refused. */ lockPath: string, /** The live owner's pid when the lock named one (absent for an unreadable lock or a lost race). */ ownerPid?: number | undefined); } /** * The coarse boot guard (§2.4): an `O_EXCL` owner file (`root/LOCK` for the whole data dir; one per store * ledger directory via {@link acquireStoreDirLock}) that forbids two processes sharing it. A second * instance fails fast ("another instance owns this data dir"). A STALE lock (the process that WROTE it is * gone — see {@link isProcessLive}, which is not the same question as "is that pid free") is pruned and * re-acquired — `proper-lockfile`/CC `concurrentSessions` parity. * * This is the family's ONE lock MECHANISM (the per-operation CAS stays in-process: one event loop + a * per-token async mutex, so there is NO per-operation flock). A holder of one of these fences guarantees * the in-process model's premise — a single writer to the directory — actually holds. Cross-process * CORRECT concurrency (many writers at once) is still the Pg/TiDB backend's job, by design: this fence * refuses the second writer, it does not coordinate it. */ export declare class BootLock { private readonly lockPath; private held; /** The verdict of the ONE release this holding gets, replayed to any later caller (see {@link release}). */ private spentVerdict; constructor(lockPath: string); /** Acquire the lock or throw. Reclaims a lock whose recorded OWNER is gone (see below). */ acquire(): void; /** * @param depth how many stale reclaim gates deep we already are (see the gate below). Bounded so a * pathological chain demands an operator instead of recursing without end. */ private acquireWithin; /** Publish our lock, or say out loud that a fresh contender got there first. */ private publishOrLoseRace; /** Take back a lock THIS process published and could not remove on release (never someone else's). */ private pruneOwnAbandoned; private writeLock; /** * Key for the {@link abandonedLocks} registry: the RESOLVED path, deliberately NOT case-folded. Folding * (as the store-table key does) would alias `/data/foo/LOCK` and `/data/Foo/LOCK` — two genuinely * different files on a case-sensitive filesystem — and this registry authorizes a DELETE. Missing an * entry costs a loud refusal; a false hit costs someone else's lock. */ private get abandonKey(); /** * Read the lock as one of three ANSWERS, never as one "undefined" that means all of them: absent (nobody * holds it), a named owner, or unreadable. Collapsing the last two is what let a release that could not * even READ the file report the lock as gone — after which the bookkeeping was dropped while the file * still named this process, and the directory was refused for the rest of the process's life. */ private inspectLock; /** * Release the lock (only if we hold it), ONCE. Best-effort; never throws. * * @returns whether the lock is now GONE — removed, or provably somebody else's. A `false` means the file * is still there and this process cannot prove it is not the owner of record, which the caller must not * treat as released: every later acquisition would read a live owner (us) and refuse a directory nobody is * using. The path is remembered as ABANDONED so a later acquisition in this process can take it back * rather than refuse a directory nobody is using — a lock carrying our own pid can only be ours * ({@link pruneOwnAbandoned} removes and republishes it; {@link resumeAbandoned} re-arms a holder over * the file as it stands). * * ONE-SHOT, and that is load-bearing. This object stops being a holder the moment it is ASKED to release * — before the filesystem is touched — and a second call REPEATS the first verdict instead of acting on * it again. A lock file names its publishing PROCESS, so two BootLocks over one path in this process are * byte-identical on disk: after a FAILED release the path can legitimately be re-published (a later acquisition here * finishing our cleanup), and a stale object that "removed its own lock" a second time would be deleting * the SUCCESSOR's fence while both sides believed they held the directory — the double-writer this whole * mechanism exists to refuse, minted from bookkeeping instead of from a race. So a second release answers * the first question again and touches nothing. */ release(): boolean; /** * The filesystem half of {@link release}: remove the lock while the file still names US, and report what * the directory looks like AFTERWARDS (never what we intended). * * "Still names us" is a READ, and `unlink` cannot be made conditional on content — so, exactly as in * {@link resumeAbandoned}, an external removal landing between the two lets this delete a file that is no * longer the one we read. That is the advisory-pid-file mechanism's exposure, identical for every holder * and every moment of a holding, not something this ordering introduces; the re-inspection below is why * the ANSWER stays honest even then. */ private removeOwnLock; /** * Resume holding a lock THIS process published and could not remove on release — the state a zero-ref * entry in {@link acquireStoreDirLock}'s table is in, and the only way a joiner may ride that entry. * * Pure INSPECTION of the disk: it publishes nothing and removes nothing. The claim is honoured only * when the file still there is the one we left (our own pid) AND no publish on this path has spent the * abandoned note since — any successful publish (ours, or a reclaim) makes some OTHER object the holder, * and re-arming here would put two holders on one path, exactly what one-shot {@link release} closes. A * `false` says the bookkeeping can no longer be trusted and the caller must go back to the disk: the * file may have been cleared (an operator, a tmp cleaner) and the directory since taken by another * OS process. * * A successful resume SPENDS the note, for the same reason a successful publish does: the note is what * authorizes {@link pruneOwnAbandoned} to DELETE this file, and while somebody holds the path that * authorization must not exist — a second acquisition here is a genuine conflict that has to be refused * by name, not a leftover to be cleaned up over a live holder. A later failed release writes it again. * * What a `true` establishes, exactly: the file was the one we left AT THE MOMENT WE LOOKED. It is not an * atomic handoff and cannot be — this fence is an advisory pid FILE, so nothing stops an operator (or a * tmp cleaner) from removing it the instant after any check, here or hours into an ordinary holding, and * a foreign process publishing over the gap. That exposure belongs to the mechanism and is the same for * a fence nobody ever released; what this check removes is the far wider window in which the removal * already happened and the table would have kept vouching for it anyway. The branch where the file is * ALREADY gone is not re-armed here at all: it falls through to a real acquisition, whose `O_EXCL` * publish is the atomic test-and-set, and a foreign owner there is refused by name. */ resumeAbandoned(): boolean; } /** One holder's share of a directory fence. Releasing is idempotent and only the LAST share unlinks. */ export interface StoreDirLockLease { /** The lock file this share is counted against (inspection/tests). */ readonly lockPath: string; /** Drop this share. The lock file survives until every share over the directory is released. */ release(): void; } /** * Take (or JOIN) the cross-process writer fence for one store directory. * * The file family's premise everywhere is "one writer per data directory"; before this, the ONLY thing * enforcing it was the `root/LOCK` a `FileStorageBackend` takes — which a store constructed directly * (every one of them is a root export) never sees, and which never covered a store the backend does not * construct at all. A second OS process then replayed a PRIVATE view of the same ledger: both sides win * a once-only CAS, and the one that never loaded the other's rows ERASES them at its next compaction. * So the fence belongs to the directory that holds the authority, taken by whoever opens it. * * Semantics: exclusive across PROCESSES (a live foreign owner ⇒ {@link FileStoreLockError} naming the * pid; a dead owner's lock is pruned as stale), refcount-JOINED inside this process (siblings over one * directory share the fence, and the last {@link StoreDirLockLease.release} unlinks it). `dir` must * already exist (the caller's `ensureDir`), since the fence lives inside it. */ export declare function acquireStoreDirLock(dir: string, opts: { label: string; fileName?: string; }): StoreDirLockLease; /** * design/84 Seam B (TOC profile) — a per-scope CONSOLIDATION lock factory for the file backend's * {@link import("../../core/consolidate-scope.js").ConsolidateScopeDeps.acquire} injection point. A single * machine may run a periodic-consolidation timer AND a task-end inline pass concurrently; this advisory lock * stops two processes consolidating the SAME scope at once (the in-process model the file backend assumes — * see {@link BootLock} — does not cover a second OS process with its own timer). * * `acquire(scope)` takes a {@link BootLock} over `lockDir/<sanitizedScope>.consolidate.lock`. It returns a * `release` callback on success, or `undefined` for EVERY refusal that lock knows how to make — a live * owner (including this process: a second pass here is still a conflict), a takeover in flight, an * unreadable file — because the caller's whole vocabulary is "busy → skip this pass", a no-op that costs * one deferred consolidation. A stale lock is reclaimed by the same protocol as any other, which is the * point of not hand-rolling one: this factory used to prune with a bare `unlink` and re-publish, and two * passes that had both read the same dead owner would then both prune and both publish — one deleting the * other's fresh lock, leaving two processes consolidating the SAME scope, which is exactly what the lock * exists to prevent. The reclaim gate serialises that, the depth cap bounds it, and the identity re-read * makes sure a reclaim prunes the owner it came for. One lock MECHANISM in this family, not two. * * The implementation lives HERE (the deployment shell's persist/exec axis), NOT core — core only DEFINES * the injection point (the constitutional split: gate-presence in core, mechanism in the profile). */ export declare function createFileConsolidationLock(lockDir: string): (scope: string) => (() => void) | undefined; /** * A process identity that survives PID REUSE: a pid, WHEN that pid's process started, and the host the * observation was made on. * * A pid alone is not an identity. The OS hands the same number out again once its holder exits, so a lock * (or lease, or any other durable "who owns this" record) carrying only a pid says no more than "somebody * with that number" — and after a crash, an unrelated process that inherits the number makes the dead * owner's record read as a LIVE owner for as long as that process lives. Adding the START TIME makes the * record falsifiable: the pid narrows the candidate to one process, the start time decides whether it is * still the SAME one. * * The start time is carried TWICE, against two different clocks, and that is the whole robustness of the * thing. Both numbers come from ONE reading of the process's elapsed time, but one is subtracted from the * WALL clock and the other from the host's UPTIME — and a platform that leaves one of them unmoved when * the wall clock is corrected moves the other. (Where `ps` measures elapsed time against the boot clock, a * corrected wall clock shifts the derived wall-clock start and leaves the uptime-relative one alone; where * it measures against a stored absolute start, it is the other way round.) A comparison that insists on * BOTH before calling a record stale therefore cannot be talked into a reclaim by a clock correction — * which matters because that reclaim would be of a LIVE writer's lock. See {@link isProcessLive}. * * Both are `null` when this host cannot produce a start time (no `ps`, a cut-down `ps` that rejects the * query, a probe too slow for its own reading to be worth anything). That is a plain "unknown" — never a * zero and never a guess — and every consumer treats an unobtainable fingerprint as UNFALSIFIABLE, keeping * the pid-only verdict: the degrade lands on yesterday's behaviour rather than on a wrong reclaim, and * says so out loud once. */ export interface ProcessFingerprint { /** The process id the rest of the record is about. */ readonly pid: number; /** Start of that pid's process against the WALL clock, to the second; `null` when unobtainable here. */ readonly startedAtMs: number | null; /** The same start against this host's UPTIME (ms after boot), to the second; `null` likewise. */ readonly startedSinceBootMs: number | null; /** The host both were read on — a fingerprint from ANOTHER host is comparable to nothing here. */ readonly host: string; } /** * The fingerprint of a process: this one when called with no pid, any other pid when named. * * ONE derivation for both halves of the mechanism — the record a holder WRITES and the probe a later reader * verifies it with — on purpose. Deriving our own start time from `process.uptime()` instead would be * cheaper, but it measures from Node's own initialisation rather than from the kernel's `exec`, so a busy * machine can put the two derivations seconds apart. A reader comparing across them would then read a * healthy holder as stale and RECLAIM A LIVE LOCK — a strictly worse failure than the pid reuse this fixes. * One derivation makes the tolerance a rounding allowance instead of a guess about bootstrap latency. * * The self case is memoized: our start time cannot change, the probe is a subprocess, and a lock publish is * the only place that needs it — so a process pays for it at most once, whatever it opens. */ export declare function processFingerprint(pid?: number): ProcessFingerprint; /** * Is the process a record names still the one that wrote it? ONE predicate, two terms: the pid is running, * AND the fingerprint has not been falsified. * * "Not falsified" is the load-bearing wording, and it is what every degrade lands on. A record with no * `startedAtMs` (the pre-fingerprint spelling, or a host that cannot produce one), a record written on * ANOTHER host (its start time was measured against a different boot — comparing it here would be * arithmetic on unrelated numbers), and a probe that cannot answer all reach the same verdict: LIVE. The * asymmetry is deliberate and is the whole safety argument — a false "dead" reclaims a directory out from * under a working process (two writers, and the ledger loses whichever rows the loser never loaded), while * a false "live" costs a refusal an operator can act on. */ export declare function isProcessLive(record: { readonly pid: number; readonly startedAtMs?: number | null; readonly startedSinceBootMs?: number | null; readonly host?: string | null; }): boolean;