@sema-agent/core
Version:
Stateless, task-oriented AI agent core
452 lines (451 loc) • 31 kB
TypeScript
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;