@quantiya/codevibe-core 2.0.82 → 2.0.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1377 -574
- package/dist/local-executor/bounded-directory.d.ts +31 -1
- package/dist/local-executor/nolink-apply.d.ts +154 -0
- package/dist/local-executor/shadow-apply-capabilities.d.ts +114 -0
- package/dist/local-executor/shadow-base-hash.d.ts +81 -0
- package/dist/local-executor/shadow-baseline-shards.d.ts +34 -0
- package/dist/local-executor/shadow-copy-strategy.d.ts +20 -0
- package/dist/local-executor/shadow-free-space.d.ts +124 -0
- package/dist/local-executor/shadow-fs-margins.d.ts +59 -0
- package/dist/local-executor/shadow-protocol.d.ts +142 -4
- package/dist/local-executor/shadow-recovery.d.ts +72 -7
- package/dist/local-executor/shadow-roots.d.ts +126 -0
- package/dist/local-executor/shadow-start-record.d.ts +116 -0
- package/dist/local-executor/shadow-volume-probe.d.ts +42 -0
- package/dist/local-executor/workspace-materializer.d.ts +52 -0
- package/dist/local-executor/workspace-shadow.d.ts +23 -1
- package/dist/orchestration-shell/cli.js +39532 -36561
- package/dist/orchestration-shell/components/InputBar.d.ts +1 -1
- package/package.json +1 -1
|
@@ -30,19 +30,71 @@ export declare const SHADOW_DIR_RE: RegExp;
|
|
|
30
30
|
/** Private create name: never classified as a runnable/recoverable snapshot. */
|
|
31
31
|
export declare const SHADOW_STAGING_DIR_RE: RegExp;
|
|
32
32
|
export declare const SHADOW_TREE_TOMBSTONE_RE: RegExp;
|
|
33
|
+
/**
|
|
34
|
+
* P63 SB-45: materializer helpers per snapshot create. Every lane runs the same per-file checks; lanes only run in
|
|
35
|
+
* parallel. Chosen from the measured scaling on APFS (`.hard-gate/p63-…/evidence/lanes-bench-*.log`).
|
|
36
|
+
*/
|
|
37
|
+
export declare const DEFAULT_MATERIALIZER_LANES = 4;
|
|
33
38
|
export declare const SHADOW_UUID_SUFFIX_RE: RegExp;
|
|
39
|
+
/** §6.2's per-spawn advisory records live in the v2 state root beside the task folders, never as a task. */
|
|
40
|
+
export declare const SHADOW_STATE_ADVISORY_DIR = "advisory";
|
|
34
41
|
export declare function isStateRootInfrastructureDirectory(name: string): boolean;
|
|
35
42
|
export declare function taskIdFromGeneratedShadowName(name: string): string | null;
|
|
36
43
|
/** Strict lifecycle-lock residue authored by `withAnchoredExclusiveLock`. */
|
|
37
44
|
export declare const SHADOW_LIFECYCLE_LOCK_ARTIFACT_RE: RegExp;
|
|
38
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Fixed ceilings on a **physical** (non-reflink) copy — **retired by §5.6** (they were 64 MiB per file and 512 MiB in
|
|
47
|
+
* total, which made an ordinary ext4 project unusable). Every task now gets a budget from the space that actually
|
|
48
|
+
* exists (`shadow-free-space.ts` `physicalCopyBudgetBytes`, the materializer's limits), and the watchdog bounds the
|
|
49
|
+
* running task. These remain only as the largest value the budget and the session ledger accept.
|
|
50
|
+
*
|
|
51
|
+
* Lifting them is safe for rollback because Slice 1 tasks live under `shadow-state-v2/`, which the 2.0.77 floor never
|
|
52
|
+
* reads (§17): no older desktop ever meets a marker above the old 512 MiB.
|
|
53
|
+
*/
|
|
39
54
|
export declare const MAX_NON_COW_FILE_BYTES: number;
|
|
40
55
|
export declare const MAX_NON_COW_WORKSPACE_BYTES: number;
|
|
56
|
+
/**
|
|
57
|
+
* Sanity bound on a `physicalCopyBytes` value read back from a **persisted** marker or owner anchor.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately a separate constant from {@link MAX_NON_COW_WORKSPACE_BYTES}, which it used to be. The runtime
|
|
60
|
+
* limit decides how much a copy may write; this one decides whether a deserialized accounting value is
|
|
61
|
+
* plausible. They shared a constant, so raising the runtime limit alone would make recovery reject the marker of
|
|
62
|
+
* every task that legitimately copied more than the old value — turning a crash during a large task into an
|
|
63
|
+
* unrecoverable one. This is the same dual-purpose shape as the 100,000-entry ceiling, and is split the same way
|
|
64
|
+
* (Hendry's split decision for that ceiling, 2026-10-24, applied to its second instance).
|
|
65
|
+
*
|
|
66
|
+
* It rose *together with* the runtime limit — raising it alone would have let recovery accept values no task
|
|
67
|
+
* could write; lowering the runtime limit alone would make recovery reject its own markers. State written under §5.1's
|
|
68
|
+
* `shadow-state-v2/` root is never read by an older desktop, so lifting both is safe for rollback (§17).
|
|
69
|
+
*/
|
|
70
|
+
export declare const MAX_PERSISTED_PHYSICAL_COPY_BYTES: number;
|
|
71
|
+
/**
|
|
72
|
+
* Hashing ceilings — **retired by §5.6** (they were 256 MiB per tracked file and 2 GiB per tracked folder, which
|
|
73
|
+
* refused ordinary media-heavy folders outright). Every reader hashes in streamed chunks of at most 1 MiB with
|
|
74
|
+
* constant memory and an fd-stable `fstat` check, so these bounded only TIME, never memory. Hendry's SB-15 decision
|
|
75
|
+
* (2026-10-24, option A) keeps the clone path's three authentication reads; a very large folder therefore starts
|
|
76
|
+
* slowly instead of being refused. They remain as the largest value the readers accept.
|
|
77
|
+
*/
|
|
41
78
|
export declare const MAX_TRACKED_FILE_LOGICAL_BYTES: number;
|
|
42
79
|
export declare const MAX_TRACKED_WORKSPACE_LOGICAL_BYTES: number;
|
|
43
80
|
/** Metadata is bounded independently of content bytes (zero-byte files still cost memory/inodes). */
|
|
44
|
-
|
|
81
|
+
/**
|
|
82
|
+
* The shadow path's tracked-entry ceilings point at the **traversal** constant, not the orchestration one
|
|
83
|
+
* (Hendry's split decision, 2026-10-24; see `bounded-directory.ts`). The value is unchanged, so every current
|
|
84
|
+
* consumer — including `snapshot-merge.ts` and `verification-gates/diff-sanity.ts`, whose own caps are Slice 2
|
|
85
|
+
* — keeps exactly the bound it has today. What changes is which constant retires when §5.2's walk supplies the
|
|
86
|
+
* count: the traversal one, leaving `durable-store.ts`'s guards on persisted records untouched.
|
|
87
|
+
*/
|
|
88
|
+
export declare const MAX_TRACKED_FILE_COUNT = 5000000;
|
|
45
89
|
export declare const MAX_TRACKED_PATH_BYTES: number;
|
|
90
|
+
/**
|
|
91
|
+
* The bound on a CHANGE SET — reviewed bundles, merge plans and diff-sanity budgets (`snapshot-merge.ts`,
|
|
92
|
+
* `verification-gates/diff-sanity.ts`). §5.6 lifts these in Slice 2 ("replaced by streaming iteration over the
|
|
93
|
+
* sharded change manifest"), so they keep the historical value here, under their own name, while the traversal
|
|
94
|
+
* ceiling above rises in Slice 1 (P63 SB-33).
|
|
95
|
+
*/
|
|
96
|
+
export declare const MAX_CHANGE_SET_FILE_COUNT = 100000;
|
|
97
|
+
export declare const MAX_CHANGE_SET_PATH_BYTES: number;
|
|
46
98
|
/** Reviewed text is intentionally bounded before any whole-file decode/read. */
|
|
47
99
|
export declare const MAX_REVIEW_FILE_BYTES: number;
|
|
48
100
|
export declare const MAX_PROMOTION_PREIMAGE_AGGREGATE_BYTES: number;
|
|
@@ -124,6 +176,21 @@ export interface ShadowStateFileV2 {
|
|
|
124
176
|
hash: string | null;
|
|
125
177
|
mode?: number | null;
|
|
126
178
|
}>;
|
|
179
|
+
/**
|
|
180
|
+
* §5.2/§5.6: when present, `baseline` is stored in these attested shards and written to disk as `[]`; every
|
|
181
|
+
* reader expands it before validation (`shadow-baseline-shards.ts`). v2 state root only.
|
|
182
|
+
*/
|
|
183
|
+
baselineRecord?: import('./shadow-baseline-shards').BaselineRecord;
|
|
184
|
+
/**
|
|
185
|
+
* Stage 2 R1-F1: baseline entries added or changed AFTER `baselineRecord` was written (capture adds every new
|
|
186
|
+
* path with a null baseline). The shards are immutable, so these travel inline; expansion lays them over the
|
|
187
|
+
* shards. Present only with `baselineRecord`.
|
|
188
|
+
*/
|
|
189
|
+
baselineDelta?: Array<{
|
|
190
|
+
path: string;
|
|
191
|
+
hash: string | null;
|
|
192
|
+
mode?: number | null;
|
|
193
|
+
}>;
|
|
127
194
|
/** Exact intended post-images, present while/after promotion for crash reconciliation. */
|
|
128
195
|
promotePostHashes?: Array<{
|
|
129
196
|
path: string;
|
|
@@ -216,12 +283,75 @@ export type PreTaskModes = Map<string, number | null>;
|
|
|
216
283
|
export interface ShadowEnv {
|
|
217
284
|
/** @deprecated Compatibility-only; filesystem snapshots do not use Git retries. */
|
|
218
285
|
sleep?: (ms: number) => Promise<void>;
|
|
219
|
-
/** Override the per-task state dir root (default `~/.codevibe/shadow-state
|
|
286
|
+
/** Override the per-task state dir root (default `~/.codevibe/shadow-state-v2`, §5.1). */
|
|
220
287
|
stateRootDir?: string;
|
|
221
288
|
/** Override the process owner root (default `~/.codevibe/process-owners`). */
|
|
222
289
|
ownerRootDir?: string;
|
|
223
|
-
/**
|
|
290
|
+
/**
|
|
291
|
+
* Override the copy root. The default is paired with the state root (`shadow-roots.ts`): the §5.1 platform copy
|
|
292
|
+
* location for the v2 state root, `os.tmpdir()` for the legacy root and for any caller-chosen state root.
|
|
293
|
+
*/
|
|
224
294
|
shadowRootDir?: string;
|
|
295
|
+
/**
|
|
296
|
+
* @internal Replaces the legacy (2.0.77) root pair that production enumerators scan as their second pass
|
|
297
|
+
* (`shadow-roots.ts`), so tests can exercise that pass with temporary directories instead of `os.tmpdir()`.
|
|
298
|
+
*/
|
|
299
|
+
legacyRoots?: {
|
|
300
|
+
stateRootDir: string;
|
|
301
|
+
shadowRootDir: string;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* @internal The other copy roots sharing this pass's state root (`shadow-roots.ts` SB-22). Set only by a v2 pass;
|
|
305
|
+
* a marker naming one of them is left to that root's pass.
|
|
306
|
+
*/
|
|
307
|
+
siblingShadowRoots?: string[];
|
|
308
|
+
/** @internal Run promotion through §9.3's no-link engine on a volume that has links (SP-C's "forced copy"). */
|
|
309
|
+
forceNoLinkApply?: {
|
|
310
|
+
mode: 'nolink';
|
|
311
|
+
modesTracked?: boolean;
|
|
312
|
+
inodeStable?: boolean;
|
|
313
|
+
linkVolume?: boolean;
|
|
314
|
+
};
|
|
315
|
+
/** @internal Per-path deterministic controls for the no-link engine (crash points, seams). */
|
|
316
|
+
noLinkApplyTest?: (rel: string) => {
|
|
317
|
+
crashAt?: string;
|
|
318
|
+
crashAfter?: 'prepared' | 'moved' | 'placing' | 'restoring' | 'restoring-link' | 'pending-restore' | 'create-conflict' | 'done';
|
|
319
|
+
seams?: string[];
|
|
320
|
+
failAfterReservation?: number;
|
|
321
|
+
failReservationCleanup?: number;
|
|
322
|
+
onSeam?: (name: string, parentDir: string) => Promise<void>;
|
|
323
|
+
} | undefined;
|
|
324
|
+
/** @internal Test injection: `/proc/self/mountinfo` text for the drvfs apply refusal (SB-51), and its platform. */
|
|
325
|
+
applyMountinfo?: {
|
|
326
|
+
mountinfo: string;
|
|
327
|
+
platform: NodeJS.Platform;
|
|
328
|
+
};
|
|
329
|
+
/** @internal Control runs only: clone file by file, as before §5.3 batching (SB-36). */
|
|
330
|
+
disableDarwinBatchClones?: boolean;
|
|
331
|
+
/**
|
|
332
|
+
* @internal Materializer helpers per snapshot create (P63 SB-45, 1–16). Default
|
|
333
|
+
* {@link DEFAULT_MATERIALIZER_LANES}; 1 is the sequential copy exactly as shipped.
|
|
334
|
+
*/
|
|
335
|
+
materializerLanes?: number;
|
|
336
|
+
/** @internal Lower the inline-baseline threshold so tests exercise sharded markers (default 8 MiB). */
|
|
337
|
+
baselineInlineMaxBytes?: number;
|
|
338
|
+
/** @internal Test injection for §5.5's free-space check (`statfs` of the copy location). */
|
|
339
|
+
copySpaceStatfs?: (path: string) => Promise<{
|
|
340
|
+
bavail: number | bigint;
|
|
341
|
+
bsize: number | bigint;
|
|
342
|
+
blocks: number | bigint;
|
|
343
|
+
}>;
|
|
344
|
+
/** @internal Test injection for §5.1's copy-location probes (`shadow-roots.ts` `CopyLocationChoiceProbe`). */
|
|
345
|
+
copyRootProbe?: {
|
|
346
|
+
platform?: NodeJS.Platform;
|
|
347
|
+
home?: string;
|
|
348
|
+
xdgCacheHome?: string | undefined;
|
|
349
|
+
isMemoryBacked?: (existingPath: string) => Promise<boolean>;
|
|
350
|
+
uid?: number;
|
|
351
|
+
statfsType?: (p: string) => Promise<number>;
|
|
352
|
+
devOf?: (p: string) => Promise<number>;
|
|
353
|
+
volumesDir?: string;
|
|
354
|
+
};
|
|
225
355
|
/** Optional cancellation signal to bound sweep execution. */
|
|
226
356
|
signal?: AbortSignal;
|
|
227
357
|
/** Optional helper timeout in milliseconds for subprocess operations. */
|
|
@@ -337,7 +467,15 @@ export interface TrackBranchSpec {
|
|
|
337
467
|
/** The pinned group base commit the branch is created at. */
|
|
338
468
|
baseSha: string;
|
|
339
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* §5.1: Slice 1+ task state lives in `~/.codevibe/shadow-state-v2/`, a root the 2.0.77 compatibility floor never
|
|
472
|
+
* reads (§17). Tasks created by 2.0.77 and earlier stay in {@link legacyStateRoot} and are still recovered from it.
|
|
473
|
+
*/
|
|
340
474
|
export declare function defaultStateRoot(): string;
|
|
475
|
+
/** The state root of 2.0.77 and earlier. Read for recovery of those tasks only; new tasks never write here. */
|
|
476
|
+
export declare function legacyStateRoot(): string;
|
|
477
|
+
/** The home directory every default root is derived from (test-overridable through `CODEVIBE_HOME_OVERRIDE`). */
|
|
478
|
+
export declare function shadowHomeDir(): string;
|
|
341
479
|
export declare function hashContent(content: string): string;
|
|
342
480
|
export declare function openAnchoredDirectory(parent: string): Promise<fs.FileHandle>;
|
|
343
481
|
export declare function fsyncDirectory(dir: string): Promise<void>;
|
|
@@ -50,6 +50,14 @@ export interface RecoveredShadow {
|
|
|
50
50
|
cleanupBlocked?: boolean;
|
|
51
51
|
cleanupOwners?: ProcessCleanupOwner[];
|
|
52
52
|
unresolvedAuthorityRetention?: UnresolvedBundlePublicationRetention;
|
|
53
|
+
/**
|
|
54
|
+
* The root pair whose scan found this shadow (§17: v2 or 2.0.77). Reopening uses it, so a legacy task is
|
|
55
|
+
* revalidated against the legacy roots rather than against the caller's defaults.
|
|
56
|
+
*/
|
|
57
|
+
rootPair?: {
|
|
58
|
+
stateRootDir: string;
|
|
59
|
+
shadowRootDir: string;
|
|
60
|
+
};
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
63
|
* Read every durable per-task shadow marker under the state root. The reconnect/
|
|
@@ -97,7 +105,18 @@ export declare function reclaimDisplacedShadow(record: DisplacedShadowMarkerReco
|
|
|
97
105
|
snapshotIncarnations?: SnapshotIncarnationIndex | null;
|
|
98
106
|
}): Promise<'reaped' | 'retained'>;
|
|
99
107
|
export declare function reconcileDisplacedSnapshots(records: readonly DisplacedShadowMarkerRecord[], keepTaskIds: ReadonlySet<string>, ttlMs: number, now: number, physicalCopyScope: string, env: ShadowEnv, reaped: string[], retained: string[], snapshotIncarnations: SnapshotIncarnationIndex | null): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Every recoverable shadow on this machine: the v2 roots, then the 2.0.77 roots (§17), each scanned as its own
|
|
110
|
+
* self-consistent pair. A v2 root of a format this desktop does not know is list-only, so it is not offered for
|
|
111
|
+
* recovery at all — recovery acts on what it is given.
|
|
112
|
+
*/
|
|
100
113
|
export declare function listRecoverableShadows(env?: ShadowEnv): Promise<RecoveredShadow[]>;
|
|
114
|
+
/** `env` pinned to one root pair, so every default inside a pass resolves to that pair and nothing else. */
|
|
115
|
+
export declare function pairEnv(env: ShadowEnv, pass: {
|
|
116
|
+
stateRootDir: string;
|
|
117
|
+
shadowRootDir: string;
|
|
118
|
+
siblingShadowRoots?: string[];
|
|
119
|
+
}): ShadowEnv;
|
|
101
120
|
/**
|
|
102
121
|
* Join or recover every known atomic writer owned by the supplied task ids
|
|
103
122
|
* before strict teardown inventory. This is a mutating reconciliation step,
|
|
@@ -119,19 +138,46 @@ export interface ShadowCleanupResidue {
|
|
|
119
138
|
*/
|
|
120
139
|
export declare function listShadowCleanupResidue(taskIds: ReadonlySet<string>, env?: ShadowEnv): Promise<ShadowCleanupResidue[]>;
|
|
121
140
|
export declare function reapCrashedStateArtifacts(env: ShadowEnv): Promise<void>;
|
|
141
|
+
export declare function sealSnapshotTreeDurably(snapshotRoot: string, snapshotRootAuthority: WorkspaceRootAuthority, env: ShadowEnv): Promise<void>;
|
|
142
|
+
export declare function createCopyShadow(workingDir: string, shadowDir: string, sourceRootAuthority: WorkspaceRootAuthority, copyFile: (source: string, destination: string, maxLogicalBytes?: number) => Promise<MaterializedFileMetadata>, excludedRoots?: readonly string[], batchClones?: DarwinBatchClones,
|
|
122
143
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* private dependency inputs use clone-or-bounded-copy materialization; only
|
|
126
|
-
* source files enter the promotable tracked set.
|
|
144
|
+
* P63 SB-45: several materializer lanes (a {@link WorkspaceMaterializerPool}'s) to copy directories in parallel.
|
|
145
|
+
* Each lane brings its own batch clones. Absent, `copyFile` / `batchClones` are the single lane.
|
|
127
146
|
*/
|
|
128
|
-
|
|
129
|
-
export declare function createCopyShadow(workingDir: string, shadowDir: string, sourceRootAuthority: WorkspaceRootAuthority, copyFile: (source: string, destination: string, maxLogicalBytes?: number) => Promise<MaterializedFileMetadata>, excludedRoots?: readonly string[]): Promise<{
|
|
147
|
+
lanes?: readonly CopyLane[]): Promise<{
|
|
130
148
|
tracked: Set<string>;
|
|
131
149
|
metadata: Map<string, MaterializedFileMetadata>;
|
|
132
150
|
}>;
|
|
151
|
+
/**
|
|
152
|
+
* Recursively copy regular files from `srcRoot/relDir` into `dstRoot/relDir`,
|
|
153
|
+
* excluding `.git`/`node_modules` directories + secret-deny-list files.
|
|
154
|
+
* Populates `tracked` with the RELATIVE path of every copied regular file.
|
|
155
|
+
* `excludedRoots` must be subtrees reachable FROM the walk — an ancestor of
|
|
156
|
+
* `srcRoot` would exclude everything (`createCopyShadow` filters those out).
|
|
157
|
+
*/
|
|
158
|
+
type CopyFileFn = (source: string, destination: string, maxLogicalBytes?: number) => Promise<MaterializedFileMetadata>;
|
|
159
|
+
/** P63 SB-45: one materializer lane — its copy and, on macOS, the batch clones that must use the same helper. */
|
|
160
|
+
export interface CopyLane {
|
|
161
|
+
copyFile: CopyFileFn;
|
|
162
|
+
batchClones?: DarwinBatchClones;
|
|
163
|
+
}
|
|
164
|
+
/** §5.3 batch-clone hooks a materializer session provides on macOS (`WorkspaceMaterializerSession`). */
|
|
165
|
+
export interface DarwinBatchClones {
|
|
166
|
+
prepare(relativeDir: string, names: readonly string[]): Promise<unknown>;
|
|
167
|
+
finish(relativeDir: string): Promise<void>;
|
|
168
|
+
}
|
|
133
169
|
/** List every regular file under the shadow (RELATIVE), excluding excluded dirs + build-env. */
|
|
134
|
-
|
|
170
|
+
/** §7.1: a copy file's stat tuple, as one comparable string. ctime cannot be set by an unprivileged process. */
|
|
171
|
+
export declare function copyStatTuple(stat: {
|
|
172
|
+
size: bigint;
|
|
173
|
+
mtimeNs: string;
|
|
174
|
+
ctimeNs: string;
|
|
175
|
+
ino: string;
|
|
176
|
+
mode: string;
|
|
177
|
+
}): string;
|
|
178
|
+
export declare function listShadowRegularFiles(shadowDir: string, shadowRootAuthority: WorkspaceRootAuthority,
|
|
179
|
+
/** §7.1: when given, receives every listed file's stat tuple (from the same isolated enumeration — no extra I/O). */
|
|
180
|
+
statsOut?: Map<string, string>): Promise<string[]>;
|
|
135
181
|
type PathBudget = MetadataBudget;
|
|
136
182
|
export declare function createPathBudget(): PathBudget;
|
|
137
183
|
export declare function assertPathCollectionBounds(paths: readonly string[], label: string): void;
|
|
@@ -282,6 +328,11 @@ interface RegisteredShadowRoot extends AnchoredFsIdentity {
|
|
|
282
328
|
version?: 1;
|
|
283
329
|
canonicalPath?: string;
|
|
284
330
|
birthtimeNs?: string;
|
|
331
|
+
/**
|
|
332
|
+
* Stage 1 R1-F2: the root is provably gone (FU-1's inode proof) but is kept because one of its markers is
|
|
333
|
+
* recovery authority. Its bytes count as reclaimed; the entry retires once no marker is authority any more.
|
|
334
|
+
*/
|
|
335
|
+
vanished?: true;
|
|
285
336
|
}
|
|
286
337
|
/**
|
|
287
338
|
* FU-1 — a registered physical-copy root whose pathname is gone and whose bytes could NOT be proven reclaimed.
|
|
@@ -293,6 +344,20 @@ interface RegisteredShadowRoot extends AnchoredFsIdentity {
|
|
|
293
344
|
* two paths in full — desktop, hosted events and durable diagnostics — is FU1-D2, an owner decision made with the
|
|
294
345
|
* trade-off stated: they are CodeVibe-registered root paths, never raw error text and never user content.
|
|
295
346
|
*/
|
|
347
|
+
/**
|
|
348
|
+
* Stage 1 R1-F2: a per-volume copy root (`<volume root>/.codevibe-shadows-<uid>`, SB-22) that is missing because its
|
|
349
|
+
* drive is not mounted. Only that exact name shape counts, so a vanished temporary or configured root keeps the
|
|
350
|
+
* shipped FU-1 reconcile. The drive is away when the volume root is missing, or (Stage 1 R2-N1) when it is still
|
|
351
|
+
* there as a plain folder on the same device as the folder holding it — a mount point left behind empty, which is
|
|
352
|
+
* what an unmount under a folder that is not `/Volumes` (or a stale `/Volumes` entry) leaves — or (Stage 1 R3-N5)
|
|
353
|
+
* when a DIFFERENT drive is mounted under the same name: its device is not the registered root's device.
|
|
354
|
+
*/
|
|
355
|
+
export declare function isOnUnmountedVolume(copyRoot: string, registeredDev?: string): Promise<boolean>;
|
|
356
|
+
/**
|
|
357
|
+
* Stage 1 R1-F2: whether a state marker is recovery authority that a reconcile must never delete — APPLYING, or an
|
|
358
|
+
* instance with an open §9.3 journal entry. Unreadable counts as authority (fail closed).
|
|
359
|
+
*/
|
|
360
|
+
export declare function markerIsRecoveryAuthority(markerFile: string): Promise<boolean>;
|
|
296
361
|
export declare class StaleShadowRootError extends Error {
|
|
297
362
|
readonly stalePath: string;
|
|
298
363
|
readonly registryPath: string;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type ShadowEnv } from './shadow-protocol';
|
|
2
|
+
/** §5.1: the v2 root's format file. */
|
|
3
|
+
export declare const SHADOW_STATE_FORMAT_FILE = "FORMAT";
|
|
4
|
+
export declare const SHADOW_STATE_FORMAT_VERSION = 2;
|
|
5
|
+
export declare const SHADOW_STATE_FORMAT_BODY: string;
|
|
6
|
+
export type ShadowRootGeneration = 'v2' | 'legacy' | 'configured';
|
|
7
|
+
export interface ShadowRootPair {
|
|
8
|
+
stateRootDir: string;
|
|
9
|
+
shadowRootDir: string;
|
|
10
|
+
generation: ShadowRootGeneration;
|
|
11
|
+
/**
|
|
12
|
+
* The other copy roots sharing this pass's state root. A marker naming one of them belongs to that root's pass,
|
|
13
|
+
* so this pass skips it instead of reporting it as broken (SB-22).
|
|
14
|
+
*/
|
|
15
|
+
siblingShadowRoots?: string[];
|
|
16
|
+
}
|
|
17
|
+
/** A copy location §5.1 refuses: every candidate is memory-backed, so a copy there would vanish at reboot. */
|
|
18
|
+
export declare class CopyLocationRefusedError extends Error {
|
|
19
|
+
readonly candidates: string[];
|
|
20
|
+
readonly code = "ECOPYLOCATION";
|
|
21
|
+
constructor(candidates: string[]);
|
|
22
|
+
}
|
|
23
|
+
export interface CopyLocationProbe {
|
|
24
|
+
platform?: NodeJS.Platform;
|
|
25
|
+
home?: string;
|
|
26
|
+
xdgCacheHome?: string | undefined;
|
|
27
|
+
/** Injected in tests; production asks `statfs`. */
|
|
28
|
+
isMemoryBacked?: (existingPath: string) => Promise<boolean>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* §5.1's copy location for a project on the home data volume.
|
|
32
|
+
*
|
|
33
|
+
* - macOS: `~/Library/Caches/CodeVibe/shadows/` — every shipped Seatbelt profile works there, and Time Machine
|
|
34
|
+
* excludes it [SP-A].
|
|
35
|
+
* - Linux / WSL2: `${XDG_CACHE_HOME:-~/.cache}/codevibe/shadows/` when that is a disk filesystem, otherwise
|
|
36
|
+
* `~/.local/state/codevibe/shadows/`, otherwise refused. Fedora's `/tmp` is tmpfs [A1-L], which is why
|
|
37
|
+
* `os.tmpdir()` is never used.
|
|
38
|
+
*
|
|
39
|
+
* Other platforms keep the shipped `os.tmpdir()`: CodeVibe supports macOS, Linux and WSL2 only.
|
|
40
|
+
*/
|
|
41
|
+
export declare function defaultCopyLocation(probe?: CopyLocationProbe): Promise<string>;
|
|
42
|
+
/** The mount point holding `p`: its highest ancestor still on `p`'s device. */
|
|
43
|
+
export declare function mountPointOf(p: string, devOf?: (p: string) => Promise<number>): Promise<string>;
|
|
44
|
+
/** §5.1: a macOS project on another APFS volume is copied beside it, so clones work [A1c]. */
|
|
45
|
+
export declare function projectVolumeCopyRoot(volumeRoot: string, uid: number): string;
|
|
46
|
+
export interface CopyLocationChoice {
|
|
47
|
+
copyRoot: string;
|
|
48
|
+
/** Why — for the task log, so a full copy into the home location is explained rather than mysterious. */
|
|
49
|
+
basis: 'home-volume' | 'project-volume' | 'no-clone-volume' | 'linux-same-fs' | 'linux-default' | 'other-platform';
|
|
50
|
+
}
|
|
51
|
+
export interface CopyLocationChoiceProbe extends CopyLocationProbe {
|
|
52
|
+
uid?: number;
|
|
53
|
+
/** Injected in tests; production asks `statfs`. */
|
|
54
|
+
statfsType?: (p: string) => Promise<number>;
|
|
55
|
+
/** Injected in tests to place a folder on "another volume"; production asks `stat`. */
|
|
56
|
+
devOf?: (p: string) => Promise<number>;
|
|
57
|
+
/** Where macOS mounts other volumes; injected in tests. */
|
|
58
|
+
volumesDir?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* §5.1, chosen per task before copying.
|
|
62
|
+
*
|
|
63
|
+
* macOS — a project on the home data volume uses `~/Library/Caches/CodeVibe/shadows/`. A project on another APFS
|
|
64
|
+
* volume uses `<volume root>/.codevibe-shadows-<uid>/`, so clones work [A1c]. A project on a no-clone volume (exFAT,
|
|
65
|
+
* FAT, HFS+, …) uses the home location with a full copy (§5.5). Either way the project volume never hosts locks or
|
|
66
|
+
* state [A1d]: state is always the v2 root on the home filesystem.
|
|
67
|
+
*
|
|
68
|
+
* Linux / WSL2 — the first disk-backed candidate (`defaultCopyLocation`), except that a btrfs/XFS project prefers a
|
|
69
|
+
* candidate on its own filesystem, so clones work. When neither candidate shares that filesystem the task takes the
|
|
70
|
+
* first disk-backed one with a free-space-gated full copy (§5.5) rather than being refused (ASSUMPTIONS SB-24).
|
|
71
|
+
*/
|
|
72
|
+
export declare function chooseCopyLocation(workingDir: string, probe?: CopyLocationChoiceProbe): Promise<CopyLocationChoice>;
|
|
73
|
+
/**
|
|
74
|
+
* Every copy root a task under the v2 state root can be in: the home location(s), plus — on macOS — each
|
|
75
|
+
* `/Volumes/<volume>/.codevibe-shadows-<uid>/` present. A project-volume root exists only while it holds tasks,
|
|
76
|
+
* so it is found by listing what is mounted rather than by a registry that a crash could leave stale.
|
|
77
|
+
*/
|
|
78
|
+
export declare function v2CopyRoots(probe?: CopyLocationChoiceProbe): Promise<string[]>;
|
|
79
|
+
/** The copy root paired with `stateRootDir` (module comment: Pairing). */
|
|
80
|
+
export declare function pairedShadowRoot(stateRootDir: string, probe?: CopyLocationProbe): Promise<string>;
|
|
81
|
+
/** The one pair a per-task operation uses. Explicit roots always win. */
|
|
82
|
+
export declare function resolveShadowRoots(env?: Pick<ShadowEnv, 'stateRootDir' | 'shadowRootDir' | 'copyRootProbe'>, probe?: CopyLocationProbe | undefined): Promise<{
|
|
83
|
+
stateRootDir: string;
|
|
84
|
+
shadowRootDir: string;
|
|
85
|
+
}>;
|
|
86
|
+
/**
|
|
87
|
+
* The pairs an enumerator scans (module comment: Two passes).
|
|
88
|
+
*
|
|
89
|
+
* The legacy pass is added only when the caller asked for the production state root and left the copy root to
|
|
90
|
+
* its default — the two conditions under which "every task on this machine" is meant. A caller that pinned either
|
|
91
|
+
* root gets exactly the pair it pinned, which keeps every test and custom configuration single-pass.
|
|
92
|
+
*
|
|
93
|
+
* `legacyRoots` exists for tests: it replaces the legacy pair so a test can exercise the second pass with
|
|
94
|
+
* temporary directories instead of the real `os.tmpdir()`.
|
|
95
|
+
*/
|
|
96
|
+
export declare function shadowRootPasses(env?: Pick<ShadowEnv, 'stateRootDir' | 'shadowRootDir' | 'legacyRoots' | 'copyRootProbe'>, probe?: CopyLocationChoiceProbe | undefined): Promise<ShadowRootPair[]>;
|
|
97
|
+
export type StateRootFormat =
|
|
98
|
+
/** No format file yet: this desktop may create it. */
|
|
99
|
+
{
|
|
100
|
+
kind: 'absent';
|
|
101
|
+
}
|
|
102
|
+
/** A zero-length FORMAT — only a crash of the pre-R1-F5 writer leaves one; this desktop rewrites it atomically. */
|
|
103
|
+
| {
|
|
104
|
+
kind: 'empty';
|
|
105
|
+
}
|
|
106
|
+
/** The format this desktop writes. */
|
|
107
|
+
| {
|
|
108
|
+
kind: 'current';
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* A format this desktop does not know — written by a newer CodeVibe, or damaged. §17: the whole v2 root is
|
|
112
|
+
* list-only, so nothing in it is resumed, applied or removed.
|
|
113
|
+
*/
|
|
114
|
+
| {
|
|
115
|
+
kind: 'unknown';
|
|
116
|
+
detail: string;
|
|
117
|
+
};
|
|
118
|
+
/** Read a v2 state root's `FORMAT` without following a symlink. */
|
|
119
|
+
export declare function readStateRootFormat(stateRootDir: string): Promise<StateRootFormat>;
|
|
120
|
+
/**
|
|
121
|
+
* Write the v2 root's `FORMAT` before the first task state, durably, or confirm the one there is ours.
|
|
122
|
+
* Throws when the root holds a format this desktop does not know: a task must not be created into it.
|
|
123
|
+
*/
|
|
124
|
+
export declare function ensureStateRootFormat(stateRootDir: string): Promise<void>;
|
|
125
|
+
/** Whether an enumerator may act on a pass. A v2 root of unknown format is list-only (§17). */
|
|
126
|
+
export declare function passIsActionable(pass: ShadowRootPair): Promise<boolean>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type MetadataBudget } from './bounded-directory';
|
|
2
|
+
import { type StartRecordMargin } from './shadow-fs-margins';
|
|
3
|
+
/** §5.2: "Records are written as sharded JSON-lines files (50,000 entries per shard …)". */
|
|
4
|
+
export declare const START_RECORD_SHARD_ENTRIES = 50000;
|
|
5
|
+
/** The on-disk format version, so an older desktop refuses a record it does not know (§17). */
|
|
6
|
+
export declare const START_RECORD_VERSION = 1;
|
|
7
|
+
export type StartRecordEntryType = 'file' | 'dir' | 'symlink' | 'fifo' | 'socket' | 'blockDevice' | 'charDevice' | 'unknown';
|
|
8
|
+
export interface StartRecordEntry {
|
|
9
|
+
path: string;
|
|
10
|
+
type: StartRecordEntryType;
|
|
11
|
+
size: string;
|
|
12
|
+
mtimeNs: string;
|
|
13
|
+
ctimeNs: string;
|
|
14
|
+
ino: string;
|
|
15
|
+
dev: string;
|
|
16
|
+
uid: string;
|
|
17
|
+
gid: string;
|
|
18
|
+
mode: string;
|
|
19
|
+
/** True when `mtime >= T0 - margin`: this entry must be fingerprinted now, not deferred to §7.3. */
|
|
20
|
+
racy?: true;
|
|
21
|
+
}
|
|
22
|
+
export interface StartRecordShard {
|
|
23
|
+
name: string;
|
|
24
|
+
sha256: string;
|
|
25
|
+
entries: number;
|
|
26
|
+
/** Stage 2 R1-F8: the exact size, checked before the shard is read. */
|
|
27
|
+
bytes?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface StartRecordManifest {
|
|
30
|
+
startRecord: typeof START_RECORD_VERSION;
|
|
31
|
+
/** Taken when the walk starts (§5.2). */
|
|
32
|
+
t0Ns: string;
|
|
33
|
+
marginMs: number;
|
|
34
|
+
marginBasis: StartRecordMargin['basis'];
|
|
35
|
+
/**
|
|
36
|
+
* Whether `ino` may be used as a file's identity later. False on FAT, exFAT and drvfs — §9.3 uses inode
|
|
37
|
+
* identity only where the apply-time probe found it stable, and only within the same boot and mount.
|
|
38
|
+
*/
|
|
39
|
+
inodeIsIdentity: boolean;
|
|
40
|
+
entryCount: number;
|
|
41
|
+
racyCount: number;
|
|
42
|
+
shards: StartRecordShard[];
|
|
43
|
+
}
|
|
44
|
+
export interface WriteStartRecordOptions {
|
|
45
|
+
/** The user's folder. Walked, never written. */
|
|
46
|
+
sourceRoot: string;
|
|
47
|
+
/** The task's state directory (§5.1: `~/.codevibe/shadow-state-v2/<task>/`). Shards are written here. */
|
|
48
|
+
stateDir: string;
|
|
49
|
+
budget?: MetadataBudget;
|
|
50
|
+
/** Supplied by `probeTimestampGranularityMs` on the copy location; omitted means the fail-closed margin. */
|
|
51
|
+
probedGranularityMs?: number;
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
/**
|
|
54
|
+
* Directory NAMES never descended into (recorded as entries, not walked) — the same dependency/repository trees
|
|
55
|
+
* the copy skips (`SHADOW_SKIP_DIRS`), so the record never costs more than the copy it describes.
|
|
56
|
+
*/
|
|
57
|
+
skipDirNames?: ReadonlySet<string>;
|
|
58
|
+
/** Absolute directories never descended into — CodeVibe's own copy/state roots when they sit inside the folder. */
|
|
59
|
+
skipAbsoluteDirs?: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
export interface WalkedStartRecord {
|
|
62
|
+
t0Ns: bigint;
|
|
63
|
+
margin: StartRecordMargin;
|
|
64
|
+
entries: StartRecordEntry[];
|
|
65
|
+
racyCount: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Walk `sourceRoot` once with `lstat` only and return the record IN MEMORY. T0 is taken before a single directory
|
|
69
|
+
* is read. Persisting is separate ({@link persistStartRecord}), so a caller can take T0 before a copy and write the
|
|
70
|
+
* record only once the task's marker exists — a failed create then leaves no record behind (P63 SB-41).
|
|
71
|
+
*/
|
|
72
|
+
export declare function walkStartRecord(opts: Omit<WriteStartRecordOptions, 'stateDir'>): Promise<WalkedStartRecord>;
|
|
73
|
+
/**
|
|
74
|
+
* Persist a walked record as FLAT files in `stateDir`: `<prefix>-NNNNN.jsonl` shards (each `O_EXCL`, fsynced) and
|
|
75
|
+
* `<prefix>.manifest.json`. Flat because a task's state container must be — a directory there could conceal a
|
|
76
|
+
* relocated marker, so strict audits reject one (P63 SB-41).
|
|
77
|
+
*/
|
|
78
|
+
export declare function persistStartRecord(stateDir: string, walked: WalkedStartRecord, filePrefix?: string): Promise<StartRecordManifest>;
|
|
79
|
+
/** Walk `sourceRoot` once and write the sharded start record into `stateDir` (walk + persist). */
|
|
80
|
+
export declare function writeStartRecord(opts: WriteStartRecordOptions): Promise<StartRecordManifest>;
|
|
81
|
+
/**
|
|
82
|
+
* Total bytes of regular files under `sourceRoot`, by the same `lstat`-only walk as the start record, reading
|
|
83
|
+
* no contents and following no symlinks.
|
|
84
|
+
*
|
|
85
|
+
* This is §5.5's `bytesToCopy`. It deliberately does NOT apply §5.7's exclusions (hidden paths, dependency trees,
|
|
86
|
+
* rebuild outputs), so it can only over-count what a full copy will write — which makes the free-space check
|
|
87
|
+
* demand more headroom, never less. Under-counting is the direction that would admit a copy that then fails with
|
|
88
|
+
* ENOSPC partway through.
|
|
89
|
+
*/
|
|
90
|
+
export declare function measureRegularFileBytes(sourceRoot: string, opts?: {
|
|
91
|
+
budget?: MetadataBudget;
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
}): Promise<{
|
|
94
|
+
bytes: number;
|
|
95
|
+
files: number;
|
|
96
|
+
}>;
|
|
97
|
+
export interface FolderChangesSinceStart {
|
|
98
|
+
changed: number;
|
|
99
|
+
created: number;
|
|
100
|
+
deleted: number;
|
|
101
|
+
/** Up to 20 example paths, for the task log (Slice 2's review INDEX lists them all). */
|
|
102
|
+
examples: string[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* §7.4: re-walk the user's folder with the start record's own rules and count every entry that differs from it —
|
|
106
|
+
* the user's own edits during the task, which reviewers should know about ("your folder changed during the task:
|
|
107
|
+
* N files"). Compares `type, size, mtimeNs, ctimeNs` and, where the record says inodes are identity, `ino`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function folderChangesSinceStart(opts: {
|
|
110
|
+
sourceRoot: string;
|
|
111
|
+
/** The state directory holding the flat record, and the record's file prefix. */
|
|
112
|
+
stateDir: string;
|
|
113
|
+
filePrefix: string;
|
|
114
|
+
skipDirNames?: ReadonlySet<string>;
|
|
115
|
+
skipAbsoluteDirs?: readonly string[];
|
|
116
|
+
}): Promise<FolderChangesSinceStart>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type ApplyMode = 'link' | 'nolink';
|
|
2
|
+
export interface VolumeCapabilities {
|
|
3
|
+
version: 1;
|
|
4
|
+
mode: ApplyMode;
|
|
5
|
+
modesTracked: boolean;
|
|
6
|
+
inodeStable: boolean;
|
|
7
|
+
bootId: string;
|
|
8
|
+
dev: string;
|
|
9
|
+
/** What each step returned, for the task log and the reviewer packet. */
|
|
10
|
+
steps: Record<'exclusiveCreate' | 'link' | 'chmod' | 'rename' | 'dirFsync' | 'inodes', string>;
|
|
11
|
+
}
|
|
12
|
+
/** Probe entry names are CodeVibe's and token-bearing, so recovery can never mistake a user file for one. */
|
|
13
|
+
export declare const PROBE_ENTRY_RE: RegExp;
|
|
14
|
+
export declare function newProbeEntryName(): string;
|
|
15
|
+
/** Linux `/proc/sys/kernel/random/boot_id`; macOS `kern.boottime`. */
|
|
16
|
+
export declare function bootIdentity(platform?: NodeJS.Platform): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Probe `projectRoot`. `journalProbeEntry` must durably record the name before this function creates anything.
|
|
19
|
+
* Never throws for an unsupported capability — that is a result. Throws only when the probe cannot run at all
|
|
20
|
+
* (the root is not writable), which the caller reports as an apply error before any user file is touched.
|
|
21
|
+
*/
|
|
22
|
+
export declare function probeVolumeCapabilities(projectRoot: string, journalProbeEntry: (name: string) => Promise<void>, opts?: {
|
|
23
|
+
platform?: NodeJS.Platform;
|
|
24
|
+
}): Promise<VolumeCapabilities>;
|
|
25
|
+
/** Remove the probe entry `name` and its two derived names — exactly those, and only if they are CodeVibe's. */
|
|
26
|
+
export declare function removeJournaledProbeEntry(projectRoot: string, name: string): Promise<void>;
|
|
27
|
+
/** §9.1: inodes count as identity only with the flag, in the same boot, on the same device. */
|
|
28
|
+
export declare function inodesTrustedNow(journaled: Pick<VolumeCapabilities, 'inodeStable' | 'bootId' | 'dev'>, projectRoot: string, platform?: NodeJS.Platform): Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* P63 SB-51 (Hendry, 2026-10-24: "fail closed on drvfs"): applying into a Windows drive from WSL2 is refused in
|
|
31
|
+
* Slice 1 — no WSL2 host was available to qualify §9.3 on a real drvfs volume. Nothing is applied; the copy is kept.
|
|
32
|
+
*/
|
|
33
|
+
export declare const DRVFS_APPLY_REFUSAL = "Applying into a Windows drive from WSL2 (drvfs) isn't supported yet \u2014 nothing was applied; the task's copy is kept.";
|
|
34
|
+
/**
|
|
35
|
+
* True when `target` lives on a WSL drvfs mount: the longest `/proc/self/mountinfo` mount point containing its
|
|
36
|
+
* real path has filesystem type `drvfs` (WSL1, some WSL2 builds) or `9p` with `aname=drvfs` (WSL2). Only Linux
|
|
37
|
+
* has drvfs; elsewhere this is false without reading anything.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isDrvfsPath(target: string, opts?: {
|
|
40
|
+
mountinfo?: string;
|
|
41
|
+
platform?: NodeJS.Platform;
|
|
42
|
+
}): Promise<boolean>;
|