/** * pre-split-control-plane-sweep.ts, the boot-time pass that ends the * two-control-plane-stores condition on a machine that ran a pre-split daemon. * * ── What was actually on disk ───────────────────────────────────────────── * * Every piece of the daemon's own state is supposed to live under * `.goodvibes//` (see the daemon's config/surface.ts). * `shellPaths.resolveUserPath(...)` adds no surface segment, the segment is * always the caller's to pass, and a set of control-plane stores forgot to * pass it. So `~/.goodvibes/control-plane/` accumulated state that belonged in * `~/.goodvibes/tui/control-plane/`, and on the owner's machine it held: * * sessions.json 274 KB, rewritten every boot by a legacy fold * whose target was this unscoped path, read by * nothing, and holding sessions the live store * did not have. * occasions-state.json live, written the day before this was found. * workspace-registrations.json live. * * The writers are repointed (control-plane-store-paths.ts). This is the other * half: what to do about the state already sitting at the old address. * * ── The four cases, and why each is what it is ──────────────────────────── * * One pass over the legacy directory, one decision per file: * * 1. THE SESSION STORE is folded, never adopted. It is the one store whose * scoped home is decided by the broker rather than by this directory's * layout (the broker's file is project-scoped), and the one with real * merge semantics of its own: an id-keyed union where the newer updatedAt * wins (session-store-importer.ts). Fold first, then retire. * * 2. NO SCOPED COUNTERPART → ADOPT: the file is MOVED to the scoped * directory. This is the migration. A move carries the state whole, parses * no shape and invents no merge, which is why a store this file has never * heard of migrates exactly as correctly as one it has. * * 3. A SCOPED COUNTERPART WITH IDENTICAL BYTES → RETIRE: the legacy copy is * redundant, so it moves to a quarantine directory named in the receipt. * * 4. A SCOPED COUNTERPART WITH DIFFERENT BYTES → REFUSE, LOUDLY. There are no * merge semantics here for an arbitrary store's shape, and inventing one * would be a guess applied to the owner's data. Both files stay exactly * where they are, the receipt names the conflict, says which copy readers * are using, and says where the other one is. A sweep that quietly picked * a winner would be the same class of defect as the one it is fixing. * * When nothing is left, the legacy directory is removed, so a machine that * migrated cleanly is left with one store and no empty decoy. Running it again * finds nothing to do. * * ORDERING: this runs before any of those stores is read. They all load lazily * on first use (see e.g. occasions/state-store.ts's `state()`), and the daemon * facade runs this before it starts the services that reach them. Adopting a * file after its store had already read an empty one would leave the store * about to overwrite what was just migrated in. * * Quarantine directories are bounded the way every other preserved-aside * artifact here is (worktree/registry.ts): an age TTL plus a count cap, newest * kept, ENOENT treated as success so two processes sweeping at once is safe. * * Nothing here throws. Anything that cannot be read, moved or removed is * recorded in the report, left alone, and the daemon boots. */ /** Directory-name prefix marking a quarantined pre-split control-plane store. */ export declare const PRE_SPLIT_QUARANTINE_PREFIX = "control-plane.pre-split-"; export interface PreSplitControlPlaneSweepReport { /** * `absent` , no legacy store directory (the ordinary case on a clean install). * `clean` , a legacy directory existed and there was nothing left to do. * `swept` , state was adopted, folded and/or retired this pass. * `conflicted`, nothing moved, and at least one file disagrees with its * scoped counterpart; the sweep refused rather than guess. */ readonly status: 'absent' | 'clean' | 'swept' | 'conflicted'; readonly legacyDirectory: string; readonly scopedDirectory: string; /** Sessions the fold added to the broker's store that were not already in it. */ readonly foldedSessions: number; /** Workspace-register rows the fold added to or refreshed in the shared tier. */ readonly foldedWorkspaceRows: number; /** Files MOVED to the scoped directory because nothing was there, the migration. */ readonly adoptedFiles: readonly string[]; /** Files retired into quarantine because the scoped copy already says the same thing. */ readonly movedFiles: readonly string[]; /** Where they were retired to, or null when nothing was retired. */ readonly quarantineDirectory: string | null; /** * Files that exist in BOTH places with DIFFERENT content. Left alone, both of * them: this sweep knows no merge semantics for an arbitrary store's shape, * and the honest move is to say so rather than pick a winner over the owner's * data. Named in the receipt. */ readonly conflictedFiles: readonly string[]; /** * Files this pass could not place, and did not guess at. Only the session * store, and only for a composition whose broker was handed a store object * rather than a path: there is then no scoped home to name for it. */ readonly skippedFiles: readonly string[]; /** * Files left at the unscoped path ON PURPOSE, because more than one product * reads them there. Not a failure and not a conflict, a deliberate * exclusion, named so the directory that survives is explained. */ readonly sharedFiles: readonly string[]; /** True when the legacy directory itself was removed because nothing was left in it. */ readonly legacyDirectoryRemoved: boolean; /** Anything that could not be read, moved or removed. Never fatal. */ readonly failures: readonly string[]; /** One owner-facing line naming what moved and why, or null when nothing did. */ readonly receipt: string | null; } /** * Fold, quarantine and disclose a pre-split control-plane store. * * Idempotent: a second run finds no duplicates (the first moved them) and * reports `clean` or `absent`. Safe to call on every boot. */ export declare function sweepPreSplitControlPlaneStore(input: { /** The unscoped, pre-split store directory, e.g. `~/.goodvibes/control-plane`. */ readonly legacyDirectory: string; /** The surface-scoped directory those stores belong in, e.g. `~/.goodvibes/tui/control-plane`. */ readonly scopedDirectory: string; /** * The session store the BROKER serves, which is project-scoped and therefore * need not sit in `scopedDirectory` at all. `null` for a broker with no file: * sessions.json is then treated like any other store. */ readonly sessionStorePath?: string | null | undefined; /** * Where the shared workspace register lives, the shared tier, which takes no * surface root. `null` leaves the legacy file alone and discloses it. */ readonly workspaceRegisterPath?: string | null | undefined; readonly now?: number | undefined; readonly pid?: number | undefined; }): Promise; /** * The owner-facing line. Names what moved, what it held, where it went, and, * when anything is still at the legacy path, says so rather than implying the * split is fully closed. */ export declare function preSplitSweepReceipt(report: PreSplitControlPlaneSweepReport): string | null; /** * Bound the quarantine directories: an age TTL plus a count cap, newest kept. * Exported so a test can drive it without waiting thirty days. */ export declare function reapQuarantineDirectories(parentDirectory: string, now?: number): { readonly expired: number; readonly overCap: number; }; //# sourceMappingURL=pre-split-control-plane-sweep.d.ts.map