@ricsam/r5d-worker 0.0.123 → 0.0.124

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 (60) hide show
  1. package/dist/cjs/main.cjs +488 -238
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/cjs/project-checkout-garbage.cjs +32 -3
  4. package/dist/cjs/project-workspace-state.cjs +51 -35
  5. package/dist/cjs/project-worktrees.cjs +79 -34
  6. package/dist/cjs/recovery-journal-protocol.cjs +56 -0
  7. package/dist/cjs/recovery-journal-runtime.cjs +133 -0
  8. package/dist/cjs/recovery-journal-thread.cjs +9 -0
  9. package/dist/cjs/recovery-journal.cjs +735 -0
  10. package/dist/cjs/recovery-store.cjs +8 -0
  11. package/dist/cjs/session-file-mutations.cjs +61 -0
  12. package/dist/cjs/working-tree-mirror.cjs +1 -0
  13. package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
  14. package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
  15. package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
  16. package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
  17. package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
  18. package/dist/cjs/workspace-git-sync.cjs +275 -201
  19. package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
  20. package/dist/mjs/main.mjs +491 -244
  21. package/dist/mjs/package.json +1 -1
  22. package/dist/mjs/project-checkout-garbage.mjs +30 -2
  23. package/dist/mjs/project-workspace-state.mjs +51 -35
  24. package/dist/mjs/project-worktrees.mjs +74 -34
  25. package/dist/mjs/recovery-journal-protocol.mjs +30 -0
  26. package/dist/mjs/recovery-journal-runtime.mjs +112 -0
  27. package/dist/mjs/recovery-journal-thread.mjs +8 -0
  28. package/dist/mjs/recovery-journal.mjs +687 -0
  29. package/dist/mjs/recovery-store.mjs +8 -0
  30. package/dist/mjs/session-file-mutations.mjs +37 -0
  31. package/dist/mjs/working-tree-mirror.mjs +1 -0
  32. package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
  33. package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
  34. package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
  35. package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
  36. package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
  37. package/dist/mjs/workspace-git-sync.mjs +264 -202
  38. package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
  39. package/dist/types/main.d.ts +24 -19
  40. package/dist/types/project-checkout-garbage.d.ts +19 -2
  41. package/dist/types/project-workspace-state.d.ts +9 -9
  42. package/dist/types/project-worktrees.d.ts +49 -5
  43. package/dist/types/recovery-journal-protocol.d.ts +35 -0
  44. package/dist/types/recovery-journal-runtime.d.ts +12 -0
  45. package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
  46. package/dist/types/recovery-journal-thread.d.ts +1 -0
  47. package/dist/types/recovery-journal.d.ts +246 -0
  48. package/dist/types/recovery-store.d.ts +6 -0
  49. package/dist/types/session-file-mutations.d.ts +22 -0
  50. package/dist/types/workspace-command-sync-policy.d.ts +22 -7
  51. package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
  52. package/dist/types/workspace-filesystem-executor.d.ts +123 -0
  53. package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
  54. package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
  55. package/dist/types/workspace-git-sync.d.ts +113 -7
  56. package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
  57. package/package.json +1 -1
  58. package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
  59. package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
  60. package/dist/types/project-snapshot-recovery-runner.d.ts +0 -10
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.123",
3
+ "version": "0.0.124",
4
4
  "type": "commonjs"
5
5
  }
@@ -34,7 +34,8 @@ __export(project_checkout_garbage_exports, {
34
34
  isStagedProjectCheckoutPath: () => isStagedProjectCheckoutPath,
35
35
  ownedStagedPath: () => ownedStagedPath,
36
36
  removeStagedProjectCheckout: () => removeStagedProjectCheckout,
37
- stageProjectCheckoutForDeletion: () => stageProjectCheckoutForDeletion
37
+ stageProjectCheckoutForDeletion: () => stageProjectCheckoutForDeletion,
38
+ stageProjectForDeletion: () => stageProjectForDeletion
38
39
  });
39
40
  module.exports = __toCommonJS(project_checkout_garbage_exports);
40
41
  var import_node_crypto = require("node:crypto");
@@ -58,11 +59,33 @@ function stageProjectCheckoutForDeletion(projectRoot, branchName, checkoutPath)
58
59
  import_node_fs.default.renameSync(checkoutPath, target);
59
60
  return target;
60
61
  }
62
+ function stageProjectForDeletion(projectsRoot, projectRoot) {
63
+ const root = import_node_path.default.resolve(projectsRoot);
64
+ const resolved = import_node_path.default.resolve(projectRoot);
65
+ const relative = import_node_path.default.relative(root, resolved);
66
+ const segments = relative.split(import_node_path.default.sep);
67
+ if (!relative || relative.startsWith("..") || import_node_path.default.isAbsolute(relative) || segments.length !== 2 || segments.some((segment) => !segment || segment.startsWith("."))) {
68
+ throw new Error(`Cannot stage ${projectRoot} for deletion: not a project root under ${projectsRoot}`);
69
+ }
70
+ if (!isRealDirectory(resolved)) throw new Error(`Cannot stage ${projectRoot} for deletion: not a directory`);
71
+ const garbageRoot = import_node_path.default.join(root, segments[0], PROJECT_DELETED_CHECKOUTS_DIRECTORY);
72
+ import_node_fs.default.mkdirSync(garbageRoot, { recursive: true });
73
+ if (!isRealDirectory(garbageRoot)) throw new Error(`Cannot stage ${projectRoot} for deletion: ${garbageRoot} is not a directory`);
74
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
75
+ const target = import_node_path.default.join(garbageRoot, `${segments[1]}-${timestamp}-${(0, import_node_crypto.randomUUID)().slice(0, 8)}`);
76
+ import_node_fs.default.renameSync(resolved, target);
77
+ return target;
78
+ }
61
79
  function isStagedProjectCheckoutPath(projectsRoot, candidate) {
62
80
  const relative = import_node_path.default.relative(import_node_path.default.resolve(projectsRoot), import_node_path.default.resolve(candidate));
63
81
  if (!relative || relative.startsWith("..") || import_node_path.default.isAbsolute(relative)) return false;
64
82
  const segments = relative.split(import_node_path.default.sep);
65
- return segments.length === 4 && segments[2] === PROJECT_DELETED_CHECKOUTS_DIRECTORY && segments[3] !== "" && !segments[3].startsWith(".");
83
+ const entry = segments[segments.length - 1];
84
+ if (!entry || entry.startsWith(".")) return false;
85
+ if (segments.length === 4)
86
+ return segments[2] === PROJECT_DELETED_CHECKOUTS_DIRECTORY && !segments[0].startsWith(".") && !segments[1].startsWith(".");
87
+ if (segments.length === 3) return segments[1] === PROJECT_DELETED_CHECKOUTS_DIRECTORY && !segments[0].startsWith(".");
88
+ return false;
66
89
  }
67
90
  function ownedStagedPath(projectsRoot, candidate) {
68
91
  const root = import_node_path.default.resolve(projectsRoot);
@@ -95,6 +118,11 @@ function discoverStagedProjectCheckouts(projectsRoot) {
95
118
  const staged = [];
96
119
  for (const namespace of realDirectoryEntries(root)) {
97
120
  if (namespace.startsWith(".")) continue;
121
+ const namespaceGarbageRoot = import_node_path.default.join(root, namespace, PROJECT_DELETED_CHECKOUTS_DIRECTORY);
122
+ for (const entry of realDirectoryEntries(namespaceGarbageRoot)) {
123
+ const candidate = import_node_path.default.join(namespaceGarbageRoot, entry);
124
+ if (ownedStagedPath(root, candidate)) staged.push(candidate);
125
+ }
98
126
  for (const project of realDirectoryEntries(import_node_path.default.join(root, namespace))) {
99
127
  if (project.startsWith(".")) continue;
100
128
  const garbageRoot = import_node_path.default.join(root, namespace, project, PROJECT_DELETED_CHECKOUTS_DIRECTORY);
@@ -186,5 +214,6 @@ class ProjectCheckoutGarbageCollector {
186
214
  isStagedProjectCheckoutPath,
187
215
  ownedStagedPath,
188
216
  removeStagedProjectCheckout,
189
- stageProjectCheckoutForDeletion
217
+ stageProjectCheckoutForDeletion,
218
+ stageProjectForDeletion
190
219
  });
@@ -314,47 +314,63 @@ function lstatIfExists(targetPath) {
314
314
  throw error;
315
315
  }
316
316
  }
317
- function ensureDurableStateRoot(stateRoot) {
317
+ async function fsyncDirectoryAsync(directory) {
318
+ const handle = await import_node_fs.default.promises.open(directory, "r");
319
+ try {
320
+ await handle.sync();
321
+ } finally {
322
+ await handle.close();
323
+ }
324
+ }
325
+ async function lstatIfExistsAsync(targetPath) {
326
+ try {
327
+ return await import_node_fs.default.promises.lstat(targetPath);
328
+ } catch (error) {
329
+ if (error.code === "ENOENT") return null;
330
+ throw error;
331
+ }
332
+ }
333
+ async function ensureDurableStateRoot(stateRoot) {
318
334
  const resolvedStateRoot = import_node_path.default.resolve(stateRoot);
319
335
  let existingAncestor = resolvedStateRoot;
320
- while (!lstatIfExists(existingAncestor)) {
336
+ while (!await lstatIfExistsAsync(existingAncestor)) {
321
337
  const parent = import_node_path.default.dirname(existingAncestor);
322
338
  if (parent === existingAncestor) throw new Error(`Project workspace state has no existing ancestor: ${resolvedStateRoot}`);
323
339
  existingAncestor = parent;
324
340
  }
325
- const ancestorStat = lstatIfExists(existingAncestor);
341
+ const ancestorStat = await lstatIfExistsAsync(existingAncestor);
326
342
  if (!ancestorStat || ancestorStat.isSymbolicLink() || !ancestorStat.isDirectory()) {
327
343
  throw new Error(`Project workspace state must descend from a real directory: ${resolvedStateRoot}`);
328
344
  }
329
- import_node_fs.default.mkdirSync(resolvedStateRoot, { recursive: true, mode: 448 });
345
+ await import_node_fs.default.promises.mkdir(resolvedStateRoot, { recursive: true, mode: 448 });
330
346
  let current = resolvedStateRoot;
331
347
  while (true) {
332
- const stat = lstatIfExists(current);
348
+ const stat = await lstatIfExistsAsync(current);
333
349
  if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
334
350
  throw new Error(`Project workspace state path must be a real directory: ${current}`);
335
351
  }
336
- fsyncDirectory(current);
352
+ await fsyncDirectoryAsync(current);
337
353
  if (current === existingAncestor) break;
338
354
  current = import_node_path.default.dirname(current);
339
355
  }
340
356
  }
341
- function writeStateAtomically(stateRoot, statePath, state, afterStateRootFsync) {
342
- ensureDurableStateRoot(stateRoot);
357
+ async function writeStateAtomically(stateRoot, statePath, state, afterStateRootFsync) {
358
+ await ensureDurableStateRoot(stateRoot);
343
359
  afterStateRootFsync?.();
344
360
  const temporaryPath = import_node_path.default.join(stateRoot, `.${PROJECT_WORKSPACE_STATE_FILE}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`);
345
- let descriptor;
361
+ let handle;
346
362
  try {
347
- descriptor = import_node_fs.default.openSync(temporaryPath, "wx", 384);
348
- import_node_fs.default.writeFileSync(descriptor, `${JSON.stringify(normalizeState(state), null, 2)}
363
+ handle = await import_node_fs.default.promises.open(temporaryPath, "wx", 384);
364
+ await handle.writeFile(`${JSON.stringify(normalizeState(state), null, 2)}
349
365
  `, "utf8");
350
- import_node_fs.default.fsyncSync(descriptor);
351
- import_node_fs.default.closeSync(descriptor);
352
- descriptor = void 0;
353
- import_node_fs.default.renameSync(temporaryPath, statePath);
354
- fsyncDirectory(stateRoot);
366
+ await handle.sync();
367
+ await handle.close();
368
+ handle = void 0;
369
+ await import_node_fs.default.promises.rename(temporaryPath, statePath);
370
+ await fsyncDirectoryAsync(stateRoot);
355
371
  } finally {
356
- if (descriptor !== void 0) import_node_fs.default.closeSync(descriptor);
357
- import_node_fs.default.rmSync(temporaryPath, { force: true });
372
+ if (handle !== void 0) await handle.close();
373
+ await import_node_fs.default.promises.rm(temporaryPath, { force: true });
358
374
  }
359
375
  }
360
376
  function createBranchTombstone(project, branchName) {
@@ -730,7 +746,7 @@ class ProjectWorkspaceStateStore {
730
746
  locallyPendingCreatedBranches: state.locallyPendingCreatedBranches
731
747
  });
732
748
  }
733
- reconcile(input) {
749
+ async reconcile(input) {
734
750
  const previous = readState(this.statePath);
735
751
  const authoritativeDesiredProjects = normalizeDesiredProjects(input.desiredProjects);
736
752
  for (const project of authoritativeDesiredProjects) {
@@ -806,11 +822,11 @@ class ProjectWorkspaceStateStore {
806
822
  enabledRepositoryTransitions: previous.enabledRepositoryTransitions.filter(({ projectId }) => authoritativeProjectIds.has(projectId)),
807
823
  tombstones
808
824
  });
809
- writeStateAtomically(this.stateRoot, this.statePath, nextState);
825
+ await writeStateAtomically(this.stateRoot, this.statePath, nextState);
810
826
  return stateView(this.projectsRoot, nextState);
811
827
  }
812
828
  /** Record a successful enabled reseed without letting disabled config consume it. */
813
- recordEnabledRepositoryTransition(input) {
829
+ async recordEnabledRepositoryTransition(input) {
814
830
  const [transition] = normalizeEnabledRepositoryTransitions([input]);
815
831
  const state = readState(this.statePath);
816
832
  if (!state.desiredProjects.some(({ projectId }) => projectId === transition.projectId)) {
@@ -819,11 +835,11 @@ class ProjectWorkspaceStateStore {
819
835
  state.enabledRepositoryTransitions = state.enabledRepositoryTransitions.filter(({ projectId }) => projectId !== transition.projectId);
820
836
  state.enabledRepositoryTransitions.push(transition);
821
837
  state.enabledRepositoryTransitions.sort((left, right) => left.projectId.localeCompare(right.projectId));
822
- writeStateAtomically(this.stateRoot, this.statePath, state);
838
+ await writeStateAtomically(this.stateRoot, this.statePath, state);
823
839
  return stateView(this.projectsRoot, state);
824
840
  }
825
841
  /** Persist the create intent before invoking Git so a crash cannot orphan an untracked worktree. */
826
- recordPendingCreatedBranch(input) {
842
+ async recordPendingCreatedBranch(input) {
827
843
  const [pending] = normalizePendingCreatedBranches([input]);
828
844
  const state = readState(this.statePath);
829
845
  const existingPending = state.locallyPendingCreatedBranches.find(
@@ -846,11 +862,11 @@ class ProjectWorkspaceStateStore {
846
862
  state.locallyPendingCreatedBranches.sort(
847
863
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
848
864
  );
849
- writeStateAtomically(this.stateRoot, this.statePath, state);
865
+ await writeStateAtomically(this.stateRoot, this.statePath, state);
850
866
  return stateView(this.projectsRoot, state);
851
867
  }
852
868
  /** Roll back an intent only when Git failed before creating its branch. */
853
- rollbackPendingCreatedBranch(input) {
869
+ async rollbackPendingCreatedBranch(input) {
854
870
  const [pending] = normalizePendingCreatedBranches([input]);
855
871
  const state = readState(this.statePath);
856
872
  const key = pendingCreatedBranchKey(pending);
@@ -863,11 +879,11 @@ class ProjectWorkspaceStateStore {
863
879
  );
864
880
  const project = state.desiredProjects.find(({ projectId }) => projectId === pending.projectId);
865
881
  if (project) project.branches = project.branches.filter(({ branchName }) => branchName !== pending.branchName);
866
- writeStateAtomically(this.stateRoot, this.statePath, state);
882
+ await writeStateAtomically(this.stateRoot, this.statePath, state);
867
883
  return stateView(this.projectsRoot, state);
868
884
  }
869
885
  /** Clear a create intent after an authoritative desired config contains it. */
870
- clearPendingCreatedBranch(input) {
886
+ async clearPendingCreatedBranch(input) {
871
887
  const [pending] = normalizePendingCreatedBranches([input]);
872
888
  const state = readState(this.statePath);
873
889
  const key = pendingCreatedBranchKey(pending);
@@ -877,11 +893,11 @@ class ProjectWorkspaceStateStore {
877
893
  state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
878
894
  (candidate) => pendingCreatedBranchKey(candidate) !== key
879
895
  );
880
- writeStateAtomically(this.stateRoot, this.statePath, state);
896
+ await writeStateAtomically(this.stateRoot, this.statePath, state);
881
897
  return stateView(this.projectsRoot, state);
882
898
  }
883
899
  /** Persist a branch tombstone before mutating its linked worktree or local ref. */
884
- beginBranchDeletion(input) {
900
+ async beginBranchDeletion(input) {
885
901
  const [target] = normalizePendingCreatedBranches([input]);
886
902
  const state = readState(this.statePath);
887
903
  const project = state.desiredProjects.find(({ projectId }) => projectId === target.projectId);
@@ -897,10 +913,10 @@ class ProjectWorkspaceStateStore {
897
913
  state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
898
914
  (candidate) => pendingCreatedBranchKey(candidate) !== key
899
915
  );
900
- writeStateAtomically(this.stateRoot, this.statePath, state);
916
+ await writeStateAtomically(this.stateRoot, this.statePath, state);
901
917
  return stateView(this.projectsRoot, state);
902
918
  }
903
- recordTreePublication(input) {
919
+ async recordTreePublication(input) {
904
920
  if (!GIT_OBJECT_ID_PATTERN.test(input.publishedHead)) throw new Error("Invalid published workspace head");
905
921
  const state = readState(this.statePath);
906
922
  const ids = new Set(input.tombstoneIds);
@@ -913,10 +929,10 @@ class ProjectWorkspaceStateStore {
913
929
  state.tombstones = state.tombstones.filter(
914
930
  (tombstone) => !tombstone.treePublishedHead || tombstone.kind === "branch" && !branchIsCurrentlyDesired(state.desiredProjects, tombstone.projectId, tombstone.branchName)
915
931
  );
916
- if (changed) writeStateAtomically(this.stateRoot, this.statePath, state);
932
+ if (changed) await writeStateAtomically(this.stateRoot, this.statePath, state);
917
933
  return stateView(this.projectsRoot, state);
918
934
  }
919
- recordMirrorRefDeletion(input) {
935
+ async recordMirrorRefDeletion(input) {
920
936
  (0, import_managed_paths.validateManagedBranchName)(input.branchName);
921
937
  const state = readState(this.statePath);
922
938
  const tombstone = state.tombstones.find(({ id }) => id === input.tombstoneId);
@@ -935,13 +951,13 @@ class ProjectWorkspaceStateStore {
935
951
  if (branchNames.every((branchName) => tombstone.mirrorRefsDeleted.includes(branchName))) {
936
952
  state.tombstones = state.tombstones.filter(({ id }) => id !== tombstone.id);
937
953
  }
938
- writeStateAtomically(this.stateRoot, this.statePath, state);
954
+ await writeStateAtomically(this.stateRoot, this.statePath, state);
939
955
  return stateView(this.projectsRoot, state);
940
956
  }
941
957
  }
942
958
  const projectWorkspaceStateTestHarness = {
943
959
  writeEmptyStateAtDurableRootBoundary(stateRoot, afterStateRootFsync) {
944
- writeStateAtomically(stateRoot, import_node_path.default.join(stateRoot, PROJECT_WORKSPACE_STATE_FILE), EMPTY_STATE, afterStateRootFsync);
960
+ return writeStateAtomically(stateRoot, import_node_path.default.join(stateRoot, PROJECT_WORKSPACE_STATE_FILE), EMPTY_STATE, afterStateRootFsync);
945
961
  }
946
962
  };
947
963
  // Annotate the CommonJS export names for ESM import in node:
@@ -29,6 +29,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  var project_worktrees_exports = {};
30
30
  __export(project_worktrees_exports, {
31
31
  PROJECT_REMOVED_CHECKOUTS_DIRECTORY: () => PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
32
+ PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID: () => PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
32
33
  PROJECT_WORKTREE_SNAPSHOT_PREFIX: () => PROJECT_WORKTREE_SNAPSHOT_PREFIX,
33
34
  ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
34
35
  applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
@@ -52,7 +53,11 @@ __export(project_worktrees_exports, {
52
53
  pushProjectMirrorHeads: () => pushProjectMirrorHeads,
53
54
  recoverStaleProjectWorktreeSnapshots: () => recoverStaleProjectWorktreeSnapshots,
54
55
  removeProjectWorktrees: () => removeProjectWorktrees,
55
- reseedProjectHeadsFromMirror: () => reseedProjectHeadsFromMirror
56
+ reseedProjectHeadsFromMirror: () => reseedProjectHeadsFromMirror,
57
+ runProjectSnapshotApplyJob: () => runProjectSnapshotApplyJob,
58
+ runProjectSnapshotCreateJob: () => runProjectSnapshotCreateJob,
59
+ runProjectSnapshotPersistJob: () => runProjectSnapshotPersistJob,
60
+ runProjectSnapshotRollbackJob: () => runProjectSnapshotRollbackJob
56
61
  });
57
62
  module.exports = __toCommonJS(project_worktrees_exports);
58
63
  var import_node_crypto = require("node:crypto");
@@ -64,6 +69,7 @@ var import_managed_paths = require("./managed-paths.cjs");
64
69
  var import_project_checkout_garbage = require("./project-checkout-garbage.cjs");
65
70
  var import_project_mirror_refs_token = require("./project-mirror-refs-token.cjs");
66
71
  var import_working_tree_mirror = require("./working-tree-mirror.cjs");
72
+ var import_workspace_filesystem_executor = require("./workspace-filesystem-executor.cjs");
67
73
  const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
68
74
  const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
69
75
  const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION = 1;
@@ -479,7 +485,7 @@ function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
479
485
  }
480
486
  }
481
487
  }
482
- function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, scope = "project") {
488
+ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, scope, ownerSessionId) {
483
489
  const resolvedProjectRoot = import_node_path.default.resolve(projectRoot);
484
490
  const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot ?? import_node_os.default.tmpdir());
485
491
  assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
@@ -502,7 +508,7 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, s
502
508
  state: "building",
503
509
  scope,
504
510
  ownerProcessId: process.pid,
505
- ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
511
+ ownerSessionId,
506
512
  projectRoot: resolvedProjectRoot,
507
513
  branches: branchSnapshots,
508
514
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -1025,7 +1031,7 @@ async function reconcileLinkedLayout(input) {
1025
1031
  plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: keep ? "keep" : "reset" });
1026
1032
  }
1027
1033
  const mutatedBranches = new Set(plan.filter(({ action }) => action !== "keep").map(({ branchName }) => branchName));
1028
- if (mutatedBranches.size > 0) input.beforeDestructiveMutation(mutatedBranches);
1034
+ if (mutatedBranches.size > 0) await input.beforeDestructiveMutation(mutatedBranches);
1029
1035
  for (const entry of plan) {
1030
1036
  if (entry.action === "keep") continue;
1031
1037
  if (entry.action === "reset") {
@@ -1066,13 +1072,19 @@ async function ensureProjectWorktrees(input) {
1066
1072
  const snapshotStorageRoot = import_node_path.default.resolve(input.snapshotRoot ?? import_node_os.default.tmpdir());
1067
1073
  assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
1068
1074
  let snapshot;
1069
- const ensureSnapshot = (branchNames) => {
1070
- snapshot ??= snapshotBranchTrees(
1071
- projectRoot,
1072
- branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches,
1073
- snapshotStorageRoot,
1074
- input.onSnapshotProgress,
1075
- branchNames ? "branches" : "project"
1075
+ const ensureSnapshot = async (branchNames) => {
1076
+ snapshot ??= await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run(
1077
+ "project_snapshot_create",
1078
+ {
1079
+ projectRoot,
1080
+ branches: (branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches).map((branch) => ({
1081
+ ...branch
1082
+ })),
1083
+ temporaryRoot: snapshotStorageRoot,
1084
+ scope: branchNames ? "branches" : "project",
1085
+ ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID
1086
+ },
1087
+ { onProgress: input.onSnapshotProgress }
1076
1088
  );
1077
1089
  return snapshot;
1078
1090
  };
@@ -1090,10 +1102,12 @@ async function ensureProjectWorktrees(input) {
1090
1102
  ...input,
1091
1103
  projectRoot,
1092
1104
  defaultBranch,
1093
- beforeDestructiveMutation: ensureSnapshot
1105
+ beforeDestructiveMutation: async (branchNames) => {
1106
+ await ensureSnapshot(branchNames);
1107
+ }
1094
1108
  }));
1095
1109
  } else {
1096
- ensureSnapshot();
1110
+ await ensureSnapshot();
1097
1111
  mirrorHeads = await initializeLinkedLayout({ ...input, projectRoot, defaultBranch });
1098
1112
  mutatedBranches = new Set(input.branches.map(({ branchName }) => branchName));
1099
1113
  }
@@ -1101,17 +1115,12 @@ async function ensureProjectWorktrees(input) {
1101
1115
  if (mutatedBranches.size > 0 && !preparedSnapshot) {
1102
1116
  throw new Error("Project worktree mutation started without a prepared recovery snapshot");
1103
1117
  }
1104
- for (const branch of input.branches) {
1105
- if (!mutatedBranches.has(branch.branchName)) continue;
1118
+ const snapshotMirrors = input.branches.flatMap((branch) => {
1119
+ if (!mutatedBranches.has(branch.branchName)) return [];
1106
1120
  const snapshotPath = preparedSnapshot?.paths.get(branch.branchName);
1107
- if (!snapshotPath) continue;
1108
- (0, import_working_tree_mirror.mirrorWorkingTree)({
1109
- sourceRoot: snapshotPath,
1110
- targetRoot: branchPath(projectRoot, branch.branchName),
1111
- sourceMode: "all",
1112
- deletionMode: "git"
1113
- });
1114
- }
1121
+ return snapshotPath ? [{ snapshotPath, targetPath: branchPath(projectRoot, branch.branchName) }] : [];
1122
+ });
1123
+ if (snapshotMirrors.length > 0) await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_snapshot_apply", { mirrors: snapshotMirrors });
1115
1124
  const states = [];
1116
1125
  for (const branch of input.branches) {
1117
1126
  const checkoutPath = branchPath(projectRoot, branch.branchName);
@@ -1127,24 +1136,23 @@ async function ensureProjectWorktrees(input) {
1127
1136
  states.sort((left, right) => left.branchName.localeCompare(right.branchName));
1128
1137
  if (snapshot) {
1129
1138
  const primaryGitDirectory = import_node_path.default.join(branchPath(projectRoot, input.primaryBranchName), ".git");
1130
- persistReconciledProjectAndConsumeSnapshot(
1139
+ await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_snapshot_persist", {
1131
1140
  snapshot,
1132
- void 0,
1133
- snapshot.manifest.scope === "branches" ? [
1141
+ durabilityPaths: snapshot.manifest.scope === "branches" ? [
1134
1142
  ...snapshot.manifest.branches.map(({ branchName }) => branchPath(projectRoot, branchName)),
1135
1143
  // Branch refs and worktree registrations moved in the shared
1136
1144
  // administrative directory; objects are fsynced by git itself.
1137
1145
  import_node_path.default.join(primaryGitDirectory, "refs"),
1138
1146
  import_node_path.default.join(primaryGitDirectory, "packed-refs"),
1139
1147
  import_node_path.default.join(primaryGitDirectory, "worktrees")
1140
- ] : void 0
1141
- );
1148
+ ] : null
1149
+ });
1142
1150
  }
1143
1151
  return states;
1144
1152
  } catch (error) {
1145
1153
  if (!snapshot) throw error;
1146
1154
  try {
1147
- rollbackProjectWorktreeSnapshot(snapshot);
1155
+ await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_snapshot_rollback", { snapshot });
1148
1156
  } catch (recoveryError) {
1149
1157
  throw new Error(
1150
1158
  `Project worktree reconciliation failed and its snapshot could not be fully restored; recovery state is retained at ${snapshot.root}`,
@@ -1275,7 +1283,7 @@ function createLinkedProjectBranch(input) {
1275
1283
  throw error;
1276
1284
  }
1277
1285
  }
1278
- function createOrRetryLinkedProjectBranch(input) {
1286
+ async function createOrRetryLinkedProjectBranch(input) {
1279
1287
  if (input.pendingRetry) {
1280
1288
  deleteLinkedProjectBranch({
1281
1289
  projectRoot: input.projectRoot,
@@ -1283,7 +1291,12 @@ function createOrRetryLinkedProjectBranch(input) {
1283
1291
  branchName: input.branchName
1284
1292
  });
1285
1293
  }
1286
- return createLinkedProjectBranch(input);
1294
+ return await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_branch_create", {
1295
+ projectRoot: input.projectRoot,
1296
+ sourceBranchName: input.sourceBranchName,
1297
+ branchName: input.branchName,
1298
+ workingTree: input.workingTree
1299
+ });
1287
1300
  }
1288
1301
  function classifyProjectCheckout(input) {
1289
1302
  const checkoutPath = branchPath(input.projectRoot, input.branchName);
@@ -1377,7 +1390,8 @@ function removeProjectWorktrees(input) {
1377
1390
  throw new Error(`Project branch ${branchName} has an in-progress Git operation`);
1378
1391
  }
1379
1392
  }
1380
- import_node_fs.default.rmSync(projectRoot, { recursive: true, force: true });
1393
+ if (!import_node_fs.default.existsSync(projectRoot)) return {};
1394
+ return { stagedForCollectionPath: (0, import_project_checkout_garbage.stageProjectForDeletion)(input.projectsRoot, projectRoot) };
1381
1395
  }
1382
1396
  async function deleteProjectMirrorBranch(input) {
1383
1397
  (0, import_managed_paths.validateManagedBranchName)(input.branchName);
@@ -1620,11 +1634,37 @@ async function reseedProjectHeadsFromMirror(input) {
1620
1634
  reseeded: moved
1621
1635
  }));
1622
1636
  }
1637
+ function runProjectSnapshotCreateJob(input, context) {
1638
+ return snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, context.progress, input.scope, input.ownerSessionId);
1639
+ }
1640
+ function runProjectSnapshotApplyJob(input) {
1641
+ for (const { snapshotPath, targetPath } of input.mirrors) {
1642
+ (0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: snapshotPath, targetRoot: targetPath, sourceMode: "all", deletionMode: "git" });
1643
+ }
1644
+ return null;
1645
+ }
1646
+ function runProjectSnapshotPersistJob(input) {
1647
+ persistReconciledProjectAndConsumeSnapshot(input.snapshot, void 0, input.durabilityPaths ?? void 0);
1648
+ return null;
1649
+ }
1650
+ function runProjectSnapshotRollbackJob(input) {
1651
+ rollbackProjectWorktreeSnapshot(input.snapshot);
1652
+ return null;
1653
+ }
1623
1654
  const projectWorktreesTestHarness = {
1624
1655
  commandArgs: gitCommandArgs,
1625
1656
  configureRepository,
1626
1657
  createPreparedSnapshot(input) {
1627
- return { root: snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, void 0, input.scope).root };
1658
+ return {
1659
+ root: snapshotBranchTrees(
1660
+ input.projectRoot,
1661
+ input.branches,
1662
+ input.temporaryRoot,
1663
+ void 0,
1664
+ input.scope ?? "project",
1665
+ PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID
1666
+ ).root
1667
+ };
1628
1668
  },
1629
1669
  consumeSnapshot(snapshotRoot) {
1630
1670
  const manifest = readSnapshotManifest(snapshotRoot);
@@ -1641,6 +1681,7 @@ const projectWorktreesTestHarness = {
1641
1681
  // Annotate the CommonJS export names for ESM import in node:
1642
1682
  0 && (module.exports = {
1643
1683
  PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
1684
+ PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
1644
1685
  PROJECT_WORKTREE_SNAPSHOT_PREFIX,
1645
1686
  ProjectBranchCreationRollbackIncompleteError,
1646
1687
  applyObservedProjectMirrorHeads,
@@ -1664,5 +1705,9 @@ const projectWorktreesTestHarness = {
1664
1705
  pushProjectMirrorHeads,
1665
1706
  recoverStaleProjectWorktreeSnapshots,
1666
1707
  removeProjectWorktrees,
1667
- reseedProjectHeadsFromMirror
1708
+ reseedProjectHeadsFromMirror,
1709
+ runProjectSnapshotApplyJob,
1710
+ runProjectSnapshotCreateJob,
1711
+ runProjectSnapshotPersistJob,
1712
+ runProjectSnapshotRollbackJob
1668
1713
  });
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var recovery_journal_protocol_exports = {};
20
+ __export(recovery_journal_protocol_exports, {
21
+ WORKER_RECOVERY_JOURNAL_OPS: () => WORKER_RECOVERY_JOURNAL_OPS,
22
+ isWorkerRecoveryJournalOp: () => isWorkerRecoveryJournalOp,
23
+ serializeWorkerRecoveryJournalError: () => serializeWorkerRecoveryJournalError
24
+ });
25
+ module.exports = __toCommonJS(recovery_journal_protocol_exports);
26
+ const WORKER_RECOVERY_JOURNAL_OPS = [
27
+ "admit",
28
+ "readmitUnknownBranchDeletion",
29
+ "record",
30
+ "unknown",
31
+ "unknownRun",
32
+ "acknowledgeResult",
33
+ "acknowledgeOutput",
34
+ "acknowledgeTerminal",
35
+ "cancelSession",
36
+ "isRequestCancelled",
37
+ "operations",
38
+ "recoveryState",
39
+ "terminals",
40
+ "outputReplayBegin",
41
+ "outputReplayNext",
42
+ "outputReplayEnd"
43
+ ];
44
+ function isWorkerRecoveryJournalOp(value) {
45
+ return typeof value === "string" && WORKER_RECOVERY_JOURNAL_OPS.includes(value);
46
+ }
47
+ function serializeWorkerRecoveryJournalError(error) {
48
+ if (!(error instanceof Error)) return { name: "Error", message: String(error) };
49
+ return { name: error.name, message: error.message, ...typeof error.stack === "string" ? { stack: error.stack } : {} };
50
+ }
51
+ // Annotate the CommonJS export names for ESM import in node:
52
+ 0 && (module.exports = {
53
+ WORKER_RECOVERY_JOURNAL_OPS,
54
+ isWorkerRecoveryJournalOp,
55
+ serializeWorkerRecoveryJournalError
56
+ });