import { type WorkspaceWalkEntry, type WorkspaceConflict } from "./workspace_format.js"; /** Hard ceiling on one scope's live bytes. A cost guardrail, not a security boundary (tenant isolation * is the server-derived scope prefix). The old 512 MiB was sized by the tarball's need to be read whole * into memory; packs stream, so this is free to be generous. */ export declare const WORKSPACE_SCOPE_MAX_BYTES: number; /** A refusal carries WHY, because "not eligible" (self-hosted — nothing was lost) and "refused" (we * threw away a snapshot the run made) are not interchangeable, and a wire that conflated them is * exactly how persistence stopped silently once before (§8, path 6). */ export type WorkspaceReservation = { ok: true; } | { ok: false; reason: "not_eligible" | "storage_limit"; maxBytes?: number; }; export type ManifestWriteResult = { ok: true; generation: string | null; } /** Someone else wrote since our read. Re-read, re-merge, retry. */ | { ok: false; conflict: true; }; /** * Everything the algorithm needs from a place to keep bytes. Two implementations; no third, and no * capability negotiation — a backend that cannot do one of these is broken, not "different". */ export interface WorkspaceBackend { /** Gate the scope's projected footprint before any bytes move, so a refusal costs no upload. */ reserve(totalBytes: number): Promise; /** The stored manifest plus the token needed to write it back conditionally. `bytes: null` = nothing * stored for this scope. */ readManifest(): Promise<{ bytes: Uint8Array | null; generation: string | null; }>; /** Conditional write. `expected` is the generation from the last read; `null` means "must not exist". */ writeManifest(bytes: Uint8Array, expected: string | null): Promise; /** Which of these digests the store already holds — one round trip that turns a retried or resumed * persist into "upload only what didn't land". */ existingPacks(digests: readonly string[]): Promise>; writePack(digest: string, bytes: Uint8Array): Promise; /** `null` when the object is missing. */ readPack(digest: string): Promise; deletePacks(digests: readonly string[]): Promise; } /** Pack compression. Injected so tests can use identity and assert framing independently of zstd. */ export interface PackCodec { compress(bytes: Uint8Array): Promise; decompress(bytes: Uint8Array): Promise; } /** The filesystem surface, narrow enough to fake in a unit test. */ export interface WorkspaceSyncFs { /** Every regular file under the selection (`undefined` = the whole root), with size + mtime. */ walk(root: string, paths: readonly string[] | undefined): Promise; readFile(root: string, relPath: string): Promise; /** Write to a workspace-relative path, creating parents. `mtimeMs` restores the recorded time, which * is what keeps the next diff from seeing every restored file as changed. */ writeUnder(root: string, relPath: string, data: Uint8Array, mtimeMs: number): Promise; } /** Why a persist stored nothing. Mirrors the SDK's `workspace_persist_skipped` reasons (§7.1). */ export type PersistSkipReason = "not_eligible" | "storage_limit" | "too_large" | "error"; export interface PersistOutcome { /** The scope's live bytes after this persist (0 when nothing was stored). */ bytes: number; packsWritten: number; packsDeleted: number; repacked: boolean; /** Paths where a concurrent run's version was overwritten by ours (§6). */ conflicts: WorkspaceConflict[]; /** Set when nothing was stored. `undefined` means the persist succeeded (possibly as a no-op). */ skipped?: { reason: PersistSkipReason; detail?: string; maxBytes?: number; }; } export interface WorkspaceSyncDeps { backend: WorkspaceBackend; fs: WorkspaceSyncFs; codec: PackCodec; workspaceRoot: string; /** Per-scope byte ceiling. Defaults to {@link WORKSPACE_SCOPE_MAX_BYTES}. */ maxScopeBytes?: number; } export declare class WorkspaceSync { private readonly deps; private readonly maxScopeBytes; /** What this run last agreed with the store about. Seeded at hydrate, advanced by each persist. * `null` = unknown, which makes the next diff write everything and delete NOTHING. */ private baseline; /** Generation token for the conditional manifest write. */ private generation; /** Set when a restore died partway through writing. Non-null disarms persist for the rest of the run: * the tree on disk is not this scope's state, and storing it would turn a failed restore into * permanent corruption. */ private restoreFailure; constructor(deps: WorkspaceSyncDeps); /** * Restore the scope into the workspace at run start. * * An EMPTY manifest is a restored state, not a missing one: a scope that was deliberately emptied must * hydrate as empty and must set a baseline, or the run's first persist would treat every path as new * and resurrect what was deleted. */ hydrate(): Promise<{ files: number; packs: number; }>; /** Why persistence is disarmed, or null. The caller reports this to the author once. */ get disarmedReason(): string | null; /** * Store the selection. `paths === undefined` means the whole workspace (`persist: true`). * * Costs O(changed bytes), not O(scope): unchanged files keep their existing pack placement and are * never re-read, re-compressed, or re-uploaded. That is what makes an agent appending one line per * iteration cheap, and it is why packs are append-only rather than rewritten in place. */ persist(paths: readonly string[] | undefined): Promise; /** Restrict a manifest to the paths this run actually holds on disk — see the call site for why the * distinction matters. Packs are recomputed so the baseline never references one it doesn't need. */ private narrowToLocal; /** Files + the packs those files reference, drawn from what the remote held plus what we just wrote. * `referencedPacks` is what drops a pack nothing points into, which is what makes it collectable. */ private assemble; /** * Build packs for the changed files and upload the ones the store doesn't already have. * * Batched so peak memory is bounded by the batch size times the pack target rather than by the size * of the change set — a `persist: true` over a large tree must not need the tree in RAM. */ private buildAndUploadPacks; /** Run `fn` over `items` with at most `limit` in flight. The first rejection propagates. */ private forEachBounded; }