@ricsam/r5d-worker 0.0.83 → 0.0.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/main.cjs CHANGED
@@ -1535,10 +1535,20 @@ function workspaceConfigurationIncidentDeferral(input) {
1535
1535
  return { incidentId };
1536
1536
  }
1537
1537
  function workspaceIncidentTerminalClearDisposition(input) {
1538
- if (!input.deferredConfiguration) return "ordinary_resume";
1539
- void input.clearedIncidentId;
1538
+ if (!input.deferredConfiguration) return "await_server_refresh";
1539
+ if (input.deferredConfiguration.incidentId !== input.clearedIncidentId) return "reconnect_for_refresh";
1540
1540
  return input.deferredConfiguration.serverRefreshExpected ? "await_server_refresh" : "reconnect_for_refresh";
1541
1541
  }
1542
+ function workspaceIncidentTerminalRefreshFence(input) {
1543
+ const disposition = workspaceIncidentTerminalClearDisposition(input);
1544
+ return {
1545
+ disposition,
1546
+ deferredConfiguration: input.deferredConfiguration ?? { incidentId: input.clearedIncidentId, serverRefreshExpected: true }
1547
+ };
1548
+ }
1549
+ function workspaceAutomaticSyncIsAdmitted(input) {
1550
+ return input.workspaceConfigured && input.activeIncidentId === null && input.deferredConfiguration === null;
1551
+ }
1542
1552
  function deferredWorkspaceTargetIsAllowed(deferredConfiguration, activeIncidentId, target) {
1543
1553
  const canonicalTarget = target.type === "workspace" && target.rootProfile === "canonical_sync";
1544
1554
  if (deferredConfiguration) return activeIncidentId === deferredConfiguration.incidentId && canonicalTarget;
@@ -1560,6 +1570,9 @@ function deferredCredentialTransitionMustWait(input) {
1560
1570
  function workspaceConfigurationIncidentSnapshotIsCurrent(capturedIncidentId, activeIncidentId) {
1561
1571
  return capturedIncidentId === null ? activeIncidentId === null : activeIncidentId === null || activeIncidentId === capturedIncidentId;
1562
1572
  }
1573
+ function workspaceConfigurationResetToCanonicalIsAllowed(requested, incidentId) {
1574
+ return requested === true && incidentId === null;
1575
+ }
1563
1576
  function deferredWorkspaceRefreshWatchdogIsCurrent(input) {
1564
1577
  return input.deferredConfiguration?.incidentId === input.capturedIncidentId && input.activeIncidentId === null;
1565
1578
  }
@@ -1632,12 +1645,15 @@ const workerGitSecurityTestHarness = {
1632
1645
  fenceUnsafeWorkspaceSyncFailure,
1633
1646
  workspaceConfigurationIncidentDeferral,
1634
1647
  workspaceIncidentTerminalClearDisposition,
1648
+ workspaceIncidentTerminalRefreshFence,
1649
+ workspaceAutomaticSyncIsAdmitted,
1635
1650
  deferredWorkspaceTargetIsAllowed,
1636
1651
  deferredWorkspaceSyncTriggerIsAllowed,
1637
1652
  workspaceOperationsAreFenced,
1638
1653
  workspaceCommandTransportIsAllowed,
1639
1654
  deferredCredentialTransitionMustWait,
1640
1655
  workspaceConfigurationIncidentSnapshotIsCurrent,
1656
+ workspaceConfigurationResetToCanonicalIsAllowed,
1641
1657
  deferredWorkspaceRefreshWatchdogIsCurrent,
1642
1658
  incidentDeferredProjectCheckouts,
1643
1659
  deferredIncidentWorkspaceConfigurationResult,
@@ -3398,6 +3414,11 @@ async function startWorker(options) {
3398
3414
  let cliUpdateInProgress = false;
3399
3415
  let reloadAfterClose = false;
3400
3416
  let shutdownAfterClose = false;
3417
+ const automaticWorkspaceSyncIsCurrentlyAdmitted = () => workspaceAutomaticSyncIsAdmitted({
3418
+ workspaceConfigured,
3419
+ activeIncidentId: activeWorkspaceIncidentId,
3420
+ deferredConfiguration: deferredWorkspaceConfiguration
3421
+ });
3401
3422
  const bearerAuthHeader = `Authorization: Bearer ${token}`;
3402
3423
  const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
3403
3424
  const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
@@ -3679,6 +3700,17 @@ async function startWorker(options) {
3679
3700
  const activeTargets = activeWorkspaceMutationTargets();
3680
3701
  return projectConfigById.get(projectId)?.executionDisabled === true || hasProjectWorktree(branchPath) && (0, import_project_worktrees.projectWorktreeOperationInProgress)(branchPath) || canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.projectBranchHasActiveWorkspaceTarget)(projectId, branchName, activeTargets);
3681
3702
  };
3703
+ const projectBranchMountExclusiveSyncBusy = (projectId, branchName, branchPath) => {
3704
+ const activeTargets = activeWorkspaceMutationTargets();
3705
+ return (0, import_workspace_automatic_sync_policy.projectBranchMountBusyDuringExclusiveSync)({
3706
+ projectId,
3707
+ branchName,
3708
+ executionDisabled: projectConfigById.get(projectId)?.executionDisabled === true,
3709
+ worktreeOperationInProgress: hasProjectWorktree(branchPath) && (0, import_project_worktrees.projectWorktreeOperationInProgress)(branchPath),
3710
+ retainedCanonicalProcessGroup: canonicalWorkspaceMutationIsActive([...credentialBearingProcessGroupTargets.values()]),
3711
+ activeTargets
3712
+ });
3713
+ };
3682
3714
  const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
3683
3715
  const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
3684
3716
  const mounts = [];
@@ -3711,7 +3743,7 @@ async function startWorker(options) {
3711
3743
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3712
3744
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3713
3745
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3714
- busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3746
+ busyForRecovery: () => projectBranchMountExclusiveSyncBusy(project.projectId, branch.branchName, branchPath)
3715
3747
  });
3716
3748
  mounts.push({
3717
3749
  id: projectPlanMountId(project.projectId, branch.branchName),
@@ -3725,7 +3757,7 @@ async function startWorker(options) {
3725
3757
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3726
3758
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3727
3759
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3728
- busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3760
+ busyForRecovery: () => projectBranchMountExclusiveSyncBusy(project.projectId, branch.branchName, branchPath)
3729
3761
  });
3730
3762
  }
3731
3763
  void projectRoot;
@@ -3748,7 +3780,10 @@ async function startWorker(options) {
3748
3780
  mutationToken: () => String(visibleWorkspaceMutationEpoch),
3749
3781
  busyForRecovery: () => {
3750
3782
  const activeTargets = activeWorkspaceMutationTargets();
3751
- return canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.hasActiveVisibleProjectsWorkspaceTarget)(activeTargets);
3783
+ return (0, import_workspace_automatic_sync_policy.workspacePlansMountBusyDuringExclusiveSync)(
3784
+ activeTargets,
3785
+ canonicalWorkspaceMutationIsActive([...credentialBearingProcessGroupTargets.values()])
3786
+ );
3752
3787
  }
3753
3788
  });
3754
3789
  for (const deletion of pendingMirrorDeletes.values()) {
@@ -4129,9 +4164,9 @@ async function startWorker(options) {
4129
4164
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
4130
4165
  throw new Error("Worker workspace configuration has not been received");
4131
4166
  }
4132
- const mounts = buildWorkspaceMounts();
4167
+ const configuredMounts = buildWorkspaceMounts();
4133
4168
  if (input.resetToCanonical) {
4134
- const resetHasBusyLiveMount = mounts.some(
4169
+ const resetHasBusyLiveMount = configuredMounts.some(
4135
4170
  (mount) => !(mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath)) && mount.busy?.()
4136
4171
  );
4137
4172
  if (resetHasBusyLiveMount) {
@@ -4147,7 +4182,7 @@ async function startWorker(options) {
4147
4182
  credentialHelper: workspaceCredentialHelper,
4148
4183
  credentialUsername: workspaceCredentialUsername,
4149
4184
  gitIdentity: workspaceGitIdentity,
4150
- mounts
4185
+ mounts: configuredMounts
4151
4186
  });
4152
4187
  applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
4153
4188
  return {
@@ -4171,6 +4206,7 @@ async function startWorker(options) {
4171
4206
  };
4172
4207
  }
4173
4208
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
4209
+ const mounts = outerRemediation ? (0, import_workspace_git_sync.workspaceGitMountsForRemediation)(configuredMounts) : configuredMounts;
4174
4210
  const observedProjectHeads = outerRemediation ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4175
4211
  const inboundMoveMountIds = new Set(
4176
4212
  observedProjectHeads.flatMap(
@@ -4322,7 +4358,7 @@ async function startWorker(options) {
4322
4358
  pendingCreatedBranchPublicationNotBefore
4323
4359
  );
4324
4360
  const initialDeferral = pendingBranchDeferral();
4325
- if (!workspaceConfigured || activeWorkspaceIncidentId || deferredWorkspaceConfiguration || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
4361
+ if (!automaticWorkspaceSyncIsCurrentlyAdmitted() || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
4326
4362
  return;
4327
4363
  }
4328
4364
  if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
@@ -4330,8 +4366,7 @@ async function startWorker(options) {
4330
4366
  () => {
4331
4367
  workspaceAutomaticTimer = void 0;
4332
4368
  const scheduledTrigger = pendingAutomaticTrigger;
4333
- if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || deferredWorkspaceConfiguration || currentWorkerSocket !== ws)
4334
- return;
4369
+ if (!scheduledTrigger || !automaticWorkspaceSyncIsCurrentlyAdmitted() || currentWorkerSocket !== ws) return;
4335
4370
  const currentDeferral = pendingBranchDeferral();
4336
4371
  if (currentDeferral?.kind === "active_target") return;
4337
4372
  if (currentDeferral?.kind === "creation_grace") {
@@ -4353,7 +4388,7 @@ async function startWorker(options) {
4353
4388
  }).finally(() => {
4354
4389
  automaticSyncInFlight = false;
4355
4390
  heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
4356
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
4391
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4357
4392
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
4358
4393
  }
4359
4394
  });
@@ -4762,6 +4797,7 @@ async function startWorker(options) {
4762
4797
  const result = await performWorkspaceSync({
4763
4798
  attemptId: crypto.randomUUID(),
4764
4799
  trigger: { type: "connect" },
4800
+ resetToCanonical: workspaceConfigurationResetToCanonicalIsAllowed(message.resetToCanonical, incidentDeferral.incidentId),
4765
4801
  assertStillAdmitted: assertConfigurationSyncStillAdmitted
4766
4802
  });
4767
4803
  assertConfigurationSyncStillAdmitted();
@@ -4860,7 +4896,7 @@ async function startWorker(options) {
4860
4896
  workspaceAutomaticTimer = void 0;
4861
4897
  }
4862
4898
  void (async () => {
4863
- if (workspaceConfigured && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
4899
+ if (automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4864
4900
  await runWorkspaceSync({
4865
4901
  trigger: { type: "manual", detail: "graceful worker shutdown" }
4866
4902
  });
@@ -4911,7 +4947,8 @@ async function startWorker(options) {
4911
4947
  execStdinV1: true,
4912
4948
  ptyEnvFilesV1: true,
4913
4949
  workspaceRemediationAncestorGuardV1: true,
4914
- workspaceIncidentConfigDeferralV1: true
4950
+ workspaceIncidentConfigDeferralV1: true,
4951
+ workspaceConfigResetToCanonicalV1: true
4915
4952
  },
4916
4953
  projectRoot: projectsRoot,
4917
4954
  artifactRoot,
@@ -4975,7 +5012,7 @@ async function startWorker(options) {
4975
5012
  }, WORKSPACE_GIT_PERIODIC_MS);
4976
5013
  workspacePeriodicTimer.unref();
4977
5014
  }
4978
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
5015
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4979
5016
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, 0);
4980
5017
  }
4981
5018
  return;
@@ -5215,35 +5252,33 @@ async function startWorker(options) {
5215
5252
  if (nextIncidentId !== previousIncidentId) advanceWorkerAdmissionGeneration();
5216
5253
  activeWorkspaceIncidentId = nextIncidentId;
5217
5254
  if (previousIncidentId && !activeWorkspaceIncidentId && (message.status === "resolved" || message.status === "confirmed" || message.status === "reset")) {
5218
- const disposition = workspaceIncidentTerminalClearDisposition({
5255
+ const refresh = workspaceIncidentTerminalRefreshFence({
5219
5256
  deferredConfiguration: deferredWorkspaceConfiguration,
5220
5257
  clearedIncidentId: previousIncidentId
5221
5258
  });
5222
- if (disposition !== "ordinary_resume") {
5223
- pendingAutomaticTrigger ??= { type: "periodic", detail: "resume after workspace incident" };
5224
- workspaceConfigured = false;
5225
- advanceWorkerAdmissionGeneration();
5226
- if (disposition === "reconnect_for_refresh") {
5227
- ws.close(1012, "Refreshing deferred workspace configuration");
5228
- } else {
5229
- if (deferredWorkspaceConfigurationRefreshTimer) clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5230
- const deferredIncidentId = deferredWorkspaceConfiguration?.incidentId;
5231
- deferredWorkspaceConfigurationRefreshTimer = setTimeout(() => {
5232
- deferredWorkspaceConfigurationRefreshTimer = void 0;
5233
- if (!deferredIncidentId || !deferredWorkspaceRefreshWatchdogIsCurrent({
5234
- capturedIncidentId: deferredIncidentId,
5235
- deferredConfiguration: deferredWorkspaceConfiguration,
5236
- activeIncidentId: activeWorkspaceIncidentId
5237
- }) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
5238
- return;
5239
- }
5240
- ws.close(1012, "Timed out waiting for deferred workspace configuration refresh");
5241
- }, WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS);
5242
- deferredWorkspaceConfigurationRefreshTimer.unref();
5243
- }
5244
- return;
5259
+ deferredWorkspaceConfiguration = refresh.deferredConfiguration;
5260
+ pendingAutomaticTrigger ??= { type: "periodic", detail: "resume after workspace incident" };
5261
+ workspaceConfigured = false;
5262
+ advanceWorkerAdmissionGeneration();
5263
+ if (refresh.disposition === "reconnect_for_refresh") {
5264
+ ws.close(1012, "Refreshing deferred workspace configuration");
5265
+ } else {
5266
+ if (deferredWorkspaceConfigurationRefreshTimer) clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5267
+ const deferredIncidentId = deferredWorkspaceConfiguration.incidentId;
5268
+ deferredWorkspaceConfigurationRefreshTimer = setTimeout(() => {
5269
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
5270
+ if (!deferredWorkspaceRefreshWatchdogIsCurrent({
5271
+ capturedIncidentId: deferredIncidentId,
5272
+ deferredConfiguration: deferredWorkspaceConfiguration,
5273
+ activeIncidentId: activeWorkspaceIncidentId
5274
+ }) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
5275
+ return;
5276
+ }
5277
+ ws.close(1012, "Timed out waiting for deferred workspace configuration refresh");
5278
+ }, WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS);
5279
+ deferredWorkspaceConfigurationRefreshTimer.unref();
5245
5280
  }
5246
- scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger ?? { type: "periodic", detail: "resume after workspace incident" }, 0);
5281
+ return;
5247
5282
  }
5248
5283
  return;
5249
5284
  }
@@ -5616,7 +5651,12 @@ async function startWorker(options) {
5616
5651
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
5617
5652
  });
5618
5653
  }
5619
- await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
5654
+ await (0, import_workspace_command_sync_policy.runReservedWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand, () => {
5655
+ if (targetReserved) {
5656
+ workspaceSyncPriorityProcessTargets.delete(message.runId);
5657
+ targetReserved = false;
5658
+ }
5659
+ });
5620
5660
  } finally {
5621
5661
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
5622
5662
  }
@@ -5690,7 +5730,7 @@ async function startWorker(options) {
5690
5730
  } finally {
5691
5731
  if (reservesVisibleWorkspace) {
5692
5732
  workspaceSyncPriorityOperationTargets.delete(message.requestId);
5693
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
5733
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
5694
5734
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, dirtyTrigger ? WORKSPACE_GIT_QUIET_MS : 0);
5695
5735
  }
5696
5736
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.83",
3
+ "version": "0.0.86",
4
4
  "type": "commonjs"
5
5
  }
@@ -23,7 +23,9 @@ __export(workspace_automatic_sync_policy_exports, {
23
23
  pendingCreatedBranchAutomaticSyncDeferral: () => pendingCreatedBranchAutomaticSyncDeferral,
24
24
  pendingCreatedBranchKey: () => pendingCreatedBranchKey,
25
25
  projectBranchHasActiveWorkspaceTarget: () => projectBranchHasActiveWorkspaceTarget,
26
- shouldDeferAutomaticWorkspaceSyncForPendingBranch: () => shouldDeferAutomaticWorkspaceSyncForPendingBranch
26
+ projectBranchMountBusyDuringExclusiveSync: () => projectBranchMountBusyDuringExclusiveSync,
27
+ shouldDeferAutomaticWorkspaceSyncForPendingBranch: () => shouldDeferAutomaticWorkspaceSyncForPendingBranch,
28
+ workspacePlansMountBusyDuringExclusiveSync: () => workspacePlansMountBusyDuringExclusiveSync
27
29
  });
28
30
  module.exports = __toCommonJS(workspace_automatic_sync_policy_exports);
29
31
  const PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS = 12e4;
@@ -43,6 +45,12 @@ function projectBranchHasActiveWorkspaceTarget(projectId, branchName, activeTarg
43
45
  }
44
46
  return false;
45
47
  }
48
+ function projectBranchMountBusyDuringExclusiveSync(input) {
49
+ return input.executionDisabled || input.worktreeOperationInProgress || input.retainedCanonicalProcessGroup || projectBranchHasActiveWorkspaceTarget(input.projectId, input.branchName, input.activeTargets);
50
+ }
51
+ function workspacePlansMountBusyDuringExclusiveSync(activeTargets, retainedCanonicalProcessGroup = false) {
52
+ return retainedCanonicalProcessGroup || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
53
+ }
46
54
  function shouldDeferAutomaticWorkspaceSyncForPendingBranch(pendingCreatedBranches, activeTargets) {
47
55
  if (pendingCreatedBranches.length === 0) return false;
48
56
  const targets = [...activeTargets];
@@ -65,5 +73,7 @@ function pendingCreatedBranchAutomaticSyncDeferral(pendingCreatedBranches, activ
65
73
  pendingCreatedBranchAutomaticSyncDeferral,
66
74
  pendingCreatedBranchKey,
67
75
  projectBranchHasActiveWorkspaceTarget,
68
- shouldDeferAutomaticWorkspaceSyncForPendingBranch
76
+ projectBranchMountBusyDuringExclusiveSync,
77
+ shouldDeferAutomaticWorkspaceSyncForPendingBranch,
78
+ workspacePlansMountBusyDuringExclusiveSync
69
79
  });
@@ -20,6 +20,7 @@ var workspace_command_sync_policy_exports = {};
20
20
  __export(workspace_command_sync_policy_exports, {
21
21
  acquireWorkspaceCommandMutation: () => acquireWorkspaceCommandMutation,
22
22
  reserveWorkspaceCommandAfterCurrentSync: () => reserveWorkspaceCommandAfterCurrentSync,
23
+ runReservedWorkspaceCommand: () => runReservedWorkspaceCommand,
23
24
  runWorkspaceCommand: () => runWorkspaceCommand,
24
25
  shouldSerializeWorkspaceCommand: () => shouldSerializeWorkspaceCommand
25
26
  });
@@ -46,6 +47,25 @@ function shouldSerializeWorkspaceCommand(target) {
46
47
  function runWorkspaceCommand(target, coordinator, operation) {
47
48
  return shouldSerializeWorkspaceCommand(target) ? coordinator.runMutation(operation) : Promise.resolve().then(operation);
48
49
  }
50
+ async function runReservedWorkspaceCommand(target, coordinator, operation, releaseReservation) {
51
+ let released = false;
52
+ const releaseOnce = () => {
53
+ if (released) return;
54
+ released = true;
55
+ releaseReservation();
56
+ };
57
+ try {
58
+ return await runWorkspaceCommand(target, coordinator, async () => {
59
+ try {
60
+ return await operation();
61
+ } finally {
62
+ releaseOnce();
63
+ }
64
+ });
65
+ } finally {
66
+ releaseOnce();
67
+ }
68
+ }
49
69
  function acquireWorkspaceCommandMutation(target, coordinator) {
50
70
  return shouldSerializeWorkspaceCommand(target) ? coordinator.acquireMutation() : Promise.resolve(void 0);
51
71
  }
@@ -53,6 +73,7 @@ function acquireWorkspaceCommandMutation(target, coordinator) {
53
73
  0 && (module.exports = {
54
74
  acquireWorkspaceCommandMutation,
55
75
  reserveWorkspaceCommandAfterCurrentSync,
76
+ runReservedWorkspaceCommand,
56
77
  runWorkspaceCommand,
57
78
  shouldSerializeWorkspaceCommand
58
79
  });
@@ -42,6 +42,7 @@ __export(workspace_git_sync_exports, {
42
42
  resetWorkspaceGit: () => resetWorkspaceGit,
43
43
  synchronizeWorkspaceGit: () => synchronizeWorkspaceGit,
44
44
  workspaceGitHydrationIsCurrent: () => workspaceGitHydrationIsCurrent,
45
+ workspaceGitMountsForRemediation: () => workspaceGitMountsForRemediation,
45
46
  workspaceGitSyncTestHarness: () => workspaceGitSyncTestHarness
46
47
  });
47
48
  module.exports = __toCommonJS(workspace_git_sync_exports);
@@ -58,6 +59,9 @@ const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
58
59
  const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
59
60
  const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
60
61
  const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
62
+ function workspaceGitMountsForRemediation(mounts) {
63
+ return mounts.map((mount) => ({ ...mount, busy: mount.busyForRecovery ?? mount.busy }));
64
+ }
61
65
  class WorkspaceRemediationAncestryError extends Error {
62
66
  constructor(message) {
63
67
  super(message);
@@ -1342,6 +1346,9 @@ function resetWorkspaceGit(input) {
1342
1346
  const outerPath = import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1343
1347
  return import_node_fs.default.existsSync(outerPath);
1344
1348
  });
1349
+ const bootstrapMountIds = new Set(bootstrapMounts.map(({ id }) => id));
1350
+ const durablyAbsentMounts = selected.skipped.filter((mount) => !bootstrapMountIds.has(mount.id) && mount.sourceMode === "all");
1351
+ const uncoveredSkippedMounts = selected.skipped.filter((mount) => !bootstrapMountIds.has(mount.id) && mount.sourceMode !== "all");
1345
1352
  const hydrationMounts = [...selected.active, ...bootstrapMounts];
1346
1353
  const requiredLiveMounts = [
1347
1354
  ...new Map([...requiredLiveMountsBeforeHydration, ...bootstrapMounts].map((mount) => [mount.id, mount])).values()
@@ -1349,7 +1356,7 @@ function resetWorkspaceGit(input) {
1349
1356
  const hydration = hydrateWorkspaceGitMountsTransactionally({
1350
1357
  workspacePath,
1351
1358
  mounts: hydrationMounts,
1352
- durabilityMounts: [...requiredLiveMounts, ...selected.tombstones],
1359
+ durabilityMounts: [...requiredLiveMounts, ...selected.tombstones, ...durablyAbsentMounts],
1353
1360
  receiptMounts: input.mounts,
1354
1361
  requiredMounts: requiredLiveMounts,
1355
1362
  recordCurrentHead: true
@@ -1365,7 +1372,7 @@ function resetWorkspaceGit(input) {
1365
1372
  remoteHead,
1366
1373
  discardedPaths,
1367
1374
  activeMountIds: hydration.hydratedMountIds,
1368
- skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1375
+ skippedMountIds: [.../* @__PURE__ */ new Set([...uncoveredSkippedMounts.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1369
1376
  };
1370
1377
  }
1371
1378
  function stagedPaths(workspacePath) {
@@ -1881,5 +1888,6 @@ const workspaceGitSyncTestHarness = {
1881
1888
  resetWorkspaceGit,
1882
1889
  synchronizeWorkspaceGit,
1883
1890
  workspaceGitHydrationIsCurrent,
1891
+ workspaceGitMountsForRemediation,
1884
1892
  workspaceGitSyncTestHarness
1885
1893
  });
package/dist/mjs/main.mjs CHANGED
@@ -28,11 +28,14 @@ import {
28
28
  PENDING_CREATED_BRANCH_AUTOMATIC_SYNC_GRACE_MS,
29
29
  pendingCreatedBranchAutomaticSyncDeferral,
30
30
  pendingCreatedBranchKey,
31
- projectBranchHasActiveWorkspaceTarget
31
+ projectBranchHasActiveWorkspaceTarget,
32
+ projectBranchMountBusyDuringExclusiveSync,
33
+ workspacePlansMountBusyDuringExclusiveSync
32
34
  } from "./workspace-automatic-sync-policy.mjs";
33
35
  import {
34
36
  acquireWorkspaceCommandMutation,
35
37
  reserveWorkspaceCommandAfterCurrentSync,
38
+ runReservedWorkspaceCommand,
36
39
  runWorkspaceCommand
37
40
  } from "./workspace-command-sync-policy.mjs";
38
41
  import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
@@ -86,6 +89,7 @@ import {
86
89
  resetWorkspaceGit,
87
90
  synchronizeWorkspaceGit,
88
91
  workspaceGitHydrationIsCurrent,
92
+ workspaceGitMountsForRemediation,
89
93
  WorkspaceRemediationAncestryError
90
94
  } from "./workspace-git-sync.mjs";
91
95
  class ProjectWorkspaceConfigurationDeferredError extends Error {
@@ -1539,10 +1543,20 @@ function workspaceConfigurationIncidentDeferral(input) {
1539
1543
  return { incidentId };
1540
1544
  }
1541
1545
  function workspaceIncidentTerminalClearDisposition(input) {
1542
- if (!input.deferredConfiguration) return "ordinary_resume";
1543
- void input.clearedIncidentId;
1546
+ if (!input.deferredConfiguration) return "await_server_refresh";
1547
+ if (input.deferredConfiguration.incidentId !== input.clearedIncidentId) return "reconnect_for_refresh";
1544
1548
  return input.deferredConfiguration.serverRefreshExpected ? "await_server_refresh" : "reconnect_for_refresh";
1545
1549
  }
1550
+ function workspaceIncidentTerminalRefreshFence(input) {
1551
+ const disposition = workspaceIncidentTerminalClearDisposition(input);
1552
+ return {
1553
+ disposition,
1554
+ deferredConfiguration: input.deferredConfiguration ?? { incidentId: input.clearedIncidentId, serverRefreshExpected: true }
1555
+ };
1556
+ }
1557
+ function workspaceAutomaticSyncIsAdmitted(input) {
1558
+ return input.workspaceConfigured && input.activeIncidentId === null && input.deferredConfiguration === null;
1559
+ }
1546
1560
  function deferredWorkspaceTargetIsAllowed(deferredConfiguration, activeIncidentId, target) {
1547
1561
  const canonicalTarget = target.type === "workspace" && target.rootProfile === "canonical_sync";
1548
1562
  if (deferredConfiguration) return activeIncidentId === deferredConfiguration.incidentId && canonicalTarget;
@@ -1564,6 +1578,9 @@ function deferredCredentialTransitionMustWait(input) {
1564
1578
  function workspaceConfigurationIncidentSnapshotIsCurrent(capturedIncidentId, activeIncidentId) {
1565
1579
  return capturedIncidentId === null ? activeIncidentId === null : activeIncidentId === null || activeIncidentId === capturedIncidentId;
1566
1580
  }
1581
+ function workspaceConfigurationResetToCanonicalIsAllowed(requested, incidentId) {
1582
+ return requested === true && incidentId === null;
1583
+ }
1567
1584
  function deferredWorkspaceRefreshWatchdogIsCurrent(input) {
1568
1585
  return input.deferredConfiguration?.incidentId === input.capturedIncidentId && input.activeIncidentId === null;
1569
1586
  }
@@ -1636,12 +1653,15 @@ const workerGitSecurityTestHarness = {
1636
1653
  fenceUnsafeWorkspaceSyncFailure,
1637
1654
  workspaceConfigurationIncidentDeferral,
1638
1655
  workspaceIncidentTerminalClearDisposition,
1656
+ workspaceIncidentTerminalRefreshFence,
1657
+ workspaceAutomaticSyncIsAdmitted,
1639
1658
  deferredWorkspaceTargetIsAllowed,
1640
1659
  deferredWorkspaceSyncTriggerIsAllowed,
1641
1660
  workspaceOperationsAreFenced,
1642
1661
  workspaceCommandTransportIsAllowed,
1643
1662
  deferredCredentialTransitionMustWait,
1644
1663
  workspaceConfigurationIncidentSnapshotIsCurrent,
1664
+ workspaceConfigurationResetToCanonicalIsAllowed,
1645
1665
  deferredWorkspaceRefreshWatchdogIsCurrent,
1646
1666
  incidentDeferredProjectCheckouts,
1647
1667
  deferredIncidentWorkspaceConfigurationResult,
@@ -3402,6 +3422,11 @@ async function startWorker(options) {
3402
3422
  let cliUpdateInProgress = false;
3403
3423
  let reloadAfterClose = false;
3404
3424
  let shutdownAfterClose = false;
3425
+ const automaticWorkspaceSyncIsCurrentlyAdmitted = () => workspaceAutomaticSyncIsAdmitted({
3426
+ workspaceConfigured,
3427
+ activeIncidentId: activeWorkspaceIncidentId,
3428
+ deferredConfiguration: deferredWorkspaceConfiguration
3429
+ });
3405
3430
  const bearerAuthHeader = `Authorization: Bearer ${token}`;
3406
3431
  const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
3407
3432
  const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
@@ -3683,6 +3708,17 @@ async function startWorker(options) {
3683
3708
  const activeTargets = activeWorkspaceMutationTargets();
3684
3709
  return projectConfigById.get(projectId)?.executionDisabled === true || hasProjectWorktree(branchPath) && projectWorktreeOperationInProgress(branchPath) || canonicalWorkspaceMutationIsActive(activeTargets) || projectBranchHasActiveWorkspaceTarget(projectId, branchName, activeTargets);
3685
3710
  };
3711
+ const projectBranchMountExclusiveSyncBusy = (projectId, branchName, branchPath) => {
3712
+ const activeTargets = activeWorkspaceMutationTargets();
3713
+ return projectBranchMountBusyDuringExclusiveSync({
3714
+ projectId,
3715
+ branchName,
3716
+ executionDisabled: projectConfigById.get(projectId)?.executionDisabled === true,
3717
+ worktreeOperationInProgress: hasProjectWorktree(branchPath) && projectWorktreeOperationInProgress(branchPath),
3718
+ retainedCanonicalProcessGroup: canonicalWorkspaceMutationIsActive([...credentialBearingProcessGroupTargets.values()]),
3719
+ activeTargets
3720
+ });
3721
+ };
3686
3722
  const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
3687
3723
  const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
3688
3724
  const mounts = [];
@@ -3715,7 +3751,7 @@ async function startWorker(options) {
3715
3751
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3716
3752
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3717
3753
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3718
- busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3754
+ busyForRecovery: () => projectBranchMountExclusiveSyncBusy(project.projectId, branch.branchName, branchPath)
3719
3755
  });
3720
3756
  mounts.push({
3721
3757
  id: projectPlanMountId(project.projectId, branch.branchName),
@@ -3729,7 +3765,7 @@ async function startWorker(options) {
3729
3765
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3730
3766
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3731
3767
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3732
- busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3768
+ busyForRecovery: () => projectBranchMountExclusiveSyncBusy(project.projectId, branch.branchName, branchPath)
3733
3769
  });
3734
3770
  }
3735
3771
  void projectRoot;
@@ -3752,7 +3788,10 @@ async function startWorker(options) {
3752
3788
  mutationToken: () => String(visibleWorkspaceMutationEpoch),
3753
3789
  busyForRecovery: () => {
3754
3790
  const activeTargets = activeWorkspaceMutationTargets();
3755
- return canonicalWorkspaceMutationIsActive(activeTargets) || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
3791
+ return workspacePlansMountBusyDuringExclusiveSync(
3792
+ activeTargets,
3793
+ canonicalWorkspaceMutationIsActive([...credentialBearingProcessGroupTargets.values()])
3794
+ );
3756
3795
  }
3757
3796
  });
3758
3797
  for (const deletion of pendingMirrorDeletes.values()) {
@@ -4133,9 +4172,9 @@ async function startWorker(options) {
4133
4172
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
4134
4173
  throw new Error("Worker workspace configuration has not been received");
4135
4174
  }
4136
- const mounts = buildWorkspaceMounts();
4175
+ const configuredMounts = buildWorkspaceMounts();
4137
4176
  if (input.resetToCanonical) {
4138
- const resetHasBusyLiveMount = mounts.some(
4177
+ const resetHasBusyLiveMount = configuredMounts.some(
4139
4178
  (mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()
4140
4179
  );
4141
4180
  if (resetHasBusyLiveMount) {
@@ -4151,7 +4190,7 @@ async function startWorker(options) {
4151
4190
  credentialHelper: workspaceCredentialHelper,
4152
4191
  credentialUsername: workspaceCredentialUsername,
4153
4192
  gitIdentity: workspaceGitIdentity,
4154
- mounts
4193
+ mounts: configuredMounts
4155
4194
  });
4156
4195
  applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
4157
4196
  return {
@@ -4175,6 +4214,7 @@ async function startWorker(options) {
4175
4214
  };
4176
4215
  }
4177
4216
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
4217
+ const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
4178
4218
  const observedProjectHeads = outerRemediation ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4179
4219
  const inboundMoveMountIds = new Set(
4180
4220
  observedProjectHeads.flatMap(
@@ -4326,7 +4366,7 @@ async function startWorker(options) {
4326
4366
  pendingCreatedBranchPublicationNotBefore
4327
4367
  );
4328
4368
  const initialDeferral = pendingBranchDeferral();
4329
- if (!workspaceConfigured || activeWorkspaceIncidentId || deferredWorkspaceConfiguration || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
4369
+ if (!automaticWorkspaceSyncIsCurrentlyAdmitted() || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
4330
4370
  return;
4331
4371
  }
4332
4372
  if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
@@ -4334,8 +4374,7 @@ async function startWorker(options) {
4334
4374
  () => {
4335
4375
  workspaceAutomaticTimer = void 0;
4336
4376
  const scheduledTrigger = pendingAutomaticTrigger;
4337
- if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || deferredWorkspaceConfiguration || currentWorkerSocket !== ws)
4338
- return;
4377
+ if (!scheduledTrigger || !automaticWorkspaceSyncIsCurrentlyAdmitted() || currentWorkerSocket !== ws) return;
4339
4378
  const currentDeferral = pendingBranchDeferral();
4340
4379
  if (currentDeferral?.kind === "active_target") return;
4341
4380
  if (currentDeferral?.kind === "creation_grace") {
@@ -4357,7 +4396,7 @@ async function startWorker(options) {
4357
4396
  }).finally(() => {
4358
4397
  automaticSyncInFlight = false;
4359
4398
  heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
4360
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
4399
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4361
4400
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
4362
4401
  }
4363
4402
  });
@@ -4766,6 +4805,7 @@ async function startWorker(options) {
4766
4805
  const result = await performWorkspaceSync({
4767
4806
  attemptId: crypto.randomUUID(),
4768
4807
  trigger: { type: "connect" },
4808
+ resetToCanonical: workspaceConfigurationResetToCanonicalIsAllowed(message.resetToCanonical, incidentDeferral.incidentId),
4769
4809
  assertStillAdmitted: assertConfigurationSyncStillAdmitted
4770
4810
  });
4771
4811
  assertConfigurationSyncStillAdmitted();
@@ -4864,7 +4904,7 @@ async function startWorker(options) {
4864
4904
  workspaceAutomaticTimer = void 0;
4865
4905
  }
4866
4906
  void (async () => {
4867
- if (workspaceConfigured && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
4907
+ if (automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4868
4908
  await runWorkspaceSync({
4869
4909
  trigger: { type: "manual", detail: "graceful worker shutdown" }
4870
4910
  });
@@ -4915,7 +4955,8 @@ async function startWorker(options) {
4915
4955
  execStdinV1: true,
4916
4956
  ptyEnvFilesV1: true,
4917
4957
  workspaceRemediationAncestorGuardV1: true,
4918
- workspaceIncidentConfigDeferralV1: true
4958
+ workspaceIncidentConfigDeferralV1: true,
4959
+ workspaceConfigResetToCanonicalV1: true
4919
4960
  },
4920
4961
  projectRoot: projectsRoot,
4921
4962
  artifactRoot,
@@ -4979,7 +5020,7 @@ async function startWorker(options) {
4979
5020
  }, WORKSPACE_GIT_PERIODIC_MS);
4980
5021
  workspacePeriodicTimer.unref();
4981
5022
  }
4982
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
5023
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4983
5024
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, 0);
4984
5025
  }
4985
5026
  return;
@@ -5219,35 +5260,33 @@ async function startWorker(options) {
5219
5260
  if (nextIncidentId !== previousIncidentId) advanceWorkerAdmissionGeneration();
5220
5261
  activeWorkspaceIncidentId = nextIncidentId;
5221
5262
  if (previousIncidentId && !activeWorkspaceIncidentId && (message.status === "resolved" || message.status === "confirmed" || message.status === "reset")) {
5222
- const disposition = workspaceIncidentTerminalClearDisposition({
5263
+ const refresh = workspaceIncidentTerminalRefreshFence({
5223
5264
  deferredConfiguration: deferredWorkspaceConfiguration,
5224
5265
  clearedIncidentId: previousIncidentId
5225
5266
  });
5226
- if (disposition !== "ordinary_resume") {
5227
- pendingAutomaticTrigger ??= { type: "periodic", detail: "resume after workspace incident" };
5228
- workspaceConfigured = false;
5229
- advanceWorkerAdmissionGeneration();
5230
- if (disposition === "reconnect_for_refresh") {
5231
- ws.close(1012, "Refreshing deferred workspace configuration");
5232
- } else {
5233
- if (deferredWorkspaceConfigurationRefreshTimer) clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5234
- const deferredIncidentId = deferredWorkspaceConfiguration?.incidentId;
5235
- deferredWorkspaceConfigurationRefreshTimer = setTimeout(() => {
5236
- deferredWorkspaceConfigurationRefreshTimer = void 0;
5237
- if (!deferredIncidentId || !deferredWorkspaceRefreshWatchdogIsCurrent({
5238
- capturedIncidentId: deferredIncidentId,
5239
- deferredConfiguration: deferredWorkspaceConfiguration,
5240
- activeIncidentId: activeWorkspaceIncidentId
5241
- }) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
5242
- return;
5243
- }
5244
- ws.close(1012, "Timed out waiting for deferred workspace configuration refresh");
5245
- }, WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS);
5246
- deferredWorkspaceConfigurationRefreshTimer.unref();
5247
- }
5248
- return;
5267
+ deferredWorkspaceConfiguration = refresh.deferredConfiguration;
5268
+ pendingAutomaticTrigger ??= { type: "periodic", detail: "resume after workspace incident" };
5269
+ workspaceConfigured = false;
5270
+ advanceWorkerAdmissionGeneration();
5271
+ if (refresh.disposition === "reconnect_for_refresh") {
5272
+ ws.close(1012, "Refreshing deferred workspace configuration");
5273
+ } else {
5274
+ if (deferredWorkspaceConfigurationRefreshTimer) clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5275
+ const deferredIncidentId = deferredWorkspaceConfiguration.incidentId;
5276
+ deferredWorkspaceConfigurationRefreshTimer = setTimeout(() => {
5277
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
5278
+ if (!deferredWorkspaceRefreshWatchdogIsCurrent({
5279
+ capturedIncidentId: deferredIncidentId,
5280
+ deferredConfiguration: deferredWorkspaceConfiguration,
5281
+ activeIncidentId: activeWorkspaceIncidentId
5282
+ }) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
5283
+ return;
5284
+ }
5285
+ ws.close(1012, "Timed out waiting for deferred workspace configuration refresh");
5286
+ }, WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS);
5287
+ deferredWorkspaceConfigurationRefreshTimer.unref();
5249
5288
  }
5250
- scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger ?? { type: "periodic", detail: "resume after workspace incident" }, 0);
5289
+ return;
5251
5290
  }
5252
5291
  return;
5253
5292
  }
@@ -5620,7 +5659,12 @@ async function startWorker(options) {
5620
5659
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
5621
5660
  });
5622
5661
  }
5623
- await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
5662
+ await runReservedWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand, () => {
5663
+ if (targetReserved) {
5664
+ workspaceSyncPriorityProcessTargets.delete(message.runId);
5665
+ targetReserved = false;
5666
+ }
5667
+ });
5624
5668
  } finally {
5625
5669
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
5626
5670
  }
@@ -5694,7 +5738,7 @@ async function startWorker(options) {
5694
5738
  } finally {
5695
5739
  if (reservesVisibleWorkspace) {
5696
5740
  workspaceSyncPriorityOperationTargets.delete(message.requestId);
5697
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
5741
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
5698
5742
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, dirtyTrigger ? WORKSPACE_GIT_QUIET_MS : 0);
5699
5743
  }
5700
5744
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.83",
3
+ "version": "0.0.86",
4
4
  "type": "module"
5
5
  }
@@ -15,6 +15,12 @@ function projectBranchHasActiveWorkspaceTarget(projectId, branchName, activeTarg
15
15
  }
16
16
  return false;
17
17
  }
18
+ function projectBranchMountBusyDuringExclusiveSync(input) {
19
+ return input.executionDisabled || input.worktreeOperationInProgress || input.retainedCanonicalProcessGroup || projectBranchHasActiveWorkspaceTarget(input.projectId, input.branchName, input.activeTargets);
20
+ }
21
+ function workspacePlansMountBusyDuringExclusiveSync(activeTargets, retainedCanonicalProcessGroup = false) {
22
+ return retainedCanonicalProcessGroup || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
23
+ }
18
24
  function shouldDeferAutomaticWorkspaceSyncForPendingBranch(pendingCreatedBranches, activeTargets) {
19
25
  if (pendingCreatedBranches.length === 0) return false;
20
26
  const targets = [...activeTargets];
@@ -36,5 +42,7 @@ export {
36
42
  pendingCreatedBranchAutomaticSyncDeferral,
37
43
  pendingCreatedBranchKey,
38
44
  projectBranchHasActiveWorkspaceTarget,
39
- shouldDeferAutomaticWorkspaceSyncForPendingBranch
45
+ projectBranchMountBusyDuringExclusiveSync,
46
+ shouldDeferAutomaticWorkspaceSyncForPendingBranch,
47
+ workspacePlansMountBusyDuringExclusiveSync
40
48
  };
@@ -20,12 +20,32 @@ function shouldSerializeWorkspaceCommand(target) {
20
20
  function runWorkspaceCommand(target, coordinator, operation) {
21
21
  return shouldSerializeWorkspaceCommand(target) ? coordinator.runMutation(operation) : Promise.resolve().then(operation);
22
22
  }
23
+ async function runReservedWorkspaceCommand(target, coordinator, operation, releaseReservation) {
24
+ let released = false;
25
+ const releaseOnce = () => {
26
+ if (released) return;
27
+ released = true;
28
+ releaseReservation();
29
+ };
30
+ try {
31
+ return await runWorkspaceCommand(target, coordinator, async () => {
32
+ try {
33
+ return await operation();
34
+ } finally {
35
+ releaseOnce();
36
+ }
37
+ });
38
+ } finally {
39
+ releaseOnce();
40
+ }
41
+ }
23
42
  function acquireWorkspaceCommandMutation(target, coordinator) {
24
43
  return shouldSerializeWorkspaceCommand(target) ? coordinator.acquireMutation() : Promise.resolve(void 0);
25
44
  }
26
45
  export {
27
46
  acquireWorkspaceCommandMutation,
28
47
  reserveWorkspaceCommandAfterCurrentSync,
48
+ runReservedWorkspaceCommand,
29
49
  runWorkspaceCommand,
30
50
  shouldSerializeWorkspaceCommand
31
51
  };
@@ -15,6 +15,9 @@ const WORKSPACE_GIT_INTEGRATED_REF = "refs/r5d/workspace-local/integrated";
15
15
  const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
16
16
  const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
17
17
  const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
18
+ function workspaceGitMountsForRemediation(mounts) {
19
+ return mounts.map((mount) => ({ ...mount, busy: mount.busyForRecovery ?? mount.busy }));
20
+ }
18
21
  class WorkspaceRemediationAncestryError extends Error {
19
22
  constructor(message) {
20
23
  super(message);
@@ -1299,6 +1302,9 @@ function resetWorkspaceGit(input) {
1299
1302
  const outerPath = path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1300
1303
  return fs.existsSync(outerPath);
1301
1304
  });
1305
+ const bootstrapMountIds = new Set(bootstrapMounts.map(({ id }) => id));
1306
+ const durablyAbsentMounts = selected.skipped.filter((mount) => !bootstrapMountIds.has(mount.id) && mount.sourceMode === "all");
1307
+ const uncoveredSkippedMounts = selected.skipped.filter((mount) => !bootstrapMountIds.has(mount.id) && mount.sourceMode !== "all");
1302
1308
  const hydrationMounts = [...selected.active, ...bootstrapMounts];
1303
1309
  const requiredLiveMounts = [
1304
1310
  ...new Map([...requiredLiveMountsBeforeHydration, ...bootstrapMounts].map((mount) => [mount.id, mount])).values()
@@ -1306,7 +1312,7 @@ function resetWorkspaceGit(input) {
1306
1312
  const hydration = hydrateWorkspaceGitMountsTransactionally({
1307
1313
  workspacePath,
1308
1314
  mounts: hydrationMounts,
1309
- durabilityMounts: [...requiredLiveMounts, ...selected.tombstones],
1315
+ durabilityMounts: [...requiredLiveMounts, ...selected.tombstones, ...durablyAbsentMounts],
1310
1316
  receiptMounts: input.mounts,
1311
1317
  requiredMounts: requiredLiveMounts,
1312
1318
  recordCurrentHead: true
@@ -1322,7 +1328,7 @@ function resetWorkspaceGit(input) {
1322
1328
  remoteHead,
1323
1329
  discardedPaths,
1324
1330
  activeMountIds: hydration.hydratedMountIds,
1325
- skippedMountIds: [.../* @__PURE__ */ new Set([...selected.skipped.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1331
+ skippedMountIds: [.../* @__PURE__ */ new Set([...uncoveredSkippedMounts.map(({ id }) => id), ...hydration.skippedMountIds])].sort()
1326
1332
  };
1327
1333
  }
1328
1334
  function stagedPaths(workspacePath) {
@@ -1837,5 +1843,6 @@ export {
1837
1843
  resetWorkspaceGit,
1838
1844
  synchronizeWorkspaceGit,
1839
1845
  workspaceGitHydrationIsCurrent,
1846
+ workspaceGitMountsForRemediation,
1840
1847
  workspaceGitSyncTestHarness
1841
1848
  };
@@ -91,6 +91,7 @@ type WorkerServerMessage = {
91
91
  gitIdentity: WorkerGitIdentity;
92
92
  githubCredential: WorkerGitHubCredential | null;
93
93
  deferWorkspaceSyncForIncidentId?: string;
94
+ resetToCanonical?: boolean;
94
95
  } | {
95
96
  type: "sync_workspace";
96
97
  requestId: string;
@@ -565,8 +566,20 @@ type DeferredWorkspaceConfiguration = {
565
566
  };
566
567
  declare function workspaceIncidentTerminalClearDisposition(input: {
567
568
  deferredConfiguration: DeferredWorkspaceConfiguration | null;
568
- clearedIncidentId: string | null;
569
- }): "ordinary_resume" | "await_server_refresh" | "reconnect_for_refresh";
569
+ clearedIncidentId: string;
570
+ }): "await_server_refresh" | "reconnect_for_refresh";
571
+ declare function workspaceIncidentTerminalRefreshFence(input: {
572
+ deferredConfiguration: DeferredWorkspaceConfiguration | null;
573
+ clearedIncidentId: string;
574
+ }): {
575
+ disposition: "await_server_refresh" | "reconnect_for_refresh";
576
+ deferredConfiguration: DeferredWorkspaceConfiguration;
577
+ };
578
+ declare function workspaceAutomaticSyncIsAdmitted(input: {
579
+ workspaceConfigured: boolean;
580
+ activeIncidentId: string | null;
581
+ deferredConfiguration: DeferredWorkspaceConfiguration | null;
582
+ }): boolean;
570
583
  declare function deferredWorkspaceTargetIsAllowed(deferredConfiguration: DeferredWorkspaceConfiguration | null, activeIncidentId: string | null, target: WorkerSessionTarget): boolean;
571
584
  declare function deferredWorkspaceSyncTriggerIsAllowed(deferredConfiguration: DeferredWorkspaceConfiguration | null, activeIncidentId: string | null, trigger: WorkspaceSyncTrigger): boolean;
572
585
  declare function workspaceOperationsAreFenced(deferredConfiguration: DeferredWorkspaceConfiguration | null, activeIncidentId: string | null): boolean;
@@ -577,6 +590,7 @@ declare function deferredCredentialTransitionMustWait(input: {
577
590
  canonicalRemediationActive: boolean;
578
591
  }): boolean;
579
592
  declare function workspaceConfigurationIncidentSnapshotIsCurrent(capturedIncidentId: string | null, activeIncidentId: string | null): boolean;
593
+ declare function workspaceConfigurationResetToCanonicalIsAllowed(requested: boolean | undefined, incidentId: string | null): boolean;
580
594
  declare function deferredWorkspaceRefreshWatchdogIsCurrent(input: {
581
595
  capturedIncidentId: string;
582
596
  deferredConfiguration: DeferredWorkspaceConfiguration | null;
@@ -629,12 +643,15 @@ export declare const workerGitSecurityTestHarness: {
629
643
  fenceUnsafeWorkspaceSyncFailure: typeof fenceUnsafeWorkspaceSyncFailure;
630
644
  workspaceConfigurationIncidentDeferral: typeof workspaceConfigurationIncidentDeferral;
631
645
  workspaceIncidentTerminalClearDisposition: typeof workspaceIncidentTerminalClearDisposition;
646
+ workspaceIncidentTerminalRefreshFence: typeof workspaceIncidentTerminalRefreshFence;
647
+ workspaceAutomaticSyncIsAdmitted: typeof workspaceAutomaticSyncIsAdmitted;
632
648
  deferredWorkspaceTargetIsAllowed: typeof deferredWorkspaceTargetIsAllowed;
633
649
  deferredWorkspaceSyncTriggerIsAllowed: typeof deferredWorkspaceSyncTriggerIsAllowed;
634
650
  workspaceOperationsAreFenced: typeof workspaceOperationsAreFenced;
635
651
  workspaceCommandTransportIsAllowed: typeof workspaceCommandTransportIsAllowed;
636
652
  deferredCredentialTransitionMustWait: typeof deferredCredentialTransitionMustWait;
637
653
  workspaceConfigurationIncidentSnapshotIsCurrent: typeof workspaceConfigurationIncidentSnapshotIsCurrent;
654
+ workspaceConfigurationResetToCanonicalIsAllowed: typeof workspaceConfigurationResetToCanonicalIsAllowed;
638
655
  deferredWorkspaceRefreshWatchdogIsCurrent: typeof deferredWorkspaceRefreshWatchdogIsCurrent;
639
656
  incidentDeferredProjectCheckouts: typeof incidentDeferredProjectCheckouts;
640
657
  deferredIncidentWorkspaceConfigurationResult: typeof deferredIncidentWorkspaceConfigurationResult;
@@ -21,6 +21,23 @@ export type PendingCreatedBranchAutomaticSyncDeferral = {
21
21
  export declare function pendingCreatedBranchKey(projectId: string, branchName: string): string;
22
22
  export declare function hasActiveVisibleProjectsWorkspaceTarget(activeTargets: Iterable<ProjectTarget | WorkspaceTarget>): boolean;
23
23
  export declare function projectBranchHasActiveWorkspaceTarget(projectId: string, branchName: string, activeTargets: Iterable<ProjectTarget | WorkspaceTarget>): boolean;
24
+ /**
25
+ * Activity fence evaluated from inside the workspace mutation gate's exclusive
26
+ * sync lease. Canonical-sync targets are deliberately absent: an immediately
27
+ * reserved follow-up remediation command may already be queued, but the gate
28
+ * proves that it cannot overlap this sync. A credential-bearing process group
29
+ * that survived command teardown is tracked separately and still fails closed.
30
+ */
31
+ export declare function projectBranchMountBusyDuringExclusiveSync(input: {
32
+ projectId: string;
33
+ branchName: string;
34
+ executionDisabled: boolean;
35
+ worktreeOperationInProgress: boolean;
36
+ retainedCanonicalProcessGroup: boolean;
37
+ activeTargets: Iterable<ProjectTarget | WorkspaceTarget>;
38
+ }): boolean;
39
+ /** Fence visible activity and any retained canonical descendant that outlived its command lease. */
40
+ export declare function workspacePlansMountBusyDuringExclusiveSync(activeTargets: Iterable<ProjectTarget | WorkspaceTarget>, retainedCanonicalProcessGroup?: boolean): boolean;
24
41
  /**
25
42
  * A workspace sync is global and synchronous enough to monopolize the worker
26
43
  * event loop. Keep automatic intent pending while an agent process is active
@@ -25,5 +25,12 @@ export declare function reserveWorkspaceCommandAfterCurrentSync(target: Workspac
25
25
  */
26
26
  export declare function shouldSerializeWorkspaceCommand(target: WorkspaceCommandTarget): boolean;
27
27
  export declare function runWorkspaceCommand<T>(target: WorkspaceCommandTarget, coordinator: WorkspaceCommandMutationCoordinator, operation: () => Promise<T> | T): Promise<T>;
28
+ /**
29
+ * Run a command whose target was reserved before asynchronous preparation.
30
+ * For a serialized canonical command, the reservation must disappear inside
31
+ * the mutation operation: removing it after `runMutation` resolves is too
32
+ * late because releasing the lease can synchronously wake a queued sync.
33
+ */
34
+ export declare function runReservedWorkspaceCommand<T>(target: WorkspaceCommandTarget, coordinator: WorkspaceCommandMutationCoordinator, operation: () => Promise<T> | T, releaseReservation: () => void): Promise<T>;
28
35
  export declare function acquireWorkspaceCommandMutation(target: WorkspaceCommandTarget, coordinator: WorkspaceCommandMutationCoordinator): Promise<(() => void) | undefined>;
29
36
  export {};
@@ -34,6 +34,14 @@ export type WorkspaceGitMount = {
34
34
  /** Activity-only fence used before checkout readiness is established on reconnect. */
35
35
  busyForRecovery?: () => boolean;
36
36
  };
37
+ /**
38
+ * Incident-bound configuration keeps project checkouts unready while the
39
+ * canonical remediation owns the workspace. The guarded remediation sync is
40
+ * nevertheless allowed to hydrate an existing checkout when its activity-only
41
+ * fence is clear; the ordinary `busy` predicate would mistake that deliberate
42
+ * unready state for concurrent visible work.
43
+ */
44
+ export declare function workspaceGitMountsForRemediation(mounts: readonly WorkspaceGitMount[]): WorkspaceGitMount[];
37
45
  export type WorkspaceGitSyncOutcome = "no_change" | "updated" | "pushed" | "large_diff_blocked" | "conflict_blocked";
38
46
  export type WorkspaceGitSyncResult = {
39
47
  outcome: WorkspaceGitSyncOutcome;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.83",
3
+ "version": "0.0.86",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",