@ricsam/r5d-worker 0.0.123 → 0.0.125

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.
Files changed (64) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/command-launcher.cjs +6 -2
  3. package/dist/cjs/main.cjs +496 -238
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/project-checkout-garbage.cjs +32 -3
  6. package/dist/cjs/project-workspace-state.cjs +51 -35
  7. package/dist/cjs/project-worktrees.cjs +79 -34
  8. package/dist/cjs/recovery-journal-protocol.cjs +56 -0
  9. package/dist/cjs/recovery-journal-runtime.cjs +133 -0
  10. package/dist/cjs/recovery-journal-thread.cjs +9 -0
  11. package/dist/cjs/recovery-journal.cjs +735 -0
  12. package/dist/cjs/recovery-store.cjs +8 -0
  13. package/dist/cjs/session-file-mutations.cjs +61 -0
  14. package/dist/cjs/working-tree-mirror.cjs +1 -0
  15. package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
  16. package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
  17. package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
  18. package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
  19. package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
  20. package/dist/cjs/workspace-git-sync.cjs +275 -201
  21. package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
  22. package/dist/mjs/command-launcher.mjs +6 -2
  23. package/dist/mjs/main.mjs +499 -244
  24. package/dist/mjs/package.json +1 -1
  25. package/dist/mjs/project-checkout-garbage.mjs +30 -2
  26. package/dist/mjs/project-workspace-state.mjs +51 -35
  27. package/dist/mjs/project-worktrees.mjs +74 -34
  28. package/dist/mjs/recovery-journal-protocol.mjs +30 -0
  29. package/dist/mjs/recovery-journal-runtime.mjs +112 -0
  30. package/dist/mjs/recovery-journal-thread.mjs +8 -0
  31. package/dist/mjs/recovery-journal.mjs +687 -0
  32. package/dist/mjs/recovery-store.mjs +8 -0
  33. package/dist/mjs/session-file-mutations.mjs +37 -0
  34. package/dist/mjs/working-tree-mirror.mjs +1 -0
  35. package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
  36. package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
  37. package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
  38. package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
  39. package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
  40. package/dist/mjs/workspace-git-sync.mjs +264 -202
  41. package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
  42. package/dist/types/command-launcher.d.ts +2 -1
  43. package/dist/types/main.d.ts +30 -19
  44. package/dist/types/project-checkout-garbage.d.ts +19 -2
  45. package/dist/types/project-workspace-state.d.ts +9 -9
  46. package/dist/types/project-worktrees.d.ts +49 -5
  47. package/dist/types/recovery-journal-protocol.d.ts +35 -0
  48. package/dist/types/recovery-journal-runtime.d.ts +12 -0
  49. package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
  50. package/dist/types/recovery-journal-thread.d.ts +1 -0
  51. package/dist/types/recovery-journal.d.ts +246 -0
  52. package/dist/types/recovery-store.d.ts +6 -0
  53. package/dist/types/session-file-mutations.d.ts +22 -0
  54. package/dist/types/workspace-command-sync-policy.d.ts +22 -7
  55. package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
  56. package/dist/types/workspace-filesystem-executor.d.ts +123 -0
  57. package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
  58. package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
  59. package/dist/types/workspace-git-sync.d.ts +113 -7
  60. package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
  61. package/package.json +1 -1
  62. package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
  63. package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
  64. package/dist/types/project-snapshot-recovery-runner.d.ts +0 -10
@@ -0,0 +1,95 @@
1
+ class WorkspaceMountHoldAbortedError extends Error {
2
+ constructor(reason) {
3
+ super(reason);
4
+ this.name = "WorkspaceMountHoldAbortedError";
5
+ }
6
+ }
7
+ class WorkspaceMountHoldFence {
8
+ holds = /* @__PURE__ */ new Map();
9
+ waiters = /* @__PURE__ */ new Set();
10
+ now;
11
+ constructor(options = {}) {
12
+ this.now = options.now ?? (() => Date.now());
13
+ }
14
+ /** Take one hold on every listed mount; the returned release is idempotent. */
15
+ hold(mountIds, description) {
16
+ const ids = [...new Set(mountIds)];
17
+ for (const id of ids) {
18
+ const existing = this.holds.get(id);
19
+ if (existing) {
20
+ existing.count += 1;
21
+ continue;
22
+ }
23
+ this.holds.set(id, { count: 1, description, sinceMs: this.now() });
24
+ }
25
+ let released = false;
26
+ return () => {
27
+ if (released) return;
28
+ released = true;
29
+ for (const id of ids) {
30
+ const existing = this.holds.get(id);
31
+ if (!existing) continue;
32
+ existing.count -= 1;
33
+ if (existing.count === 0) this.holds.delete(id);
34
+ }
35
+ this.wake();
36
+ };
37
+ }
38
+ isHeld(mountIds) {
39
+ return mountIds.some((id) => this.holds.has(id));
40
+ }
41
+ /** Human-readable holder of the first held mount among `mountIds`, for diagnostics. */
42
+ describeHold(mountIds) {
43
+ for (const id of mountIds) {
44
+ const hold = this.holds.get(id);
45
+ if (hold) return `${hold.description} (held for ${Math.round((this.now() - hold.sinceMs) / 1e3)}s)`;
46
+ }
47
+ return null;
48
+ }
49
+ /** Every held mount id, sorted, for status reporting. */
50
+ heldMountIds() {
51
+ return [...this.holds.keys()].sort();
52
+ }
53
+ /**
54
+ * Resolves in the same turn as the release that leaves none of `mountIds`
55
+ * held, so the caller can re-check and reserve without an interleaving
56
+ * hold. Resolves immediately when nothing is held. Rejects with
57
+ * WorkspaceMountHoldAbortedError when `signal` aborts first.
58
+ */
59
+ waitForRelease(mountIds, options = {}) {
60
+ if (options.signal?.aborted) {
61
+ return Promise.reject(new WorkspaceMountHoldAbortedError(abortReason(options.signal)));
62
+ }
63
+ if (!this.isHeld(mountIds)) return Promise.resolve();
64
+ return new Promise((resolve, reject) => {
65
+ const waiter = { mountIds, resolve, reject, signal: options.signal };
66
+ if (options.signal) {
67
+ const signal = options.signal;
68
+ waiter.onAbort = () => {
69
+ this.waiters.delete(waiter);
70
+ reject(new WorkspaceMountHoldAbortedError(abortReason(signal)));
71
+ };
72
+ signal.addEventListener("abort", waiter.onAbort, { once: true });
73
+ }
74
+ this.waiters.add(waiter);
75
+ });
76
+ }
77
+ wake() {
78
+ for (const waiter of [...this.waiters]) {
79
+ if (this.isHeld(waiter.mountIds)) continue;
80
+ this.waiters.delete(waiter);
81
+ if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener("abort", waiter.onAbort);
82
+ waiter.resolve();
83
+ }
84
+ }
85
+ }
86
+ function abortReason(signal) {
87
+ const reason = signal.reason;
88
+ if (reason instanceof Error) return reason.message;
89
+ if (typeof reason === "string" && reason) return reason;
90
+ return "Workspace mount reservation was aborted";
91
+ }
92
+ export {
93
+ WorkspaceMountHoldAbortedError,
94
+ WorkspaceMountHoldFence
95
+ };
@@ -86,7 +86,8 @@ export declare class WorkerCommandLauncher {
86
86
  capacityReport(): CommandLaunchCapacityReport;
87
87
  private notifyCapacityChange;
88
88
  acquire(request: CommandLaunchRequest): Promise<CommandLaunchLease>;
89
- cancel(id: string): Promise<void>;
89
+ /** Revoke a launch. Returns whether this launcher still held it (queued or acquired, not yet released). */
90
+ cancel(id: string): Promise<boolean>;
90
91
  cancelSession(sessionId: string): Promise<void>;
91
92
  cancelMatching(matches: (id: string, sessionId?: string) => boolean): Promise<void>;
92
93
  close(): Promise<void>;
@@ -4,6 +4,7 @@ import { WORKER_RESUMABLE_PROTOCOL, type WorkerRecoveryClientMessage, type Worke
4
4
  import { Database } from "bun:sqlite";
5
5
  import type { WorkerGitIdentity } from "./git-identity";
6
6
  import { configureGitHubRegistryAuthFiles, type PreparedPrivateAuthFileGeneration } from "./registry-auth";
7
+ import { WorkspaceMountHoldFence } from "./workspace-mount-hold-fence";
7
8
  import { type WorkspaceHydrationTransactionHooks } from "./workspace-git-sync";
8
9
  type WorkerProjectConfig = {
9
10
  projectId: string;
@@ -48,19 +49,22 @@ declare function workerServerHeartbeatAction(input: {
48
49
  watchdogSweepDelayed: boolean;
49
50
  }): "wait" | "start_probe" | "terminate";
50
51
  /**
51
- * A hydration transaction blocks this event loop until it ends, so no pong,
52
- * progress, or result can leave the process while it runs; on a slow disk
53
- * that is hours. Declare a hydration heartbeat lease first: the server keeps
54
- * the control socket open and defers the affected request deadlines while the
55
- * lease is active instead of reading the silence as a dead worker, which is
56
- * what turned one slow hydration into a mandatory canonical reset (incident
57
- * 3bccab03). Bun writes the frame to the socket synchronously; the extra
58
- * event-loop turn lets control messages that were already queued run before
59
- * the busy observation that precedes the transaction body.
52
+ * A hydration transaction runs on the workspace filesystem executor thread
53
+ * and on a stalled disk it can run for a long time; the server bounds its
54
+ * deadlines by a hydration heartbeat lease rather than by the ordinary
55
+ * request timeout. Declare that lease first: the server keeps the control
56
+ * socket open and defers the affected request deadlines while the lease is
57
+ * active, which is what turned one slow hydration into a mandatory canonical
58
+ * reset before the lease existed (incident 3bccab03). The extra event-loop
59
+ * turn lets control messages that were already queued run before the busy
60
+ * observation that precedes the transaction. The hooks also hold every mount
61
+ * a job reads or writes on the process-wide fence, so command admission for
62
+ * those mounts waits until the job ended (incident 10).
60
63
  */
61
64
  declare function createWorkspaceHydrationLeaseHooks(send: (message: Extract<WorkerClientMessage, {
62
65
  type: "heartbeat_lease";
63
- }>) => void, options?: {
66
+ }>) => void, options: {
67
+ fence: WorkspaceMountHoldFence;
64
68
  yieldToEventLoop?: () => Promise<void>;
65
69
  log?: (line: string) => void;
66
70
  now?: () => number;
@@ -69,6 +73,7 @@ export declare const workerLifecycleTestHarness: {
69
73
  boundedWorkerLifecycleProgress: typeof boundedWorkerLifecycleProgress;
70
74
  workerServerHeartbeatAction: typeof workerServerHeartbeatAction;
71
75
  createWorkspaceHydrationLeaseHooks: typeof createWorkspaceHydrationLeaseHooks;
76
+ workerCommandHasWorkspaceEffect: typeof workerCommandHasWorkspaceEffect;
72
77
  };
73
78
  type WorkspaceSyncTrigger = {
74
79
  type: "connect" | "inbound_head" | "write" | "edit" | "shell_inline" | "process_terminal" | "process_cancel" | "periodic" | "manual" | "remediation" | "remediation_confirm" | "remediation_reset";
@@ -136,6 +141,8 @@ export type WorkerSessionTarget = {
136
141
  ownerUserId: string;
137
142
  rootProfile: "visible_projects" | "canonical_sync";
138
143
  };
144
+ type WorkerCancelScope = "unstarted";
145
+ type WorkerCancelOutcome = "unstarted" | "started" | "stopped" | "unknown";
139
146
  type WorkerClientMessage = WorkerRecoveryClientMessage | {
140
147
  type: "capacity_report";
141
148
  capacity: import("./command-launcher").CommandLaunchCapacityReport & {
@@ -257,6 +264,8 @@ type WorkerClientMessage = WorkerRecoveryClientMessage | {
257
264
  runId: string;
258
265
  cancelled: boolean;
259
266
  message?: string;
267
+ /** `unstarted`: a launch that had not spawned was revoked; `started`: a spawned process was left alone; `stopped`; `unknown`: no record, cancellation recorded. */
268
+ outcome?: WorkerCancelOutcome;
260
269
  } | {
261
270
  type: "pong";
262
271
  } | {
@@ -528,6 +537,8 @@ type WorkerServerMessage = WorkerRecoveryServerMessage | {
528
537
  type: "cancel";
529
538
  requestId: string;
530
539
  runId: string;
540
+ /** `unstarted`: the server's start-deadline abandonment; never touch a spawned process. */
541
+ scope?: WorkerCancelScope;
531
542
  } | {
532
543
  type: "ping";
533
544
  };
@@ -1017,12 +1028,12 @@ export declare function prepareBuiltInToolPathsForTarget(input: {
1017
1028
  rootDir: string;
1018
1029
  access: "read" | "write";
1019
1030
  }): Promise<WorkerBuiltInToolPaths | undefined>;
1020
- export declare function readWorkerTextFile(branchPath: string, filePath: string, offset?: number, limit?: number, builtInPaths?: WorkerBuiltInToolPaths): WorkerReadFileResult;
1021
- export declare function writeWorkerTextFile(branchPath: string, filePath: string, content: string, builtInPaths?: WorkerBuiltInToolPaths): WorkerWriteFileResult;
1031
+ export declare function readWorkerTextFile(branchPath: string, filePath: string, offset?: number, limit?: number, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerReadFileResult>;
1032
+ export declare function writeWorkerTextFile(branchPath: string, filePath: string, content: string, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerWriteFileResult>;
1022
1033
  export declare function editWorkerTextFile(branchPath: string, filePath: string, edits: Array<{
1023
1034
  oldText: string;
1024
1035
  newText: string;
1025
- }>, builtInPaths?: WorkerBuiltInToolPaths): WorkerEditFileResult;
1036
+ }>, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerEditFileResult>;
1026
1037
  declare function executeWriteFileOperation(input: {
1027
1038
  message: Extract<WorkerServerMessage, {
1028
1039
  type: "write";
@@ -1049,17 +1060,17 @@ export declare function grepWorkerFiles(branchPath: string, input: {
1049
1060
  glob?: string;
1050
1061
  caseSensitive?: boolean;
1051
1062
  limit?: number;
1052
- }, builtInPaths?: WorkerBuiltInToolPaths): WorkerGrepResult;
1063
+ }, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerGrepResult>;
1053
1064
  export declare function findWorkerFiles(branchPath: string, input: {
1054
1065
  pattern?: string;
1055
1066
  path?: string;
1056
1067
  entryType?: string;
1057
1068
  limit?: number;
1058
- }, builtInPaths?: WorkerBuiltInToolPaths): WorkerFindResult;
1059
- export declare function listWorkerDirectory(branchPath: string, inputPath?: string, inputLimit?: number, builtInPaths?: WorkerBuiltInToolPaths): WorkerLsResult;
1060
- export declare function listWorkerCodeDirectory(branchPath: string, inputPath: string): WorkerCodeListResult;
1061
- export declare function readWorkerCodeFile(branchPath: string, inputPath: string): WorkerCodeReadResult;
1062
- export declare function readWorkerImageFile(branchPath: string, filePath: string, builtInPaths?: WorkerBuiltInToolPaths): WorkerViewFileBytesResult;
1069
+ }, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerFindResult>;
1070
+ export declare function listWorkerDirectory(branchPath: string, inputPath?: string, inputLimit?: number, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerLsResult>;
1071
+ export declare function listWorkerCodeDirectory(branchPath: string, inputPath: string): Promise<WorkerCodeListResult>;
1072
+ export declare function readWorkerCodeFile(branchPath: string, inputPath: string): Promise<WorkerCodeReadResult>;
1073
+ export declare function readWorkerImageFile(branchPath: string, filePath: string, builtInPaths?: WorkerBuiltInToolPaths): Promise<WorkerViewFileBytesResult>;
1063
1074
  declare function executeOperation(input: {
1064
1075
  message: WorkerOperationServerMessage;
1065
1076
  resolvedTarget: ResolvedWorkerSessionTarget;
@@ -18,7 +18,20 @@ export declare const PROJECT_DELETED_CHECKOUTS_DIRECTORY = ".r5d-deleted";
18
18
  * later collection walk through it, so staging refuses rather than continue.
19
19
  */
20
20
  export declare function stageProjectCheckoutForDeletion(projectRoot: string, branchName: string, checkoutPath: string): string;
21
- /** True when `candidate` is lexically a direct entry of some project's garbage directory under `projectsRoot`. */
21
+ /**
22
+ * Rename a whole project root out of the active namespace, into the
23
+ * namespace's garbage directory, so removing a deleted project never walks
24
+ * its checkouts on the worker event loop. Same guarantees as
25
+ * stageProjectCheckoutForDeletion: one same-filesystem rename, a real garbage
26
+ * directory, collection by the garbage collector later.
27
+ */
28
+ export declare function stageProjectForDeletion(projectsRoot: string, projectRoot: string): string;
29
+ /**
30
+ * True when `candidate` is lexically a direct entry of a project's garbage
31
+ * directory (`<namespace>/<project>/.r5d-deleted/<entry>`, a staged checkout)
32
+ * or of a namespace's garbage directory (`<namespace>/.r5d-deleted/<entry>`,
33
+ * a staged whole project) under `projectsRoot`.
34
+ */
22
35
  export declare function isStagedProjectCheckoutPath(projectsRoot: string, candidate: string): boolean;
23
36
  /**
24
37
  * Whether a lexically staged path is safe to remove right now: every ancestor
@@ -30,7 +43,11 @@ export declare function isStagedProjectCheckoutPath(projectsRoot: string, candid
30
43
  export declare function ownedStagedPath(projectsRoot: string, candidate: string): {
31
44
  kind: "directory" | "link";
32
45
  } | null;
33
- /** Every owned staged checkout under `<projectsRoot>/<namespace>/<project>/.r5d-deleted/`; linked ancestors are not followed. */
46
+ /**
47
+ * Every owned staged checkout under `<projectsRoot>/<namespace>/<project>/.r5d-deleted/`
48
+ * and every owned staged project under `<projectsRoot>/<namespace>/.r5d-deleted/`;
49
+ * linked ancestors are not followed.
50
+ */
34
51
  export declare function discoverStagedProjectCheckouts(projectsRoot: string): string[];
35
52
  /**
36
53
  * Delete a staged tree in a child process so a large recursive removal never
@@ -160,29 +160,29 @@ export declare class ProjectWorkspaceStateStore {
160
160
  desiredProjects: readonly ProjectWorkspaceDesiredProjectInput[];
161
161
  locallyPendingCreatedBranches?: readonly LocallyPendingCreatedProjectBranch[];
162
162
  preserveOnlyBranches?: readonly PreserveOnlyProjectBranch[];
163
- }): ProjectWorkspaceStateView;
163
+ }): Promise<ProjectWorkspaceStateView>;
164
164
  /** Record a successful enabled reseed without letting disabled config consume it. */
165
- recordEnabledRepositoryTransition(input: EnabledRepositoryTransition): ProjectWorkspaceStateView;
165
+ recordEnabledRepositoryTransition(input: EnabledRepositoryTransition): Promise<ProjectWorkspaceStateView>;
166
166
  /** Persist the create intent before invoking Git so a crash cannot orphan an untracked worktree. */
167
167
  recordPendingCreatedBranch(input: LocallyPendingCreatedProjectBranch & {
168
168
  branchId: string;
169
- }): ProjectWorkspaceStateView;
169
+ }): Promise<ProjectWorkspaceStateView>;
170
170
  /** Roll back an intent only when Git failed before creating its branch. */
171
- rollbackPendingCreatedBranch(input: LocallyPendingCreatedProjectBranch): ProjectWorkspaceStateView;
171
+ rollbackPendingCreatedBranch(input: LocallyPendingCreatedProjectBranch): Promise<ProjectWorkspaceStateView>;
172
172
  /** Clear a create intent after an authoritative desired config contains it. */
173
- clearPendingCreatedBranch(input: LocallyPendingCreatedProjectBranch): ProjectWorkspaceStateView;
173
+ clearPendingCreatedBranch(input: LocallyPendingCreatedProjectBranch): Promise<ProjectWorkspaceStateView>;
174
174
  /** Persist a branch tombstone before mutating its linked worktree or local ref. */
175
- beginBranchDeletion(input: LocallyPendingCreatedProjectBranch): ProjectWorkspaceStateView;
175
+ beginBranchDeletion(input: LocallyPendingCreatedProjectBranch): Promise<ProjectWorkspaceStateView>;
176
176
  recordTreePublication(input: {
177
177
  tombstoneIds: readonly string[];
178
178
  publishedHead: string;
179
- }): ProjectWorkspaceStateView;
179
+ }): Promise<ProjectWorkspaceStateView>;
180
180
  recordMirrorRefDeletion(input: {
181
181
  tombstoneId: string;
182
182
  branchName: string;
183
- }): ProjectWorkspaceStateView;
183
+ }): Promise<ProjectWorkspaceStateView>;
184
184
  }
185
185
  export declare const projectWorkspaceStateTestHarness: {
186
- writeEmptyStateAtDurableRootBoundary(stateRoot: string, afterStateRootFsync: () => void): void;
186
+ writeEmptyStateAtDurableRootBoundary(stateRoot: string, afterStateRootFsync: () => void): Promise<void>;
187
187
  };
188
188
  export {};
@@ -1,14 +1,21 @@
1
1
  import { type ProjectMirrorMount, type ProjectMirrorRefsTokens } from "./project-mirror-refs-token";
2
+ import type { WorkspaceFilesystemJobContext } from "./workspace-filesystem-job-types";
2
3
  export declare const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
3
4
  declare const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION: 1;
5
+ /**
6
+ * Distinguishes this control thread's live snapshots from stale ones left by
7
+ * an earlier process that reused the PID. The control thread passes it to
8
+ * every snapshot job, so the executor thread never invents its own.
9
+ */
10
+ export declare const PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID: `${string}-${string}-${string}-${string}-${string}`;
4
11
  /**
5
12
  * `project` snapshots cover every configured branch and restore by replacing
6
13
  * the whole project root. `branches` snapshots cover only the checkouts one
7
14
  * reconciliation rewrites and restore those paths individually, so a rollback
8
15
  * never touches a checkout the transaction did not.
9
16
  */
10
- type ProjectWorktreeSnapshotScope = "project" | "branches";
11
- type ProjectWorktreeSnapshotManifest = {
17
+ export type ProjectWorktreeSnapshotScope = "project" | "branches";
18
+ export type ProjectWorktreeSnapshotManifest = {
12
19
  version: typeof PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION;
13
20
  state: "building" | "prepared" | "restoring" | "consumed";
14
21
  scope: ProjectWorktreeSnapshotScope;
@@ -21,6 +28,11 @@ type ProjectWorktreeSnapshotManifest = {
21
28
  }>;
22
29
  createdAt: string;
23
30
  };
31
+ export type ProjectWorktreeSnapshot = {
32
+ root: string;
33
+ manifest: ProjectWorktreeSnapshotManifest;
34
+ paths: Map<string, string>;
35
+ };
24
36
  export type ProjectWorktreeSnapshotProgress = {
25
37
  operation: "snapshot" | "restore";
26
38
  phase: "scanning" | "copying" | "moving" | "syncing" | "cleaning";
@@ -161,10 +173,10 @@ export declare function createOrRetryLinkedProjectBranch(input: {
161
173
  branchName: string;
162
174
  workingTree: ProjectWorktreeWorkingTreeMode;
163
175
  pendingRetry: boolean;
164
- }): {
176
+ }): Promise<{
165
177
  branchPath: string;
166
178
  baseCommitHash: string;
167
- };
179
+ }>;
168
180
  export type ProjectCheckoutState = "linked" | "missing" | "not_a_worktree";
169
181
  export type ProjectCheckoutInventory = {
170
182
  /** Branches whose checkout is this project's primary clone or one of its linked worktrees. */
@@ -216,10 +228,18 @@ export declare function deleteLinkedProjectBranch(input: {
216
228
  */
217
229
  stagedForCollectionPath?: string;
218
230
  };
231
+ /**
232
+ * Retire a whole project: one rename into the namespace garbage directory
233
+ * (the tree, primary Git objects included, is removed by the collector
234
+ * outside every lease), or nothing when the root is already gone.
235
+ */
219
236
  export declare function removeProjectWorktrees(input: {
237
+ projectsRoot: string;
220
238
  projectRoot: string;
221
239
  branchNames: readonly string[];
222
- }): void;
240
+ }): {
241
+ stagedForCollectionPath?: string;
242
+ };
223
243
  export declare function deleteProjectMirrorBranch(input: {
224
244
  gitDirectory: string;
225
245
  branchName: string;
@@ -335,6 +355,30 @@ export declare function reseedProjectHeadsFromMirror(input: ProjectMirrorHeadUpd
335
355
  mirrorHead: string | null;
336
356
  reseeded: boolean;
337
357
  }>>;
358
+ /** Executor job body: `snapshotBranchTrees` with progress relayed to the control thread. */
359
+ export declare function runProjectSnapshotCreateJob(input: {
360
+ projectRoot: string;
361
+ branches: ProjectWorktreeBranch[];
362
+ temporaryRoot: string;
363
+ scope: ProjectWorktreeSnapshotScope;
364
+ ownerSessionId: string;
365
+ }, context: WorkspaceFilesystemJobContext): ProjectWorktreeSnapshot;
366
+ /** Executor job body: mirror captured checkouts back over reconciled worktrees. */
367
+ export declare function runProjectSnapshotApplyJob(input: {
368
+ mirrors: Array<{
369
+ snapshotPath: string;
370
+ targetPath: string;
371
+ }>;
372
+ }): null;
373
+ /** Executor job body: `persistReconciledProjectAndConsumeSnapshot`. */
374
+ export declare function runProjectSnapshotPersistJob(input: {
375
+ snapshot: ProjectWorktreeSnapshot;
376
+ durabilityPaths: string[] | null;
377
+ }): null;
378
+ /** Executor job body: `rollbackProjectWorktreeSnapshot`. */
379
+ export declare function runProjectSnapshotRollbackJob(input: {
380
+ snapshot: ProjectWorktreeSnapshot;
381
+ }): null;
338
382
  export declare const projectWorktreesTestHarness: {
339
383
  commandArgs: typeof gitCommandArgs;
340
384
  configureRepository: typeof configureRepository;
@@ -0,0 +1,35 @@
1
+ /** Messages between the control thread and the recovery journal thread. */
2
+ export declare const WORKER_RECOVERY_JOURNAL_OPS: readonly ["admit", "readmitUnknownBranchDeletion", "record", "unknown", "unknownRun", "acknowledgeResult", "acknowledgeOutput", "acknowledgeTerminal", "cancelSession", "isRequestCancelled", "operations", "recoveryState", "terminals", "outputReplayBegin", "outputReplayNext", "outputReplayEnd"];
3
+ export type WorkerRecoveryJournalOp = (typeof WORKER_RECOVERY_JOURNAL_OPS)[number];
4
+ export declare function isWorkerRecoveryJournalOp(value: unknown): value is WorkerRecoveryJournalOp;
5
+ export type WorkerRecoveryJournalEntry = {
6
+ seq: number;
7
+ op: WorkerRecoveryJournalOp;
8
+ args: unknown[];
9
+ };
10
+ export type WorkerRecoveryJournalBatchRequest = {
11
+ type: "batch";
12
+ id: number;
13
+ entries: WorkerRecoveryJournalEntry[];
14
+ };
15
+ export type WorkerRecoveryJournalSerializedError = {
16
+ name: string;
17
+ message: string;
18
+ stack?: string;
19
+ };
20
+ export type WorkerRecoveryJournalThreadReply = {
21
+ type: "ready";
22
+ ledgerId: string;
23
+ } | {
24
+ type: "batch_result";
25
+ id: number;
26
+ results: Array<{
27
+ seq: number;
28
+ value: unknown;
29
+ }>;
30
+ } | {
31
+ type: "batch_failure";
32
+ id: number;
33
+ error: WorkerRecoveryJournalSerializedError;
34
+ };
35
+ export declare function serializeWorkerRecoveryJournalError(error: unknown): WorkerRecoveryJournalSerializedError;
@@ -0,0 +1,12 @@
1
+ import type { MessagePort } from "node:worker_threads";
2
+ /**
3
+ * The recovery journal thread's loop. It owns the one WorkerRecoveryStore
4
+ * handle of the process and applies the control thread's entries strictly in
5
+ * sequence, one durable commit per batch. Everything the store guarantees
6
+ * (acceptance before effects, offsets, terminal lengths, unknown gating,
7
+ * acknowledgement retention, cancel snapshots) is unchanged; only the thread
8
+ * running it moved. `beforeBatch` exists for the stall fixture used by tests.
9
+ */
10
+ export declare function serveRecoveryJournal(port: MessagePort, filename: string, hooks?: {
11
+ beforeBatch?: () => void;
12
+ }): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};