/** * manager.ts * * WorkspaceCheckpointManager, the coarse, whole-workspace rewind layer. * * Complements (does not replace) FileUndoManager (../../state/file-undo.ts), * which stays as the fine-grained, in-memory, per-file /undo layer. This * manager persists across sessions, snapshots the ENTIRE workspace tree, and * survives process restarts (backed by git objects + a JSON manifest on * disk), which is the wrong shape for FileUndoManager but exactly the shape * needed for "revert everything turn N did" or "restore the workspace to how * it looked an hour ago". * * Storage layout (workspace-local, see side-git.ts for the git mechanics): * /.goodvibes/checkpoints/git , side GIT_DIR * /.goodvibes/checkpoints/index.json , manifest (JsonFileStore) * * Checkpoint refs live at refs/goodvibes/checkpoints/ inside the side * repo, entirely separate from the user's real git refs and from compaction's * `cpt_` boundary commits (which are conversation snapshots, not filesystem * snapshots, and are not stored in git at all, see types.ts's header comment * for the full disambiguation). * * Automatic snapshots subscribe to EXISTING runtime bus events * (TURN_COMPLETED / TURN_ERROR / TURN_CANCEL / AGENT_COMPLETED), no new * event contract is introduced by this module. */ import { type PruneResult } from '../../runtime/retention/index.js'; import type { WorkspaceCheckpoint, CheckpointDiff, RestoreResult, CheckpointSessionChanges } from './types.js'; export type { CreateCheckpointOptions, RestoreOptions, ListCheckpointsFilter, CheckpointSessionResolveContext, CheckpointSessionResolver, WorkspaceCheckpointManagerOptions, } from './manager-options.js'; import type { CreateCheckpointOptions, RestoreOptions, ListCheckpointsFilter, CheckpointSessionResolver, WorkspaceCheckpointManagerOptions } from './manager-options.js'; /** * WorkspaceCheckpointManager, create/list/diff/restore/gc for whole-workspace * git-backed snapshots, plus automatic snapshotting on existing turn/agent * lifecycle events. */ export declare class WorkspaceCheckpointManager { workspaceRoot: string; private checkpointRootDir; private sideGit; private manifestStore; private manifestPath; private retentionPolicy; private readonly now; private readonly runtimeBus; /** Resolves a triggering turn/agent to its owning session id for automatic-snapshot stamping. May be replaced post-construction via `setSessionResolver`. */ private resolveSessionId; private readonly unsubscribers; private readonly rawWorkspaceRoot; private readonly explicitCheckpointDir; private readonly surfaceCheckpointsDir; private readonly retentionOverride; private readonly preferGitRoot; private readonly allowBroadRoot; private readonly allowLargeFirstSnapshot; private readonly maxFirstSnapshotFiles; private readonly autoRetention; private readonly homeDir; private readonly daemonStateDir; /** Non-null (the honest refusal message) when the resolved root was refused as too broad; drives both the skipped auto-subscription and rejected explicit `create()` calls. */ private rootRefusal; private checkpoints; private initialized; private initPromise; /** * Promise-chain mutex serializing every public operation that touches the * side repo's shared index or shared object store: `create`, `restore`, * `gc`, and `diff` (see each method's `*Internal` body below). Without * this, an auto-snapshot `create()` firing on a bus event (TURN_COMPLETED, * TURN_ERROR, TURN_CANCEL, or AGENT_COMPLETED) could run its `git add -A` * in between `restore()`'s * `read-tree --reset` and `checkout-index -a -f`, silently corrupting the * restore; two concurrent `create()` calls share the same hazard on the * index, and a same-tick `gc()` could treat a not-yet-ref'd loose commit * from an in-flight `create()` as unreachable and prune it out from under * it. `diff()` is included too: the single-argument `git diff ` * form (diffing a checkpoint against the live working tree) refreshes the * index's stat cache as a side effect, which is itself a write. * * Each public method below only does `await this.init()` (idempotent, safe * to race) before calling `withLock`; the actual git-touching work lives in * a same-named `*Internal` method. Internal callers that need another * locked operation's behavior (e.g. `restore()`'s safety checkpoint) call * the `*Internal` method directly, never the public wrapper, so a single * logical operation never tries to re-enter its own lock. * * This in-process chain only serializes callers within THIS process. Two * separate processes sharing the same checkpoint directory (two daemon * instances, or a daemon and a CLI invocation, pointed at the same * workspace) have no in-process chain in common, see the cross-process * file lock acquired alongside it below (cross-process-lock.ts). */ private lockChain; private withLock; constructor(opts: WorkspaceCheckpointManagerOptions); /** * (Re)construct every root-bound collaborator for `root`: storage dir, side * git runner, manifest store, and retention policy (whose pruner closes over * the side git runner, so it is rebuilt alongside it). Called from the * constructor with the raw root, and again from init() if the git-root * preference resolves a different root. */ private buildForRoot; /** * Install (or replace) the session resolver used to stamp `sessionId` onto * automatic snapshots. Wired after construction because the daemon's * agent→session mapping (the session broker) is built alongside this manager; * the subscription reads the resolver at each event, so a later install takes * effect for all subsequent snapshots. */ setSessionResolver(resolver: CheckpointSessionResolver | undefined): void; /** * Idempotent setup: init the side repo, load the manifest (re-hydrating the * in-memory RetentionPolicy so retention state survives process restarts), * and subscribe to automatic-snapshot events if a runtime bus was provided. * Safe to call multiple times; concurrent callers share one in-flight init. */ init(): Promise; private _init; /** * Resolve the effective root (preferring the enclosing git repo top level) * and decide whether it is too broad to snapshot. When the git-root * preference moves the root, every root-bound collaborator is rebuilt via * `buildForRoot`. A refused root sets `rootRefusal` (logged with the * override name in `_init`) so automatic subscription is skipped and explicit * `create()` calls fail honestly; the `allowBroadRoot` override clears it. */ private resolveAndGuardRoot; /** Cheap, non-blocking retention sweep: skips when auto-retention is off, the root was refused, or nothing is over-limit; else fires the lock-serialized `gc()` fire-and-forget (like the auto-snapshot path). */ private maybeRunRetention; /** * Subscribes to the EXISTING turn/agent lifecycle events, no new event * types are introduced. A snapshot is taken at the boundary AFTER each * turn/agent-run finishes, meaning "revert everything turn N did" means * restoring the checkpoint captured BEFORE turn N, i.e. checkpoint[N-1] in * `list()`'s (newest-first) ordering, an intentional off-by-one, documented * here rather than hidden: this module always snapshots "where things ended * up", never "where things started". * * Listener bodies are wrapped so a failure NEVER throws back into the bus: * `RuntimeEventBus.emit()` only catches synchronous throws from a listener, * not rejections from a returned (but un-awaited) promise, so every * auto-snapshot call here is deliberately `.catch()`-guarded. */ private subscribeToAutomaticSnapshots; /** * Create a new checkpoint. Returns `null` (a cheap no-op) when the current * workspace tree is identical to the parent checkpoint's tree, no commit, * no ref, no manifest entry is created in that case. * * Serialized against every other index-touching operation on this manager *, see `withLock`. */ create(opts: CreateCheckpointOptions): Promise; private createInternal; /** List checkpoints, newest-first. `list()[0]` is always the most recent checkpoint. */ list(filter?: ListCheckpointsFilter): Promise; /** * Diff two checkpoints, or a checkpoint against the live working tree when * `b` is omitted. * * Serialized against every other index-touching operation on this manager *, see `withLock`. The single-argument form (`b` omitted, diffing against * the live working tree) refreshes the side index's stat cache as a side * effect, so it is not purely read-only. */ diff(a: string, b?: string): Promise; private diffInternal; /** * Aggregate the file changes a single session made, the "what changed in * this session" surface a remote view needs, one diff spanning every * turn/agent snapshot stamped with `sessionId` (see * {@link computeSessionChanges} for the base/latest selection and the honest * empty result for a session with no stamped checkpoints). Both endpoints are * committed side-repo trees, so this is a pure two-tree diff; it still takes * the lock to stay consistent with a concurrent create()/gc(). */ sessionChanges(sessionId: string): Promise; /** * Restore the workspace to the state captured by checkpoint `id`. * * By default (`safetyCheckpoint: true`) takes a checkpoint of the CURRENT * state first, so a restore is itself undoable via another restore. * * Whole-workspace restore (no `opts.paths`): * 1. Snapshot the current tracked-file set (via the safety checkpoint, or * a transient write-tree when `safetyCheckpoint: false`), this is the * "before" set. * 2. Reset the side index to the target checkpoint's tree and check every * file in it out to disk (re-creates anything the checkpoint had that * is currently missing or modified). * 3. Remove exactly the files that were in the "before" set but are NOT * in the target checkpoint's tree (files created/tracked after the * checkpoint). Anything NOT in the "before" set, i.e. any untracked * path outside what this engine has ever snapshotted, is never * touched, by construction: it never appears in either set. * * Scoped restore (`opts.paths` provided) only checks out those paths from * the target tree; it never removes files outside the given paths. * * Serialized against every other index-touching operation on this manager *, see `withLock`. Without this, an auto-snapshot `create()` firing on a * bus event could run its `git add -A` in between the `read-tree --reset` * and `checkout-index -a -f` calls below, silently corrupting the restore. */ restore(id: string, opts?: RestoreOptions): Promise; private restoreInternal; /** * Apply retention limits: `RetentionPolicy` selects prune candidates, * `WorkspaceCheckpointPruner` deletes their refs, then (only if anything * was actually deleted) a single `git gc --prune=now` reclaims the now- * unreachable objects. This never touches compaction's boundary commits, * they are tracked in an entirely separate `RetentionPolicy` instance * (../../runtime/compaction) with no shared state. * * Reclamation only works because checkpoint commits are parentless (see * `SideGitRunner.commitTree`): a pruned ref's commit has no descendant * keeping it reachable via a git parent pointer, so once its ref is * deleted it is genuinely unreachable and `--prune=now` frees it. * * Serialized against every other index/object-store-touching operation on * this manager, see `withLock`. Without this, a `create()` racing this * method could write a loose commit object that isn't ref'd yet at the * moment `--prune=now` runs, and lose it. */ gc(): Promise; private gcInternal; /** Unsubscribe from the runtime bus. Does not touch anything on disk. */ dispose(): void; /** * Before the FIRST-ever whole-workspace snapshot for this store, refuse if * the sweep would capture more than `maxFirstSnapshotFiles` files, a cheap * `ls-files`-style enumeration (no blobs written), not a full stage. This * catches an over-broad root (e.g. a home directory) before it materializes * a large object store, rather than proceeding silently. Only the first * snapshot is checked (a store with existing checkpoints has already proven * its root is sane), and only whole-workspace sweeps (a scoped `paths` create * is bounded by construction). The `allowLargeFirstSnapshot` override skips it. */ private guardFirstSnapshotSize; private mostRecentCheckpoint; private requireCheckpoint; private defaultLabel; /** * Approximate incremental bytes introduced by a checkpoint: sum of on-disk * sizes of the changed paths, read immediately after they were staged and * committed (so they still reflect exactly the content just captured). * Deleted paths (no longer on disk) contribute 0. This is deliberately not * exact git object-store accounting, it exists for retention's `maxSizeBytes` * bookkeeping, not for a byte-perfect audit. */ private computeSizeBytes; /** * Load the manifest, moving it aside instead of failing when it cannot be * parsed. * * An index.json left unreadable by an unclean shutdown must not wedge the * whole checkpoint feature: the file is quarantined with a receipt and the * manager starts from an empty index, which the next save rewrites. Only the * index is lost, the checkpoint commits themselves stay in the side repo and * remain reachable by ref. */ private loadManifestOrQuarantine; private persistManifest; } //# sourceMappingURL=manager.d.ts.map