@ricsam/r5d-worker 0.0.82 → 0.0.83

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,86 @@ 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 "ordinary_resume";
1539
+ void input.clearedIncidentId;
1540
+ return input.deferredConfiguration.serverRefreshExpected ? "await_server_refresh" : "reconnect_for_refresh";
1541
+ }
1542
+ function deferredWorkspaceTargetIsAllowed(deferredConfiguration, activeIncidentId, target) {
1543
+ const canonicalTarget = target.type === "workspace" && target.rootProfile === "canonical_sync";
1544
+ if (deferredConfiguration) return activeIncidentId === deferredConfiguration.incidentId && canonicalTarget;
1545
+ return activeIncidentId === null || canonicalTarget;
1546
+ }
1547
+ function deferredWorkspaceSyncTriggerIsAllowed(deferredConfiguration, activeIncidentId, trigger) {
1548
+ const incidentId = deferredConfiguration?.incidentId ?? activeIncidentId;
1549
+ return incidentId === null || (deferredConfiguration === null || activeIncidentId === deferredConfiguration.incidentId) && (trigger.type === "remediation" || trigger.type === "remediation_confirm" || trigger.type === "remediation_reset");
1550
+ }
1551
+ function workspaceOperationsAreFenced(deferredConfiguration, activeIncidentId) {
1552
+ return deferredConfiguration !== null || activeIncidentId !== null;
1553
+ }
1554
+ function workspaceCommandTransportIsAllowed(deferredConfiguration, activeIncidentId, transport) {
1555
+ return !workspaceOperationsAreFenced(deferredConfiguration, activeIncidentId) || transport === "exec_start";
1556
+ }
1557
+ function deferredCredentialTransitionMustWait(input) {
1558
+ return input.incidentId !== null && input.transitionPhase !== "current" && input.canonicalRemediationActive;
1559
+ }
1560
+ function workspaceConfigurationIncidentSnapshotIsCurrent(capturedIncidentId, activeIncidentId) {
1561
+ return capturedIncidentId === null ? activeIncidentId === null : activeIncidentId === null || activeIncidentId === capturedIncidentId;
1562
+ }
1563
+ function deferredWorkspaceRefreshWatchdogIsCurrent(input) {
1564
+ return input.deferredConfiguration?.incidentId === input.capturedIncidentId && input.activeIncidentId === null;
1565
+ }
1566
+ function incidentDeferredProjectCheckouts(projects) {
1567
+ return projects.flatMap(
1568
+ (project) => project.executionDisabled ? [] : project.branches.map(({ branchName }) => ({ projectId: project.projectId, branchName }))
1569
+ ).sort((left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName));
1570
+ }
1571
+ function deferredIncidentWorkspaceConfigurationResult(input) {
1572
+ return {
1573
+ type: "workspace_sync",
1574
+ attemptId: input.attemptId,
1575
+ workerLabel: input.workerLabel,
1576
+ trigger: { type: "connect", detail: "workspace synchronization deferred for active remediation" },
1577
+ outcome: "no_change",
1578
+ startingHead: input.head,
1579
+ ...input.head ? { localHead: input.head } : {},
1580
+ rebaseCount: 0,
1581
+ diffSizeBytes: 0,
1582
+ gitStatus: "",
1583
+ affectedProjects: [],
1584
+ affectedPaths: [],
1585
+ activeMountIds: [],
1586
+ skippedMountIds: [...input.skippedMountIds].sort(),
1587
+ activeProjectBranchPublications: [],
1588
+ discardedPaths: [],
1589
+ localChangesDiscarded: false
1590
+ };
1591
+ }
1592
+ function workspaceSyncFailureHydrationIsSafe(input) {
1593
+ if (input.error instanceof import_workspace_git_sync.WorkspaceRemediationAncestryError) return true;
1594
+ if (input.resetToCanonical) return false;
1595
+ try {
1596
+ return input.inspectCurrentHydration();
1597
+ } catch {
1598
+ return false;
1599
+ }
1600
+ }
1519
1601
  function fenceUnsafeWorkspaceSyncFailure(input) {
1520
1602
  if (input.hydrationCurrent) return false;
1521
1603
  input.invalidateExecution();
@@ -1548,7 +1630,21 @@ const workerGitSecurityTestHarness = {
1548
1630
  credentialReapContractStatus,
1549
1631
  verifiedCredentialReapContract,
1550
1632
  fenceUnsafeWorkspaceSyncFailure,
1633
+ workspaceConfigurationIncidentDeferral,
1634
+ workspaceIncidentTerminalClearDisposition,
1635
+ deferredWorkspaceTargetIsAllowed,
1636
+ deferredWorkspaceSyncTriggerIsAllowed,
1637
+ workspaceOperationsAreFenced,
1638
+ workspaceCommandTransportIsAllowed,
1639
+ deferredCredentialTransitionMustWait,
1640
+ workspaceConfigurationIncidentSnapshotIsCurrent,
1641
+ deferredWorkspaceRefreshWatchdogIsCurrent,
1642
+ incidentDeferredProjectCheckouts,
1643
+ deferredIncidentWorkspaceConfigurationResult,
1644
+ workspaceSyncFailureHydrationIsSafe,
1551
1645
  assertWorkerChildAdmission,
1646
+ executeWriteFileOperation,
1647
+ executeEditFileOperation,
1552
1648
  terminateCredentialBearingChildren,
1553
1649
  terminateCredentialBearingChildrenWithRetention,
1554
1650
  async commitCredentialGeneration(prepared, credential, children, beforeMutation, previousGenerationFenced = false) {
@@ -1699,6 +1795,15 @@ function hasProjectWorktree(checkoutPath) {
1699
1795
  return false;
1700
1796
  }
1701
1797
  }
1798
+ function canonicalSyncTerminalHead(resolvedTarget, readHead = (rootPath) => runGit(["rev-parse", "HEAD"], { cwd: rootPath })) {
1799
+ if (resolvedTarget.target.type !== "workspace" || resolvedTarget.target.rootProfile !== "canonical_sync") return void 0;
1800
+ try {
1801
+ const head = readHead(resolvedTarget.rootPath);
1802
+ return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head) ? head : void 0;
1803
+ } catch {
1804
+ return void 0;
1805
+ }
1806
+ }
1702
1807
  function describeWorkerSessionTarget(target) {
1703
1808
  return target.type === "project" ? `${target.projectId}/${target.branchName}` : `${target.ownerUserId}/${target.rootProfile}`;
1704
1809
  }
@@ -2003,8 +2108,10 @@ async function executeWriteFileOperation(input) {
2003
2108
  planRoot: input.planRoot,
2004
2109
  access: "write"
2005
2110
  });
2111
+ input.assertAdmission();
2006
2112
  const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
2007
2113
  return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
2114
+ input.assertAdmission();
2008
2115
  return writeWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.content, builtInPaths);
2009
2116
  });
2010
2117
  }
@@ -2020,8 +2127,10 @@ async function executeEditFileOperation(input) {
2020
2127
  planRoot: input.planRoot,
2021
2128
  access: "write"
2022
2129
  });
2130
+ input.assertAdmission();
2023
2131
  const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
2024
2132
  return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
2133
+ input.assertAdmission();
2025
2134
  return editWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.edits, builtInPaths);
2026
2135
  });
2027
2136
  }
@@ -2597,13 +2706,15 @@ async function executeStreamingCommand(input) {
2597
2706
  });
2598
2707
  })
2599
2708
  ]);
2709
+ const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2600
2710
  const terminal = {
2601
2711
  type: "exec_exit",
2602
2712
  runId: input.message.runId,
2603
2713
  exitCode,
2604
2714
  durationMs: Date.now() - startedAt,
2605
2715
  ...timedOut ? { timedOut: true } : {},
2606
- ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2716
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2717
+ ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2607
2718
  };
2608
2719
  pendingProcessTerminals.set(input.message.runId, terminal);
2609
2720
  sendWorkerMessage(input.ws, terminal);
@@ -2611,12 +2722,14 @@ async function executeStreamingCommand(input) {
2611
2722
  if (!started && error instanceof StaleWorkerAdmissionError) throw error;
2612
2723
  const message = error instanceof Error ? error.message : String(error);
2613
2724
  if (started) {
2725
+ const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2614
2726
  const terminal = {
2615
2727
  type: "exec_error",
2616
2728
  runId: input.message.runId,
2617
2729
  error: message,
2618
2730
  durationMs: Date.now() - startedAt,
2619
- ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2731
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2732
+ ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2620
2733
  };
2621
2734
  pendingProcessTerminals.set(input.message.runId, terminal);
2622
2735
  sendWorkerMessage(input.ws, terminal);
@@ -3271,6 +3384,9 @@ async function startWorker(options) {
3271
3384
  let workspaceGitIdentity = null;
3272
3385
  let workspaceConfigured = false;
3273
3386
  let activeWorkspaceIncidentId = null;
3387
+ let deferredWorkspaceConfiguration = null;
3388
+ let deferredWorkspaceConfigurationRefreshTimer;
3389
+ let workspaceConfigurationReceiptGeneration = 0;
3274
3390
  let workspaceAutomaticTimer;
3275
3391
  let workspacePeriodicTimer;
3276
3392
  let pendingAutomaticTrigger;
@@ -3988,6 +4104,7 @@ async function startWorker(options) {
3988
4104
  ...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
3989
4105
  ...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
3990
4106
  ...result.conflictKind ? { conflictKind: result.conflictKind } : {},
4107
+ ...result.verifiedAncestorHeads ? { verifiedAncestorHeads: result.verifiedAncestorHeads } : {},
3991
4108
  ...result.error ? { error: result.error } : {}
3992
4109
  };
3993
4110
  };
@@ -4073,10 +4190,13 @@ async function startWorker(options) {
4073
4190
  commitDetail: input.confirmationReason ?? input.trigger.detail,
4074
4191
  allowLargeDiff: input.confirmedLargeDiff,
4075
4192
  skipMountMirror: outerRemediation,
4193
+ requiredAncestorHeads: input.requiredAncestorHeads,
4194
+ assertStillAdmitted: input.assertStillAdmitted,
4076
4195
  afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
4077
4196
  pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
4078
4197
  }
4079
4198
  });
4199
+ input.assertStillAdmitted?.();
4080
4200
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
4081
4201
  applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
4082
4202
  }
@@ -4099,6 +4219,28 @@ async function startWorker(options) {
4099
4219
  const runWorkspaceSync = async (input) => {
4100
4220
  const attemptId = input.attemptId ?? crypto.randomUUID();
4101
4221
  const requestedAt = Date.now();
4222
+ const admittedGeneration = workerAdmissionGeneration;
4223
+ const admittedActiveIncidentId = activeWorkspaceIncidentId;
4224
+ const admittedDeferredIncidentId = deferredWorkspaceConfiguration?.incidentId ?? null;
4225
+ const assertStillAdmitted = () => {
4226
+ if (admittedGeneration !== workerAdmissionGeneration || admittedActiveIncidentId !== activeWorkspaceIncidentId || admittedDeferredIncidentId !== (deferredWorkspaceConfiguration?.incidentId ?? null) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4227
+ throw new Error("Workspace synchronization admission changed while asynchronous work was in flight");
4228
+ }
4229
+ };
4230
+ if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4231
+ const result2 = {
4232
+ ...failedWorkspaceSyncResult(
4233
+ attemptId,
4234
+ input.trigger,
4235
+ new Error(
4236
+ `Workspace synchronization is deferred for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}; only remediation synchronization is allowed`
4237
+ )
4238
+ ),
4239
+ telemetry: { totalMs: 0, queueMs: 0, prepareMs: 0, synchronizeMs: 0 }
4240
+ };
4241
+ if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result2);
4242
+ return result2;
4243
+ }
4102
4244
  if (!input.requestId && input.sendResult !== false) {
4103
4245
  if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4104
4246
  throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
@@ -4113,23 +4255,32 @@ async function startWorker(options) {
4113
4255
  result = await workspaceSyncSingleFlight.runExclusive(async () => {
4114
4256
  queueEnteredAt = Date.now();
4115
4257
  syncStartedAt = Date.now();
4258
+ if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4259
+ return failedWorkspaceSyncResult(
4260
+ attemptId,
4261
+ input.trigger,
4262
+ new Error(
4263
+ `Workspace synchronization was fenced while queued by incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
4264
+ )
4265
+ );
4266
+ }
4116
4267
  try {
4268
+ assertStillAdmitted();
4117
4269
  return await performWorkspaceSync({
4118
4270
  attemptId,
4119
4271
  trigger: input.trigger,
4120
4272
  confirmedLargeDiff: input.confirmedLargeDiff,
4121
4273
  confirmationReason: input.confirmationReason,
4122
- resetToCanonical: input.resetToCanonical
4274
+ resetToCanonical: input.resetToCanonical,
4275
+ requiredAncestorHeads: input.requiredAncestorHeads,
4276
+ assertStillAdmitted
4123
4277
  });
4124
4278
  } 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
- }
4279
+ const hydrationCurrent = workspaceSyncFailureHydrationIsSafe({
4280
+ error,
4281
+ resetToCanonical: input.resetToCanonical === true,
4282
+ inspectCurrentHydration: () => (0, import_workspace_git_sync.workspaceGitHydrationIsCurrent)(workspaceShadowRoot, buildWorkspaceMounts())
4283
+ });
4133
4284
  if (!hydrationCurrent) {
4134
4285
  process.stderr.write(
4135
4286
  `[r5d-worker] workspace synchronization failed with an incompletely hydrated visible tree; exiting for exact recovery: ${error instanceof Error ? error.message : String(error)}
@@ -4171,7 +4322,7 @@ async function startWorker(options) {
4171
4322
  pendingCreatedBranchPublicationNotBefore
4172
4323
  );
4173
4324
  const initialDeferral = pendingBranchDeferral();
4174
- if (!workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
4325
+ if (!workspaceConfigured || activeWorkspaceIncidentId || deferredWorkspaceConfiguration || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
4175
4326
  return;
4176
4327
  }
4177
4328
  if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
@@ -4179,7 +4330,8 @@ async function startWorker(options) {
4179
4330
  () => {
4180
4331
  workspaceAutomaticTimer = void 0;
4181
4332
  const scheduledTrigger = pendingAutomaticTrigger;
4182
- if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
4333
+ if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || deferredWorkspaceConfiguration || currentWorkerSocket !== ws)
4334
+ return;
4183
4335
  const currentDeferral = pendingBranchDeferral();
4184
4336
  if (currentDeferral?.kind === "active_target") return;
4185
4337
  if (currentDeferral?.kind === "creation_grace") {
@@ -4201,7 +4353,7 @@ async function startWorker(options) {
4201
4353
  }).finally(() => {
4202
4354
  automaticSyncInFlight = false;
4203
4355
  heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
4204
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
4356
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
4205
4357
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
4206
4358
  }
4207
4359
  });
@@ -4217,6 +4369,11 @@ async function startWorker(options) {
4217
4369
  const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
4218
4370
  const resolveMessageTarget = (target) => {
4219
4371
  if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
4372
+ if (!deferredWorkspaceTargetIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, target)) {
4373
+ throw new Error(
4374
+ `Worker workspace configuration is fenced for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}; only canonical remediation commands are allowed`
4375
+ );
4376
+ }
4220
4377
  if (target.type === "project" && !readyProjectIds.has(target.projectId)) {
4221
4378
  throw new Error(`Project ${target.projectId} is not ready on this worker`);
4222
4379
  }
@@ -4230,7 +4387,26 @@ async function startWorker(options) {
4230
4387
  projectConfigById
4231
4388
  });
4232
4389
  };
4233
- const configureWorkerWorkspace = async (message) => {
4390
+ const configureWorkerWorkspace = async (message, receiptGeneration) => {
4391
+ const incidentDeferral = workspaceConfigurationIncidentDeferral({
4392
+ requestedIncidentId: message.deferWorkspaceSyncForIncidentId,
4393
+ activeIncidentId: activeWorkspaceIncidentId
4394
+ });
4395
+ if (incidentDeferral.error) {
4396
+ workspaceConfigured = false;
4397
+ return {
4398
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, incidentDeferral.error),
4399
+ pending: incidentDeferredProjectCheckouts(message.projects),
4400
+ aheadOfOriginBranches: []
4401
+ };
4402
+ }
4403
+ if (incidentDeferral.incidentId) {
4404
+ workspaceConfigured = false;
4405
+ deferredWorkspaceConfiguration = {
4406
+ incidentId: incidentDeferral.incidentId,
4407
+ serverRefreshExpected: message.deferWorkspaceSyncForIncidentId === incidentDeferral.incidentId
4408
+ };
4409
+ }
4234
4410
  const incomingCredentialGenerationFingerprint = credentialGenerationFingerprint({
4235
4411
  ...message,
4236
4412
  workerBaseUrl: baseUrl,
@@ -4241,6 +4417,22 @@ async function startWorker(options) {
4241
4417
  pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
4242
4418
  incomingFingerprint: incomingCredentialGenerationFingerprint
4243
4419
  });
4420
+ if (deferredCredentialTransitionMustWait({
4421
+ incidentId: incidentDeferral.incidentId,
4422
+ transitionPhase: credentialTransitionPhase,
4423
+ canonicalRemediationActive: canonicalWorkspaceMutationIsActive(activeWorkspaceMutationTargets())
4424
+ })) {
4425
+ return {
4426
+ result: failedWorkspaceSyncResult(
4427
+ crypto.randomUUID(),
4428
+ { type: "connect" },
4429
+ new Error("Workspace credential rotation is deferred until the active canonical remediation command finishes")
4430
+ ),
4431
+ pending: incidentDeferredProjectCheckouts(message.projects),
4432
+ aheadOfOriginBranches: [],
4433
+ ...incidentDeferral.incidentId ? { deferredWorkspaceSyncForIncidentId: incidentDeferral.incidentId } : {}
4434
+ };
4435
+ }
4244
4436
  const credentialReapStatus = credentialReapContractStatus();
4245
4437
  const verifiedCredentialReapContractNow = credentialReapStatus === "verified_systemd";
4246
4438
  const credentialRestartIsContained = credentialGenerationRestartIsContained({
@@ -4354,6 +4546,12 @@ async function startWorker(options) {
4354
4546
  workspaceSyncRequestsInFlight += 1;
4355
4547
  try {
4356
4548
  return await workspaceSyncSingleFlight.runExclusive(async () => {
4549
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4550
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4551
+ }
4552
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4553
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4554
+ }
4357
4555
  for (const project of message.projects) (0, import_repository_transition_policy.assertRepositoryTransitionState)(project);
4358
4556
  const busyConfigurationChanges = (0, import_workspace_project_config_policy.busyProjectConfigurationChangeIds)({
4359
4557
  currentProjects: [...projectConfigById.values()],
@@ -4375,10 +4573,12 @@ async function startWorker(options) {
4375
4573
  const preserveOnlyBranches = message.projects.flatMap(
4376
4574
  (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
4377
4575
  );
4378
- projectWorkspaceState = projectWorkspaceStateStore.reconcile({
4379
- desiredProjects: message.projects,
4380
- preserveOnlyBranches
4381
- });
4576
+ if (!incidentDeferral.incidentId) {
4577
+ projectWorkspaceState = projectWorkspaceStateStore.reconcile({
4578
+ desiredProjects: message.projects,
4579
+ preserveOnlyBranches
4580
+ });
4581
+ }
4382
4582
  const stillPendingCreatedBranches = new Map(
4383
4583
  projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
4384
4584
  (0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(branch.projectId, branch.branchName),
@@ -4438,6 +4638,12 @@ async function startWorker(options) {
4438
4638
  void 0,
4439
4639
  credentialPublicationPreauthorized
4440
4640
  );
4641
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4642
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4643
+ }
4644
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4645
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4646
+ }
4441
4647
  configuredCredentialGenerationFingerprint = incomingCredentialGenerationFingerprint;
4442
4648
  pendingCredentialGenerationIntentAtProcessStart = null;
4443
4649
  bootstrapCredentialGenerationFingerprintAtProcessStart = null;
@@ -4452,6 +4658,34 @@ async function startWorker(options) {
4452
4658
  branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
4453
4659
  }));
4454
4660
  const nextProjectConfigById = new Map(effectiveProjects.map((project) => [project.projectId, project]));
4661
+ if (incidentDeferral.incidentId) {
4662
+ (0, import_workspace_git_sync.configureExistingWorkspaceGitForRemediation)({
4663
+ workspacePath: workspaceShadowRoot,
4664
+ remoteUrl: message.workspaceRemoteUrl,
4665
+ credentialHelper: nextWorkspaceCredentialHelper,
4666
+ credentialUsername: workerCredentialUsername,
4667
+ gitIdentity: message.gitIdentity
4668
+ });
4669
+ projectConfigById.clear();
4670
+ for (const project of effectiveProjects) projectConfigById.set(project.projectId, project);
4671
+ readyProjectIds.clear();
4672
+ reconciledProjectConfigFingerprints.clear();
4673
+ pendingCheckouts.clear();
4674
+ const pending2 = incidentDeferredProjectCheckouts(message.projects);
4675
+ for (const checkout of pending2) pendingCheckouts.set(projectBranchKey(checkout.projectId, checkout.branchName), checkout);
4676
+ workspaceConfigured = activeWorkspaceIncidentId === incidentDeferral.incidentId;
4677
+ return {
4678
+ result: deferredIncidentWorkspaceConfigurationResult({
4679
+ attemptId: crypto.randomUUID(),
4680
+ workerLabel: label,
4681
+ head: workspaceLocalHead(),
4682
+ skippedMountIds: buildWorkspaceMounts().map(({ id }) => id)
4683
+ }),
4684
+ pending: pending2,
4685
+ aheadOfOriginBranches: [],
4686
+ deferredWorkspaceSyncForIncidentId: incidentDeferral.incidentId
4687
+ };
4688
+ }
4455
4689
  (0, import_workspace_git_sync.ensureWorkspaceGitClone)({
4456
4690
  workspacePath: workspaceShadowRoot,
4457
4691
  remoteUrl: message.workspaceRemoteUrl,
@@ -4519,10 +4753,24 @@ async function startWorker(options) {
4519
4753
  { ignoreBusy: true }
4520
4754
  );
4521
4755
  ensureConfiguredProjects();
4756
+ const configurationSyncAdmissionGeneration = workerAdmissionGeneration;
4757
+ const assertConfigurationSyncStillAdmitted = () => {
4758
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration || configurationSyncAdmissionGeneration !== workerAdmissionGeneration || !workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4759
+ throw new Error("Workspace configuration synchronization admission changed while asynchronous work was in flight");
4760
+ }
4761
+ };
4522
4762
  const result = await performWorkspaceSync({
4523
4763
  attemptId: crypto.randomUUID(),
4524
- trigger: { type: "connect" }
4764
+ trigger: { type: "connect" },
4765
+ assertStillAdmitted: assertConfigurationSyncStillAdmitted
4525
4766
  });
4767
+ assertConfigurationSyncStillAdmitted();
4768
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4769
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4770
+ }
4771
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4772
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4773
+ }
4526
4774
  const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
4527
4775
  if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
4528
4776
  const activeMountIds = new Set(result.activeMountIds ?? []);
@@ -4539,6 +4787,11 @@ async function startWorker(options) {
4539
4787
  });
4540
4788
  }
4541
4789
  }
4790
+ if (deferredWorkspaceConfigurationRefreshTimer) {
4791
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
4792
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
4793
+ }
4794
+ deferredWorkspaceConfiguration = null;
4542
4795
  workspaceConfigured = true;
4543
4796
  const pending = [...pendingCheckouts.values()].sort(
4544
4797
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
@@ -4546,6 +4799,13 @@ async function startWorker(options) {
4546
4799
  return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
4547
4800
  });
4548
4801
  } catch (error) {
4802
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4803
+ return {
4804
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
4805
+ pending: incidentDeferredProjectCheckouts(message.projects),
4806
+ aheadOfOriginBranches: []
4807
+ };
4808
+ }
4549
4809
  if (error instanceof import_registry_auth.RegistryAuthConfigurationError) {
4550
4810
  workspaceConfigured = false;
4551
4811
  githubCredential = null;
@@ -4600,7 +4860,7 @@ async function startWorker(options) {
4600
4860
  workspaceAutomaticTimer = void 0;
4601
4861
  }
4602
4862
  void (async () => {
4603
- if (workspaceConfigured && !activeWorkspaceIncidentId) {
4863
+ if (workspaceConfigured && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
4604
4864
  await runWorkspaceSync({
4605
4865
  trigger: { type: "manual", detail: "graceful worker shutdown" }
4606
4866
  });
@@ -4649,7 +4909,9 @@ async function startWorker(options) {
4649
4909
  updateClis: true,
4650
4910
  browserPortForwarding: true,
4651
4911
  execStdinV1: true,
4652
- ptyEnvFilesV1: true
4912
+ ptyEnvFilesV1: true,
4913
+ workspaceRemediationAncestorGuardV1: true,
4914
+ workspaceIncidentConfigDeferralV1: true
4653
4915
  },
4654
4916
  projectRoot: projectsRoot,
4655
4917
  artifactRoot,
@@ -4685,26 +4947,35 @@ async function startWorker(options) {
4685
4947
  return;
4686
4948
  }
4687
4949
  if (message.type === "workspace_config") {
4950
+ const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
4951
+ const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
4688
4952
  advanceWorkerAdmissionGeneration();
4689
- const configured = await configureWorkerWorkspace(message);
4953
+ const configured = await configureWorkerWorkspace(message, receiptGeneration);
4690
4954
  sendWorkerMessageFromCurrentSource(ws, {
4691
4955
  type: "workspace_configured",
4692
4956
  requestId: message.requestId,
4693
4957
  result: configured.result,
4694
4958
  pendingCheckouts: configured.pending,
4695
- aheadOfOriginBranches: configured.aheadOfOriginBranches
4959
+ aheadOfOriginBranches: configured.aheadOfOriginBranches,
4960
+ ...configured.deferredWorkspaceSyncForIncidentId ? { deferredWorkspaceSyncForIncidentId: configured.deferredWorkspaceSyncForIncidentId } : {}
4696
4961
  });
4697
4962
  process.stdout.write(
4698
4963
  `[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
4699
4964
  `
4700
4965
  );
4966
+ if (refreshesDeferredConfiguration && configured.result.outcome === "failed" && deferredWorkspaceConfiguration !== null && currentWorkerSocket === ws) {
4967
+ workspaceConfigured = false;
4968
+ advanceWorkerAdmissionGeneration();
4969
+ ws.close(1012, "Deferred workspace configuration refresh failed");
4970
+ return;
4971
+ }
4701
4972
  if (!workspacePeriodicTimer) {
4702
4973
  workspacePeriodicTimer = setInterval(() => {
4703
4974
  scheduleAutomaticWorkspaceSync({ type: "periodic", detail: "periodic workspace reconciliation" }, 0);
4704
4975
  }, WORKSPACE_GIT_PERIODIC_MS);
4705
4976
  workspacePeriodicTimer.unref();
4706
4977
  }
4707
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
4978
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
4708
4979
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, 0);
4709
4980
  }
4710
4981
  return;
@@ -4716,11 +4987,20 @@ async function startWorker(options) {
4716
4987
  trigger: message.trigger,
4717
4988
  confirmedLargeDiff: message.confirmedLargeDiff,
4718
4989
  confirmationReason: message.confirmationReason,
4719
- resetToCanonical: message.resetToCanonical
4990
+ resetToCanonical: message.resetToCanonical,
4991
+ requiredAncestorHeads: message.requiredAncestorHeads
4720
4992
  });
4721
4993
  return;
4722
4994
  }
4723
4995
  if (message.type === "create_project_branch") {
4996
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
4997
+ sendWorkerMessage(ws, {
4998
+ type: "operation_result",
4999
+ requestId: message.requestId,
5000
+ error: `Project operations are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5001
+ });
5002
+ return;
5003
+ }
4724
5004
  try {
4725
5005
  const pendingBranch = {
4726
5006
  branchId: message.branchId,
@@ -4728,6 +5008,11 @@ async function startWorker(options) {
4728
5008
  branchName: message.targetBranch
4729
5009
  };
4730
5010
  const created = await workspaceSyncSingleFlight.runMutation(() => {
5011
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5012
+ throw new Error(
5013
+ `Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5014
+ );
5015
+ }
4731
5016
  const project = projectConfigById.get(message.projectId);
4732
5017
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
4733
5018
  (0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(project);
@@ -4811,9 +5096,22 @@ async function startWorker(options) {
4811
5096
  return;
4812
5097
  }
4813
5098
  if (message.type === "delete_project_branch") {
5099
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5100
+ sendWorkerMessage(ws, {
5101
+ type: "operation_result",
5102
+ requestId: message.requestId,
5103
+ error: `Project operations are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5104
+ });
5105
+ return;
5106
+ }
4814
5107
  try {
4815
5108
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
4816
5109
  const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
5110
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5111
+ throw new Error(
5112
+ `Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5113
+ );
5114
+ }
4817
5115
  const project = projectConfigById.get(message.projectId);
4818
5116
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
4819
5117
  (0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(project);
@@ -4913,8 +5211,38 @@ async function startWorker(options) {
4913
5211
  }
4914
5212
  if (message.type === "workspace_incident_updated") {
4915
5213
  const previousIncidentId = activeWorkspaceIncidentId;
4916
- activeWorkspaceIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
5214
+ const nextIncidentId = (0, import_workspace_incident_state.applyWorkspaceIncidentUpdate)(activeWorkspaceIncidentId, message);
5215
+ if (nextIncidentId !== previousIncidentId) advanceWorkerAdmissionGeneration();
5216
+ activeWorkspaceIncidentId = nextIncidentId;
4917
5217
  if (previousIncidentId && !activeWorkspaceIncidentId && (message.status === "resolved" || message.status === "confirmed" || message.status === "reset")) {
5218
+ const disposition = workspaceIncidentTerminalClearDisposition({
5219
+ deferredConfiguration: deferredWorkspaceConfiguration,
5220
+ clearedIncidentId: previousIncidentId
5221
+ });
5222
+ if (disposition !== "ordinary_resume") {
5223
+ pendingAutomaticTrigger ??= { type: "periodic", detail: "resume after workspace incident" };
5224
+ workspaceConfigured = false;
5225
+ advanceWorkerAdmissionGeneration();
5226
+ if (disposition === "reconnect_for_refresh") {
5227
+ ws.close(1012, "Refreshing deferred workspace configuration");
5228
+ } else {
5229
+ if (deferredWorkspaceConfigurationRefreshTimer) clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5230
+ const deferredIncidentId = deferredWorkspaceConfiguration?.incidentId;
5231
+ deferredWorkspaceConfigurationRefreshTimer = setTimeout(() => {
5232
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
5233
+ if (!deferredIncidentId || !deferredWorkspaceRefreshWatchdogIsCurrent({
5234
+ capturedIncidentId: deferredIncidentId,
5235
+ deferredConfiguration: deferredWorkspaceConfiguration,
5236
+ activeIncidentId: activeWorkspaceIncidentId
5237
+ }) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
5238
+ return;
5239
+ }
5240
+ ws.close(1012, "Timed out waiting for deferred workspace configuration refresh");
5241
+ }, WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS);
5242
+ deferredWorkspaceConfigurationRefreshTimer.unref();
5243
+ }
5244
+ return;
5245
+ }
4918
5246
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger ?? { type: "periodic", detail: "resume after workspace incident" }, 0);
4919
5247
  }
4920
5248
  return;
@@ -5018,6 +5346,12 @@ async function startWorker(options) {
5018
5346
  sendAck({ error: `Process run ${message.runId} is not active on this worker` });
5019
5347
  return;
5020
5348
  }
5349
+ if (!deferredWorkspaceTargetIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, active.target)) {
5350
+ sendAck({
5351
+ error: `Process input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
5352
+ });
5353
+ return;
5354
+ }
5021
5355
  if (!active.interactive || !active.stdin) {
5022
5356
  sendAck({
5023
5357
  error: `Process run ${message.runId} has no open stdin. Start a new shell command with "interactive": true to write to its stdin.`
@@ -5063,6 +5397,15 @@ async function startWorker(options) {
5063
5397
  });
5064
5398
  return;
5065
5399
  }
5400
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5401
+ sendWorkerMessage(ws, {
5402
+ type: "pty_error",
5403
+ requestId: message.requestId,
5404
+ ptyId: message.ptyId,
5405
+ error: `Shells are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5406
+ });
5407
+ return;
5408
+ }
5066
5409
  await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
5067
5410
  workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
5068
5411
  if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
@@ -5071,6 +5414,11 @@ async function startWorker(options) {
5071
5414
  let mutationLeaseTransferred = false;
5072
5415
  try {
5073
5416
  releaseWorkspaceMutation = await (0, import_workspace_command_sync_policy.acquireWorkspaceCommandMutation)(message.target, workspaceSyncSingleFlight);
5417
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5418
+ throw new Error(
5419
+ `Shell opening was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5420
+ );
5421
+ }
5074
5422
  const resolvedTarget = resolveMessageTarget(message.target);
5075
5423
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
5076
5424
  `);
@@ -5103,6 +5451,14 @@ async function startWorker(options) {
5103
5451
  }
5104
5452
  if (message.type === "pty_input") {
5105
5453
  const activePty = activePtys.get(message.ptyId);
5454
+ if (activePty && workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5455
+ sendWorkerMessage(ws, {
5456
+ type: "pty_error",
5457
+ ptyId: message.ptyId,
5458
+ error: `Shell input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
5459
+ });
5460
+ return;
5461
+ }
5106
5462
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
5107
5463
  markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
5108
5464
  }
@@ -5132,6 +5488,17 @@ async function startWorker(options) {
5132
5488
  });
5133
5489
  return;
5134
5490
  }
5491
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5492
+ sendWorkerMessage(ws, {
5493
+ type: "exec_result",
5494
+ requestId: message.requestId,
5495
+ stdout: "",
5496
+ stderr: "",
5497
+ exitCode: 1,
5498
+ error: `One-shot commands are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5499
+ });
5500
+ return;
5501
+ }
5135
5502
  let result;
5136
5503
  let targetReserved = false;
5137
5504
  const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
@@ -5144,6 +5511,11 @@ async function startWorker(options) {
5144
5511
  });
5145
5512
  }
5146
5513
  const runCommand = async () => {
5514
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5515
+ throw new Error(
5516
+ `One-shot command was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5517
+ );
5518
+ }
5147
5519
  const resolvedTarget = resolveMessageTarget(message.target);
5148
5520
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
5149
5521
  `);
@@ -5154,7 +5526,14 @@ async function startWorker(options) {
5154
5526
  token,
5155
5527
  artifactRoot,
5156
5528
  planRoot,
5157
- assertAdmission: assertMessageAdmission
5529
+ assertAdmission: () => {
5530
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5531
+ throw new Error(
5532
+ `One-shot command was fenced before spawn by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5533
+ );
5534
+ }
5535
+ assertMessageAdmission();
5536
+ }
5158
5537
  });
5159
5538
  };
5160
5539
  result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
@@ -5254,6 +5633,14 @@ async function startWorker(options) {
5254
5633
  return;
5255
5634
  }
5256
5635
  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") {
5636
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5637
+ sendWorkerMessage(ws, {
5638
+ type: "operation_result",
5639
+ requestId: message.requestId,
5640
+ error: `Workspace operations are deferred for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}; use a canonical remediation shell`
5641
+ });
5642
+ return;
5643
+ }
5257
5644
  const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
5258
5645
  const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
5259
5646
  if (reservesVisibleWorkspace) {
@@ -5272,7 +5659,8 @@ async function startWorker(options) {
5272
5659
  baseUrl,
5273
5660
  token,
5274
5661
  artifactRoot,
5275
- planRoot
5662
+ planRoot,
5663
+ assertAdmission: assertMessageAdmission
5276
5664
  });
5277
5665
  });
5278
5666
  ws.send(
@@ -5302,7 +5690,7 @@ async function startWorker(options) {
5302
5690
  } finally {
5303
5691
  if (reservesVisibleWorkspace) {
5304
5692
  workspaceSyncPriorityOperationTargets.delete(message.requestId);
5305
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
5693
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
5306
5694
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, dirtyTrigger ? WORKSPACE_GIT_QUIET_MS : 0);
5307
5695
  }
5308
5696
  }
@@ -5338,6 +5726,10 @@ async function startWorker(options) {
5338
5726
  clearInterval(workspacePeriodicTimer);
5339
5727
  workspacePeriodicTimer = void 0;
5340
5728
  }
5729
+ if (deferredWorkspaceConfigurationRefreshTimer) {
5730
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5731
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
5732
+ }
5341
5733
  if (terminalReplayTimer) {
5342
5734
  clearInterval(terminalReplayTimer);
5343
5735
  terminalReplayTimer = void 0;