@ricsam/r5d-worker 0.0.82 → 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/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";
@@ -79,12 +82,15 @@ import {
79
82
  pruneAuthoritativelyDesiredBranchDeletions
80
83
  } from "./project-workspace-state.mjs";
81
84
  import {
85
+ configureExistingWorkspaceGitForRemediation,
82
86
  ensureWorkspaceGitClone,
83
87
  hydrateWorkspaceGitMounts,
84
88
  recoverWorkspaceGitHydration,
85
89
  resetWorkspaceGit,
86
90
  synchronizeWorkspaceGit,
87
- workspaceGitHydrationIsCurrent
91
+ workspaceGitHydrationIsCurrent,
92
+ workspaceGitMountsForRemediation,
93
+ WorkspaceRemediationAncestryError
88
94
  } from "./workspace-git-sync.mjs";
89
95
  class ProjectWorkspaceConfigurationDeferredError extends Error {
90
96
  }
@@ -99,6 +105,7 @@ const DEFAULT_READ_MAX_BYTES = 5e4;
99
105
  const MAX_LINE_LENGTH = 2e3;
100
106
  const WORKSPACE_GIT_QUIET_MS = 5e3;
101
107
  const WORKSPACE_GIT_PERIODIC_MS = 6e4;
108
+ const WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS = 6e4;
102
109
  const PTY_INPUT_BUSY_GRACE_MS = 3e3;
103
110
  const PTY_FOREGROUND_POLL_MS = 1e3;
104
111
  const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
@@ -257,6 +264,7 @@ const workerPtyTestHarness = {
257
264
  removeEnvFiles: removePtyEnvFiles,
258
265
  resolveEnvFileReferences: resolvePtyEnvFileReferences,
259
266
  commandHasWorkspaceEffect: workerCommandHasWorkspaceEffect,
267
+ canonicalSyncTerminalHead,
260
268
  ptyIsWorkspaceBusy: workerPtyIsWorkspaceBusy,
261
269
  parseLinuxForegroundBusy: parseLinuxPtyForegroundBusy
262
270
  };
@@ -1518,6 +1526,99 @@ function credentialReapContractStatus(probe) {
1518
1526
  function verifiedCredentialReapContract(probe) {
1519
1527
  return credentialReapContractStatus(probe) === "verified_systemd";
1520
1528
  }
1529
+ function workspaceConfigurationIncidentDeferral(input) {
1530
+ const incidentId = input.requestedIncidentId ?? input.activeIncidentId;
1531
+ if (incidentId === null) return { incidentId: null };
1532
+ if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(incidentId)) {
1533
+ return { incidentId: null, error: new Error("Workspace configuration incident deferral requires a canonical incident UUID") };
1534
+ }
1535
+ if (input.requestedIncidentId !== void 0 && input.requestedIncidentId !== input.activeIncidentId) {
1536
+ return {
1537
+ incidentId: null,
1538
+ error: new Error(
1539
+ `Workspace configuration incident deferral ${input.requestedIncidentId} does not match active incident ${input.activeIncidentId ?? "none"}`
1540
+ )
1541
+ };
1542
+ }
1543
+ return { incidentId };
1544
+ }
1545
+ function workspaceIncidentTerminalClearDisposition(input) {
1546
+ if (!input.deferredConfiguration) return "await_server_refresh";
1547
+ if (input.deferredConfiguration.incidentId !== input.clearedIncidentId) return "reconnect_for_refresh";
1548
+ return input.deferredConfiguration.serverRefreshExpected ? "await_server_refresh" : "reconnect_for_refresh";
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
+ }
1560
+ function deferredWorkspaceTargetIsAllowed(deferredConfiguration, activeIncidentId, target) {
1561
+ const canonicalTarget = target.type === "workspace" && target.rootProfile === "canonical_sync";
1562
+ if (deferredConfiguration) return activeIncidentId === deferredConfiguration.incidentId && canonicalTarget;
1563
+ return activeIncidentId === null || canonicalTarget;
1564
+ }
1565
+ function deferredWorkspaceSyncTriggerIsAllowed(deferredConfiguration, activeIncidentId, trigger) {
1566
+ const incidentId = deferredConfiguration?.incidentId ?? activeIncidentId;
1567
+ return incidentId === null || (deferredConfiguration === null || activeIncidentId === deferredConfiguration.incidentId) && (trigger.type === "remediation" || trigger.type === "remediation_confirm" || trigger.type === "remediation_reset");
1568
+ }
1569
+ function workspaceOperationsAreFenced(deferredConfiguration, activeIncidentId) {
1570
+ return deferredConfiguration !== null || activeIncidentId !== null;
1571
+ }
1572
+ function workspaceCommandTransportIsAllowed(deferredConfiguration, activeIncidentId, transport) {
1573
+ return !workspaceOperationsAreFenced(deferredConfiguration, activeIncidentId) || transport === "exec_start";
1574
+ }
1575
+ function deferredCredentialTransitionMustWait(input) {
1576
+ return input.incidentId !== null && input.transitionPhase !== "current" && input.canonicalRemediationActive;
1577
+ }
1578
+ function workspaceConfigurationIncidentSnapshotIsCurrent(capturedIncidentId, activeIncidentId) {
1579
+ return capturedIncidentId === null ? activeIncidentId === null : activeIncidentId === null || activeIncidentId === capturedIncidentId;
1580
+ }
1581
+ function workspaceConfigurationResetToCanonicalIsAllowed(requested, incidentId) {
1582
+ return requested === true && incidentId === null;
1583
+ }
1584
+ function deferredWorkspaceRefreshWatchdogIsCurrent(input) {
1585
+ return input.deferredConfiguration?.incidentId === input.capturedIncidentId && input.activeIncidentId === null;
1586
+ }
1587
+ function incidentDeferredProjectCheckouts(projects) {
1588
+ return projects.flatMap(
1589
+ (project) => project.executionDisabled ? [] : project.branches.map(({ branchName }) => ({ projectId: project.projectId, branchName }))
1590
+ ).sort((left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName));
1591
+ }
1592
+ function deferredIncidentWorkspaceConfigurationResult(input) {
1593
+ return {
1594
+ type: "workspace_sync",
1595
+ attemptId: input.attemptId,
1596
+ workerLabel: input.workerLabel,
1597
+ trigger: { type: "connect", detail: "workspace synchronization deferred for active remediation" },
1598
+ outcome: "no_change",
1599
+ startingHead: input.head,
1600
+ ...input.head ? { localHead: input.head } : {},
1601
+ rebaseCount: 0,
1602
+ diffSizeBytes: 0,
1603
+ gitStatus: "",
1604
+ affectedProjects: [],
1605
+ affectedPaths: [],
1606
+ activeMountIds: [],
1607
+ skippedMountIds: [...input.skippedMountIds].sort(),
1608
+ activeProjectBranchPublications: [],
1609
+ discardedPaths: [],
1610
+ localChangesDiscarded: false
1611
+ };
1612
+ }
1613
+ function workspaceSyncFailureHydrationIsSafe(input) {
1614
+ if (input.error instanceof WorkspaceRemediationAncestryError) return true;
1615
+ if (input.resetToCanonical) return false;
1616
+ try {
1617
+ return input.inspectCurrentHydration();
1618
+ } catch {
1619
+ return false;
1620
+ }
1621
+ }
1521
1622
  function fenceUnsafeWorkspaceSyncFailure(input) {
1522
1623
  if (input.hydrationCurrent) return false;
1523
1624
  input.invalidateExecution();
@@ -1550,7 +1651,24 @@ const workerGitSecurityTestHarness = {
1550
1651
  credentialReapContractStatus,
1551
1652
  verifiedCredentialReapContract,
1552
1653
  fenceUnsafeWorkspaceSyncFailure,
1654
+ workspaceConfigurationIncidentDeferral,
1655
+ workspaceIncidentTerminalClearDisposition,
1656
+ workspaceIncidentTerminalRefreshFence,
1657
+ workspaceAutomaticSyncIsAdmitted,
1658
+ deferredWorkspaceTargetIsAllowed,
1659
+ deferredWorkspaceSyncTriggerIsAllowed,
1660
+ workspaceOperationsAreFenced,
1661
+ workspaceCommandTransportIsAllowed,
1662
+ deferredCredentialTransitionMustWait,
1663
+ workspaceConfigurationIncidentSnapshotIsCurrent,
1664
+ workspaceConfigurationResetToCanonicalIsAllowed,
1665
+ deferredWorkspaceRefreshWatchdogIsCurrent,
1666
+ incidentDeferredProjectCheckouts,
1667
+ deferredIncidentWorkspaceConfigurationResult,
1668
+ workspaceSyncFailureHydrationIsSafe,
1553
1669
  assertWorkerChildAdmission,
1670
+ executeWriteFileOperation,
1671
+ executeEditFileOperation,
1554
1672
  terminateCredentialBearingChildren,
1555
1673
  terminateCredentialBearingChildrenWithRetention,
1556
1674
  async commitCredentialGeneration(prepared, credential, children, beforeMutation, previousGenerationFenced = false) {
@@ -1701,6 +1819,15 @@ function hasProjectWorktree(checkoutPath) {
1701
1819
  return false;
1702
1820
  }
1703
1821
  }
1822
+ function canonicalSyncTerminalHead(resolvedTarget, readHead = (rootPath) => runGit(["rev-parse", "HEAD"], { cwd: rootPath })) {
1823
+ if (resolvedTarget.target.type !== "workspace" || resolvedTarget.target.rootProfile !== "canonical_sync") return void 0;
1824
+ try {
1825
+ const head = readHead(resolvedTarget.rootPath);
1826
+ return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head) ? head : void 0;
1827
+ } catch {
1828
+ return void 0;
1829
+ }
1830
+ }
1704
1831
  function describeWorkerSessionTarget(target) {
1705
1832
  return target.type === "project" ? `${target.projectId}/${target.branchName}` : `${target.ownerUserId}/${target.rootProfile}`;
1706
1833
  }
@@ -2005,8 +2132,10 @@ async function executeWriteFileOperation(input) {
2005
2132
  planRoot: input.planRoot,
2006
2133
  access: "write"
2007
2134
  });
2135
+ input.assertAdmission();
2008
2136
  const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
2009
2137
  return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
2138
+ input.assertAdmission();
2010
2139
  return writeWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.content, builtInPaths);
2011
2140
  });
2012
2141
  }
@@ -2022,8 +2151,10 @@ async function executeEditFileOperation(input) {
2022
2151
  planRoot: input.planRoot,
2023
2152
  access: "write"
2024
2153
  });
2154
+ input.assertAdmission();
2025
2155
  const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
2026
2156
  return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
2157
+ input.assertAdmission();
2027
2158
  return editWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.edits, builtInPaths);
2028
2159
  });
2029
2160
  }
@@ -2599,13 +2730,15 @@ async function executeStreamingCommand(input) {
2599
2730
  });
2600
2731
  })
2601
2732
  ]);
2733
+ const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2602
2734
  const terminal = {
2603
2735
  type: "exec_exit",
2604
2736
  runId: input.message.runId,
2605
2737
  exitCode,
2606
2738
  durationMs: Date.now() - startedAt,
2607
2739
  ...timedOut ? { timedOut: true } : {},
2608
- ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2740
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2741
+ ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2609
2742
  };
2610
2743
  pendingProcessTerminals.set(input.message.runId, terminal);
2611
2744
  sendWorkerMessage(input.ws, terminal);
@@ -2613,12 +2746,14 @@ async function executeStreamingCommand(input) {
2613
2746
  if (!started && error instanceof StaleWorkerAdmissionError) throw error;
2614
2747
  const message = error instanceof Error ? error.message : String(error);
2615
2748
  if (started) {
2749
+ const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2616
2750
  const terminal = {
2617
2751
  type: "exec_error",
2618
2752
  runId: input.message.runId,
2619
2753
  error: message,
2620
2754
  durationMs: Date.now() - startedAt,
2621
- ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2755
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2756
+ ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2622
2757
  };
2623
2758
  pendingProcessTerminals.set(input.message.runId, terminal);
2624
2759
  sendWorkerMessage(input.ws, terminal);
@@ -3273,6 +3408,9 @@ async function startWorker(options) {
3273
3408
  let workspaceGitIdentity = null;
3274
3409
  let workspaceConfigured = false;
3275
3410
  let activeWorkspaceIncidentId = null;
3411
+ let deferredWorkspaceConfiguration = null;
3412
+ let deferredWorkspaceConfigurationRefreshTimer;
3413
+ let workspaceConfigurationReceiptGeneration = 0;
3276
3414
  let workspaceAutomaticTimer;
3277
3415
  let workspacePeriodicTimer;
3278
3416
  let pendingAutomaticTrigger;
@@ -3284,6 +3422,11 @@ async function startWorker(options) {
3284
3422
  let cliUpdateInProgress = false;
3285
3423
  let reloadAfterClose = false;
3286
3424
  let shutdownAfterClose = false;
3425
+ const automaticWorkspaceSyncIsCurrentlyAdmitted = () => workspaceAutomaticSyncIsAdmitted({
3426
+ workspaceConfigured,
3427
+ activeIncidentId: activeWorkspaceIncidentId,
3428
+ deferredConfiguration: deferredWorkspaceConfiguration
3429
+ });
3287
3430
  const bearerAuthHeader = `Authorization: Bearer ${token}`;
3288
3431
  const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
3289
3432
  const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
@@ -3565,6 +3708,17 @@ async function startWorker(options) {
3565
3708
  const activeTargets = activeWorkspaceMutationTargets();
3566
3709
  return projectConfigById.get(projectId)?.executionDisabled === true || hasProjectWorktree(branchPath) && projectWorktreeOperationInProgress(branchPath) || canonicalWorkspaceMutationIsActive(activeTargets) || projectBranchHasActiveWorkspaceTarget(projectId, branchName, activeTargets);
3567
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
+ };
3568
3722
  const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
3569
3723
  const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
3570
3724
  const mounts = [];
@@ -3597,7 +3751,7 @@ async function startWorker(options) {
3597
3751
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3598
3752
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3599
3753
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3600
- busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3754
+ busyForRecovery: () => projectBranchMountExclusiveSyncBusy(project.projectId, branch.branchName, branchPath)
3601
3755
  });
3602
3756
  mounts.push({
3603
3757
  id: projectPlanMountId(project.projectId, branch.branchName),
@@ -3611,7 +3765,7 @@ async function startWorker(options) {
3611
3765
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3612
3766
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3613
3767
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3614
- busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3768
+ busyForRecovery: () => projectBranchMountExclusiveSyncBusy(project.projectId, branch.branchName, branchPath)
3615
3769
  });
3616
3770
  }
3617
3771
  void projectRoot;
@@ -3634,7 +3788,10 @@ async function startWorker(options) {
3634
3788
  mutationToken: () => String(visibleWorkspaceMutationEpoch),
3635
3789
  busyForRecovery: () => {
3636
3790
  const activeTargets = activeWorkspaceMutationTargets();
3637
- return canonicalWorkspaceMutationIsActive(activeTargets) || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
3791
+ return workspacePlansMountBusyDuringExclusiveSync(
3792
+ activeTargets,
3793
+ canonicalWorkspaceMutationIsActive([...credentialBearingProcessGroupTargets.values()])
3794
+ );
3638
3795
  }
3639
3796
  });
3640
3797
  for (const deletion of pendingMirrorDeletes.values()) {
@@ -3990,6 +4147,7 @@ async function startWorker(options) {
3990
4147
  ...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
3991
4148
  ...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
3992
4149
  ...result.conflictKind ? { conflictKind: result.conflictKind } : {},
4150
+ ...result.verifiedAncestorHeads ? { verifiedAncestorHeads: result.verifiedAncestorHeads } : {},
3993
4151
  ...result.error ? { error: result.error } : {}
3994
4152
  };
3995
4153
  };
@@ -4014,9 +4172,9 @@ async function startWorker(options) {
4014
4172
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
4015
4173
  throw new Error("Worker workspace configuration has not been received");
4016
4174
  }
4017
- const mounts = buildWorkspaceMounts();
4175
+ const configuredMounts = buildWorkspaceMounts();
4018
4176
  if (input.resetToCanonical) {
4019
- const resetHasBusyLiveMount = mounts.some(
4177
+ const resetHasBusyLiveMount = configuredMounts.some(
4020
4178
  (mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()
4021
4179
  );
4022
4180
  if (resetHasBusyLiveMount) {
@@ -4032,7 +4190,7 @@ async function startWorker(options) {
4032
4190
  credentialHelper: workspaceCredentialHelper,
4033
4191
  credentialUsername: workspaceCredentialUsername,
4034
4192
  gitIdentity: workspaceGitIdentity,
4035
- mounts
4193
+ mounts: configuredMounts
4036
4194
  });
4037
4195
  applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
4038
4196
  return {
@@ -4056,6 +4214,7 @@ async function startWorker(options) {
4056
4214
  };
4057
4215
  }
4058
4216
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
4217
+ const mounts = outerRemediation ? workspaceGitMountsForRemediation(configuredMounts) : configuredMounts;
4059
4218
  const observedProjectHeads = outerRemediation ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4060
4219
  const inboundMoveMountIds = new Set(
4061
4220
  observedProjectHeads.flatMap(
@@ -4075,10 +4234,13 @@ async function startWorker(options) {
4075
4234
  commitDetail: input.confirmationReason ?? input.trigger.detail,
4076
4235
  allowLargeDiff: input.confirmedLargeDiff,
4077
4236
  skipMountMirror: outerRemediation,
4237
+ requiredAncestorHeads: input.requiredAncestorHeads,
4238
+ assertStillAdmitted: input.assertStillAdmitted,
4078
4239
  afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
4079
4240
  pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
4080
4241
  }
4081
4242
  });
4243
+ input.assertStillAdmitted?.();
4082
4244
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
4083
4245
  applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
4084
4246
  }
@@ -4101,6 +4263,28 @@ async function startWorker(options) {
4101
4263
  const runWorkspaceSync = async (input) => {
4102
4264
  const attemptId = input.attemptId ?? crypto.randomUUID();
4103
4265
  const requestedAt = Date.now();
4266
+ const admittedGeneration = workerAdmissionGeneration;
4267
+ const admittedActiveIncidentId = activeWorkspaceIncidentId;
4268
+ const admittedDeferredIncidentId = deferredWorkspaceConfiguration?.incidentId ?? null;
4269
+ const assertStillAdmitted = () => {
4270
+ if (admittedGeneration !== workerAdmissionGeneration || admittedActiveIncidentId !== activeWorkspaceIncidentId || admittedDeferredIncidentId !== (deferredWorkspaceConfiguration?.incidentId ?? null) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4271
+ throw new Error("Workspace synchronization admission changed while asynchronous work was in flight");
4272
+ }
4273
+ };
4274
+ if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4275
+ const result2 = {
4276
+ ...failedWorkspaceSyncResult(
4277
+ attemptId,
4278
+ input.trigger,
4279
+ new Error(
4280
+ `Workspace synchronization is deferred for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}; only remediation synchronization is allowed`
4281
+ )
4282
+ ),
4283
+ telemetry: { totalMs: 0, queueMs: 0, prepareMs: 0, synchronizeMs: 0 }
4284
+ };
4285
+ if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result2);
4286
+ return result2;
4287
+ }
4104
4288
  if (!input.requestId && input.sendResult !== false) {
4105
4289
  if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4106
4290
  throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
@@ -4115,23 +4299,32 @@ async function startWorker(options) {
4115
4299
  result = await workspaceSyncSingleFlight.runExclusive(async () => {
4116
4300
  queueEnteredAt = Date.now();
4117
4301
  syncStartedAt = Date.now();
4302
+ if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4303
+ return failedWorkspaceSyncResult(
4304
+ attemptId,
4305
+ input.trigger,
4306
+ new Error(
4307
+ `Workspace synchronization was fenced while queued by incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
4308
+ )
4309
+ );
4310
+ }
4118
4311
  try {
4312
+ assertStillAdmitted();
4119
4313
  return await performWorkspaceSync({
4120
4314
  attemptId,
4121
4315
  trigger: input.trigger,
4122
4316
  confirmedLargeDiff: input.confirmedLargeDiff,
4123
4317
  confirmationReason: input.confirmationReason,
4124
- resetToCanonical: input.resetToCanonical
4318
+ resetToCanonical: input.resetToCanonical,
4319
+ requiredAncestorHeads: input.requiredAncestorHeads,
4320
+ assertStillAdmitted
4125
4321
  });
4126
4322
  } catch (error) {
4127
- let hydrationCurrent = input.resetToCanonical !== true;
4128
- if (hydrationCurrent) {
4129
- try {
4130
- hydrationCurrent = workspaceGitHydrationIsCurrent(workspaceShadowRoot, buildWorkspaceMounts());
4131
- } catch {
4132
- hydrationCurrent = false;
4133
- }
4134
- }
4323
+ const hydrationCurrent = workspaceSyncFailureHydrationIsSafe({
4324
+ error,
4325
+ resetToCanonical: input.resetToCanonical === true,
4326
+ inspectCurrentHydration: () => workspaceGitHydrationIsCurrent(workspaceShadowRoot, buildWorkspaceMounts())
4327
+ });
4135
4328
  if (!hydrationCurrent) {
4136
4329
  process.stderr.write(
4137
4330
  `[r5d-worker] workspace synchronization failed with an incompletely hydrated visible tree; exiting for exact recovery: ${error instanceof Error ? error.message : String(error)}
@@ -4173,7 +4366,7 @@ async function startWorker(options) {
4173
4366
  pendingCreatedBranchPublicationNotBefore
4174
4367
  );
4175
4368
  const initialDeferral = pendingBranchDeferral();
4176
- if (!workspaceConfigured || activeWorkspaceIncidentId || 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") {
4177
4370
  return;
4178
4371
  }
4179
4372
  if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
@@ -4181,7 +4374,7 @@ async function startWorker(options) {
4181
4374
  () => {
4182
4375
  workspaceAutomaticTimer = void 0;
4183
4376
  const scheduledTrigger = pendingAutomaticTrigger;
4184
- if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
4377
+ if (!scheduledTrigger || !automaticWorkspaceSyncIsCurrentlyAdmitted() || currentWorkerSocket !== ws) return;
4185
4378
  const currentDeferral = pendingBranchDeferral();
4186
4379
  if (currentDeferral?.kind === "active_target") return;
4187
4380
  if (currentDeferral?.kind === "creation_grace") {
@@ -4203,7 +4396,7 @@ async function startWorker(options) {
4203
4396
  }).finally(() => {
4204
4397
  automaticSyncInFlight = false;
4205
4398
  heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
4206
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
4399
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4207
4400
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
4208
4401
  }
4209
4402
  });
@@ -4219,6 +4412,11 @@ async function startWorker(options) {
4219
4412
  const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
4220
4413
  const resolveMessageTarget = (target) => {
4221
4414
  if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
4415
+ if (!deferredWorkspaceTargetIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, target)) {
4416
+ throw new Error(
4417
+ `Worker workspace configuration is fenced for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}; only canonical remediation commands are allowed`
4418
+ );
4419
+ }
4222
4420
  if (target.type === "project" && !readyProjectIds.has(target.projectId)) {
4223
4421
  throw new Error(`Project ${target.projectId} is not ready on this worker`);
4224
4422
  }
@@ -4232,7 +4430,26 @@ async function startWorker(options) {
4232
4430
  projectConfigById
4233
4431
  });
4234
4432
  };
4235
- const configureWorkerWorkspace = async (message) => {
4433
+ const configureWorkerWorkspace = async (message, receiptGeneration) => {
4434
+ const incidentDeferral = workspaceConfigurationIncidentDeferral({
4435
+ requestedIncidentId: message.deferWorkspaceSyncForIncidentId,
4436
+ activeIncidentId: activeWorkspaceIncidentId
4437
+ });
4438
+ if (incidentDeferral.error) {
4439
+ workspaceConfigured = false;
4440
+ return {
4441
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, incidentDeferral.error),
4442
+ pending: incidentDeferredProjectCheckouts(message.projects),
4443
+ aheadOfOriginBranches: []
4444
+ };
4445
+ }
4446
+ if (incidentDeferral.incidentId) {
4447
+ workspaceConfigured = false;
4448
+ deferredWorkspaceConfiguration = {
4449
+ incidentId: incidentDeferral.incidentId,
4450
+ serverRefreshExpected: message.deferWorkspaceSyncForIncidentId === incidentDeferral.incidentId
4451
+ };
4452
+ }
4236
4453
  const incomingCredentialGenerationFingerprint = credentialGenerationFingerprint({
4237
4454
  ...message,
4238
4455
  workerBaseUrl: baseUrl,
@@ -4243,6 +4460,22 @@ async function startWorker(options) {
4243
4460
  pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
4244
4461
  incomingFingerprint: incomingCredentialGenerationFingerprint
4245
4462
  });
4463
+ if (deferredCredentialTransitionMustWait({
4464
+ incidentId: incidentDeferral.incidentId,
4465
+ transitionPhase: credentialTransitionPhase,
4466
+ canonicalRemediationActive: canonicalWorkspaceMutationIsActive(activeWorkspaceMutationTargets())
4467
+ })) {
4468
+ return {
4469
+ result: failedWorkspaceSyncResult(
4470
+ crypto.randomUUID(),
4471
+ { type: "connect" },
4472
+ new Error("Workspace credential rotation is deferred until the active canonical remediation command finishes")
4473
+ ),
4474
+ pending: incidentDeferredProjectCheckouts(message.projects),
4475
+ aheadOfOriginBranches: [],
4476
+ ...incidentDeferral.incidentId ? { deferredWorkspaceSyncForIncidentId: incidentDeferral.incidentId } : {}
4477
+ };
4478
+ }
4246
4479
  const credentialReapStatus = credentialReapContractStatus();
4247
4480
  const verifiedCredentialReapContractNow = credentialReapStatus === "verified_systemd";
4248
4481
  const credentialRestartIsContained = credentialGenerationRestartIsContained({
@@ -4356,6 +4589,12 @@ async function startWorker(options) {
4356
4589
  workspaceSyncRequestsInFlight += 1;
4357
4590
  try {
4358
4591
  return await workspaceSyncSingleFlight.runExclusive(async () => {
4592
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4593
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4594
+ }
4595
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4596
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4597
+ }
4359
4598
  for (const project of message.projects) assertRepositoryTransitionState(project);
4360
4599
  const busyConfigurationChanges = busyProjectConfigurationChangeIds({
4361
4600
  currentProjects: [...projectConfigById.values()],
@@ -4377,10 +4616,12 @@ async function startWorker(options) {
4377
4616
  const preserveOnlyBranches = message.projects.flatMap(
4378
4617
  (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
4379
4618
  );
4380
- projectWorkspaceState = projectWorkspaceStateStore.reconcile({
4381
- desiredProjects: message.projects,
4382
- preserveOnlyBranches
4383
- });
4619
+ if (!incidentDeferral.incidentId) {
4620
+ projectWorkspaceState = projectWorkspaceStateStore.reconcile({
4621
+ desiredProjects: message.projects,
4622
+ preserveOnlyBranches
4623
+ });
4624
+ }
4384
4625
  const stillPendingCreatedBranches = new Map(
4385
4626
  projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
4386
4627
  pendingCreatedBranchKey(branch.projectId, branch.branchName),
@@ -4440,6 +4681,12 @@ async function startWorker(options) {
4440
4681
  void 0,
4441
4682
  credentialPublicationPreauthorized
4442
4683
  );
4684
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4685
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4686
+ }
4687
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4688
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4689
+ }
4443
4690
  configuredCredentialGenerationFingerprint = incomingCredentialGenerationFingerprint;
4444
4691
  pendingCredentialGenerationIntentAtProcessStart = null;
4445
4692
  bootstrapCredentialGenerationFingerprintAtProcessStart = null;
@@ -4454,6 +4701,34 @@ async function startWorker(options) {
4454
4701
  branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
4455
4702
  }));
4456
4703
  const nextProjectConfigById = new Map(effectiveProjects.map((project) => [project.projectId, project]));
4704
+ if (incidentDeferral.incidentId) {
4705
+ configureExistingWorkspaceGitForRemediation({
4706
+ workspacePath: workspaceShadowRoot,
4707
+ remoteUrl: message.workspaceRemoteUrl,
4708
+ credentialHelper: nextWorkspaceCredentialHelper,
4709
+ credentialUsername: workerCredentialUsername,
4710
+ gitIdentity: message.gitIdentity
4711
+ });
4712
+ projectConfigById.clear();
4713
+ for (const project of effectiveProjects) projectConfigById.set(project.projectId, project);
4714
+ readyProjectIds.clear();
4715
+ reconciledProjectConfigFingerprints.clear();
4716
+ pendingCheckouts.clear();
4717
+ const pending2 = incidentDeferredProjectCheckouts(message.projects);
4718
+ for (const checkout of pending2) pendingCheckouts.set(projectBranchKey(checkout.projectId, checkout.branchName), checkout);
4719
+ workspaceConfigured = activeWorkspaceIncidentId === incidentDeferral.incidentId;
4720
+ return {
4721
+ result: deferredIncidentWorkspaceConfigurationResult({
4722
+ attemptId: crypto.randomUUID(),
4723
+ workerLabel: label,
4724
+ head: workspaceLocalHead(),
4725
+ skippedMountIds: buildWorkspaceMounts().map(({ id }) => id)
4726
+ }),
4727
+ pending: pending2,
4728
+ aheadOfOriginBranches: [],
4729
+ deferredWorkspaceSyncForIncidentId: incidentDeferral.incidentId
4730
+ };
4731
+ }
4457
4732
  ensureWorkspaceGitClone({
4458
4733
  workspacePath: workspaceShadowRoot,
4459
4734
  remoteUrl: message.workspaceRemoteUrl,
@@ -4521,10 +4796,25 @@ async function startWorker(options) {
4521
4796
  { ignoreBusy: true }
4522
4797
  );
4523
4798
  ensureConfiguredProjects();
4799
+ const configurationSyncAdmissionGeneration = workerAdmissionGeneration;
4800
+ const assertConfigurationSyncStillAdmitted = () => {
4801
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration || configurationSyncAdmissionGeneration !== workerAdmissionGeneration || !workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4802
+ throw new Error("Workspace configuration synchronization admission changed while asynchronous work was in flight");
4803
+ }
4804
+ };
4524
4805
  const result = await performWorkspaceSync({
4525
4806
  attemptId: crypto.randomUUID(),
4526
- trigger: { type: "connect" }
4807
+ trigger: { type: "connect" },
4808
+ resetToCanonical: workspaceConfigurationResetToCanonicalIsAllowed(message.resetToCanonical, incidentDeferral.incidentId),
4809
+ assertStillAdmitted: assertConfigurationSyncStillAdmitted
4527
4810
  });
4811
+ assertConfigurationSyncStillAdmitted();
4812
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4813
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4814
+ }
4815
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4816
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4817
+ }
4528
4818
  const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
4529
4819
  if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
4530
4820
  const activeMountIds = new Set(result.activeMountIds ?? []);
@@ -4541,6 +4831,11 @@ async function startWorker(options) {
4541
4831
  });
4542
4832
  }
4543
4833
  }
4834
+ if (deferredWorkspaceConfigurationRefreshTimer) {
4835
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
4836
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
4837
+ }
4838
+ deferredWorkspaceConfiguration = null;
4544
4839
  workspaceConfigured = true;
4545
4840
  const pending = [...pendingCheckouts.values()].sort(
4546
4841
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
@@ -4548,6 +4843,13 @@ async function startWorker(options) {
4548
4843
  return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
4549
4844
  });
4550
4845
  } catch (error) {
4846
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4847
+ return {
4848
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
4849
+ pending: incidentDeferredProjectCheckouts(message.projects),
4850
+ aheadOfOriginBranches: []
4851
+ };
4852
+ }
4551
4853
  if (error instanceof RegistryAuthConfigurationError) {
4552
4854
  workspaceConfigured = false;
4553
4855
  githubCredential = null;
@@ -4602,7 +4904,7 @@ async function startWorker(options) {
4602
4904
  workspaceAutomaticTimer = void 0;
4603
4905
  }
4604
4906
  void (async () => {
4605
- if (workspaceConfigured && !activeWorkspaceIncidentId) {
4907
+ if (automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4606
4908
  await runWorkspaceSync({
4607
4909
  trigger: { type: "manual", detail: "graceful worker shutdown" }
4608
4910
  });
@@ -4651,7 +4953,10 @@ async function startWorker(options) {
4651
4953
  updateClis: true,
4652
4954
  browserPortForwarding: true,
4653
4955
  execStdinV1: true,
4654
- ptyEnvFilesV1: true
4956
+ ptyEnvFilesV1: true,
4957
+ workspaceRemediationAncestorGuardV1: true,
4958
+ workspaceIncidentConfigDeferralV1: true,
4959
+ workspaceConfigResetToCanonicalV1: true
4655
4960
  },
4656
4961
  projectRoot: projectsRoot,
4657
4962
  artifactRoot,
@@ -4687,26 +4992,35 @@ async function startWorker(options) {
4687
4992
  return;
4688
4993
  }
4689
4994
  if (message.type === "workspace_config") {
4995
+ const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
4996
+ const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
4690
4997
  advanceWorkerAdmissionGeneration();
4691
- const configured = await configureWorkerWorkspace(message);
4998
+ const configured = await configureWorkerWorkspace(message, receiptGeneration);
4692
4999
  sendWorkerMessageFromCurrentSource(ws, {
4693
5000
  type: "workspace_configured",
4694
5001
  requestId: message.requestId,
4695
5002
  result: configured.result,
4696
5003
  pendingCheckouts: configured.pending,
4697
- aheadOfOriginBranches: configured.aheadOfOriginBranches
5004
+ aheadOfOriginBranches: configured.aheadOfOriginBranches,
5005
+ ...configured.deferredWorkspaceSyncForIncidentId ? { deferredWorkspaceSyncForIncidentId: configured.deferredWorkspaceSyncForIncidentId } : {}
4698
5006
  });
4699
5007
  process.stdout.write(
4700
5008
  `[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
4701
5009
  `
4702
5010
  );
5011
+ if (refreshesDeferredConfiguration && configured.result.outcome === "failed" && deferredWorkspaceConfiguration !== null && currentWorkerSocket === ws) {
5012
+ workspaceConfigured = false;
5013
+ advanceWorkerAdmissionGeneration();
5014
+ ws.close(1012, "Deferred workspace configuration refresh failed");
5015
+ return;
5016
+ }
4703
5017
  if (!workspacePeriodicTimer) {
4704
5018
  workspacePeriodicTimer = setInterval(() => {
4705
5019
  scheduleAutomaticWorkspaceSync({ type: "periodic", detail: "periodic workspace reconciliation" }, 0);
4706
5020
  }, WORKSPACE_GIT_PERIODIC_MS);
4707
5021
  workspacePeriodicTimer.unref();
4708
5022
  }
4709
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
5023
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4710
5024
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, 0);
4711
5025
  }
4712
5026
  return;
@@ -4718,11 +5032,20 @@ async function startWorker(options) {
4718
5032
  trigger: message.trigger,
4719
5033
  confirmedLargeDiff: message.confirmedLargeDiff,
4720
5034
  confirmationReason: message.confirmationReason,
4721
- resetToCanonical: message.resetToCanonical
5035
+ resetToCanonical: message.resetToCanonical,
5036
+ requiredAncestorHeads: message.requiredAncestorHeads
4722
5037
  });
4723
5038
  return;
4724
5039
  }
4725
5040
  if (message.type === "create_project_branch") {
5041
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5042
+ sendWorkerMessage(ws, {
5043
+ type: "operation_result",
5044
+ requestId: message.requestId,
5045
+ error: `Project operations are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5046
+ });
5047
+ return;
5048
+ }
4726
5049
  try {
4727
5050
  const pendingBranch = {
4728
5051
  branchId: message.branchId,
@@ -4730,6 +5053,11 @@ async function startWorker(options) {
4730
5053
  branchName: message.targetBranch
4731
5054
  };
4732
5055
  const created = await workspaceSyncSingleFlight.runMutation(() => {
5056
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5057
+ throw new Error(
5058
+ `Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5059
+ );
5060
+ }
4733
5061
  const project = projectConfigById.get(message.projectId);
4734
5062
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
4735
5063
  assertRepositoryExecutionEnabled(project);
@@ -4813,9 +5141,22 @@ async function startWorker(options) {
4813
5141
  return;
4814
5142
  }
4815
5143
  if (message.type === "delete_project_branch") {
5144
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5145
+ sendWorkerMessage(ws, {
5146
+ type: "operation_result",
5147
+ requestId: message.requestId,
5148
+ error: `Project operations are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5149
+ });
5150
+ return;
5151
+ }
4816
5152
  try {
4817
5153
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
4818
5154
  const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
5155
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5156
+ throw new Error(
5157
+ `Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5158
+ );
5159
+ }
4819
5160
  const project = projectConfigById.get(message.projectId);
4820
5161
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
4821
5162
  assertRepositoryExecutionEnabled(project);
@@ -4915,9 +5256,37 @@ async function startWorker(options) {
4915
5256
  }
4916
5257
  if (message.type === "workspace_incident_updated") {
4917
5258
  const previousIncidentId = activeWorkspaceIncidentId;
4918
- activeWorkspaceIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
5259
+ const nextIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
5260
+ if (nextIncidentId !== previousIncidentId) advanceWorkerAdmissionGeneration();
5261
+ activeWorkspaceIncidentId = nextIncidentId;
4919
5262
  if (previousIncidentId && !activeWorkspaceIncidentId && (message.status === "resolved" || message.status === "confirmed" || message.status === "reset")) {
4920
- scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger ?? { type: "periodic", detail: "resume after workspace incident" }, 0);
5263
+ const refresh = workspaceIncidentTerminalRefreshFence({
5264
+ deferredConfiguration: deferredWorkspaceConfiguration,
5265
+ clearedIncidentId: previousIncidentId
5266
+ });
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();
5288
+ }
5289
+ return;
4921
5290
  }
4922
5291
  return;
4923
5292
  }
@@ -5020,6 +5389,12 @@ async function startWorker(options) {
5020
5389
  sendAck({ error: `Process run ${message.runId} is not active on this worker` });
5021
5390
  return;
5022
5391
  }
5392
+ if (!deferredWorkspaceTargetIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, active.target)) {
5393
+ sendAck({
5394
+ error: `Process input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
5395
+ });
5396
+ return;
5397
+ }
5023
5398
  if (!active.interactive || !active.stdin) {
5024
5399
  sendAck({
5025
5400
  error: `Process run ${message.runId} has no open stdin. Start a new shell command with "interactive": true to write to its stdin.`
@@ -5065,6 +5440,15 @@ async function startWorker(options) {
5065
5440
  });
5066
5441
  return;
5067
5442
  }
5443
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5444
+ sendWorkerMessage(ws, {
5445
+ type: "pty_error",
5446
+ requestId: message.requestId,
5447
+ ptyId: message.ptyId,
5448
+ error: `Shells are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5449
+ });
5450
+ return;
5451
+ }
5068
5452
  await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
5069
5453
  workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
5070
5454
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
@@ -5073,6 +5457,11 @@ async function startWorker(options) {
5073
5457
  let mutationLeaseTransferred = false;
5074
5458
  try {
5075
5459
  releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
5460
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5461
+ throw new Error(
5462
+ `Shell opening was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5463
+ );
5464
+ }
5076
5465
  const resolvedTarget = resolveMessageTarget(message.target);
5077
5466
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
5078
5467
  `);
@@ -5105,6 +5494,14 @@ async function startWorker(options) {
5105
5494
  }
5106
5495
  if (message.type === "pty_input") {
5107
5496
  const activePty = activePtys.get(message.ptyId);
5497
+ if (activePty && workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5498
+ sendWorkerMessage(ws, {
5499
+ type: "pty_error",
5500
+ ptyId: message.ptyId,
5501
+ error: `Shell input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
5502
+ });
5503
+ return;
5504
+ }
5108
5505
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
5109
5506
  markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
5110
5507
  }
@@ -5134,6 +5531,17 @@ async function startWorker(options) {
5134
5531
  });
5135
5532
  return;
5136
5533
  }
5534
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5535
+ sendWorkerMessage(ws, {
5536
+ type: "exec_result",
5537
+ requestId: message.requestId,
5538
+ stdout: "",
5539
+ stderr: "",
5540
+ exitCode: 1,
5541
+ error: `One-shot commands are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5542
+ });
5543
+ return;
5544
+ }
5137
5545
  let result;
5138
5546
  let targetReserved = false;
5139
5547
  const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
@@ -5146,6 +5554,11 @@ async function startWorker(options) {
5146
5554
  });
5147
5555
  }
5148
5556
  const runCommand = async () => {
5557
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5558
+ throw new Error(
5559
+ `One-shot command was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5560
+ );
5561
+ }
5149
5562
  const resolvedTarget = resolveMessageTarget(message.target);
5150
5563
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
5151
5564
  `);
@@ -5156,7 +5569,14 @@ async function startWorker(options) {
5156
5569
  token,
5157
5570
  artifactRoot,
5158
5571
  planRoot,
5159
- assertAdmission: assertMessageAdmission
5572
+ assertAdmission: () => {
5573
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5574
+ throw new Error(
5575
+ `One-shot command was fenced before spawn by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5576
+ );
5577
+ }
5578
+ assertMessageAdmission();
5579
+ }
5160
5580
  });
5161
5581
  };
5162
5582
  result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
@@ -5239,7 +5659,12 @@ async function startWorker(options) {
5239
5659
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
5240
5660
  });
5241
5661
  }
5242
- 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
+ });
5243
5668
  } finally {
5244
5669
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
5245
5670
  }
@@ -5256,6 +5681,14 @@ async function startWorker(options) {
5256
5681
  return;
5257
5682
  }
5258
5683
  if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read") {
5684
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5685
+ sendWorkerMessage(ws, {
5686
+ type: "operation_result",
5687
+ requestId: message.requestId,
5688
+ error: `Workspace operations are deferred for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}; use a canonical remediation shell`
5689
+ });
5690
+ return;
5691
+ }
5259
5692
  const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
5260
5693
  const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
5261
5694
  if (reservesVisibleWorkspace) {
@@ -5274,7 +5707,8 @@ async function startWorker(options) {
5274
5707
  baseUrl,
5275
5708
  token,
5276
5709
  artifactRoot,
5277
- planRoot
5710
+ planRoot,
5711
+ assertAdmission: assertMessageAdmission
5278
5712
  });
5279
5713
  });
5280
5714
  ws.send(
@@ -5304,7 +5738,7 @@ async function startWorker(options) {
5304
5738
  } finally {
5305
5739
  if (reservesVisibleWorkspace) {
5306
5740
  workspaceSyncPriorityOperationTargets.delete(message.requestId);
5307
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
5741
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
5308
5742
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, dirtyTrigger ? WORKSPACE_GIT_QUIET_MS : 0);
5309
5743
  }
5310
5744
  }
@@ -5340,6 +5774,10 @@ async function startWorker(options) {
5340
5774
  clearInterval(workspacePeriodicTimer);
5341
5775
  workspacePeriodicTimer = void 0;
5342
5776
  }
5777
+ if (deferredWorkspaceConfigurationRefreshTimer) {
5778
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5779
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
5780
+ }
5343
5781
  if (terminalReplayTimer) {
5344
5782
  clearInterval(terminalReplayTimer);
5345
5783
  terminalReplayTimer = void 0;