@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/cjs/main.cjs CHANGED
@@ -97,6 +97,7 @@ const DEFAULT_READ_MAX_BYTES = 5e4;
97
97
  const MAX_LINE_LENGTH = 2e3;
98
98
  const WORKSPACE_GIT_QUIET_MS = 5e3;
99
99
  const WORKSPACE_GIT_PERIODIC_MS = 6e4;
100
+ const WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS = 6e4;
100
101
  const PTY_INPUT_BUSY_GRACE_MS = 3e3;
101
102
  const PTY_FOREGROUND_POLL_MS = 1e3;
102
103
  const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
@@ -255,6 +256,7 @@ const workerPtyTestHarness = {
255
256
  removeEnvFiles: removePtyEnvFiles,
256
257
  resolveEnvFileReferences: resolvePtyEnvFileReferences,
257
258
  commandHasWorkspaceEffect: workerCommandHasWorkspaceEffect,
259
+ canonicalSyncTerminalHead,
258
260
  ptyIsWorkspaceBusy: workerPtyIsWorkspaceBusy,
259
261
  parseLinuxForegroundBusy: parseLinuxPtyForegroundBusy
260
262
  };
@@ -1516,6 +1518,99 @@ function credentialReapContractStatus(probe) {
1516
1518
  function verifiedCredentialReapContract(probe) {
1517
1519
  return credentialReapContractStatus(probe) === "verified_systemd";
1518
1520
  }
1521
+ function workspaceConfigurationIncidentDeferral(input) {
1522
+ const incidentId = input.requestedIncidentId ?? input.activeIncidentId;
1523
+ if (incidentId === null) return { incidentId: null };
1524
+ if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(incidentId)) {
1525
+ return { incidentId: null, error: new Error("Workspace configuration incident deferral requires a canonical incident UUID") };
1526
+ }
1527
+ if (input.requestedIncidentId !== void 0 && input.requestedIncidentId !== input.activeIncidentId) {
1528
+ return {
1529
+ incidentId: null,
1530
+ error: new Error(
1531
+ `Workspace configuration incident deferral ${input.requestedIncidentId} does not match active incident ${input.activeIncidentId ?? "none"}`
1532
+ )
1533
+ };
1534
+ }
1535
+ return { incidentId };
1536
+ }
1537
+ function workspaceIncidentTerminalClearDisposition(input) {
1538
+ if (!input.deferredConfiguration) return "await_server_refresh";
1539
+ if (input.deferredConfiguration.incidentId !== input.clearedIncidentId) return "reconnect_for_refresh";
1540
+ return input.deferredConfiguration.serverRefreshExpected ? "await_server_refresh" : "reconnect_for_refresh";
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
+ }
1552
+ function deferredWorkspaceTargetIsAllowed(deferredConfiguration, activeIncidentId, target) {
1553
+ const canonicalTarget = target.type === "workspace" && target.rootProfile === "canonical_sync";
1554
+ if (deferredConfiguration) return activeIncidentId === deferredConfiguration.incidentId && canonicalTarget;
1555
+ return activeIncidentId === null || canonicalTarget;
1556
+ }
1557
+ function deferredWorkspaceSyncTriggerIsAllowed(deferredConfiguration, activeIncidentId, trigger) {
1558
+ const incidentId = deferredConfiguration?.incidentId ?? activeIncidentId;
1559
+ return incidentId === null || (deferredConfiguration === null || activeIncidentId === deferredConfiguration.incidentId) && (trigger.type === "remediation" || trigger.type === "remediation_confirm" || trigger.type === "remediation_reset");
1560
+ }
1561
+ function workspaceOperationsAreFenced(deferredConfiguration, activeIncidentId) {
1562
+ return deferredConfiguration !== null || activeIncidentId !== null;
1563
+ }
1564
+ function workspaceCommandTransportIsAllowed(deferredConfiguration, activeIncidentId, transport) {
1565
+ return !workspaceOperationsAreFenced(deferredConfiguration, activeIncidentId) || transport === "exec_start";
1566
+ }
1567
+ function deferredCredentialTransitionMustWait(input) {
1568
+ return input.incidentId !== null && input.transitionPhase !== "current" && input.canonicalRemediationActive;
1569
+ }
1570
+ function workspaceConfigurationIncidentSnapshotIsCurrent(capturedIncidentId, activeIncidentId) {
1571
+ return capturedIncidentId === null ? activeIncidentId === null : activeIncidentId === null || activeIncidentId === capturedIncidentId;
1572
+ }
1573
+ function workspaceConfigurationResetToCanonicalIsAllowed(requested, incidentId) {
1574
+ return requested === true && incidentId === null;
1575
+ }
1576
+ function deferredWorkspaceRefreshWatchdogIsCurrent(input) {
1577
+ return input.deferredConfiguration?.incidentId === input.capturedIncidentId && input.activeIncidentId === null;
1578
+ }
1579
+ function incidentDeferredProjectCheckouts(projects) {
1580
+ return projects.flatMap(
1581
+ (project) => project.executionDisabled ? [] : project.branches.map(({ branchName }) => ({ projectId: project.projectId, branchName }))
1582
+ ).sort((left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName));
1583
+ }
1584
+ function deferredIncidentWorkspaceConfigurationResult(input) {
1585
+ return {
1586
+ type: "workspace_sync",
1587
+ attemptId: input.attemptId,
1588
+ workerLabel: input.workerLabel,
1589
+ trigger: { type: "connect", detail: "workspace synchronization deferred for active remediation" },
1590
+ outcome: "no_change",
1591
+ startingHead: input.head,
1592
+ ...input.head ? { localHead: input.head } : {},
1593
+ rebaseCount: 0,
1594
+ diffSizeBytes: 0,
1595
+ gitStatus: "",
1596
+ affectedProjects: [],
1597
+ affectedPaths: [],
1598
+ activeMountIds: [],
1599
+ skippedMountIds: [...input.skippedMountIds].sort(),
1600
+ activeProjectBranchPublications: [],
1601
+ discardedPaths: [],
1602
+ localChangesDiscarded: false
1603
+ };
1604
+ }
1605
+ function workspaceSyncFailureHydrationIsSafe(input) {
1606
+ if (input.error instanceof import_workspace_git_sync.WorkspaceRemediationAncestryError) return true;
1607
+ if (input.resetToCanonical) return false;
1608
+ try {
1609
+ return input.inspectCurrentHydration();
1610
+ } catch {
1611
+ return false;
1612
+ }
1613
+ }
1519
1614
  function fenceUnsafeWorkspaceSyncFailure(input) {
1520
1615
  if (input.hydrationCurrent) return false;
1521
1616
  input.invalidateExecution();
@@ -1548,7 +1643,24 @@ const workerGitSecurityTestHarness = {
1548
1643
  credentialReapContractStatus,
1549
1644
  verifiedCredentialReapContract,
1550
1645
  fenceUnsafeWorkspaceSyncFailure,
1646
+ workspaceConfigurationIncidentDeferral,
1647
+ workspaceIncidentTerminalClearDisposition,
1648
+ workspaceIncidentTerminalRefreshFence,
1649
+ workspaceAutomaticSyncIsAdmitted,
1650
+ deferredWorkspaceTargetIsAllowed,
1651
+ deferredWorkspaceSyncTriggerIsAllowed,
1652
+ workspaceOperationsAreFenced,
1653
+ workspaceCommandTransportIsAllowed,
1654
+ deferredCredentialTransitionMustWait,
1655
+ workspaceConfigurationIncidentSnapshotIsCurrent,
1656
+ workspaceConfigurationResetToCanonicalIsAllowed,
1657
+ deferredWorkspaceRefreshWatchdogIsCurrent,
1658
+ incidentDeferredProjectCheckouts,
1659
+ deferredIncidentWorkspaceConfigurationResult,
1660
+ workspaceSyncFailureHydrationIsSafe,
1551
1661
  assertWorkerChildAdmission,
1662
+ executeWriteFileOperation,
1663
+ executeEditFileOperation,
1552
1664
  terminateCredentialBearingChildren,
1553
1665
  terminateCredentialBearingChildrenWithRetention,
1554
1666
  async commitCredentialGeneration(prepared, credential, children, beforeMutation, previousGenerationFenced = false) {
@@ -1699,6 +1811,15 @@ function hasProjectWorktree(checkoutPath) {
1699
1811
  return false;
1700
1812
  }
1701
1813
  }
1814
+ function canonicalSyncTerminalHead(resolvedTarget, readHead = (rootPath) => runGit(["rev-parse", "HEAD"], { cwd: rootPath })) {
1815
+ if (resolvedTarget.target.type !== "workspace" || resolvedTarget.target.rootProfile !== "canonical_sync") return void 0;
1816
+ try {
1817
+ const head = readHead(resolvedTarget.rootPath);
1818
+ return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head) ? head : void 0;
1819
+ } catch {
1820
+ return void 0;
1821
+ }
1822
+ }
1702
1823
  function describeWorkerSessionTarget(target) {
1703
1824
  return target.type === "project" ? `${target.projectId}/${target.branchName}` : `${target.ownerUserId}/${target.rootProfile}`;
1704
1825
  }
@@ -2003,8 +2124,10 @@ async function executeWriteFileOperation(input) {
2003
2124
  planRoot: input.planRoot,
2004
2125
  access: "write"
2005
2126
  });
2127
+ input.assertAdmission();
2006
2128
  const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
2007
2129
  return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
2130
+ input.assertAdmission();
2008
2131
  return writeWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.content, builtInPaths);
2009
2132
  });
2010
2133
  }
@@ -2020,8 +2143,10 @@ async function executeEditFileOperation(input) {
2020
2143
  planRoot: input.planRoot,
2021
2144
  access: "write"
2022
2145
  });
2146
+ input.assertAdmission();
2023
2147
  const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
2024
2148
  return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
2149
+ input.assertAdmission();
2025
2150
  return editWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.edits, builtInPaths);
2026
2151
  });
2027
2152
  }
@@ -2597,13 +2722,15 @@ async function executeStreamingCommand(input) {
2597
2722
  });
2598
2723
  })
2599
2724
  ]);
2725
+ const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2600
2726
  const terminal = {
2601
2727
  type: "exec_exit",
2602
2728
  runId: input.message.runId,
2603
2729
  exitCode,
2604
2730
  durationMs: Date.now() - startedAt,
2605
2731
  ...timedOut ? { timedOut: true } : {},
2606
- ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2732
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2733
+ ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2607
2734
  };
2608
2735
  pendingProcessTerminals.set(input.message.runId, terminal);
2609
2736
  sendWorkerMessage(input.ws, terminal);
@@ -2611,12 +2738,14 @@ async function executeStreamingCommand(input) {
2611
2738
  if (!started && error instanceof StaleWorkerAdmissionError) throw error;
2612
2739
  const message = error instanceof Error ? error.message : String(error);
2613
2740
  if (started) {
2741
+ const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2614
2742
  const terminal = {
2615
2743
  type: "exec_error",
2616
2744
  runId: input.message.runId,
2617
2745
  error: message,
2618
2746
  durationMs: Date.now() - startedAt,
2619
- ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2747
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2748
+ ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2620
2749
  };
2621
2750
  pendingProcessTerminals.set(input.message.runId, terminal);
2622
2751
  sendWorkerMessage(input.ws, terminal);
@@ -3271,6 +3400,9 @@ async function startWorker(options) {
3271
3400
  let workspaceGitIdentity = null;
3272
3401
  let workspaceConfigured = false;
3273
3402
  let activeWorkspaceIncidentId = null;
3403
+ let deferredWorkspaceConfiguration = null;
3404
+ let deferredWorkspaceConfigurationRefreshTimer;
3405
+ let workspaceConfigurationReceiptGeneration = 0;
3274
3406
  let workspaceAutomaticTimer;
3275
3407
  let workspacePeriodicTimer;
3276
3408
  let pendingAutomaticTrigger;
@@ -3282,6 +3414,11 @@ async function startWorker(options) {
3282
3414
  let cliUpdateInProgress = false;
3283
3415
  let reloadAfterClose = false;
3284
3416
  let shutdownAfterClose = false;
3417
+ const automaticWorkspaceSyncIsCurrentlyAdmitted = () => workspaceAutomaticSyncIsAdmitted({
3418
+ workspaceConfigured,
3419
+ activeIncidentId: activeWorkspaceIncidentId,
3420
+ deferredConfiguration: deferredWorkspaceConfiguration
3421
+ });
3285
3422
  const bearerAuthHeader = `Authorization: Bearer ${token}`;
3286
3423
  const workerCredentialUsername = credentialUsernameForAuthHeader(bearerAuthHeader);
3287
3424
  const projectBranchKey = (projectId, branchName) => `${projectId}\0${branchName}`;
@@ -3563,6 +3700,17 @@ async function startWorker(options) {
3563
3700
  const activeTargets = activeWorkspaceMutationTargets();
3564
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);
3565
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
+ };
3566
3714
  const projectBranchMountBusy = (projectId, branchName, branchPath) => !readyProjectIds.has(projectId) || projectBranchMountActivityBusy(projectId, branchName, branchPath);
3567
3715
  const buildWorkspaceMounts = (projects = projectConfigById.values()) => {
3568
3716
  const mounts = [];
@@ -3595,7 +3743,7 @@ async function startWorker(options) {
3595
3743
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3596
3744
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3597
3745
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3598
- busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3746
+ busyForRecovery: () => projectBranchMountExclusiveSyncBusy(project.projectId, branch.branchName, branchPath)
3599
3747
  });
3600
3748
  mounts.push({
3601
3749
  id: projectPlanMountId(project.projectId, branch.branchName),
@@ -3609,7 +3757,7 @@ async function startWorker(options) {
3609
3757
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3610
3758
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3611
3759
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3612
- busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3760
+ busyForRecovery: () => projectBranchMountExclusiveSyncBusy(project.projectId, branch.branchName, branchPath)
3613
3761
  });
3614
3762
  }
3615
3763
  void projectRoot;
@@ -3632,7 +3780,10 @@ async function startWorker(options) {
3632
3780
  mutationToken: () => String(visibleWorkspaceMutationEpoch),
3633
3781
  busyForRecovery: () => {
3634
3782
  const activeTargets = activeWorkspaceMutationTargets();
3635
- 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
+ );
3636
3787
  }
3637
3788
  });
3638
3789
  for (const deletion of pendingMirrorDeletes.values()) {
@@ -3988,6 +4139,7 @@ async function startWorker(options) {
3988
4139
  ...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
3989
4140
  ...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
3990
4141
  ...result.conflictKind ? { conflictKind: result.conflictKind } : {},
4142
+ ...result.verifiedAncestorHeads ? { verifiedAncestorHeads: result.verifiedAncestorHeads } : {},
3991
4143
  ...result.error ? { error: result.error } : {}
3992
4144
  };
3993
4145
  };
@@ -4012,9 +4164,9 @@ async function startWorker(options) {
4012
4164
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
4013
4165
  throw new Error("Worker workspace configuration has not been received");
4014
4166
  }
4015
- const mounts = buildWorkspaceMounts();
4167
+ const configuredMounts = buildWorkspaceMounts();
4016
4168
  if (input.resetToCanonical) {
4017
- const resetHasBusyLiveMount = mounts.some(
4169
+ const resetHasBusyLiveMount = configuredMounts.some(
4018
4170
  (mount) => !(mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath)) && mount.busy?.()
4019
4171
  );
4020
4172
  if (resetHasBusyLiveMount) {
@@ -4030,7 +4182,7 @@ async function startWorker(options) {
4030
4182
  credentialHelper: workspaceCredentialHelper,
4031
4183
  credentialUsername: workspaceCredentialUsername,
4032
4184
  gitIdentity: workspaceGitIdentity,
4033
- mounts
4185
+ mounts: configuredMounts
4034
4186
  });
4035
4187
  applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
4036
4188
  return {
@@ -4054,6 +4206,7 @@ async function startWorker(options) {
4054
4206
  };
4055
4207
  }
4056
4208
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
4209
+ const mounts = outerRemediation ? (0, import_workspace_git_sync.workspaceGitMountsForRemediation)(configuredMounts) : configuredMounts;
4057
4210
  const observedProjectHeads = outerRemediation ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4058
4211
  const inboundMoveMountIds = new Set(
4059
4212
  observedProjectHeads.flatMap(
@@ -4073,10 +4226,13 @@ async function startWorker(options) {
4073
4226
  commitDetail: input.confirmationReason ?? input.trigger.detail,
4074
4227
  allowLargeDiff: input.confirmedLargeDiff,
4075
4228
  skipMountMirror: outerRemediation,
4229
+ requiredAncestorHeads: input.requiredAncestorHeads,
4230
+ assertStillAdmitted: input.assertStillAdmitted,
4076
4231
  afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
4077
4232
  pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
4078
4233
  }
4079
4234
  });
4235
+ input.assertStillAdmitted?.();
4080
4236
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
4081
4237
  applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
4082
4238
  }
@@ -4099,6 +4255,28 @@ async function startWorker(options) {
4099
4255
  const runWorkspaceSync = async (input) => {
4100
4256
  const attemptId = input.attemptId ?? crypto.randomUUID();
4101
4257
  const requestedAt = Date.now();
4258
+ const admittedGeneration = workerAdmissionGeneration;
4259
+ const admittedActiveIncidentId = activeWorkspaceIncidentId;
4260
+ const admittedDeferredIncidentId = deferredWorkspaceConfiguration?.incidentId ?? null;
4261
+ const assertStillAdmitted = () => {
4262
+ if (admittedGeneration !== workerAdmissionGeneration || admittedActiveIncidentId !== activeWorkspaceIncidentId || admittedDeferredIncidentId !== (deferredWorkspaceConfiguration?.incidentId ?? null) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4263
+ throw new Error("Workspace synchronization admission changed while asynchronous work was in flight");
4264
+ }
4265
+ };
4266
+ if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4267
+ const result2 = {
4268
+ ...failedWorkspaceSyncResult(
4269
+ attemptId,
4270
+ input.trigger,
4271
+ new Error(
4272
+ `Workspace synchronization is deferred for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}; only remediation synchronization is allowed`
4273
+ )
4274
+ ),
4275
+ telemetry: { totalMs: 0, queueMs: 0, prepareMs: 0, synchronizeMs: 0 }
4276
+ };
4277
+ if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result2);
4278
+ return result2;
4279
+ }
4102
4280
  if (!input.requestId && input.sendResult !== false) {
4103
4281
  if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4104
4282
  throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
@@ -4113,23 +4291,32 @@ async function startWorker(options) {
4113
4291
  result = await workspaceSyncSingleFlight.runExclusive(async () => {
4114
4292
  queueEnteredAt = Date.now();
4115
4293
  syncStartedAt = Date.now();
4294
+ if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4295
+ return failedWorkspaceSyncResult(
4296
+ attemptId,
4297
+ input.trigger,
4298
+ new Error(
4299
+ `Workspace synchronization was fenced while queued by incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
4300
+ )
4301
+ );
4302
+ }
4116
4303
  try {
4304
+ assertStillAdmitted();
4117
4305
  return await performWorkspaceSync({
4118
4306
  attemptId,
4119
4307
  trigger: input.trigger,
4120
4308
  confirmedLargeDiff: input.confirmedLargeDiff,
4121
4309
  confirmationReason: input.confirmationReason,
4122
- resetToCanonical: input.resetToCanonical
4310
+ resetToCanonical: input.resetToCanonical,
4311
+ requiredAncestorHeads: input.requiredAncestorHeads,
4312
+ assertStillAdmitted
4123
4313
  });
4124
4314
  } catch (error) {
4125
- let hydrationCurrent = input.resetToCanonical !== true;
4126
- if (hydrationCurrent) {
4127
- try {
4128
- hydrationCurrent = (0, import_workspace_git_sync.workspaceGitHydrationIsCurrent)(workspaceShadowRoot, buildWorkspaceMounts());
4129
- } catch {
4130
- hydrationCurrent = false;
4131
- }
4132
- }
4315
+ const hydrationCurrent = workspaceSyncFailureHydrationIsSafe({
4316
+ error,
4317
+ resetToCanonical: input.resetToCanonical === true,
4318
+ inspectCurrentHydration: () => (0, import_workspace_git_sync.workspaceGitHydrationIsCurrent)(workspaceShadowRoot, buildWorkspaceMounts())
4319
+ });
4133
4320
  if (!hydrationCurrent) {
4134
4321
  process.stderr.write(
4135
4322
  `[r5d-worker] workspace synchronization failed with an incompletely hydrated visible tree; exiting for exact recovery: ${error instanceof Error ? error.message : String(error)}
@@ -4171,7 +4358,7 @@ async function startWorker(options) {
4171
4358
  pendingCreatedBranchPublicationNotBefore
4172
4359
  );
4173
4360
  const initialDeferral = pendingBranchDeferral();
4174
- if (!workspaceConfigured || activeWorkspaceIncidentId || 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") {
4175
4362
  return;
4176
4363
  }
4177
4364
  if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
@@ -4179,7 +4366,7 @@ async function startWorker(options) {
4179
4366
  () => {
4180
4367
  workspaceAutomaticTimer = void 0;
4181
4368
  const scheduledTrigger = pendingAutomaticTrigger;
4182
- if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
4369
+ if (!scheduledTrigger || !automaticWorkspaceSyncIsCurrentlyAdmitted() || currentWorkerSocket !== ws) return;
4183
4370
  const currentDeferral = pendingBranchDeferral();
4184
4371
  if (currentDeferral?.kind === "active_target") return;
4185
4372
  if (currentDeferral?.kind === "creation_grace") {
@@ -4201,7 +4388,7 @@ async function startWorker(options) {
4201
4388
  }).finally(() => {
4202
4389
  automaticSyncInFlight = false;
4203
4390
  heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
4204
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
4391
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4205
4392
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
4206
4393
  }
4207
4394
  });
@@ -4217,6 +4404,11 @@ async function startWorker(options) {
4217
4404
  const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
4218
4405
  const resolveMessageTarget = (target) => {
4219
4406
  if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
4407
+ if (!deferredWorkspaceTargetIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, target)) {
4408
+ throw new Error(
4409
+ `Worker workspace configuration is fenced for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}; only canonical remediation commands are allowed`
4410
+ );
4411
+ }
4220
4412
  if (target.type === "project" && !readyProjectIds.has(target.projectId)) {
4221
4413
  throw new Error(`Project ${target.projectId} is not ready on this worker`);
4222
4414
  }
@@ -4230,7 +4422,26 @@ async function startWorker(options) {
4230
4422
  projectConfigById
4231
4423
  });
4232
4424
  };
4233
- const configureWorkerWorkspace = async (message) => {
4425
+ const configureWorkerWorkspace = async (message, receiptGeneration) => {
4426
+ const incidentDeferral = workspaceConfigurationIncidentDeferral({
4427
+ requestedIncidentId: message.deferWorkspaceSyncForIncidentId,
4428
+ activeIncidentId: activeWorkspaceIncidentId
4429
+ });
4430
+ if (incidentDeferral.error) {
4431
+ workspaceConfigured = false;
4432
+ return {
4433
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, incidentDeferral.error),
4434
+ pending: incidentDeferredProjectCheckouts(message.projects),
4435
+ aheadOfOriginBranches: []
4436
+ };
4437
+ }
4438
+ if (incidentDeferral.incidentId) {
4439
+ workspaceConfigured = false;
4440
+ deferredWorkspaceConfiguration = {
4441
+ incidentId: incidentDeferral.incidentId,
4442
+ serverRefreshExpected: message.deferWorkspaceSyncForIncidentId === incidentDeferral.incidentId
4443
+ };
4444
+ }
4234
4445
  const incomingCredentialGenerationFingerprint = credentialGenerationFingerprint({
4235
4446
  ...message,
4236
4447
  workerBaseUrl: baseUrl,
@@ -4241,6 +4452,22 @@ async function startWorker(options) {
4241
4452
  pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
4242
4453
  incomingFingerprint: incomingCredentialGenerationFingerprint
4243
4454
  });
4455
+ if (deferredCredentialTransitionMustWait({
4456
+ incidentId: incidentDeferral.incidentId,
4457
+ transitionPhase: credentialTransitionPhase,
4458
+ canonicalRemediationActive: canonicalWorkspaceMutationIsActive(activeWorkspaceMutationTargets())
4459
+ })) {
4460
+ return {
4461
+ result: failedWorkspaceSyncResult(
4462
+ crypto.randomUUID(),
4463
+ { type: "connect" },
4464
+ new Error("Workspace credential rotation is deferred until the active canonical remediation command finishes")
4465
+ ),
4466
+ pending: incidentDeferredProjectCheckouts(message.projects),
4467
+ aheadOfOriginBranches: [],
4468
+ ...incidentDeferral.incidentId ? { deferredWorkspaceSyncForIncidentId: incidentDeferral.incidentId } : {}
4469
+ };
4470
+ }
4244
4471
  const credentialReapStatus = credentialReapContractStatus();
4245
4472
  const verifiedCredentialReapContractNow = credentialReapStatus === "verified_systemd";
4246
4473
  const credentialRestartIsContained = credentialGenerationRestartIsContained({
@@ -4354,6 +4581,12 @@ async function startWorker(options) {
4354
4581
  workspaceSyncRequestsInFlight += 1;
4355
4582
  try {
4356
4583
  return await workspaceSyncSingleFlight.runExclusive(async () => {
4584
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4585
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4586
+ }
4587
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4588
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4589
+ }
4357
4590
  for (const project of message.projects) (0, import_repository_transition_policy.assertRepositoryTransitionState)(project);
4358
4591
  const busyConfigurationChanges = (0, import_workspace_project_config_policy.busyProjectConfigurationChangeIds)({
4359
4592
  currentProjects: [...projectConfigById.values()],
@@ -4375,10 +4608,12 @@ async function startWorker(options) {
4375
4608
  const preserveOnlyBranches = message.projects.flatMap(
4376
4609
  (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
4377
4610
  );
4378
- projectWorkspaceState = projectWorkspaceStateStore.reconcile({
4379
- desiredProjects: message.projects,
4380
- preserveOnlyBranches
4381
- });
4611
+ if (!incidentDeferral.incidentId) {
4612
+ projectWorkspaceState = projectWorkspaceStateStore.reconcile({
4613
+ desiredProjects: message.projects,
4614
+ preserveOnlyBranches
4615
+ });
4616
+ }
4382
4617
  const stillPendingCreatedBranches = new Map(
4383
4618
  projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
4384
4619
  (0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(branch.projectId, branch.branchName),
@@ -4438,6 +4673,12 @@ async function startWorker(options) {
4438
4673
  void 0,
4439
4674
  credentialPublicationPreauthorized
4440
4675
  );
4676
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4677
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4678
+ }
4679
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4680
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4681
+ }
4441
4682
  configuredCredentialGenerationFingerprint = incomingCredentialGenerationFingerprint;
4442
4683
  pendingCredentialGenerationIntentAtProcessStart = null;
4443
4684
  bootstrapCredentialGenerationFingerprintAtProcessStart = null;
@@ -4452,6 +4693,34 @@ async function startWorker(options) {
4452
4693
  branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
4453
4694
  }));
4454
4695
  const nextProjectConfigById = new Map(effectiveProjects.map((project) => [project.projectId, project]));
4696
+ if (incidentDeferral.incidentId) {
4697
+ (0, import_workspace_git_sync.configureExistingWorkspaceGitForRemediation)({
4698
+ workspacePath: workspaceShadowRoot,
4699
+ remoteUrl: message.workspaceRemoteUrl,
4700
+ credentialHelper: nextWorkspaceCredentialHelper,
4701
+ credentialUsername: workerCredentialUsername,
4702
+ gitIdentity: message.gitIdentity
4703
+ });
4704
+ projectConfigById.clear();
4705
+ for (const project of effectiveProjects) projectConfigById.set(project.projectId, project);
4706
+ readyProjectIds.clear();
4707
+ reconciledProjectConfigFingerprints.clear();
4708
+ pendingCheckouts.clear();
4709
+ const pending2 = incidentDeferredProjectCheckouts(message.projects);
4710
+ for (const checkout of pending2) pendingCheckouts.set(projectBranchKey(checkout.projectId, checkout.branchName), checkout);
4711
+ workspaceConfigured = activeWorkspaceIncidentId === incidentDeferral.incidentId;
4712
+ return {
4713
+ result: deferredIncidentWorkspaceConfigurationResult({
4714
+ attemptId: crypto.randomUUID(),
4715
+ workerLabel: label,
4716
+ head: workspaceLocalHead(),
4717
+ skippedMountIds: buildWorkspaceMounts().map(({ id }) => id)
4718
+ }),
4719
+ pending: pending2,
4720
+ aheadOfOriginBranches: [],
4721
+ deferredWorkspaceSyncForIncidentId: incidentDeferral.incidentId
4722
+ };
4723
+ }
4455
4724
  (0, import_workspace_git_sync.ensureWorkspaceGitClone)({
4456
4725
  workspacePath: workspaceShadowRoot,
4457
4726
  remoteUrl: message.workspaceRemoteUrl,
@@ -4519,10 +4788,25 @@ async function startWorker(options) {
4519
4788
  { ignoreBusy: true }
4520
4789
  );
4521
4790
  ensureConfiguredProjects();
4791
+ const configurationSyncAdmissionGeneration = workerAdmissionGeneration;
4792
+ const assertConfigurationSyncStillAdmitted = () => {
4793
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration || configurationSyncAdmissionGeneration !== workerAdmissionGeneration || !workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4794
+ throw new Error("Workspace configuration synchronization admission changed while asynchronous work was in flight");
4795
+ }
4796
+ };
4522
4797
  const result = await performWorkspaceSync({
4523
4798
  attemptId: crypto.randomUUID(),
4524
- trigger: { type: "connect" }
4799
+ trigger: { type: "connect" },
4800
+ resetToCanonical: workspaceConfigurationResetToCanonicalIsAllowed(message.resetToCanonical, incidentDeferral.incidentId),
4801
+ assertStillAdmitted: assertConfigurationSyncStillAdmitted
4525
4802
  });
4803
+ assertConfigurationSyncStillAdmitted();
4804
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4805
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4806
+ }
4807
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4808
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4809
+ }
4526
4810
  const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
4527
4811
  if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
4528
4812
  const activeMountIds = new Set(result.activeMountIds ?? []);
@@ -4539,6 +4823,11 @@ async function startWorker(options) {
4539
4823
  });
4540
4824
  }
4541
4825
  }
4826
+ if (deferredWorkspaceConfigurationRefreshTimer) {
4827
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
4828
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
4829
+ }
4830
+ deferredWorkspaceConfiguration = null;
4542
4831
  workspaceConfigured = true;
4543
4832
  const pending = [...pendingCheckouts.values()].sort(
4544
4833
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
@@ -4546,6 +4835,13 @@ async function startWorker(options) {
4546
4835
  return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
4547
4836
  });
4548
4837
  } catch (error) {
4838
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4839
+ return {
4840
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
4841
+ pending: incidentDeferredProjectCheckouts(message.projects),
4842
+ aheadOfOriginBranches: []
4843
+ };
4844
+ }
4549
4845
  if (error instanceof import_registry_auth.RegistryAuthConfigurationError) {
4550
4846
  workspaceConfigured = false;
4551
4847
  githubCredential = null;
@@ -4600,7 +4896,7 @@ async function startWorker(options) {
4600
4896
  workspaceAutomaticTimer = void 0;
4601
4897
  }
4602
4898
  void (async () => {
4603
- if (workspaceConfigured && !activeWorkspaceIncidentId) {
4899
+ if (automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4604
4900
  await runWorkspaceSync({
4605
4901
  trigger: { type: "manual", detail: "graceful worker shutdown" }
4606
4902
  });
@@ -4649,7 +4945,10 @@ async function startWorker(options) {
4649
4945
  updateClis: true,
4650
4946
  browserPortForwarding: true,
4651
4947
  execStdinV1: true,
4652
- ptyEnvFilesV1: true
4948
+ ptyEnvFilesV1: true,
4949
+ workspaceRemediationAncestorGuardV1: true,
4950
+ workspaceIncidentConfigDeferralV1: true,
4951
+ workspaceConfigResetToCanonicalV1: true
4653
4952
  },
4654
4953
  projectRoot: projectsRoot,
4655
4954
  artifactRoot,
@@ -4685,26 +4984,35 @@ async function startWorker(options) {
4685
4984
  return;
4686
4985
  }
4687
4986
  if (message.type === "workspace_config") {
4987
+ const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
4988
+ const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
4688
4989
  advanceWorkerAdmissionGeneration();
4689
- const configured = await configureWorkerWorkspace(message);
4990
+ const configured = await configureWorkerWorkspace(message, receiptGeneration);
4690
4991
  sendWorkerMessageFromCurrentSource(ws, {
4691
4992
  type: "workspace_configured",
4692
4993
  requestId: message.requestId,
4693
4994
  result: configured.result,
4694
4995
  pendingCheckouts: configured.pending,
4695
- aheadOfOriginBranches: configured.aheadOfOriginBranches
4996
+ aheadOfOriginBranches: configured.aheadOfOriginBranches,
4997
+ ...configured.deferredWorkspaceSyncForIncidentId ? { deferredWorkspaceSyncForIncidentId: configured.deferredWorkspaceSyncForIncidentId } : {}
4696
4998
  });
4697
4999
  process.stdout.write(
4698
5000
  `[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
4699
5001
  `
4700
5002
  );
5003
+ if (refreshesDeferredConfiguration && configured.result.outcome === "failed" && deferredWorkspaceConfiguration !== null && currentWorkerSocket === ws) {
5004
+ workspaceConfigured = false;
5005
+ advanceWorkerAdmissionGeneration();
5006
+ ws.close(1012, "Deferred workspace configuration refresh failed");
5007
+ return;
5008
+ }
4701
5009
  if (!workspacePeriodicTimer) {
4702
5010
  workspacePeriodicTimer = setInterval(() => {
4703
5011
  scheduleAutomaticWorkspaceSync({ type: "periodic", detail: "periodic workspace reconciliation" }, 0);
4704
5012
  }, WORKSPACE_GIT_PERIODIC_MS);
4705
5013
  workspacePeriodicTimer.unref();
4706
5014
  }
4707
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
5015
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
4708
5016
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, 0);
4709
5017
  }
4710
5018
  return;
@@ -4716,11 +5024,20 @@ async function startWorker(options) {
4716
5024
  trigger: message.trigger,
4717
5025
  confirmedLargeDiff: message.confirmedLargeDiff,
4718
5026
  confirmationReason: message.confirmationReason,
4719
- resetToCanonical: message.resetToCanonical
5027
+ resetToCanonical: message.resetToCanonical,
5028
+ requiredAncestorHeads: message.requiredAncestorHeads
4720
5029
  });
4721
5030
  return;
4722
5031
  }
4723
5032
  if (message.type === "create_project_branch") {
5033
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5034
+ sendWorkerMessage(ws, {
5035
+ type: "operation_result",
5036
+ requestId: message.requestId,
5037
+ error: `Project operations are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5038
+ });
5039
+ return;
5040
+ }
4724
5041
  try {
4725
5042
  const pendingBranch = {
4726
5043
  branchId: message.branchId,
@@ -4728,6 +5045,11 @@ async function startWorker(options) {
4728
5045
  branchName: message.targetBranch
4729
5046
  };
4730
5047
  const created = await workspaceSyncSingleFlight.runMutation(() => {
5048
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5049
+ throw new Error(
5050
+ `Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5051
+ );
5052
+ }
4731
5053
  const project = projectConfigById.get(message.projectId);
4732
5054
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
4733
5055
  (0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(project);
@@ -4811,9 +5133,22 @@ async function startWorker(options) {
4811
5133
  return;
4812
5134
  }
4813
5135
  if (message.type === "delete_project_branch") {
5136
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5137
+ sendWorkerMessage(ws, {
5138
+ type: "operation_result",
5139
+ requestId: message.requestId,
5140
+ error: `Project operations are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5141
+ });
5142
+ return;
5143
+ }
4814
5144
  try {
4815
5145
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
4816
5146
  const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
5147
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5148
+ throw new Error(
5149
+ `Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5150
+ );
5151
+ }
4817
5152
  const project = projectConfigById.get(message.projectId);
4818
5153
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
4819
5154
  (0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(project);
@@ -4913,9 +5248,37 @@ async function startWorker(options) {
4913
5248
  }
4914
5249
  if (message.type === "workspace_incident_updated") {
4915
5250
  const previousIncidentId = activeWorkspaceIncidentId;
4916
- activeWorkspaceIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
5251
+ const nextIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
5252
+ if (nextIncidentId !== previousIncidentId) advanceWorkerAdmissionGeneration();
5253
+ activeWorkspaceIncidentId = nextIncidentId;
4917
5254
  if (previousIncidentId && !activeWorkspaceIncidentId && (message.status === "resolved" || message.status === "confirmed" || message.status === "reset")) {
4918
- scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger ?? { type: "periodic", detail: "resume after workspace incident" }, 0);
5255
+ const refresh = workspaceIncidentTerminalRefreshFence({
5256
+ deferredConfiguration: deferredWorkspaceConfiguration,
5257
+ clearedIncidentId: previousIncidentId
5258
+ });
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();
5280
+ }
5281
+ return;
4919
5282
  }
4920
5283
  return;
4921
5284
  }
@@ -5018,6 +5381,12 @@ async function startWorker(options) {
5018
5381
  sendAck({ error: `Process run ${message.runId} is not active on this worker` });
5019
5382
  return;
5020
5383
  }
5384
+ if (!deferredWorkspaceTargetIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, active.target)) {
5385
+ sendAck({
5386
+ error: `Process input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
5387
+ });
5388
+ return;
5389
+ }
5021
5390
  if (!active.interactive || !active.stdin) {
5022
5391
  sendAck({
5023
5392
  error: `Process run ${message.runId} has no open stdin. Start a new shell command with "interactive": true to write to its stdin.`
@@ -5063,6 +5432,15 @@ async function startWorker(options) {
5063
5432
  });
5064
5433
  return;
5065
5434
  }
5435
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5436
+ sendWorkerMessage(ws, {
5437
+ type: "pty_error",
5438
+ requestId: message.requestId,
5439
+ ptyId: message.ptyId,
5440
+ error: `Shells are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5441
+ });
5442
+ return;
5443
+ }
5066
5444
  await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
5067
5445
  workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
5068
5446
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
@@ -5071,6 +5449,11 @@ async function startWorker(options) {
5071
5449
  let mutationLeaseTransferred = false;
5072
5450
  try {
5073
5451
  releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
5452
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5453
+ throw new Error(
5454
+ `Shell opening was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5455
+ );
5456
+ }
5074
5457
  const resolvedTarget = resolveMessageTarget(message.target);
5075
5458
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
5076
5459
  `);
@@ -5103,6 +5486,14 @@ async function startWorker(options) {
5103
5486
  }
5104
5487
  if (message.type === "pty_input") {
5105
5488
  const activePty = activePtys.get(message.ptyId);
5489
+ if (activePty && workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5490
+ sendWorkerMessage(ws, {
5491
+ type: "pty_error",
5492
+ ptyId: message.ptyId,
5493
+ error: `Shell input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
5494
+ });
5495
+ return;
5496
+ }
5106
5497
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
5107
5498
  markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
5108
5499
  }
@@ -5132,6 +5523,17 @@ async function startWorker(options) {
5132
5523
  });
5133
5524
  return;
5134
5525
  }
5526
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5527
+ sendWorkerMessage(ws, {
5528
+ type: "exec_result",
5529
+ requestId: message.requestId,
5530
+ stdout: "",
5531
+ stderr: "",
5532
+ exitCode: 1,
5533
+ error: `One-shot commands are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5534
+ });
5535
+ return;
5536
+ }
5135
5537
  let result;
5136
5538
  let targetReserved = false;
5137
5539
  const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
@@ -5144,6 +5546,11 @@ async function startWorker(options) {
5144
5546
  });
5145
5547
  }
5146
5548
  const runCommand = async () => {
5549
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5550
+ throw new Error(
5551
+ `One-shot command was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5552
+ );
5553
+ }
5147
5554
  const resolvedTarget = resolveMessageTarget(message.target);
5148
5555
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
5149
5556
  `);
@@ -5154,7 +5561,14 @@ async function startWorker(options) {
5154
5561
  token,
5155
5562
  artifactRoot,
5156
5563
  planRoot,
5157
- assertAdmission: assertMessageAdmission
5564
+ assertAdmission: () => {
5565
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5566
+ throw new Error(
5567
+ `One-shot command was fenced before spawn by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5568
+ );
5569
+ }
5570
+ assertMessageAdmission();
5571
+ }
5158
5572
  });
5159
5573
  };
5160
5574
  result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
@@ -5237,7 +5651,12 @@ async function startWorker(options) {
5237
5651
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
5238
5652
  });
5239
5653
  }
5240
- 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
+ });
5241
5660
  } finally {
5242
5661
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
5243
5662
  }
@@ -5254,6 +5673,14 @@ async function startWorker(options) {
5254
5673
  return;
5255
5674
  }
5256
5675
  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") {
5676
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5677
+ sendWorkerMessage(ws, {
5678
+ type: "operation_result",
5679
+ requestId: message.requestId,
5680
+ error: `Workspace operations are deferred for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}; use a canonical remediation shell`
5681
+ });
5682
+ return;
5683
+ }
5257
5684
  const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
5258
5685
  const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
5259
5686
  if (reservesVisibleWorkspace) {
@@ -5272,7 +5699,8 @@ async function startWorker(options) {
5272
5699
  baseUrl,
5273
5700
  token,
5274
5701
  artifactRoot,
5275
- planRoot
5702
+ planRoot,
5703
+ assertAdmission: assertMessageAdmission
5276
5704
  });
5277
5705
  });
5278
5706
  ws.send(
@@ -5302,7 +5730,7 @@ async function startWorker(options) {
5302
5730
  } finally {
5303
5731
  if (reservesVisibleWorkspace) {
5304
5732
  workspaceSyncPriorityOperationTargets.delete(message.requestId);
5305
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
5733
+ if (pendingAutomaticTrigger && automaticWorkspaceSyncIsCurrentlyAdmitted()) {
5306
5734
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, dirtyTrigger ? WORKSPACE_GIT_QUIET_MS : 0);
5307
5735
  }
5308
5736
  }
@@ -5338,6 +5766,10 @@ async function startWorker(options) {
5338
5766
  clearInterval(workspacePeriodicTimer);
5339
5767
  workspacePeriodicTimer = void 0;
5340
5768
  }
5769
+ if (deferredWorkspaceConfigurationRefreshTimer) {
5770
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5771
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
5772
+ }
5341
5773
  if (terminalReplayTimer) {
5342
5774
  clearInterval(terminalReplayTimer);
5343
5775
  terminalReplayTimer = void 0;