/** * Content too large for one record, written beside the log before the record * that names it. * * A record is capped at `SESSION_RECORD_MAX_BYTES`. A body above the cap goes * to `/tool-results/.txt`, with a manifest at * `.txt.manifest.json`, and the record carries a {@link SpillRef}: both * paths relative to the session directory, the body's byte length and its * SHA-256. * * ## Ordering * * The body is written to a private temporary name, fsynced, and published by * `link` (which refuses to replace an existing name); the manifest the same * way; then the directory is fsynced. Only after all of that does the caller * append the record. So a record never exists without its spill: a crash * before the append leaves an unreferenced spill, which costs space and * nothing else; a crash after it leaves a record whose spill is already on * stable storage. * * ## No clobbering * * A spill name is derived from a stable key (a tool call id, a message id), * so two writes can name the same file. `link` makes the first one win. A * second write with the same bytes adopts the existing file; one with * different bytes is refused, because replacing the file would silently * change what an earlier record's hash vouches for. * * The durable-write helpers below are copied from `utils/atomic-write.ts` * (the fsync-then-rename ordering, the directory fsync, the per-attempt * temporary name), so this module does not depend on a file the cutover * rewrites. */ /** The spill as a record names it. Paths are relative to the session directory. */ export interface SpillRef { readonly path: string; readonly manifest: string; readonly bytes: number; readonly sha256: string; } /** The manifest written beside a spilled body. */ export interface SpillManifest { readonly v: 1; readonly kind: 'spill'; /** What the body holds: a serialised message, a compaction summary, or plain text. */ readonly content: 'message' | 'messages' | 'text'; /** The key the name was derived from (a tool call id or a message id). */ readonly key: string; readonly bytes: number; readonly sha256: string; readonly createdAt: string; } /** The directory, inside a session directory, that holds spills. */ export declare const SPILL_DIR = "tool-results"; /** A spill that is missing, a different length, or a different hash than its record says. */ export declare class SpillIntegrityError extends Error { readonly path: string; readonly name = "SpillIntegrityError"; constructor(path: string, message: string, options?: ErrorOptions); } export declare function sha256Hex(value: string | Uint8Array): string; /** The file name a key spills to: `.txt`, the same rule as `SessionPaths.toolResultFile`. */ export declare function spillFileName(key: string): string; /** A sidecar name private to one write: pid, a counter and random bytes. */ export declare function temporaryPathFor(filePath: string): string; /** * Make the entries of `directory` survive a power loss. Throws on POSIX when * the sync fails; does nothing on Windows, where a directory cannot be opened * for fsync and NTFS journals its metadata. */ export declare function syncDirectory(directory: string): Promise; /** Rename, retrying briefly while Windows reports the target contended. */ export declare function renameWithRetry(from: string, to: string): Promise; /** * Write `content` to a private temporary file and fsync it. The caller * publishes it (rename or link) and removes it on failure. */ export declare function writeSyncedTemporary(target: string, content: string): Promise; /** * Publish `content` at `target` atomically and durably, replacing whatever is * there: fsynced body, rename, fsynced directory. */ export declare function durableReplaceFile(target: string, content: string): Promise; /** Where spills are kept. The disk log uses files; the in-memory log a map. */ export interface SpillStore { write(key: string, content: SpillManifest['content'], text: string): Promise; read(ref: SpillRef): Promise; } /** * Spills on disk, under `/tool-results/`. * * `write` resolves only once the body, the manifest and the directory entries * naming them are fsynced, which is what lets the caller append the record * next. */ export declare class DiskSpillStore implements SpillStore { readonly sessionDir: string; private readonly now; constructor(sessionDir: string, now?: () => number); write(key: string, content: SpillManifest['content'], text: string): Promise; read(ref: SpillRef): Promise; } /** Spills held in process, for `InMemorySessionLog`. Same naming and checks as the disk store. */ export declare class InMemorySpillStore implements SpillStore { #private; write(key: string, _content: SpillManifest['content'], text: string): Promise; read(ref: SpillRef): Promise; } //# sourceMappingURL=spill.d.ts.map