/** * The DURABLE SOURCE read. * * **The in-process bus is a WAKE SIGNAL, never the data path.** Every harness Cotal connects to * already persists an ordered, timestamped record of what the agent did: Claude Code writes a * session JSONL, Codex writes a rollout JSONL, OpenCode persists to its own store behind * `session.messages()`. A connector that instead reads the harness's in-memory event bus loses * everything observed since the last publish the moment the process dies — silently, and with no * gap visible to any consumer, because nothing was ever published to leave a hole in. * * So the contract is: the bus tells us *that* something changed; this reads *what* changed, from * the source of record, resuming at a cursor that survives a crash. * * The cursor is OPAQUE to the caller and defined by each source. A file source uses a byte offset; * a store-backed source uses whatever ordering key its own API exposes. The caller persists it and * hands it back — it never parses it. */ /** * One record, WITH THE CURSOR THAT RESUMES AFTER IT — not just the value. * * **The per-record cursor is not a convenience; without it one-unit-one-frame is unimplementable.** * The emitter may turn one read into several frames, and each frame must be its own durable * pending/publish/ack cycle *with its own `sourceCursor`*. A read that offers only a single * end-of-batch cursor gives a frame covering records 0..i exactly one legal cursor value to store: * the one that says every record in the batch was consumed. Fold that frame, crash, and the frontier * has advanced past records the later frames were carrying — with no `seq` gap and nothing for a * consumer to notice. That is the silent loss this whole plane exists to make impossible, reached * through the resume path rather than the publish one. * * The alternative, one read being indivisibly one frame, is worse: a restart's read returns * everything appended since the cursor, so "a single unit that cannot fit FAILS LOUD" * would fire on ordinary catch-up traffic. */ export interface SourceRecord { value: T; /** Pass to the next {@link DurableSource.read} to resume immediately after this record. */ cursor: string; } /** One read: the records that became available, and the cursor to resume from AFTER them. */ export interface SourceRead { records: SourceRecord[]; /** * Pass to the next {@link DurableSource.read}. Advances ONLY past fully-consumed records. * * Equal to the last record's cursor when the read is non-empty — an implementation MUST keep * those two agreeing, and {@link JsonlFileSource} asserts it rather than assuming it. * It is still carried separately because an EMPTY read has a cursor and no last record: a fresh * adopt and a no-new-data poll both need one, and the cursor-only advance is defined on it. */ cursor: string; } /** An append-only record of what a session did, re-readable after a crash. */ export interface DurableSource { /** For diagnostics and for naming which adapter produced a frame. */ readonly kind: string; /** * Read forward from `cursor` (or from the current end when it is `undefined` — a fresh adopt * must not rebroadcast a session's entire history). * * MUST NOT return a partially-written record, and MUST NOT advance the cursor past one. */ read(cursor: string | undefined): Promise>; } /** * A durable source over an append-only JSONL file — the shape both the Claude session transcript * and the Codex rollout log take. * * **The partial-line rule is the whole point of this class.** The writer is a separate process * appending concurrently, so a read can land mid-line. Parsing that yields either a throw or, far * worse, a truncated object that looks valid. So: only content up to the LAST newline is consumed, * and the cursor advances only that far. A trailing fragment is left for the next read, when the * writer has finished it. * * Unparseable COMPLETE lines are a different case and are NOT skipped silently — they surface, so a * format change is loud rather than a quiet hole in the record. */ export declare class JsonlFileSource implements DurableSource { private readonly path; readonly kind = "jsonl-file"; constructor(path: string); /** * A cursor is `::` — canonical, and BOUND TO THE FILE'S IDENTITY. * * The offset alone is not enough: a source replaced by an unrelated file of the same size or * larger reads as an ordinary append, and the reader resumes at a byte offset inside a document * it has never seen. Carrying `dev`/`ino` makes replacement DETECTABLE, which is the only thing * that lets it fail loud instead of returning fabricated records. * * Parsed strictly: `Number()` coerces `" "` to 0 — replaying all history — and accepts `"1e0"`, * `"01"`, `"+1"`. A cursor is persisted state handed back to us later, so a non-canonical one * means something upstream is wrong, not something to guess at. */ private static parseCursor; /** * A seal over **the last 512 bytes before the cursor** — a BOUNDED form of the invariant a * resumable offset wants: *the bytes immediately before my cursor are still the bytes that were * there when I stopped.* * * **THE BOUND IS PART OF THE GUARANTEE AND IS STATED HERE BECAUSE IT IS NOT THE WHOLE PREFIX.** * A rewrite confined to bytes EARLIER than `offset - 512` that preserves the sealed window, the * inode and the size is **not detected** — reproduced independently by three reviewers, including * with an ordinary same-length in-place PII scrub of earlier transcript lines. That is a real * limitation with known edges: a scrub via temp-file+rename changes the inode and IS caught; one * that changes length trips the offset/identity path and IS caught; one touching the sealed window * IS caught. Only same-inode, same-size, in-place, wholly-outside-the-window escapes. * * What that costs is bounded and worth naming precisely: the emit path stays correct, because the * cursor never moves backwards and forward records are read from bytes the rewrite did not touch. * What is lost is the ability to detect that already-consumed on-disk history drifted after we * read it. If whole-prefix integrity is ever required, this span must cover it — or the cursor has * to carry a rolling hash instead of a window. * * `dev`/`ino` catch unlink-and-recreate, but **not an in-place rewrite** (`writeFileSync` with no * unlink keeps the inode), and that case resumes at a byte offset inside a different document and * emits fragments of it as records (`fmae-rev-eng`, CONFIRMED). Size cannot catch it either when * the replacement is larger. * * Note what this deliberately does NOT flag: a rewrite that reproduces the same preceding bytes. * There the consumed prefix is genuinely unchanged, so resuming is correct — the seal states an * invariant rather than guessing at intent. */ private static sealAt; /** Offset just past the last COMPLETE line at or before `limit` — a safe boundary to resume at. */ private static lastCompleteBoundary; /** * Read every complete record from byte zero. * * This is deliberately NOT the meaning of `read(undefined)`: an ordinary fresh adopt must still * park at the current complete boundary and never replay retained history. A connector may call * this only when its own runtime has explicitly said the file is a NEW session and records can * already exist before the connector's first lifecycle hook (Claude's positional startup prompt * is one such case). * * The cursor is minted by the same identity + seal path as every other read. Constructing one in a * connector would duplicate the opaque cursor format and eventually drift from it. */ readFromBeginning(): Promise>; read(cursor: string | undefined): Promise>; } //# sourceMappingURL=durable-source.d.ts.map