/** * workspace/registration/store.ts * * The daemon-side registered-workspace store (user-scoped state, injectable * I/O) plus the impure worktree-link probe the resolver's git metadata comes * from. * * PERSISTENCE follows the sibling control-plane stores (PrincipalStore, * ChannelProfileStore …): a versioned JSON document over the shared * PersistentStore, which supports a `:memory:` path for deterministic tests, * that is the injectable-I/O seam. The persisted `workspaces` array is * field-identical to the agent registry so its file migrates in. * * ROOT-GUARD. `add` refuses an absurdly broad root ($HOME, the filesystem root, * or the daemon state dir) via the SAME broadRootReason the checkpoint manager * already uses, a registration store must never let automatic coverage sweep a * whole home directory. * * CONCURRENCY. Every mutation here is a READ-MODIFY-WRITE: `read()` loads the * whole document, the method edits one array, and `persist()` replaces the * file. `PersistentStore.persist` is one atomic replacement, so nobody sees a * torn file, but it does not close the window between the read and the write, * which is exactly what its own header says belongs to the caller that owns the * read. Two registrations interleaved there lose one of the two roots outright: * both read the same array, both append their own record, and the second write * replaces the first. The lost root then has no coverage and nothing says so. * * And this store, alone among the daemon's stores, is contended ACROSS * PROCESSES: `goodvibes register` in a project directory writes the same * user-scoped file the running daemon writes, so an in-process queue on its own * would order this process's writes and still lose the other's. Every * read-modify-write therefore runs under BOTH, the in-process chain and the * advisory lock at `.lock`, which is the shape `push/subscription-store.ts` * already uses for the same reason. */ import { type RegisteredWorkspaceRecord, type WorkspaceGitMetadata, type WorkspaceRegistrySnapshot, type WorkspaceResolution } from './types.js'; export interface WorkspaceRegistrationStoreOptions { /** Persistence path (or `:memory:` for tests). */ readonly path: string; /** The user's home directory, refused as a broad root. */ readonly homeDir: string; /** The daemon state directory (~/.goodvibes), refused as a broad root. */ readonly daemonStateDir: string; /** * A second path to READ from when `path` does not exist yet, the pre-split * location, during the one auto-update cycle before the daemon's boot fold * moves the register into the shared tier. Never written to: a * read-modify-write that started from the fallback still persists to `path`. * See registration/shared-register-path.ts. */ readonly fallbackReadPath?: string | undefined; /** Injectable worktree-link probe; defaults to a real `git rev-parse` probe. */ readonly probe?: (path: string) => WorkspaceGitMetadata; } export interface RegisterWorkspaceResult { readonly record: RegisteredWorkspaceRecord; readonly alreadyRegistered: boolean; } export declare class WorkspaceRegistrationStore { private readonly store; /** Read-only source for the pre-split location; see fallbackReadPath. */ private readonly fallbackStore; private readonly homeDir; private readonly daemonStateDir; private readonly probe; /** Orders read-modify-writes within this process. See the header. */ private queue; constructor(options: WorkspaceRegistrationStoreOptions); /** * Run `fn` as the only read-modify-write against this file, in this process * AND across processes. * * The chain orders callers here; the advisory lock keeps the CLI's * registration from being clobbered by the daemon's, and vice versa. A * `:memory:` store has no file to contend on and takes the chain only. * * The chain tracks COMPLETION, never OUTCOME: `next.catch` keeps it alive * after a rejection, so one failed registration cannot wedge the store for * the life of the process, and the rejection still reaches the caller that * owns it and nobody else. `then(guarded, guarded)` rather than `then(guarded)` * for the same reason, a settled predecessor must run the next one either way. */ private run; private read; snapshot(): Promise; /** * Register a root, refusing an empty or absurdly broad one. Idempotent on * the normalized root, but provenance UPGRADES: re-adding an existing root * with `origin`/`checkpointEligible` stamps those fields (this is how the * checkpoint-owning consumer marks its roots on boot, including records * migrated before provenance existed). Absent options never strip an * existing stamp, one surface's plain self-recording cannot demote another * consumer's eligibility. */ add(root: string, opts?: { readonly label?: string; readonly origin?: string; readonly checkpointEligible?: boolean; }): Promise; /** Remove a registered root. Returns whether anything was removed (honest boolean, never a phantom). */ remove(root: string): Promise<{ readonly root: string; readonly removed: boolean; }>; /** Remember a subtree-scoped decline at a root. Idempotent. Used by prompt consumers, not a wire verb. */ decline(root: string): Promise<{ readonly root: string; readonly alreadyDeclined: boolean; }>; /** * Resolve a path against the registry. When `git` is omitted, the store probes * the worktree→main-repo link itself so a linked sibling worktree inherits its * main repo's registration. */ resolve(path: string, git?: WorkspaceGitMetadata): Promise; private requireRegistrableRoot; } //# sourceMappingURL=store.d.ts.map