@ricsam/r5d-worker 0.0.132 → 0.0.133

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 (47) hide show
  1. package/dist/cjs/atomic-rename.cjs +303 -0
  2. package/dist/cjs/git-blob-hash.cjs +41 -0
  3. package/dist/cjs/main.cjs +187 -38
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/three-way-merge.cjs +346 -0
  6. package/dist/cjs/working-tree-mirror.cjs +1049 -64
  7. package/dist/cjs/workspace-command-sync-policy.cjs +8 -4
  8. package/dist/cjs/workspace-command-targets.cjs +63 -0
  9. package/dist/cjs/workspace-filesystem-job-types.cjs +11 -1
  10. package/dist/cjs/workspace-filesystem-jobs.cjs +2 -0
  11. package/dist/cjs/workspace-git-sync.cjs +846 -61
  12. package/dist/cjs/workspace-hydration-ledger.cjs +66 -0
  13. package/dist/cjs/workspace-hydration-merge.cjs +433 -0
  14. package/dist/cjs/workspace-hydration-recovery-state.cjs +53 -0
  15. package/dist/cjs/workspace-merge-projection.cjs +81 -10
  16. package/dist/cjs/workspace-project-config-policy.cjs +19 -12
  17. package/dist/mjs/atomic-rename.mjs +261 -0
  18. package/dist/mjs/git-blob-hash.mjs +16 -0
  19. package/dist/mjs/main.mjs +196 -39
  20. package/dist/mjs/package.json +1 -1
  21. package/dist/mjs/three-way-merge.mjs +318 -0
  22. package/dist/mjs/working-tree-mirror.mjs +1035 -64
  23. package/dist/mjs/workspace-command-sync-policy.mjs +8 -4
  24. package/dist/mjs/workspace-command-targets.mjs +37 -0
  25. package/dist/mjs/workspace-filesystem-job-types.mjs +11 -1
  26. package/dist/mjs/workspace-filesystem-jobs.mjs +4 -0
  27. package/dist/mjs/workspace-git-sync.mjs +854 -62
  28. package/dist/mjs/workspace-hydration-ledger.mjs +42 -0
  29. package/dist/mjs/workspace-hydration-merge.mjs +399 -0
  30. package/dist/mjs/workspace-hydration-recovery-state.mjs +29 -0
  31. package/dist/mjs/workspace-merge-projection.mjs +85 -11
  32. package/dist/mjs/workspace-project-config-policy.mjs +16 -10
  33. package/dist/types/atomic-rename.d.ts +78 -0
  34. package/dist/types/git-blob-hash.d.ts +10 -0
  35. package/dist/types/main.d.ts +21 -2
  36. package/dist/types/three-way-merge.d.ts +77 -0
  37. package/dist/types/working-tree-mirror.d.ts +270 -7
  38. package/dist/types/workspace-command-sync-policy.d.ts +12 -6
  39. package/dist/types/workspace-command-targets.d.ts +37 -0
  40. package/dist/types/workspace-filesystem-job-types.d.ts +46 -4
  41. package/dist/types/workspace-git-sync.d.ts +125 -3
  42. package/dist/types/workspace-hydration-ledger.d.ts +43 -0
  43. package/dist/types/workspace-hydration-merge.d.ts +95 -0
  44. package/dist/types/workspace-hydration-recovery-state.d.ts +10 -0
  45. package/dist/types/workspace-merge-projection.d.ts +19 -1
  46. package/dist/types/workspace-project-config-policy.d.ts +17 -3
  47. package/package.json +2 -2
package/dist/mjs/main.mjs CHANGED
@@ -62,11 +62,21 @@ import {
62
62
  } from "./workspace-command-sync-policy.mjs";
63
63
  import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
64
64
  import { WorkspaceMountHoldAbortedError, WorkspaceMountHoldFence } from "./workspace-mount-hold-fence.mjs";
65
+ import {
66
+ commandTargets,
67
+ describeCommandAdditionalTargets,
68
+ normalizeCommandAdditionalTargets
69
+ } from "./workspace-command-targets.mjs";
65
70
  import { installWorkspaceFilesystemExecutorFailureHandler, workspaceFilesystemExecutor } from "./workspace-filesystem-executor.mjs";
71
+ import { findHydrationRecovery } from "./workspace-hydration-recovery-state.mjs";
66
72
  import { WorkspaceProjectionLedger } from "./workspace-projection-ledger.mjs";
67
73
  import { WorkspaceSyncCoalescer } from "./workspace-sync-coalescer.mjs";
68
74
  import { checkoutPathMovePublicationComplete } from "./workspace-path-move.mjs";
69
- import { busyProjectConfigurationChangeIds, deferredProjectConfigurationPendingBranches } from "./workspace-project-config-policy.mjs";
75
+ import {
76
+ busyProjectConfigurationChanges,
77
+ deferredProjectConfigurationPendingBranches,
78
+ describeBusyProjectConfigurationChanges
79
+ } from "./workspace-project-config-policy.mjs";
70
80
  import { creatorLocalProjectBranchIsAuthorized, workerProjectBranchDisposition } from "./workspace-preserve-only-policy.mjs";
71
81
  import { assertProjectBranchDeletionIncarnation } from "./workspace-branch-incarnation-policy.mjs";
72
82
  import { activeProjectBranchPublicationEvidence } from "./workspace-publication-evidence.mjs";
@@ -170,6 +180,16 @@ const workerLifecycleTestHarness = {
170
180
  workerCommandHasWorkspaceEffect
171
181
  };
172
182
  class ProjectWorkspaceConfigurationDeferredError extends Error {
183
+ constructor(changes) {
184
+ super(
185
+ `Project workspace configuration deferred while visible checkout activity is active: ${describeBusyProjectConfigurationChanges(changes)}`
186
+ );
187
+ this.changes = changes;
188
+ this.name = "ProjectWorkspaceConfigurationDeferredError";
189
+ this.projectIds = changes.map(({ projectId }) => projectId);
190
+ }
191
+ changes;
192
+ projectIds;
173
193
  }
174
194
  class WorkerServerUnavailableError extends Error {
175
195
  name = "WorkerServerUnavailableError";
@@ -393,13 +413,12 @@ const workspaceSyncSingleFlight = {
393
413
  afterCurrent() {
394
414
  return workspaceSyncQueue;
395
415
  },
396
- mountHold(target, signal) {
397
- if (target.type !== "project") return null;
398
- const mountIds = [projectMountId(target.projectId, target.branchName)];
399
- if (!workspaceMountHoldFence.isHeld(mountIds)) return null;
416
+ mountHold(targets, signal) {
417
+ const mountIds = targets.flatMap((target) => target.type === "project" ? [projectMountId(target.projectId, target.branchName)] : []);
418
+ if (mountIds.length === 0 || !workspaceMountHoldFence.isHeld(mountIds)) return null;
400
419
  const holder = workspaceMountHoldFence.describeHold(mountIds);
401
420
  process.stdout.write(
402
- `[r5d-worker] command for ${target.projectId}/${target.branchName} waits for ${holder ?? "a workspace filesystem job"}
421
+ `[r5d-worker] command for ${targets.map(describeWorkerSessionTarget).join(", ")} waits for ${holder ?? "a workspace filesystem job"}
403
422
  `
404
423
  );
405
424
  return workspaceMountHoldFence.waitForRelease(mountIds, { signal });
@@ -1012,6 +1031,7 @@ function validateProjectId(projectId) {
1012
1031
  function validateBranchName(branchName) {
1013
1032
  validateManagedBranchName(branchName);
1014
1033
  }
1034
+ const commandTargetValidators = { validateProjectId, validateBranchName };
1015
1035
  const NON_RECURSIVE_GIT_CONFIG_ARGS = [
1016
1036
  "-c",
1017
1037
  "submodule.recurse=false",
@@ -2963,6 +2983,7 @@ async function executeCommand(input) {
2963
2983
  activeProcesses.set(input.message.runId, {
2964
2984
  process: subprocess,
2965
2985
  target: input.resolvedTarget.target,
2986
+ additionalTargets: [...input.additionalTargets],
2966
2987
  sessionId: input.message.sessionId ?? "",
2967
2988
  mode: "foreground",
2968
2989
  pid: subprocess.pid,
@@ -3096,6 +3117,7 @@ async function executeStreamingCommand(input) {
3096
3117
  activeProcesses.set(input.message.runId, {
3097
3118
  process: subprocess,
3098
3119
  target: input.resolvedTarget.target,
3120
+ additionalTargets: [...input.additionalTargets],
3099
3121
  sessionId: input.message.sessionId,
3100
3122
  mode: input.message.mode,
3101
3123
  pid: subprocess.pid,
@@ -3336,6 +3358,7 @@ function buildActiveProcessReports() {
3336
3358
  return Array.from(activeProcesses.entries()).filter(([, active]) => active.sessionId.length > 0).map(([runId, active]) => ({
3337
3359
  runId,
3338
3360
  target: active.target,
3361
+ ...active.additionalTargets.length > 0 ? { additionalTargets: active.additionalTargets.flatMap((target) => target.type === "project" ? [target] : []) } : {},
3339
3362
  sessionId: active.sessionId,
3340
3363
  platform: process.platform,
3341
3364
  arch: process.arch,
@@ -3743,6 +3766,7 @@ async function openPty(input) {
3743
3766
  activePtys.set(input.message.ptyId, {
3744
3767
  ...ptyProcess,
3745
3768
  target: input.resolvedTarget.target,
3769
+ additionalTargets: [...input.additionalTargets],
3746
3770
  foregroundBusy: true,
3747
3771
  lastInputAt: 0,
3748
3772
  ...input.releaseWorkspaceMutation ? { releaseWorkspaceMutation: input.releaseWorkspaceMutation } : {}
@@ -3968,6 +3992,7 @@ async function startWorker(options, projectRuntime = {
3968
3992
  let workspaceCredentialUsername = null;
3969
3993
  let workspaceGitIdentity = projectRuntime.gitIdentity;
3970
3994
  let workspaceConfigured = false;
3995
+ let hydrationRecoveryRequired;
3971
3996
  let activeWorkspaceIncidentId = null;
3972
3997
  let deferredWorkspaceConfiguration = null;
3973
3998
  let deferredWorkspaceConfigurationRefreshTimer;
@@ -4267,12 +4292,12 @@ async function startWorker(options, projectRuntime = {
4267
4292
  });
4268
4293
  };
4269
4294
  const activeWorkspaceMutationTargets = () => [
4270
- ...Array.from(activeProcesses.values()).filter(workerCommandHasWorkspaceEffect).map(({ target }) => target),
4295
+ ...Array.from(activeProcesses.values()).filter(workerCommandHasWorkspaceEffect).flatMap(({ target, additionalTargets }) => [target, ...additionalTargets]),
4271
4296
  ...credentialBearingProcessGroupTargets.values(),
4272
- ...workspaceSyncPriorityProcessTargets.values(),
4297
+ ...Array.from(workspaceSyncPriorityProcessTargets.values()).flat(),
4273
4298
  ...workspaceSyncPriorityOperationTargets.values(),
4274
- ...Array.from(activePtys.values()).filter((pty) => workerPtyIsWorkspaceBusy(pty)).map(({ target }) => target),
4275
- ...workspaceSyncPriorityPtyTargets.values()
4299
+ ...Array.from(activePtys.values()).filter((pty) => workerPtyIsWorkspaceBusy(pty)).flatMap(({ target, additionalTargets }) => [target, ...additionalTargets]),
4300
+ ...Array.from(workspaceSyncPriorityPtyTargets.values()).flat()
4276
4301
  ];
4277
4302
  let visibleWorkspaceMutationEpoch = 0;
4278
4303
  const projectWorkspaceMutationEpochs = /* @__PURE__ */ new Map();
@@ -4284,6 +4309,9 @@ async function startWorker(options, projectRuntime = {
4284
4309
  const key = projectBranchKey(target.projectId, target.branchName);
4285
4310
  projectWorkspaceMutationEpochs.set(key, (projectWorkspaceMutationEpochs.get(key) ?? 0) + 1);
4286
4311
  };
4312
+ const recordAdditionalTargetMutations = (additional) => {
4313
+ for (const target of additional) recordVisibleWorkspaceMutation(target);
4314
+ };
4287
4315
  const projectWorkspaceMutationToken = (projectId, branchName) => `${visibleWorkspaceMutationEpoch}:${projectWorkspaceMutationEpochs.get(projectBranchKey(projectId, branchName)) ?? 0}`;
4288
4316
  const canonicalWorkspaceMutationIsActive = (targets) => targets.some((target) => target.type === "workspace" && target.rootProfile === "canonical_sync");
4289
4317
  const projectBranchMountActivityBusy = (projectId, branchName, branchPath) => {
@@ -4369,6 +4397,24 @@ async function startWorker(options, projectRuntime = {
4369
4397
  preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
4370
4398
  ...initialPublicationSourceBranch ? { initialPublicationSourceRelativePath: workspaceProjectRelativePath(project.projectId, initialPublicationSourceBranch) } : {},
4371
4399
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
4400
+ describeHolders: () => [
4401
+ ...new Set(
4402
+ [
4403
+ ...[...activeProcesses.values()].filter(
4404
+ (entry) => projectBranchHasActiveWorkspaceTarget(project.projectId, branch.branchName, [
4405
+ entry.target,
4406
+ ...entry.additionalTargets
4407
+ ])
4408
+ ).map((entry) => entry.sessionId),
4409
+ ...[...activePtys.entries()].filter(
4410
+ ([, entry]) => projectBranchHasActiveWorkspaceTarget(project.projectId, branch.branchName, [
4411
+ entry.target,
4412
+ ...entry.additionalTargets
4413
+ ])
4414
+ ).map(([ptyId]) => `pty:${ptyId}`)
4415
+ ].filter((id) => typeof id === "string")
4416
+ )
4417
+ ].join(", ") || "no live declared holder",
4372
4418
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
4373
4419
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
4374
4420
  busyForRecovery: () => projectBranchMountExclusiveSyncBusy(project.projectId, branch.branchName, branchPath)
@@ -4894,6 +4940,7 @@ async function startWorker(options, projectRuntime = {
4894
4940
  affectedPaths: result.affectedPaths,
4895
4941
  activeMountIds: result.activeMountIds,
4896
4942
  skippedMountIds: result.skippedMountIds,
4943
+ ...result.mountSkipReasons ? { mountSkipReasons: result.mountSkipReasons } : {},
4897
4944
  ...missingCheckouts ? { missingCheckouts } : {},
4898
4945
  activeProjectBranchPublications,
4899
4946
  discardedPaths: [],
@@ -4906,8 +4953,11 @@ async function startWorker(options, projectRuntime = {
4906
4953
  };
4907
4954
  };
4908
4955
  const failedWorkspaceSyncResult = async (attemptId, trigger, error) => {
4956
+ const recovery = findHydrationRecovery(error);
4957
+ if (recovery) hydrationRecoveryRequired = recovery;
4909
4958
  const localHead = await workspaceLocalHead();
4910
4959
  return {
4960
+ ...recovery ? { failureReason: "hydration_recovery_required", hydrationRecovery: recovery } : {},
4911
4961
  type: "workspace_sync",
4912
4962
  attemptId,
4913
4963
  workerLabel: label,
@@ -5130,6 +5180,12 @@ async function startWorker(options, projectRuntime = {
5130
5180
  } catch (error) {
5131
5181
  workspaceProjectionLedger.reset();
5132
5182
  const errorMessage = error instanceof Error ? error.message : String(error);
5183
+ if (findHydrationRecovery(error)) {
5184
+ workspaceConfigured = false;
5185
+ projectRuntime.configuration = void 0;
5186
+ advanceWorkerAdmissionGeneration();
5187
+ return await failedWorkspaceSyncResult(attemptId, input.trigger, error);
5188
+ }
5133
5189
  const hydrationRecoverable = workspaceSyncFailureHydrationIsSafe({
5134
5190
  error,
5135
5191
  resetToCanonical: input.resetToCanonical === true,
@@ -5373,6 +5429,10 @@ async function startWorker(options, projectRuntime = {
5373
5429
  projectRuntime.markWorkspaceDirty = markWorkspaceDirty;
5374
5430
  const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
5375
5431
  const resolveMessageTarget = (target) => {
5432
+ if (hydrationRecoveryRequired)
5433
+ throw new Error(
5434
+ `Workspace hydration recovery required for transaction ${hydrationRecoveryRequired.transactionId}; inspect r5dctl workspace status`
5435
+ );
5376
5436
  if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
5377
5437
  if (!deferredWorkspaceTargetIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, target)) {
5378
5438
  throw new Error(
@@ -5392,6 +5452,11 @@ async function startWorker(options, projectRuntime = {
5392
5452
  projectConfigById
5393
5453
  });
5394
5454
  };
5455
+ const resolveCommandMessageTarget = (target, additionalTargets) => {
5456
+ const primary = resolveMessageTarget(target);
5457
+ for (const additional of additionalTargets) resolveMessageTarget(additional);
5458
+ return primary;
5459
+ };
5395
5460
  const configureWorkerWorkspace = async (message, receiptGeneration) => {
5396
5461
  const configurationOperationId = crypto.randomUUID();
5397
5462
  sendLifecycleProgress({
@@ -5566,7 +5631,7 @@ async function startWorker(options, projectRuntime = {
5566
5631
  throw new Error("Workspace configuration was superseded by a different active workspace incident");
5567
5632
  }
5568
5633
  for (const project of message.projects) assertRepositoryTransitionState(project);
5569
- const busyConfigurationChanges = busyProjectConfigurationChangeIds({
5634
+ const busyConfigurationChanges = busyProjectConfigurationChanges({
5570
5635
  currentProjects: [...projectConfigById.values()],
5571
5636
  incomingProjects: message.projects,
5572
5637
  activeTargets: activeWorkspaceMutationTargets(),
@@ -5577,12 +5642,44 @@ async function startWorker(options, projectRuntime = {
5577
5642
  return readyProjectIds.has(project.projectId) && reconciledProjectConfigFingerprints.get(project.projectId) === fingerprint && project.branches.every(({ branchName }) => hasProjectWorktree(configuredProjectBranchPath(projectsRoot, project, branchName)));
5578
5643
  }
5579
5644
  });
5580
- if (busyConfigurationChanges.length > 0) {
5581
- throw new ProjectWorkspaceConfigurationDeferredError(
5582
- `Project workspace configuration deferred while visible checkout activity is active: ${busyConfigurationChanges.join(", ")}`
5645
+ if (busyConfigurationChanges.length > 0) throw new ProjectWorkspaceConfigurationDeferredError(busyConfigurationChanges);
5646
+ workspaceConfigured = false;
5647
+ if (message.recoverHydrationTransactionId) {
5648
+ if (incidentDeferral.incidentId || message.resetToCanonical)
5649
+ throw new Error("Hydration preservation cannot run during a canonical reset or another workspace incident");
5650
+ if (activeProcesses.size || activePtys.size || credentialBearingProcessGroups.size || pendingProcessTerminals.size || activeWorkspaceMutationTargets().length) {
5651
+ throw new Error("Stop active worker processes and terminals before preserving interrupted hydration");
5652
+ }
5653
+ const preservedHydration = await workspaceFilesystemExecutor().run("hydration_preserve_interrupted", {
5654
+ workspacePath: workspaceShadowRoot,
5655
+ expectedTransactionId: message.recoverHydrationTransactionId
5656
+ });
5657
+ process.stdout.write(
5658
+ `[r5d-worker] Preserved hydration ${preservedHydration.transactionId}; archived evidence: ${preservedHydration.archivePath}
5659
+ `
5660
+ );
5661
+ for (const entry of preservedHydration.retainedEntries ?? []) {
5662
+ process.stdout.write(
5663
+ `[r5d-worker] Retained writer evidence for ${entry.mountId}: ${entry.sourcePath}; archived copy: ${entry.archivePath}
5664
+ `
5665
+ );
5666
+ }
5667
+ hydrationRecoveryRequired = void 0;
5668
+ }
5669
+ const interruptedHydration = await workspaceFilesystemExecutor().run("hydration_recovery_inspect", {
5670
+ workspacePath: workspaceShadowRoot
5671
+ });
5672
+ if (interruptedHydration) {
5673
+ throw Object.assign(
5674
+ new Error(
5675
+ `Interrupted hydration ${interruptedHydration.transactionId} requires explicit recovery; checkout files and snapshots are preserved`
5676
+ ),
5677
+ {
5678
+ code: "hydration_recovery_required",
5679
+ hydrationRecovery: interruptedHydration
5680
+ }
5583
5681
  );
5584
5682
  }
5585
- workspaceConfigured = false;
5586
5683
  workspaceProjectionLedger.reset();
5587
5684
  const preserveOnlyBranches = message.projects.flatMap(
5588
5685
  (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
@@ -5848,6 +5945,7 @@ async function startWorker(options, projectRuntime = {
5848
5945
  deferredWorkspaceConfigurationRefreshTimer = void 0;
5849
5946
  }
5850
5947
  deferredWorkspaceConfiguration = null;
5948
+ hydrationRecoveryRequired = void 0;
5851
5949
  workspaceConfigured = true;
5852
5950
  const pending = [...pendingCheckouts.values()].sort(
5853
5951
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
@@ -5890,7 +5988,8 @@ async function startWorker(options, projectRuntime = {
5890
5988
  requireCredentialGenerationRestart(true, void 0, 1);
5891
5989
  }
5892
5990
  workspaceConfigured = false;
5893
- const deferred = error instanceof ProjectWorkspaceConfigurationDeferredError || error instanceof ProjectWorkspacePendingBranchPathChangeError;
5991
+ const busyDeferral = error instanceof ProjectWorkspaceConfigurationDeferredError ? error : void 0;
5992
+ const deferred = busyDeferral !== void 0 || error instanceof ProjectWorkspacePendingBranchPathChangeError;
5894
5993
  if (deferred) pendingCheckouts.clear();
5895
5994
  for (const pending of deferredProjectConfigurationPendingBranches(message.projects)) {
5896
5995
  pendingCheckouts.set(projectBranchKey(pending.projectId, pending.branchName), pending);
@@ -5900,7 +5999,11 @@ async function startWorker(options, projectRuntime = {
5900
5999
  pending: [...pendingCheckouts.values()].sort(
5901
6000
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
5902
6001
  ),
5903
- aheadOfOriginBranches: []
6002
+ aheadOfOriginBranches: [],
6003
+ // A busy deferral is typed so the server can keep this socket, finish
6004
+ // receipt recovery and renew the execution lease instead of closing
6005
+ // the connection and retrying registration until the lease expires.
6006
+ ...busyDeferral ? { deferredForBusyProjectIds: busyDeferral.projectIds } : {}
5904
6007
  };
5905
6008
  } finally {
5906
6009
  workspaceSyncRequestsInFlight -= 1;
@@ -6089,7 +6192,9 @@ async function startWorker(options, projectRuntime = {
6089
6192
  commandControlLaneV1: workerCommandLauncher?.supportsControlLane === true,
6090
6193
  projectMirrorLeaseV1: true,
6091
6194
  projectMirrorRefsTokensV1: true,
6092
- projectBranchWorkingTreeModeV1: true
6195
+ projectBranchWorkingTreeModeV1: true,
6196
+ commandAdditionalTargetsV1: true,
6197
+ workspaceHydrationRecoveryV1: true
6093
6198
  },
6094
6199
  root: rootDir
6095
6200
  }
@@ -6325,7 +6430,7 @@ async function startWorker(options, projectRuntime = {
6325
6430
  }
6326
6431
  const digest = workerOperationDigest(message);
6327
6432
  const cached = projectRuntime.configuration;
6328
- const canReuseConfiguration = cached?.digest === digest && cached.workspaceSync?.remoteUrl === message.workspaceRemoteUrl && Boolean(cached.workspaceSync.credentialHelper) && cached.workspaceSync.credentialUsername === workerCredentialUsername && cached.workspaceSync.gitIdentity?.name === message.gitIdentity.name && cached.workspaceSync.gitIdentity?.email === message.gitIdentity.email && !unsafeWorkspaceSyncBouncePending && credentialGenerationFingerprint({ ...message, workerBaseUrl: baseUrl, workerAuthHeader: bearerAuthHeader }) === configuredCredentialGenerationFingerprint;
6433
+ const canReuseConfiguration = !message.recoverHydrationTransactionId && !hydrationRecoveryRequired && cached?.digest === digest && cached.workspaceSync?.remoteUrl === message.workspaceRemoteUrl && Boolean(cached.workspaceSync.credentialHelper) && cached.workspaceSync.credentialUsername === workerCredentialUsername && cached.workspaceSync.gitIdentity?.name === message.gitIdentity.name && cached.workspaceSync.gitIdentity?.email === message.gitIdentity.email && !unsafeWorkspaceSyncBouncePending && credentialGenerationFingerprint({ ...message, workerBaseUrl: baseUrl, workerAuthHeader: bearerAuthHeader }) === configuredCredentialGenerationFingerprint;
6329
6434
  if (!canReuseConfiguration) {
6330
6435
  advanceWorkerAdmissionGeneration();
6331
6436
  projectRuntime.configuration = void 0;
@@ -6337,7 +6442,8 @@ async function startWorker(options, projectRuntime = {
6337
6442
  result: configured.result,
6338
6443
  pendingCheckouts: configured.pending,
6339
6444
  aheadOfOriginBranches: configured.aheadOfOriginBranches,
6340
- ...configured.deferredWorkspaceSyncForIncidentId ? { deferredWorkspaceSyncForIncidentId: configured.deferredWorkspaceSyncForIncidentId } : {}
6445
+ ...configured.deferredWorkspaceSyncForIncidentId ? { deferredWorkspaceSyncForIncidentId: configured.deferredWorkspaceSyncForIncidentId } : {},
6446
+ ...configured.deferredForBusyProjectIds ? { deferredForBusyProjectIds: configured.deferredForBusyProjectIds } : {}
6341
6447
  });
6342
6448
  if (receiptGeneration !== workspaceConfigurationReceiptGeneration || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
6343
6449
  sendConfigured();
@@ -6368,7 +6474,17 @@ async function startWorker(options, projectRuntime = {
6368
6474
  if (configured.result.outcome !== "failed") unsafeWorkspaceSyncRecoveryAttempts = 0;
6369
6475
  sendConfigured();
6370
6476
  sendLifecycleProgress(
6371
- configured.result.outcome === "failed" ? {
6477
+ configured.result.hydrationRecovery ? {
6478
+ phase: "failed",
6479
+ operationId: configured.result.attemptId,
6480
+ detail: `Hydration recovery required: ${configured.result.hydrationRecovery.transactionId}; files and snapshots preserved`
6481
+ } : configured.deferredForBusyProjectIds ? {
6482
+ // Not a failure: the previous generation stays on disk and the
6483
+ // server retries once the busy checkouts are idle.
6484
+ phase: "configuring",
6485
+ operationId: configured.result.attemptId,
6486
+ detail: `Workspace configuration deferred while checkout activity is active on ${configured.deferredForBusyProjectIds.length} project(s)`
6487
+ } : configured.result.outcome === "failed" ? {
6372
6488
  phase: "failed",
6373
6489
  operationId: configured.result.attemptId,
6374
6490
  detail: configured.result.error ?? "Workspace configuration failed"
@@ -6381,10 +6497,12 @@ async function startWorker(options, projectRuntime = {
6381
6497
  }
6382
6498
  );
6383
6499
  process.stdout.write(
6384
- `[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
6500
+ configured.result.hydrationRecovery ? `[r5d-worker] workspace hydration recovery required for ${configured.result.hydrationRecovery.transactionId}; execution remains fenced, files and snapshots preserved
6501
+ ` : configured.deferredForBusyProjectIds ? `[r5d-worker] workspace configuration deferred while visible checkout activity is active on ${configured.deferredForBusyProjectIds.join(", ")}: ${configured.pending.length} checkout(s) pending until the server retries
6502
+ ` : `[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
6385
6503
  `
6386
6504
  );
6387
- if (refreshesDeferredConfiguration && configured.result.outcome === "failed" && deferredWorkspaceConfiguration !== null && currentWorkerSocket === ws) {
6505
+ if (refreshesDeferredConfiguration && configured.result.outcome === "failed" && configured.result.failureReason !== "hydration_recovery_required" && !configured.deferredForBusyProjectIds && deferredWorkspaceConfiguration !== null && currentWorkerSocket === ws) {
6388
6506
  workspaceConfigured = false;
6389
6507
  advanceWorkerAdmissionGeneration();
6390
6508
  ws.close(1012, "Deferred workspace configuration refresh failed");
@@ -6776,6 +6894,7 @@ async function startWorker(options, projectRuntime = {
6776
6894
  if (active.workspaceEffect !== "none" && targetMayMutateVisibleWorkspace(active.target)) {
6777
6895
  recordVisibleWorkspaceMutation(active.target);
6778
6896
  }
6897
+ if (active.workspaceEffect !== "none") recordAdditionalTargetMutations(active.additionalTargets);
6779
6898
  try {
6780
6899
  let bytesWritten = 0;
6781
6900
  if (message.data !== void 0 && message.data.length > 0) {
@@ -6825,16 +6944,19 @@ async function startWorker(options, projectRuntime = {
6825
6944
  let resourcesTransferred = false;
6826
6945
  let releaseWorkspaceMutation;
6827
6946
  let mutationLeaseTransferred = false;
6947
+ let additionalTargets = [];
6828
6948
  try {
6949
+ additionalTargets = normalizeCommandAdditionalTargets(message.target, message.additionalTargets, commandTargetValidators);
6829
6950
  resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6830
6951
  const reservationAbort = registerPendingReservation(`pty:${message.ptyId}`, void 0);
6831
6952
  try {
6832
6953
  await reserveWorkspaceCommandAfterCurrentSync(
6833
- message.target,
6954
+ commandTargets(message.target, additionalTargets),
6834
6955
  workspaceSyncSingleFlight,
6835
6956
  () => {
6836
- workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
6957
+ workspaceSyncPriorityPtyTargets.set(message.ptyId, commandTargets(message.target, additionalTargets));
6837
6958
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
6959
+ recordAdditionalTargetMutations(additionalTargets);
6838
6960
  },
6839
6961
  { signal: reservationAbort.signal }
6840
6962
  );
@@ -6847,19 +6969,26 @@ async function startWorker(options, projectRuntime = {
6847
6969
  `Shell opening was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
6848
6970
  );
6849
6971
  }
6850
- const resolvedTarget = resolveMessageTarget(message.target);
6851
- process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
6852
- `);
6972
+ const resolvedTarget = resolveCommandMessageTarget(message.target, additionalTargets);
6973
+ process.stdout.write(
6974
+ `[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}${describeCommandAdditionalTargets(additionalTargets)}
6975
+ `
6976
+ );
6853
6977
  await openPty({
6854
6978
  resources,
6855
6979
  ws,
6856
6980
  message,
6857
6981
  resolvedTarget,
6982
+ additionalTargets,
6858
6983
  rootDir,
6859
- assertAdmission: assertMessageAdmission,
6984
+ assertAdmission: () => {
6985
+ assertMessageAdmission();
6986
+ resolveCommandMessageTarget(message.target, additionalTargets);
6987
+ },
6860
6988
  ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
6861
6989
  ...targetMayMutateVisibleWorkspace(message.target) ? {
6862
6990
  onTerminal: () => {
6991
+ recordAdditionalTargetMutations(additionalTargets);
6863
6992
  markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true, message.target);
6864
6993
  }
6865
6994
  } : {}
@@ -6892,6 +7021,7 @@ async function startWorker(options, projectRuntime = {
6892
7021
  return;
6893
7022
  }
6894
7023
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
7024
+ recordAdditionalTargetMutations(activePty.additionalTargets);
6895
7025
  markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
6896
7026
  }
6897
7027
  writePty(ws, message);
@@ -6908,6 +7038,7 @@ async function startWorker(options, projectRuntime = {
6908
7038
  abortPendingReservation(`pty:${message.ptyId}`, "Shell closed before it started");
6909
7039
  await closePty(message);
6910
7040
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
7041
+ recordAdditionalTargetMutations(activePty.additionalTargets);
6911
7042
  markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
6912
7043
  }
6913
7044
  sendWorkerMessage(ws, { type: "operation_completed", requestId: message.requestId });
@@ -6939,18 +7070,21 @@ async function startWorker(options, projectRuntime = {
6939
7070
  let resources;
6940
7071
  let targetReserved = false;
6941
7072
  const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
7073
+ let additionalTargets = [];
6942
7074
  try {
7075
+ additionalTargets = normalizeCommandAdditionalTargets(message.target, message.additionalTargets, commandTargetValidators);
6943
7076
  resources = await acquireWorkerCommandLaunch(ws, message, assertMessageAdmission);
6944
7077
  if (hasWorkspaceEffect) {
6945
7078
  const reservationAbort = registerPendingReservation(`run:${message.runId}`, message.sessionId);
6946
7079
  try {
6947
7080
  await reserveWorkspaceCommandAfterCurrentSync(
6948
- message.target,
7081
+ commandTargets(message.target, additionalTargets),
6949
7082
  workspaceSyncSingleFlight,
6950
7083
  () => {
6951
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
7084
+ workspaceSyncPriorityProcessTargets.set(message.runId, commandTargets(message.target, additionalTargets));
6952
7085
  targetReserved = true;
6953
7086
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
7087
+ recordAdditionalTargetMutations(additionalTargets);
6954
7088
  },
6955
7089
  { signal: reservationAbort.signal }
6956
7090
  );
@@ -6964,13 +7098,16 @@ async function startWorker(options, projectRuntime = {
6964
7098
  `One-shot command was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
6965
7099
  );
6966
7100
  }
6967
- const resolvedTarget = resolveMessageTarget(message.target);
6968
- process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
6969
- `);
7101
+ const resolvedTarget = resolveCommandMessageTarget(message.target, additionalTargets);
7102
+ process.stdout.write(
7103
+ `[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}${describeCommandAdditionalTargets(additionalTargets)}
7104
+ `
7105
+ );
6970
7106
  return await executeCommand({
6971
7107
  resources,
6972
7108
  message,
6973
7109
  resolvedTarget,
7110
+ additionalTargets,
6974
7111
  baseUrl,
6975
7112
  token,
6976
7113
  rootDir,
@@ -6988,6 +7125,7 @@ async function startWorker(options, projectRuntime = {
6988
7125
  );
6989
7126
  }
6990
7127
  assertMessageAdmission();
7128
+ resolveCommandMessageTarget(message.target, additionalTargets);
6991
7129
  }
6992
7130
  });
6993
7131
  };
@@ -7013,6 +7151,7 @@ async function startWorker(options, projectRuntime = {
7013
7151
  }
7014
7152
  sendSerializedWorkerMessage(ws, JSON.stringify(result));
7015
7153
  if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
7154
+ recordAdditionalTargetMutations(additionalTargets);
7016
7155
  markWorkspaceDirty(
7017
7156
  { type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
7018
7157
  true,
@@ -7038,11 +7177,23 @@ async function startWorker(options, projectRuntime = {
7038
7177
  });
7039
7178
  const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
7040
7179
  let resources;
7180
+ let additionalTargets;
7181
+ try {
7182
+ additionalTargets = normalizeCommandAdditionalTargets(message.target, message.additionalTargets, commandTargetValidators);
7183
+ } catch (error) {
7184
+ sendWorkerMessageFromCurrentSource(ws, {
7185
+ type: "exec_start_error",
7186
+ requestId: message.requestId,
7187
+ runId: message.runId,
7188
+ error: error instanceof Error ? error.message : String(error)
7189
+ });
7190
+ return;
7191
+ }
7041
7192
  const runCommand = async () => {
7042
7193
  try {
7043
- const resolvedTarget = resolveMessageTarget(message.target);
7194
+ const resolvedTarget = resolveCommandMessageTarget(message.target, additionalTargets);
7044
7195
  process.stdout.write(
7045
- `[r5d-worker] exec_start ${message.runId}${message.commandClass === "control" ? " (control)" : ""}: ${message.argv.join(" ")}
7196
+ `[r5d-worker] exec_start ${message.runId}${message.commandClass === "control" ? " (control)" : ""}: ${message.argv.join(" ")}${describeCommandAdditionalTargets(additionalTargets)}
7046
7197
  `
7047
7198
  );
7048
7199
  await executeStreamingCommand({
@@ -7050,6 +7201,7 @@ async function startWorker(options, projectRuntime = {
7050
7201
  ws,
7051
7202
  message,
7052
7203
  resolvedTarget,
7204
+ additionalTargets,
7053
7205
  baseUrl,
7054
7206
  token,
7055
7207
  rootDir,
@@ -7060,10 +7212,14 @@ async function startWorker(options, projectRuntime = {
7060
7212
  active
7061
7213
  });
7062
7214
  },
7063
- assertAdmission: assertMessageAdmission
7215
+ assertAdmission: () => {
7216
+ assertMessageAdmission();
7217
+ resolveCommandMessageTarget(message.target, additionalTargets);
7218
+ }
7064
7219
  });
7065
7220
  } finally {
7066
7221
  if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
7222
+ recordAdditionalTargetMutations(additionalTargets);
7067
7223
  markWorkspaceDirty(
7068
7224
  {
7069
7225
  type: "process_terminal",
@@ -7085,12 +7241,13 @@ async function startWorker(options, projectRuntime = {
7085
7241
  const reservationAbort = registerPendingReservation(`run:${message.runId}`, message.sessionId);
7086
7242
  try {
7087
7243
  await reserveWorkspaceCommandAfterCurrentSync(
7088
- message.target,
7244
+ commandTargets(message.target, additionalTargets),
7089
7245
  workspaceSyncSingleFlight,
7090
7246
  () => {
7091
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
7247
+ workspaceSyncPriorityProcessTargets.set(message.runId, commandTargets(message.target, additionalTargets));
7092
7248
  targetReserved = true;
7093
7249
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
7250
+ recordAdditionalTargetMutations(additionalTargets);
7094
7251
  },
7095
7252
  { signal: reservationAbort.signal }
7096
7253
  );
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.132",
3
+ "version": "0.0.133",
4
4
  "type": "module"
5
5
  }