@ricsam/r5d-worker 0.0.81 → 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/mjs/main.mjs CHANGED
@@ -79,12 +79,14 @@ import {
79
79
  pruneAuthoritativelyDesiredBranchDeletions
80
80
  } from "./project-workspace-state.mjs";
81
81
  import {
82
+ configureExistingWorkspaceGitForRemediation,
82
83
  ensureWorkspaceGitClone,
83
84
  hydrateWorkspaceGitMounts,
84
85
  recoverWorkspaceGitHydration,
85
86
  resetWorkspaceGit,
86
87
  synchronizeWorkspaceGit,
87
- workspaceGitHydrationIsCurrent
88
+ workspaceGitHydrationIsCurrent,
89
+ WorkspaceRemediationAncestryError
88
90
  } from "./workspace-git-sync.mjs";
89
91
  class ProjectWorkspaceConfigurationDeferredError extends Error {
90
92
  }
@@ -99,6 +101,11 @@ const DEFAULT_READ_MAX_BYTES = 5e4;
99
101
  const MAX_LINE_LENGTH = 2e3;
100
102
  const WORKSPACE_GIT_QUIET_MS = 5e3;
101
103
  const WORKSPACE_GIT_PERIODIC_MS = 6e4;
104
+ const WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS = 6e4;
105
+ const PTY_INPUT_BUSY_GRACE_MS = 3e3;
106
+ const PTY_FOREGROUND_POLL_MS = 1e3;
107
+ const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
108
+ const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
102
109
  const activeProcesses = /* @__PURE__ */ new Map();
103
110
  const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
104
111
  const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
@@ -156,6 +163,107 @@ function assertWorkerChildAdmission(input) {
156
163
  throw new StaleWorkerAdmissionError();
157
164
  }
158
165
  }
166
+ function pathIsInsideRoot(rootPath, candidatePath) {
167
+ const relative = path.relative(rootPath, candidatePath);
168
+ return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
169
+ }
170
+ function resolvePtyEnvFilePath(requestedPath, temporaryRoot = os.tmpdir()) {
171
+ const canonicalTemporaryRoot = fs.realpathSync.native(temporaryRoot);
172
+ if (requestedPath.startsWith(PTY_TMP_PATH_PREFIX)) {
173
+ const filename = requestedPath.slice(PTY_TMP_PATH_PREFIX.length);
174
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(filename)) {
175
+ throw new Error("PTY environment file temporary token must contain one safe filename");
176
+ }
177
+ return path.join(canonicalTemporaryRoot, filename);
178
+ }
179
+ if (!path.isAbsolute(requestedPath)) {
180
+ throw new Error(`PTY environment file path must be absolute or use ${PTY_TMP_PATH_PREFIX}`);
181
+ }
182
+ const resolvedPath = path.resolve(requestedPath);
183
+ const canonicalParent = fs.realpathSync.native(path.dirname(resolvedPath));
184
+ const canonicalPath = path.join(canonicalParent, path.basename(resolvedPath));
185
+ if (!pathIsInsideRoot(canonicalTemporaryRoot, canonicalPath) || canonicalPath === canonicalTemporaryRoot) {
186
+ throw new Error(`PTY environment file path must be inside ${canonicalTemporaryRoot}`);
187
+ }
188
+ return canonicalPath;
189
+ }
190
+ function removePtyEnvFiles(paths) {
191
+ for (const filePath of paths) {
192
+ try {
193
+ fs.rmSync(filePath, { force: true });
194
+ } catch (error) {
195
+ process.stderr.write(
196
+ `[r5d-worker] failed to remove PTY environment file ${filePath}: ${error instanceof Error ? error.message : String(error)}
197
+ `
198
+ );
199
+ }
200
+ }
201
+ }
202
+ function stagePtyEnvFiles(envFiles, temporaryRoot = os.tmpdir()) {
203
+ const staged = { paths: [], resolvedByRequestedPath: /* @__PURE__ */ new Map() };
204
+ try {
205
+ for (const envFile of envFiles ?? []) {
206
+ if (!envFile || typeof envFile.path !== "string" || typeof envFile.content !== "string") {
207
+ throw new Error("PTY environment files require string path and content values");
208
+ }
209
+ if (envFile.mode !== void 0 && envFile.mode !== 384) {
210
+ throw new Error("PTY environment files must use mode 0600");
211
+ }
212
+ const resolvedPath = resolvePtyEnvFilePath(envFile.path, temporaryRoot);
213
+ if (staged.resolvedByRequestedPath.has(envFile.path) || staged.paths.includes(resolvedPath)) {
214
+ throw new Error(`Duplicate PTY environment file path: ${envFile.path}`);
215
+ }
216
+ const descriptor = fs.openSync(
217
+ resolvedPath,
218
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0),
219
+ 384
220
+ );
221
+ staged.paths.push(resolvedPath);
222
+ staged.resolvedByRequestedPath.set(envFile.path, resolvedPath);
223
+ try {
224
+ fs.writeFileSync(descriptor, envFile.content, "utf8");
225
+ fs.fchmodSync(descriptor, 384);
226
+ fs.fsyncSync(descriptor);
227
+ } finally {
228
+ fs.closeSync(descriptor);
229
+ }
230
+ }
231
+ return staged;
232
+ } catch (error) {
233
+ removePtyEnvFiles(staged.paths);
234
+ throw error;
235
+ }
236
+ }
237
+ function resolvePtyEnvFileReferences(env, staged) {
238
+ return Object.fromEntries(Object.entries(env).map(([name, value]) => [name, staged.resolvedByRequestedPath.get(value) ?? value]));
239
+ }
240
+ function workerCommandHasWorkspaceEffect(message) {
241
+ return message.workspaceEffect !== "none";
242
+ }
243
+ function workerPtyIsWorkspaceBusy(pty, now = Date.now(), foregroundIdleEnabled = PTY_FOREGROUND_IDLE_ENABLED) {
244
+ return !foregroundIdleEnabled || pty.foregroundBusy || now - pty.lastInputAt < PTY_INPUT_BUSY_GRACE_MS;
245
+ }
246
+ function parseLinuxPtyForegroundBusy(stat) {
247
+ const commandEnd = stat.lastIndexOf(")");
248
+ if (commandEnd < 0) return null;
249
+ const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
250
+ const processGroup = Number(fields[2]);
251
+ const foregroundProcessGroup = Number(fields[5]);
252
+ if (!Number.isSafeInteger(processGroup) || processGroup <= 0 || !Number.isSafeInteger(foregroundProcessGroup)) return null;
253
+ return foregroundProcessGroup !== processGroup;
254
+ }
255
+ const workerPtyTestHarness = {
256
+ temporaryPathPrefix: PTY_TMP_PATH_PREFIX,
257
+ inputBusyGraceMs: PTY_INPUT_BUSY_GRACE_MS,
258
+ resolveEnvFilePath: resolvePtyEnvFilePath,
259
+ stageEnvFiles: stagePtyEnvFiles,
260
+ removeEnvFiles: removePtyEnvFiles,
261
+ resolveEnvFileReferences: resolvePtyEnvFileReferences,
262
+ commandHasWorkspaceEffect: workerCommandHasWorkspaceEffect,
263
+ canonicalSyncTerminalHead,
264
+ ptyIsWorkspaceBusy: workerPtyIsWorkspaceBusy,
265
+ parseLinuxForegroundBusy: parseLinuxPtyForegroundBusy
266
+ };
159
267
  function defaultConfigPath() {
160
268
  return path.join(os.homedir(), ".config", "r5d", "r5dctl", "config.json");
161
269
  }
@@ -1414,6 +1522,86 @@ function credentialReapContractStatus(probe) {
1414
1522
  function verifiedCredentialReapContract(probe) {
1415
1523
  return credentialReapContractStatus(probe) === "verified_systemd";
1416
1524
  }
1525
+ function workspaceConfigurationIncidentDeferral(input) {
1526
+ const incidentId = input.requestedIncidentId ?? input.activeIncidentId;
1527
+ if (incidentId === null) return { incidentId: null };
1528
+ if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(incidentId)) {
1529
+ return { incidentId: null, error: new Error("Workspace configuration incident deferral requires a canonical incident UUID") };
1530
+ }
1531
+ if (input.requestedIncidentId !== void 0 && input.requestedIncidentId !== input.activeIncidentId) {
1532
+ return {
1533
+ incidentId: null,
1534
+ error: new Error(
1535
+ `Workspace configuration incident deferral ${input.requestedIncidentId} does not match active incident ${input.activeIncidentId ?? "none"}`
1536
+ )
1537
+ };
1538
+ }
1539
+ return { incidentId };
1540
+ }
1541
+ function workspaceIncidentTerminalClearDisposition(input) {
1542
+ if (!input.deferredConfiguration) return "ordinary_resume";
1543
+ void input.clearedIncidentId;
1544
+ return input.deferredConfiguration.serverRefreshExpected ? "await_server_refresh" : "reconnect_for_refresh";
1545
+ }
1546
+ function deferredWorkspaceTargetIsAllowed(deferredConfiguration, activeIncidentId, target) {
1547
+ const canonicalTarget = target.type === "workspace" && target.rootProfile === "canonical_sync";
1548
+ if (deferredConfiguration) return activeIncidentId === deferredConfiguration.incidentId && canonicalTarget;
1549
+ return activeIncidentId === null || canonicalTarget;
1550
+ }
1551
+ function deferredWorkspaceSyncTriggerIsAllowed(deferredConfiguration, activeIncidentId, trigger) {
1552
+ const incidentId = deferredConfiguration?.incidentId ?? activeIncidentId;
1553
+ return incidentId === null || (deferredConfiguration === null || activeIncidentId === deferredConfiguration.incidentId) && (trigger.type === "remediation" || trigger.type === "remediation_confirm" || trigger.type === "remediation_reset");
1554
+ }
1555
+ function workspaceOperationsAreFenced(deferredConfiguration, activeIncidentId) {
1556
+ return deferredConfiguration !== null || activeIncidentId !== null;
1557
+ }
1558
+ function workspaceCommandTransportIsAllowed(deferredConfiguration, activeIncidentId, transport) {
1559
+ return !workspaceOperationsAreFenced(deferredConfiguration, activeIncidentId) || transport === "exec_start";
1560
+ }
1561
+ function deferredCredentialTransitionMustWait(input) {
1562
+ return input.incidentId !== null && input.transitionPhase !== "current" && input.canonicalRemediationActive;
1563
+ }
1564
+ function workspaceConfigurationIncidentSnapshotIsCurrent(capturedIncidentId, activeIncidentId) {
1565
+ return capturedIncidentId === null ? activeIncidentId === null : activeIncidentId === null || activeIncidentId === capturedIncidentId;
1566
+ }
1567
+ function deferredWorkspaceRefreshWatchdogIsCurrent(input) {
1568
+ return input.deferredConfiguration?.incidentId === input.capturedIncidentId && input.activeIncidentId === null;
1569
+ }
1570
+ function incidentDeferredProjectCheckouts(projects) {
1571
+ return projects.flatMap(
1572
+ (project) => project.executionDisabled ? [] : project.branches.map(({ branchName }) => ({ projectId: project.projectId, branchName }))
1573
+ ).sort((left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName));
1574
+ }
1575
+ function deferredIncidentWorkspaceConfigurationResult(input) {
1576
+ return {
1577
+ type: "workspace_sync",
1578
+ attemptId: input.attemptId,
1579
+ workerLabel: input.workerLabel,
1580
+ trigger: { type: "connect", detail: "workspace synchronization deferred for active remediation" },
1581
+ outcome: "no_change",
1582
+ startingHead: input.head,
1583
+ ...input.head ? { localHead: input.head } : {},
1584
+ rebaseCount: 0,
1585
+ diffSizeBytes: 0,
1586
+ gitStatus: "",
1587
+ affectedProjects: [],
1588
+ affectedPaths: [],
1589
+ activeMountIds: [],
1590
+ skippedMountIds: [...input.skippedMountIds].sort(),
1591
+ activeProjectBranchPublications: [],
1592
+ discardedPaths: [],
1593
+ localChangesDiscarded: false
1594
+ };
1595
+ }
1596
+ function workspaceSyncFailureHydrationIsSafe(input) {
1597
+ if (input.error instanceof WorkspaceRemediationAncestryError) return true;
1598
+ if (input.resetToCanonical) return false;
1599
+ try {
1600
+ return input.inspectCurrentHydration();
1601
+ } catch {
1602
+ return false;
1603
+ }
1604
+ }
1417
1605
  function fenceUnsafeWorkspaceSyncFailure(input) {
1418
1606
  if (input.hydrationCurrent) return false;
1419
1607
  input.invalidateExecution();
@@ -1446,7 +1634,21 @@ const workerGitSecurityTestHarness = {
1446
1634
  credentialReapContractStatus,
1447
1635
  verifiedCredentialReapContract,
1448
1636
  fenceUnsafeWorkspaceSyncFailure,
1637
+ workspaceConfigurationIncidentDeferral,
1638
+ workspaceIncidentTerminalClearDisposition,
1639
+ deferredWorkspaceTargetIsAllowed,
1640
+ deferredWorkspaceSyncTriggerIsAllowed,
1641
+ workspaceOperationsAreFenced,
1642
+ workspaceCommandTransportIsAllowed,
1643
+ deferredCredentialTransitionMustWait,
1644
+ workspaceConfigurationIncidentSnapshotIsCurrent,
1645
+ deferredWorkspaceRefreshWatchdogIsCurrent,
1646
+ incidentDeferredProjectCheckouts,
1647
+ deferredIncidentWorkspaceConfigurationResult,
1648
+ workspaceSyncFailureHydrationIsSafe,
1449
1649
  assertWorkerChildAdmission,
1650
+ executeWriteFileOperation,
1651
+ executeEditFileOperation,
1450
1652
  terminateCredentialBearingChildren,
1451
1653
  terminateCredentialBearingChildrenWithRetention,
1452
1654
  async commitCredentialGeneration(prepared, credential, children, beforeMutation, previousGenerationFenced = false) {
@@ -1597,6 +1799,15 @@ function hasProjectWorktree(checkoutPath) {
1597
1799
  return false;
1598
1800
  }
1599
1801
  }
1802
+ function canonicalSyncTerminalHead(resolvedTarget, readHead = (rootPath) => runGit(["rev-parse", "HEAD"], { cwd: rootPath })) {
1803
+ if (resolvedTarget.target.type !== "workspace" || resolvedTarget.target.rootProfile !== "canonical_sync") return void 0;
1804
+ try {
1805
+ const head = readHead(resolvedTarget.rootPath);
1806
+ return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(head) ? head : void 0;
1807
+ } catch {
1808
+ return void 0;
1809
+ }
1810
+ }
1600
1811
  function describeWorkerSessionTarget(target) {
1601
1812
  return target.type === "project" ? `${target.projectId}/${target.branchName}` : `${target.ownerUserId}/${target.rootProfile}`;
1602
1813
  }
@@ -1901,8 +2112,10 @@ async function executeWriteFileOperation(input) {
1901
2112
  planRoot: input.planRoot,
1902
2113
  access: "write"
1903
2114
  });
2115
+ input.assertAdmission();
1904
2116
  const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
1905
2117
  return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
2118
+ input.assertAdmission();
1906
2119
  return writeWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.content, builtInPaths);
1907
2120
  });
1908
2121
  }
@@ -1918,8 +2131,10 @@ async function executeEditFileOperation(input) {
1918
2131
  planRoot: input.planRoot,
1919
2132
  access: "write"
1920
2133
  });
2134
+ input.assertAdmission();
1921
2135
  const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
1922
2136
  return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
2137
+ input.assertAdmission();
1923
2138
  return editWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.edits, builtInPaths);
1924
2139
  });
1925
2140
  }
@@ -2337,7 +2552,9 @@ async function executeCommand(input) {
2337
2552
  });
2338
2553
  spawnedProcess = subprocess;
2339
2554
  credentialBearingProcessGroups.set(subprocess.pid, subprocess);
2340
- credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
2555
+ if (workerCommandHasWorkspaceEffect(input.message)) {
2556
+ credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
2557
+ }
2341
2558
  activeProcesses.set(input.message.runId, {
2342
2559
  process: subprocess,
2343
2560
  target: input.resolvedTarget.target,
@@ -2348,7 +2565,8 @@ async function executeCommand(input) {
2348
2565
  argv: input.message.argv,
2349
2566
  command: input.message.argv.join(" "),
2350
2567
  cwd,
2351
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
2568
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2569
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2352
2570
  });
2353
2571
  if (input.message.timeoutMs) {
2354
2572
  timeout = setTimeout(() => {
@@ -2434,7 +2652,9 @@ async function executeStreamingCommand(input) {
2434
2652
  });
2435
2653
  spawnedProcess = subprocess;
2436
2654
  credentialBearingProcessGroups.set(subprocess.pid, subprocess);
2437
- credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
2655
+ if (workerCommandHasWorkspaceEffect(input.message)) {
2656
+ credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
2657
+ }
2438
2658
  activeProcesses.set(input.message.runId, {
2439
2659
  process: subprocess,
2440
2660
  target: input.resolvedTarget.target,
@@ -2447,7 +2667,8 @@ async function executeStreamingCommand(input) {
2447
2667
  command: input.message.command,
2448
2668
  cwd,
2449
2669
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2450
- ...interactive ? { interactive: true, stdin: subprocess.stdin } : {}
2670
+ ...interactive ? { interactive: true, stdin: subprocess.stdin } : {},
2671
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2451
2672
  });
2452
2673
  started = true;
2453
2674
  sendWorkerMessage(input.ws, {
@@ -2489,12 +2710,15 @@ async function executeStreamingCommand(input) {
2489
2710
  });
2490
2711
  })
2491
2712
  ]);
2713
+ const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2492
2714
  const terminal = {
2493
2715
  type: "exec_exit",
2494
2716
  runId: input.message.runId,
2495
2717
  exitCode,
2496
2718
  durationMs: Date.now() - startedAt,
2497
- ...timedOut ? { timedOut: true } : {}
2719
+ ...timedOut ? { timedOut: true } : {},
2720
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2721
+ ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2498
2722
  };
2499
2723
  pendingProcessTerminals.set(input.message.runId, terminal);
2500
2724
  sendWorkerMessage(input.ws, terminal);
@@ -2502,11 +2726,14 @@ async function executeStreamingCommand(input) {
2502
2726
  if (!started && error instanceof StaleWorkerAdmissionError) throw error;
2503
2727
  const message = error instanceof Error ? error.message : String(error);
2504
2728
  if (started) {
2729
+ const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2505
2730
  const terminal = {
2506
2731
  type: "exec_error",
2507
2732
  runId: input.message.runId,
2508
2733
  error: message,
2509
- durationMs: Date.now() - startedAt
2734
+ durationMs: Date.now() - startedAt,
2735
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2736
+ ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2510
2737
  };
2511
2738
  pendingProcessTerminals.set(input.message.runId, terminal);
2512
2739
  sendWorkerMessage(input.ws, terminal);
@@ -2587,7 +2814,8 @@ function buildActiveProcessReports() {
2587
2814
  command: active.command,
2588
2815
  ...active.cwd ? { cwd: active.cwd } : {},
2589
2816
  startedAt: active.startedAt,
2590
- ...active.interactive ? { interactive: true } : {}
2817
+ ...active.interactive ? { interactive: true } : {},
2818
+ ...active.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2591
2819
  }));
2592
2820
  }
2593
2821
  function sendActiveProcessReport(ws) {
@@ -2599,8 +2827,13 @@ function sendActiveProcessReport(ws) {
2599
2827
  const PTY_BRIDGE_SCRIPT = String.raw`
2600
2828
  const readline = require("node:readline");
2601
2829
  const nodePty = require("node-pty");
2830
+ const fs = require("node:fs");
2831
+ const { execFile } = require("node:child_process");
2602
2832
 
2603
2833
  let ptyProcess = null;
2834
+ let foregroundTimer = null;
2835
+ let foregroundPollInFlight = false;
2836
+ let lastForegroundBusy = null;
2604
2837
 
2605
2838
  function send(message, callback) {
2606
2839
  process.stdout.write(JSON.stringify(message) + "\n", callback);
@@ -2610,6 +2843,46 @@ function decode(data) {
2610
2843
  return Buffer.from(data, "base64").toString("utf8");
2611
2844
  }
2612
2845
 
2846
+ function emitForeground(busy) {
2847
+ if (busy === lastForegroundBusy) return;
2848
+ lastForegroundBusy = busy;
2849
+ send({ type: "foreground", busy });
2850
+ }
2851
+
2852
+ function linuxForegroundBusy(pid) {
2853
+ const stat = fs.readFileSync("/proc/" + pid + "/stat", "utf8");
2854
+ const commandEnd = stat.lastIndexOf(")");
2855
+ if (commandEnd < 0) throw new Error("invalid /proc stat");
2856
+ const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
2857
+ const processGroup = Number(fields[2]);
2858
+ const foregroundProcessGroup = Number(fields[5]);
2859
+ if (!Number.isSafeInteger(processGroup) || processGroup <= 0 || !Number.isSafeInteger(foregroundProcessGroup)) {
2860
+ throw new Error("invalid process group fields");
2861
+ }
2862
+ return foregroundProcessGroup !== processGroup;
2863
+ }
2864
+
2865
+ function pollForeground() {
2866
+ if (!ptyProcess || !Number.isSafeInteger(ptyProcess.pid) || ptyProcess.pid <= 0) return;
2867
+ if (process.platform === "linux") {
2868
+ try {
2869
+ emitForeground(linuxForegroundBusy(ptyProcess.pid));
2870
+ } catch {
2871
+ emitForeground(true);
2872
+ }
2873
+ return;
2874
+ }
2875
+ if (process.platform !== "darwin" || foregroundPollInFlight) return;
2876
+ foregroundPollInFlight = true;
2877
+ const pid = ptyProcess.pid;
2878
+ execFile("/bin/ps", ["-o", "tpgid=", "-p", String(pid)], (error, stdout) => {
2879
+ foregroundPollInFlight = false;
2880
+ if (!ptyProcess || ptyProcess.pid !== pid) return;
2881
+ const foregroundProcessGroup = Number(String(stdout).trim());
2882
+ emitForeground(Boolean(error) || !Number.isSafeInteger(foregroundProcessGroup) || foregroundProcessGroup !== pid);
2883
+ });
2884
+ }
2885
+
2613
2886
  const rl = readline.createInterface({ input: process.stdin });
2614
2887
 
2615
2888
  rl.on("line", (line) => {
@@ -2628,9 +2901,14 @@ rl.on("line", (line) => {
2628
2901
  send({ type: "output", data: Buffer.from(data, "utf8").toString("base64") });
2629
2902
  });
2630
2903
  ptyProcess.onExit((event) => {
2904
+ if (foregroundTimer) clearInterval(foregroundTimer);
2905
+ foregroundTimer = null;
2631
2906
  send({ type: "exit", exitCode: event.exitCode, signal: event.signal }, () => process.exit(0));
2632
2907
  });
2633
- send({ type: "opened" });
2908
+ send({ type: "opened", pid: ptyProcess.pid });
2909
+ pollForeground();
2910
+ foregroundTimer = setInterval(pollForeground, ${PTY_FOREGROUND_POLL_MS});
2911
+ foregroundTimer.unref();
2634
2912
  return;
2635
2913
  }
2636
2914
 
@@ -2723,7 +3001,11 @@ function createNodePtyBridge(options) {
2723
3001
  };
2724
3002
  const handleEvent = (event) => {
2725
3003
  if (event.type === "opened") {
2726
- options.onOpened();
3004
+ options.onOpened(event.pid);
3005
+ return;
3006
+ }
3007
+ if (event.type === "foreground") {
3008
+ options.onForeground(event.busy);
2727
3009
  return;
2728
3010
  }
2729
3011
  if (event.type === "output") {
@@ -2804,6 +3086,9 @@ function createNodePtyBridge(options) {
2804
3086
  }
2805
3087
  };
2806
3088
  }
3089
+ const workerPtyBridgeTestHarness = {
3090
+ create: createNodePtyBridge
3091
+ };
2807
3092
  function resolveHostShell(command, platform = process.platform) {
2808
3093
  if (platform === "win32") {
2809
3094
  const file2 = process.env.COMSPEC || "powershell.exe";
@@ -2839,64 +3124,90 @@ async function openPty(input) {
2839
3124
  }
2840
3125
  });
2841
3126
  input.assertAdmission();
2842
- const ptyProcess = createNodePtyBridge({
2843
- file: shell.file,
2844
- args: shell.args,
2845
- ptyOptions: {
2846
- name: "xterm-256color",
2847
- cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
2848
- rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
2849
- cwd: input.resolvedTarget.rootPath,
2850
- env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, planProcessEnv, targetProcessEnv, shell.env ?? {}])
2851
- },
2852
- onOpened: () => {
2853
- sendWorkerMessage(input.ws, {
2854
- type: "pty_opened",
2855
- requestId: input.message.requestId,
2856
- ptyId: input.message.ptyId
2857
- });
2858
- },
2859
- onOutput: (data) => {
2860
- outputCoalescer.push(data);
2861
- },
2862
- onExit: (event) => {
2863
- outputCoalescer.flush();
2864
- input.releaseWorkspaceMutation?.();
2865
- activePtys.delete(input.message.ptyId);
2866
- input.onTerminal?.();
2867
- sendWorkerMessage(input.ws, {
2868
- type: "pty_exit",
2869
- ptyId: input.message.ptyId,
2870
- exitCode: event.exitCode,
2871
- signal: event.signal
2872
- });
2873
- },
2874
- onError: (error) => {
2875
- outputCoalescer.flush();
2876
- input.releaseWorkspaceMutation?.();
2877
- activePtys.delete(input.message.ptyId);
2878
- input.onTerminal?.();
2879
- sendWorkerMessage(input.ws, {
2880
- type: "pty_error",
2881
- requestId: input.message.requestId,
2882
- ptyId: input.message.ptyId,
2883
- error: error.message
2884
- });
2885
- },
2886
- onTerminationError: (error) => {
2887
- sendWorkerMessage(input.ws, {
2888
- type: "pty_error",
2889
- requestId: input.message.requestId,
2890
- ptyId: input.message.ptyId,
2891
- error: error.message
2892
- });
2893
- }
2894
- });
2895
- activePtys.set(input.message.ptyId, {
2896
- ...ptyProcess,
2897
- target: input.resolvedTarget.target,
2898
- ...input.releaseWorkspaceMutation ? { releaseWorkspaceMutation: input.releaseWorkspaceMutation } : {}
2899
- });
3127
+ const stagedEnvFiles = stagePtyEnvFiles(input.message.envFiles);
3128
+ let envFilesRemoved = false;
3129
+ const cleanupEnvFiles = () => {
3130
+ if (envFilesRemoved) return;
3131
+ envFilesRemoved = true;
3132
+ removePtyEnvFiles(stagedEnvFiles.paths);
3133
+ };
3134
+ try {
3135
+ const ptyProcess = createNodePtyBridge({
3136
+ file: shell.file,
3137
+ args: shell.args,
3138
+ ptyOptions: {
3139
+ name: "xterm-256color",
3140
+ cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
3141
+ rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
3142
+ cwd: input.resolvedTarget.rootPath,
3143
+ env: workerChildProcessEnvironment([
3144
+ githubProcessEnv(),
3145
+ resolvePtyEnvFileReferences(input.message.env ?? {}, stagedEnvFiles),
3146
+ planProcessEnv,
3147
+ targetProcessEnv,
3148
+ shell.env ?? {}
3149
+ ])
3150
+ },
3151
+ onOpened: () => {
3152
+ sendWorkerMessage(input.ws, {
3153
+ type: "pty_opened",
3154
+ requestId: input.message.requestId,
3155
+ ptyId: input.message.ptyId
3156
+ });
3157
+ },
3158
+ onForeground: (busy) => {
3159
+ const activePty = activePtys.get(input.message.ptyId);
3160
+ if (activePty && (busy || input.message.command === void 0)) activePty.foregroundBusy = busy;
3161
+ },
3162
+ onOutput: (data) => {
3163
+ outputCoalescer.push(data);
3164
+ },
3165
+ onExit: (event) => {
3166
+ outputCoalescer.flush();
3167
+ input.releaseWorkspaceMutation?.();
3168
+ activePtys.delete(input.message.ptyId);
3169
+ cleanupEnvFiles();
3170
+ input.onTerminal?.();
3171
+ sendWorkerMessage(input.ws, {
3172
+ type: "pty_exit",
3173
+ ptyId: input.message.ptyId,
3174
+ exitCode: event.exitCode,
3175
+ signal: event.signal
3176
+ });
3177
+ },
3178
+ onError: (error) => {
3179
+ outputCoalescer.flush();
3180
+ input.releaseWorkspaceMutation?.();
3181
+ activePtys.delete(input.message.ptyId);
3182
+ cleanupEnvFiles();
3183
+ input.onTerminal?.();
3184
+ sendWorkerMessage(input.ws, {
3185
+ type: "pty_error",
3186
+ requestId: input.message.requestId,
3187
+ ptyId: input.message.ptyId,
3188
+ error: error.message
3189
+ });
3190
+ },
3191
+ onTerminationError: (error) => {
3192
+ sendWorkerMessage(input.ws, {
3193
+ type: "pty_error",
3194
+ requestId: input.message.requestId,
3195
+ ptyId: input.message.ptyId,
3196
+ error: error.message
3197
+ });
3198
+ }
3199
+ });
3200
+ activePtys.set(input.message.ptyId, {
3201
+ ...ptyProcess,
3202
+ target: input.resolvedTarget.target,
3203
+ foregroundBusy: true,
3204
+ lastInputAt: 0,
3205
+ ...input.releaseWorkspaceMutation ? { releaseWorkspaceMutation: input.releaseWorkspaceMutation } : {}
3206
+ });
3207
+ } catch (error) {
3208
+ cleanupEnvFiles();
3209
+ throw error;
3210
+ }
2900
3211
  }
2901
3212
  function writePty(ws, message) {
2902
3213
  const ptyProcess = activePtys.get(message.ptyId);
@@ -2908,6 +3219,7 @@ function writePty(ws, message) {
2908
3219
  });
2909
3220
  return;
2910
3221
  }
3222
+ ptyProcess.lastInputAt = Date.now();
2911
3223
  ptyProcess.write(message.data);
2912
3224
  }
2913
3225
  function resizePty(message) {
@@ -3076,6 +3388,9 @@ async function startWorker(options) {
3076
3388
  let workspaceGitIdentity = null;
3077
3389
  let workspaceConfigured = false;
3078
3390
  let activeWorkspaceIncidentId = null;
3391
+ let deferredWorkspaceConfiguration = null;
3392
+ let deferredWorkspaceConfigurationRefreshTimer;
3393
+ let workspaceConfigurationReceiptGeneration = 0;
3079
3394
  let workspaceAutomaticTimer;
3080
3395
  let workspacePeriodicTimer;
3081
3396
  let pendingAutomaticTrigger;
@@ -3345,13 +3660,24 @@ async function startWorker(options) {
3345
3660
  });
3346
3661
  };
3347
3662
  const activeWorkspaceMutationTargets = () => [
3348
- ...Array.from(activeProcesses.values(), ({ target }) => target),
3663
+ ...Array.from(activeProcesses.values()).filter(workerCommandHasWorkspaceEffect).map(({ target }) => target),
3349
3664
  ...credentialBearingProcessGroupTargets.values(),
3350
3665
  ...workspaceSyncPriorityProcessTargets.values(),
3351
3666
  ...workspaceSyncPriorityOperationTargets.values(),
3352
- ...Array.from(activePtys.values(), ({ target }) => target),
3667
+ ...Array.from(activePtys.values()).filter((pty) => workerPtyIsWorkspaceBusy(pty)).map(({ target }) => target),
3353
3668
  ...workspaceSyncPriorityPtyTargets.values()
3354
3669
  ];
3670
+ let visibleWorkspaceMutationEpoch = 0;
3671
+ const projectWorkspaceMutationEpochs = /* @__PURE__ */ new Map();
3672
+ const recordVisibleWorkspaceMutation = (target) => {
3673
+ if (target.type === "workspace") {
3674
+ if (target.rootProfile === "visible_projects") visibleWorkspaceMutationEpoch += 1;
3675
+ return;
3676
+ }
3677
+ const key = projectBranchKey(target.projectId, target.branchName);
3678
+ projectWorkspaceMutationEpochs.set(key, (projectWorkspaceMutationEpochs.get(key) ?? 0) + 1);
3679
+ };
3680
+ const projectWorkspaceMutationToken = (projectId, branchName) => `${visibleWorkspaceMutationEpoch}:${projectWorkspaceMutationEpochs.get(projectBranchKey(projectId, branchName)) ?? 0}`;
3355
3681
  const canonicalWorkspaceMutationIsActive = (targets) => targets.some((target) => target.type === "workspace" && target.rootProfile === "canonical_sync");
3356
3682
  const projectBranchMountActivityBusy = (projectId, branchName, branchPath) => {
3357
3683
  const activeTargets = activeWorkspaceMutationTargets();
@@ -3388,6 +3714,7 @@ async function startWorker(options) {
3388
3714
  preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3389
3715
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3390
3716
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3717
+ mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3391
3718
  busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3392
3719
  });
3393
3720
  mounts.push({
@@ -3401,6 +3728,7 @@ async function startWorker(options) {
3401
3728
  preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3402
3729
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3403
3730
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3731
+ mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3404
3732
  busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3405
3733
  });
3406
3734
  }
@@ -3421,6 +3749,7 @@ async function startWorker(options) {
3421
3749
  const activeTargets = activeWorkspaceMutationTargets();
3422
3750
  return canonicalWorkspaceMutationIsActive(activeTargets) || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
3423
3751
  },
3752
+ mutationToken: () => String(visibleWorkspaceMutationEpoch),
3424
3753
  busyForRecovery: () => {
3425
3754
  const activeTargets = activeWorkspaceMutationTargets();
3426
3755
  return canonicalWorkspaceMutationIsActive(activeTargets) || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
@@ -3778,6 +4107,8 @@ async function startWorker(options) {
3778
4107
  localChangesDiscarded: false,
3779
4108
  ...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
3780
4109
  ...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
4110
+ ...result.conflictKind ? { conflictKind: result.conflictKind } : {},
4111
+ ...result.verifiedAncestorHeads ? { verifiedAncestorHeads: result.verifiedAncestorHeads } : {},
3781
4112
  ...result.error ? { error: result.error } : {}
3782
4113
  };
3783
4114
  };
@@ -3844,10 +4175,7 @@ async function startWorker(options) {
3844
4175
  };
3845
4176
  }
3846
4177
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
3847
- const ordinaryCycleHasBusyLiveMount = mounts.some(
3848
- (mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()
3849
- );
3850
- const observedProjectHeads = outerRemediation || ordinaryCycleHasBusyLiveMount ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4178
+ const observedProjectHeads = outerRemediation ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
3851
4179
  const inboundMoveMountIds = new Set(
3852
4180
  observedProjectHeads.flatMap(
3853
4181
  ({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
@@ -3866,10 +4194,13 @@ async function startWorker(options) {
3866
4194
  commitDetail: input.confirmationReason ?? input.trigger.detail,
3867
4195
  allowLargeDiff: input.confirmedLargeDiff,
3868
4196
  skipMountMirror: outerRemediation,
4197
+ requiredAncestorHeads: input.requiredAncestorHeads,
4198
+ assertStillAdmitted: input.assertStillAdmitted,
3869
4199
  afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
3870
4200
  pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
3871
4201
  }
3872
4202
  });
4203
+ input.assertStillAdmitted?.();
3873
4204
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
3874
4205
  applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
3875
4206
  }
@@ -3892,6 +4223,28 @@ async function startWorker(options) {
3892
4223
  const runWorkspaceSync = async (input) => {
3893
4224
  const attemptId = input.attemptId ?? crypto.randomUUID();
3894
4225
  const requestedAt = Date.now();
4226
+ const admittedGeneration = workerAdmissionGeneration;
4227
+ const admittedActiveIncidentId = activeWorkspaceIncidentId;
4228
+ const admittedDeferredIncidentId = deferredWorkspaceConfiguration?.incidentId ?? null;
4229
+ const assertStillAdmitted = () => {
4230
+ if (admittedGeneration !== workerAdmissionGeneration || admittedActiveIncidentId !== activeWorkspaceIncidentId || admittedDeferredIncidentId !== (deferredWorkspaceConfiguration?.incidentId ?? null) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || !deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4231
+ throw new Error("Workspace synchronization admission changed while asynchronous work was in flight");
4232
+ }
4233
+ };
4234
+ if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4235
+ const result2 = {
4236
+ ...failedWorkspaceSyncResult(
4237
+ attemptId,
4238
+ input.trigger,
4239
+ new Error(
4240
+ `Workspace synchronization is deferred for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}; only remediation synchronization is allowed`
4241
+ )
4242
+ ),
4243
+ telemetry: { totalMs: 0, queueMs: 0, prepareMs: 0, synchronizeMs: 0 }
4244
+ };
4245
+ if (input.sendResult !== false) sendWorkspaceSyncResult(input.requestId, result2);
4246
+ return result2;
4247
+ }
3895
4248
  if (!input.requestId && input.sendResult !== false) {
3896
4249
  if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
3897
4250
  throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
@@ -3906,23 +4259,32 @@ async function startWorker(options) {
3906
4259
  result = await workspaceSyncSingleFlight.runExclusive(async () => {
3907
4260
  queueEnteredAt = Date.now();
3908
4261
  syncStartedAt = Date.now();
4262
+ if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
4263
+ return failedWorkspaceSyncResult(
4264
+ attemptId,
4265
+ input.trigger,
4266
+ new Error(
4267
+ `Workspace synchronization was fenced while queued by incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
4268
+ )
4269
+ );
4270
+ }
3909
4271
  try {
4272
+ assertStillAdmitted();
3910
4273
  return await performWorkspaceSync({
3911
4274
  attemptId,
3912
4275
  trigger: input.trigger,
3913
4276
  confirmedLargeDiff: input.confirmedLargeDiff,
3914
4277
  confirmationReason: input.confirmationReason,
3915
- resetToCanonical: input.resetToCanonical
4278
+ resetToCanonical: input.resetToCanonical,
4279
+ requiredAncestorHeads: input.requiredAncestorHeads,
4280
+ assertStillAdmitted
3916
4281
  });
3917
4282
  } catch (error) {
3918
- let hydrationCurrent = input.resetToCanonical !== true;
3919
- if (hydrationCurrent) {
3920
- try {
3921
- hydrationCurrent = workspaceGitHydrationIsCurrent(workspaceShadowRoot, buildWorkspaceMounts());
3922
- } catch {
3923
- hydrationCurrent = false;
3924
- }
3925
- }
4283
+ const hydrationCurrent = workspaceSyncFailureHydrationIsSafe({
4284
+ error,
4285
+ resetToCanonical: input.resetToCanonical === true,
4286
+ inspectCurrentHydration: () => workspaceGitHydrationIsCurrent(workspaceShadowRoot, buildWorkspaceMounts())
4287
+ });
3926
4288
  if (!hydrationCurrent) {
3927
4289
  process.stderr.write(
3928
4290
  `[r5d-worker] workspace synchronization failed with an incompletely hydrated visible tree; exiting for exact recovery: ${error instanceof Error ? error.message : String(error)}
@@ -3964,7 +4326,7 @@ async function startWorker(options) {
3964
4326
  pendingCreatedBranchPublicationNotBefore
3965
4327
  );
3966
4328
  const initialDeferral = pendingBranchDeferral();
3967
- if (!workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
4329
+ if (!workspaceConfigured || activeWorkspaceIncidentId || deferredWorkspaceConfiguration || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN || automaticSyncInFlight || initialDeferral?.kind === "active_target") {
3968
4330
  return;
3969
4331
  }
3970
4332
  if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
@@ -3972,7 +4334,8 @@ async function startWorker(options) {
3972
4334
  () => {
3973
4335
  workspaceAutomaticTimer = void 0;
3974
4336
  const scheduledTrigger = pendingAutomaticTrigger;
3975
- if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
4337
+ if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || deferredWorkspaceConfiguration || currentWorkerSocket !== ws)
4338
+ return;
3976
4339
  const currentDeferral = pendingBranchDeferral();
3977
4340
  if (currentDeferral?.kind === "active_target") return;
3978
4341
  if (currentDeferral?.kind === "creation_grace") {
@@ -3994,7 +4357,7 @@ async function startWorker(options) {
3994
4357
  }).finally(() => {
3995
4358
  automaticSyncInFlight = false;
3996
4359
  heartbeatBusyGrace = grantWorkerHeartbeatBusyGrace(lastServerHeartbeatAt, heartbeatBusyGrace);
3997
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
4360
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
3998
4361
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
3999
4362
  }
4000
4363
  });
@@ -4003,12 +4366,18 @@ async function startWorker(options) {
4003
4366
  );
4004
4367
  workspaceAutomaticTimer.unref();
4005
4368
  };
4006
- const markWorkspaceDirty = (trigger, force = false) => {
4369
+ const markWorkspaceDirty = (trigger, force = false, target) => {
4370
+ if (target) recordVisibleWorkspaceMutation(target);
4007
4371
  scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
4008
4372
  };
4009
4373
  const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
4010
4374
  const resolveMessageTarget = (target) => {
4011
4375
  if (!workspaceConfigured) throw new Error("Worker workspace configuration has not completed successfully");
4376
+ if (!deferredWorkspaceTargetIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, target)) {
4377
+ throw new Error(
4378
+ `Worker workspace configuration is fenced for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}; only canonical remediation commands are allowed`
4379
+ );
4380
+ }
4012
4381
  if (target.type === "project" && !readyProjectIds.has(target.projectId)) {
4013
4382
  throw new Error(`Project ${target.projectId} is not ready on this worker`);
4014
4383
  }
@@ -4022,7 +4391,26 @@ async function startWorker(options) {
4022
4391
  projectConfigById
4023
4392
  });
4024
4393
  };
4025
- const configureWorkerWorkspace = async (message) => {
4394
+ const configureWorkerWorkspace = async (message, receiptGeneration) => {
4395
+ const incidentDeferral = workspaceConfigurationIncidentDeferral({
4396
+ requestedIncidentId: message.deferWorkspaceSyncForIncidentId,
4397
+ activeIncidentId: activeWorkspaceIncidentId
4398
+ });
4399
+ if (incidentDeferral.error) {
4400
+ workspaceConfigured = false;
4401
+ return {
4402
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, incidentDeferral.error),
4403
+ pending: incidentDeferredProjectCheckouts(message.projects),
4404
+ aheadOfOriginBranches: []
4405
+ };
4406
+ }
4407
+ if (incidentDeferral.incidentId) {
4408
+ workspaceConfigured = false;
4409
+ deferredWorkspaceConfiguration = {
4410
+ incidentId: incidentDeferral.incidentId,
4411
+ serverRefreshExpected: message.deferWorkspaceSyncForIncidentId === incidentDeferral.incidentId
4412
+ };
4413
+ }
4026
4414
  const incomingCredentialGenerationFingerprint = credentialGenerationFingerprint({
4027
4415
  ...message,
4028
4416
  workerBaseUrl: baseUrl,
@@ -4033,6 +4421,22 @@ async function startWorker(options) {
4033
4421
  pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
4034
4422
  incomingFingerprint: incomingCredentialGenerationFingerprint
4035
4423
  });
4424
+ if (deferredCredentialTransitionMustWait({
4425
+ incidentId: incidentDeferral.incidentId,
4426
+ transitionPhase: credentialTransitionPhase,
4427
+ canonicalRemediationActive: canonicalWorkspaceMutationIsActive(activeWorkspaceMutationTargets())
4428
+ })) {
4429
+ return {
4430
+ result: failedWorkspaceSyncResult(
4431
+ crypto.randomUUID(),
4432
+ { type: "connect" },
4433
+ new Error("Workspace credential rotation is deferred until the active canonical remediation command finishes")
4434
+ ),
4435
+ pending: incidentDeferredProjectCheckouts(message.projects),
4436
+ aheadOfOriginBranches: [],
4437
+ ...incidentDeferral.incidentId ? { deferredWorkspaceSyncForIncidentId: incidentDeferral.incidentId } : {}
4438
+ };
4439
+ }
4036
4440
  const credentialReapStatus = credentialReapContractStatus();
4037
4441
  const verifiedCredentialReapContractNow = credentialReapStatus === "verified_systemd";
4038
4442
  const credentialRestartIsContained = credentialGenerationRestartIsContained({
@@ -4146,6 +4550,12 @@ async function startWorker(options) {
4146
4550
  workspaceSyncRequestsInFlight += 1;
4147
4551
  try {
4148
4552
  return await workspaceSyncSingleFlight.runExclusive(async () => {
4553
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4554
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4555
+ }
4556
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4557
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4558
+ }
4149
4559
  for (const project of message.projects) assertRepositoryTransitionState(project);
4150
4560
  const busyConfigurationChanges = busyProjectConfigurationChangeIds({
4151
4561
  currentProjects: [...projectConfigById.values()],
@@ -4167,10 +4577,12 @@ async function startWorker(options) {
4167
4577
  const preserveOnlyBranches = message.projects.flatMap(
4168
4578
  (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
4169
4579
  );
4170
- projectWorkspaceState = projectWorkspaceStateStore.reconcile({
4171
- desiredProjects: message.projects,
4172
- preserveOnlyBranches
4173
- });
4580
+ if (!incidentDeferral.incidentId) {
4581
+ projectWorkspaceState = projectWorkspaceStateStore.reconcile({
4582
+ desiredProjects: message.projects,
4583
+ preserveOnlyBranches
4584
+ });
4585
+ }
4174
4586
  const stillPendingCreatedBranches = new Map(
4175
4587
  projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
4176
4588
  pendingCreatedBranchKey(branch.projectId, branch.branchName),
@@ -4230,6 +4642,12 @@ async function startWorker(options) {
4230
4642
  void 0,
4231
4643
  credentialPublicationPreauthorized
4232
4644
  );
4645
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4646
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4647
+ }
4648
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4649
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4650
+ }
4233
4651
  configuredCredentialGenerationFingerprint = incomingCredentialGenerationFingerprint;
4234
4652
  pendingCredentialGenerationIntentAtProcessStart = null;
4235
4653
  bootstrapCredentialGenerationFingerprintAtProcessStart = null;
@@ -4244,6 +4662,34 @@ async function startWorker(options) {
4244
4662
  branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
4245
4663
  }));
4246
4664
  const nextProjectConfigById = new Map(effectiveProjects.map((project) => [project.projectId, project]));
4665
+ if (incidentDeferral.incidentId) {
4666
+ configureExistingWorkspaceGitForRemediation({
4667
+ workspacePath: workspaceShadowRoot,
4668
+ remoteUrl: message.workspaceRemoteUrl,
4669
+ credentialHelper: nextWorkspaceCredentialHelper,
4670
+ credentialUsername: workerCredentialUsername,
4671
+ gitIdentity: message.gitIdentity
4672
+ });
4673
+ projectConfigById.clear();
4674
+ for (const project of effectiveProjects) projectConfigById.set(project.projectId, project);
4675
+ readyProjectIds.clear();
4676
+ reconciledProjectConfigFingerprints.clear();
4677
+ pendingCheckouts.clear();
4678
+ const pending2 = incidentDeferredProjectCheckouts(message.projects);
4679
+ for (const checkout of pending2) pendingCheckouts.set(projectBranchKey(checkout.projectId, checkout.branchName), checkout);
4680
+ workspaceConfigured = activeWorkspaceIncidentId === incidentDeferral.incidentId;
4681
+ return {
4682
+ result: deferredIncidentWorkspaceConfigurationResult({
4683
+ attemptId: crypto.randomUUID(),
4684
+ workerLabel: label,
4685
+ head: workspaceLocalHead(),
4686
+ skippedMountIds: buildWorkspaceMounts().map(({ id }) => id)
4687
+ }),
4688
+ pending: pending2,
4689
+ aheadOfOriginBranches: [],
4690
+ deferredWorkspaceSyncForIncidentId: incidentDeferral.incidentId
4691
+ };
4692
+ }
4247
4693
  ensureWorkspaceGitClone({
4248
4694
  workspacePath: workspaceShadowRoot,
4249
4695
  remoteUrl: message.workspaceRemoteUrl,
@@ -4264,7 +4710,7 @@ async function startWorker(options) {
4264
4710
  sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
4265
4711
  busy: mount.busyForRecovery ?? mount.busy
4266
4712
  }));
4267
- recoverWorkspaceGitHydration(workspaceShadowRoot, recoveryMounts);
4713
+ recoverWorkspaceGitHydration(workspaceShadowRoot, recoveryMounts, { preserveStaleBases: true });
4268
4714
  for (const project of message.projects) {
4269
4715
  for (const { branchName } of project.preserveOnlyBranches) {
4270
4716
  if (stillPendingCreatedBranches.has(pendingCreatedBranchKey(project.projectId, branchName))) {
@@ -4311,10 +4757,24 @@ async function startWorker(options) {
4311
4757
  { ignoreBusy: true }
4312
4758
  );
4313
4759
  ensureConfiguredProjects();
4760
+ const configurationSyncAdmissionGeneration = workerAdmissionGeneration;
4761
+ const assertConfigurationSyncStillAdmitted = () => {
4762
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration || configurationSyncAdmissionGeneration !== workerAdmissionGeneration || !workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
4763
+ throw new Error("Workspace configuration synchronization admission changed while asynchronous work was in flight");
4764
+ }
4765
+ };
4314
4766
  const result = await performWorkspaceSync({
4315
4767
  attemptId: crypto.randomUUID(),
4316
- trigger: { type: "connect" }
4768
+ trigger: { type: "connect" },
4769
+ assertStillAdmitted: assertConfigurationSyncStillAdmitted
4317
4770
  });
4771
+ assertConfigurationSyncStillAdmitted();
4772
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4773
+ throw new Error("Workspace configuration was superseded by a newer server generation");
4774
+ }
4775
+ if (!workspaceConfigurationIncidentSnapshotIsCurrent(incidentDeferral.incidentId, activeWorkspaceIncidentId)) {
4776
+ throw new Error("Workspace configuration was superseded by a different active workspace incident");
4777
+ }
4318
4778
  const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
4319
4779
  if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
4320
4780
  const activeMountIds = new Set(result.activeMountIds ?? []);
@@ -4331,6 +4791,11 @@ async function startWorker(options) {
4331
4791
  });
4332
4792
  }
4333
4793
  }
4794
+ if (deferredWorkspaceConfigurationRefreshTimer) {
4795
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
4796
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
4797
+ }
4798
+ deferredWorkspaceConfiguration = null;
4334
4799
  workspaceConfigured = true;
4335
4800
  const pending = [...pendingCheckouts.values()].sort(
4336
4801
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
@@ -4338,6 +4803,13 @@ async function startWorker(options) {
4338
4803
  return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
4339
4804
  });
4340
4805
  } catch (error) {
4806
+ if (receiptGeneration !== workspaceConfigurationReceiptGeneration) {
4807
+ return {
4808
+ result: failedWorkspaceSyncResult(crypto.randomUUID(), { type: "connect" }, error),
4809
+ pending: incidentDeferredProjectCheckouts(message.projects),
4810
+ aheadOfOriginBranches: []
4811
+ };
4812
+ }
4341
4813
  if (error instanceof RegistryAuthConfigurationError) {
4342
4814
  workspaceConfigured = false;
4343
4815
  githubCredential = null;
@@ -4392,7 +4864,7 @@ async function startWorker(options) {
4392
4864
  workspaceAutomaticTimer = void 0;
4393
4865
  }
4394
4866
  void (async () => {
4395
- if (workspaceConfigured && !activeWorkspaceIncidentId) {
4867
+ if (workspaceConfigured && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
4396
4868
  await runWorkspaceSync({
4397
4869
  trigger: { type: "manual", detail: "graceful worker shutdown" }
4398
4870
  });
@@ -4440,7 +4912,10 @@ async function startWorker(options) {
4440
4912
  capabilities: {
4441
4913
  updateClis: true,
4442
4914
  browserPortForwarding: true,
4443
- execStdinV1: true
4915
+ execStdinV1: true,
4916
+ ptyEnvFilesV1: true,
4917
+ workspaceRemediationAncestorGuardV1: true,
4918
+ workspaceIncidentConfigDeferralV1: true
4444
4919
  },
4445
4920
  projectRoot: projectsRoot,
4446
4921
  artifactRoot,
@@ -4476,26 +4951,35 @@ async function startWorker(options) {
4476
4951
  return;
4477
4952
  }
4478
4953
  if (message.type === "workspace_config") {
4954
+ const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
4955
+ const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
4479
4956
  advanceWorkerAdmissionGeneration();
4480
- const configured = await configureWorkerWorkspace(message);
4957
+ const configured = await configureWorkerWorkspace(message, receiptGeneration);
4481
4958
  sendWorkerMessageFromCurrentSource(ws, {
4482
4959
  type: "workspace_configured",
4483
4960
  requestId: message.requestId,
4484
4961
  result: configured.result,
4485
4962
  pendingCheckouts: configured.pending,
4486
- aheadOfOriginBranches: configured.aheadOfOriginBranches
4963
+ aheadOfOriginBranches: configured.aheadOfOriginBranches,
4964
+ ...configured.deferredWorkspaceSyncForIncidentId ? { deferredWorkspaceSyncForIncidentId: configured.deferredWorkspaceSyncForIncidentId } : {}
4487
4965
  });
4488
4966
  process.stdout.write(
4489
4967
  `[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
4490
4968
  `
4491
4969
  );
4970
+ if (refreshesDeferredConfiguration && configured.result.outcome === "failed" && deferredWorkspaceConfiguration !== null && currentWorkerSocket === ws) {
4971
+ workspaceConfigured = false;
4972
+ advanceWorkerAdmissionGeneration();
4973
+ ws.close(1012, "Deferred workspace configuration refresh failed");
4974
+ return;
4975
+ }
4492
4976
  if (!workspacePeriodicTimer) {
4493
4977
  workspacePeriodicTimer = setInterval(() => {
4494
4978
  scheduleAutomaticWorkspaceSync({ type: "periodic", detail: "periodic workspace reconciliation" }, 0);
4495
4979
  }, WORKSPACE_GIT_PERIODIC_MS);
4496
4980
  workspacePeriodicTimer.unref();
4497
4981
  }
4498
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
4982
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
4499
4983
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, 0);
4500
4984
  }
4501
4985
  return;
@@ -4507,11 +4991,20 @@ async function startWorker(options) {
4507
4991
  trigger: message.trigger,
4508
4992
  confirmedLargeDiff: message.confirmedLargeDiff,
4509
4993
  confirmationReason: message.confirmationReason,
4510
- resetToCanonical: message.resetToCanonical
4994
+ resetToCanonical: message.resetToCanonical,
4995
+ requiredAncestorHeads: message.requiredAncestorHeads
4511
4996
  });
4512
4997
  return;
4513
4998
  }
4514
4999
  if (message.type === "create_project_branch") {
5000
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5001
+ sendWorkerMessage(ws, {
5002
+ type: "operation_result",
5003
+ requestId: message.requestId,
5004
+ error: `Project operations are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5005
+ });
5006
+ return;
5007
+ }
4515
5008
  try {
4516
5009
  const pendingBranch = {
4517
5010
  branchId: message.branchId,
@@ -4519,6 +5012,11 @@ async function startWorker(options) {
4519
5012
  branchName: message.targetBranch
4520
5013
  };
4521
5014
  const created = await workspaceSyncSingleFlight.runMutation(() => {
5015
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5016
+ throw new Error(
5017
+ `Project branch creation was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5018
+ );
5019
+ }
4522
5020
  const project = projectConfigById.get(message.projectId);
4523
5021
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
4524
5022
  assertRepositoryExecutionEnabled(project);
@@ -4602,9 +5100,22 @@ async function startWorker(options) {
4602
5100
  return;
4603
5101
  }
4604
5102
  if (message.type === "delete_project_branch") {
5103
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5104
+ sendWorkerMessage(ws, {
5105
+ type: "operation_result",
5106
+ requestId: message.requestId,
5107
+ error: `Project operations are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5108
+ });
5109
+ return;
5110
+ }
4605
5111
  try {
4606
5112
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
4607
5113
  const deletionNeedsSync = await workspaceSyncSingleFlight.runMutation(() => {
5114
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5115
+ throw new Error(
5116
+ `Project branch deletion was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5117
+ );
5118
+ }
4608
5119
  const project = projectConfigById.get(message.projectId);
4609
5120
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
4610
5121
  assertRepositoryExecutionEnabled(project);
@@ -4704,8 +5215,38 @@ async function startWorker(options) {
4704
5215
  }
4705
5216
  if (message.type === "workspace_incident_updated") {
4706
5217
  const previousIncidentId = activeWorkspaceIncidentId;
4707
- activeWorkspaceIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
5218
+ const nextIncidentId = applyWorkspaceIncidentUpdate(activeWorkspaceIncidentId, message);
5219
+ if (nextIncidentId !== previousIncidentId) advanceWorkerAdmissionGeneration();
5220
+ activeWorkspaceIncidentId = nextIncidentId;
4708
5221
  if (previousIncidentId && !activeWorkspaceIncidentId && (message.status === "resolved" || message.status === "confirmed" || message.status === "reset")) {
5222
+ const disposition = workspaceIncidentTerminalClearDisposition({
5223
+ deferredConfiguration: deferredWorkspaceConfiguration,
5224
+ clearedIncidentId: previousIncidentId
5225
+ });
5226
+ if (disposition !== "ordinary_resume") {
5227
+ pendingAutomaticTrigger ??= { type: "periodic", detail: "resume after workspace incident" };
5228
+ workspaceConfigured = false;
5229
+ advanceWorkerAdmissionGeneration();
5230
+ if (disposition === "reconnect_for_refresh") {
5231
+ ws.close(1012, "Refreshing deferred workspace configuration");
5232
+ } else {
5233
+ if (deferredWorkspaceConfigurationRefreshTimer) clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5234
+ const deferredIncidentId = deferredWorkspaceConfiguration?.incidentId;
5235
+ deferredWorkspaceConfigurationRefreshTimer = setTimeout(() => {
5236
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
5237
+ if (!deferredIncidentId || !deferredWorkspaceRefreshWatchdogIsCurrent({
5238
+ capturedIncidentId: deferredIncidentId,
5239
+ deferredConfiguration: deferredWorkspaceConfiguration,
5240
+ activeIncidentId: activeWorkspaceIncidentId
5241
+ }) || currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
5242
+ return;
5243
+ }
5244
+ ws.close(1012, "Timed out waiting for deferred workspace configuration refresh");
5245
+ }, WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS);
5246
+ deferredWorkspaceConfigurationRefreshTimer.unref();
5247
+ }
5248
+ return;
5249
+ }
4709
5250
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger ?? { type: "periodic", detail: "resume after workspace incident" }, 0);
4710
5251
  }
4711
5252
  return;
@@ -4809,12 +5350,21 @@ async function startWorker(options) {
4809
5350
  sendAck({ error: `Process run ${message.runId} is not active on this worker` });
4810
5351
  return;
4811
5352
  }
5353
+ if (!deferredWorkspaceTargetIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, active.target)) {
5354
+ sendAck({
5355
+ error: `Process input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
5356
+ });
5357
+ return;
5358
+ }
4812
5359
  if (!active.interactive || !active.stdin) {
4813
5360
  sendAck({
4814
5361
  error: `Process run ${message.runId} has no open stdin. Start a new shell command with "interactive": true to write to its stdin.`
4815
5362
  });
4816
5363
  return;
4817
5364
  }
5365
+ if (active.workspaceEffect !== "none" && targetMayMutateVisibleWorkspace(active.target)) {
5366
+ recordVisibleWorkspaceMutation(active.target);
5367
+ }
4818
5368
  try {
4819
5369
  let bytesWritten = 0;
4820
5370
  if (message.data !== void 0 && message.data.length > 0) {
@@ -4825,8 +5375,8 @@ async function startWorker(options) {
4825
5375
  await active.stdin.end();
4826
5376
  active.stdin = void 0;
4827
5377
  }
4828
- if (targetMayMutateVisibleWorkspace(active.target)) {
4829
- markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` });
5378
+ if (active.workspaceEffect !== "none" && targetMayMutateVisibleWorkspace(active.target)) {
5379
+ markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` }, false, active.target);
4830
5380
  }
4831
5381
  sendAck({
4832
5382
  result: {
@@ -4851,13 +5401,28 @@ async function startWorker(options) {
4851
5401
  });
4852
5402
  return;
4853
5403
  }
5404
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5405
+ sendWorkerMessage(ws, {
5406
+ type: "pty_error",
5407
+ requestId: message.requestId,
5408
+ ptyId: message.ptyId,
5409
+ error: `Shells are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5410
+ });
5411
+ return;
5412
+ }
4854
5413
  await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
4855
5414
  workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
5415
+ if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
4856
5416
  });
4857
5417
  let releaseWorkspaceMutation;
4858
5418
  let mutationLeaseTransferred = false;
4859
5419
  try {
4860
5420
  releaseWorkspaceMutation = await acquireWorkspaceCommandMutation(message.target, workspaceSyncSingleFlight);
5421
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5422
+ throw new Error(
5423
+ `Shell opening was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5424
+ );
5425
+ }
4861
5426
  const resolvedTarget = resolveMessageTarget(message.target);
4862
5427
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
4863
5428
  `);
@@ -4870,7 +5435,7 @@ async function startWorker(options) {
4870
5435
  ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
4871
5436
  ...targetMayMutateVisibleWorkspace(message.target) ? {
4872
5437
  onTerminal: () => {
4873
- markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true);
5438
+ markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true, message.target);
4874
5439
  }
4875
5440
  } : {}
4876
5441
  });
@@ -4890,8 +5455,16 @@ async function startWorker(options) {
4890
5455
  }
4891
5456
  if (message.type === "pty_input") {
4892
5457
  const activePty = activePtys.get(message.ptyId);
5458
+ if (activePty && workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5459
+ sendWorkerMessage(ws, {
5460
+ type: "pty_error",
5461
+ ptyId: message.ptyId,
5462
+ error: `Shell input is deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId ?? "unknown"}`
5463
+ });
5464
+ return;
5465
+ }
4893
5466
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
4894
- markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` });
5467
+ markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
4895
5468
  }
4896
5469
  writePty(ws, message);
4897
5470
  return;
@@ -4904,7 +5477,7 @@ async function startWorker(options) {
4904
5477
  const activePty = activePtys.get(message.ptyId);
4905
5478
  closePty(message);
4906
5479
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
4907
- markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true);
5480
+ markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
4908
5481
  }
4909
5482
  return;
4910
5483
  }
@@ -4919,14 +5492,34 @@ async function startWorker(options) {
4919
5492
  });
4920
5493
  return;
4921
5494
  }
5495
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5496
+ sendWorkerMessage(ws, {
5497
+ type: "exec_result",
5498
+ requestId: message.requestId,
5499
+ stdout: "",
5500
+ stderr: "",
5501
+ exitCode: 1,
5502
+ error: `One-shot commands are deferred for workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5503
+ });
5504
+ return;
5505
+ }
4922
5506
  let result;
4923
5507
  let targetReserved = false;
5508
+ const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
4924
5509
  try {
4925
- await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
4926
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
4927
- targetReserved = true;
4928
- });
5510
+ if (hasWorkspaceEffect) {
5511
+ await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
5512
+ workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
5513
+ targetReserved = true;
5514
+ if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
5515
+ });
5516
+ }
4929
5517
  const runCommand = async () => {
5518
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5519
+ throw new Error(
5520
+ `One-shot command was fenced while queued by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5521
+ );
5522
+ }
4930
5523
  const resolvedTarget = resolveMessageTarget(message.target);
4931
5524
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
4932
5525
  `);
@@ -4937,7 +5530,14 @@ async function startWorker(options) {
4937
5530
  token,
4938
5531
  artifactRoot,
4939
5532
  planRoot,
4940
- assertAdmission: assertMessageAdmission
5533
+ assertAdmission: () => {
5534
+ if (!workspaceCommandTransportIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, "exec")) {
5535
+ throw new Error(
5536
+ `One-shot command was fenced before spawn by workspace incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}`
5537
+ );
5538
+ }
5539
+ assertMessageAdmission();
5540
+ }
4941
5541
  });
4942
5542
  };
4943
5543
  result = await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
@@ -4955,10 +5555,11 @@ async function startWorker(options) {
4955
5555
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
4956
5556
  }
4957
5557
  ws.send(JSON.stringify(result));
4958
- if (targetMayMutateVisibleWorkspace(message.target)) {
5558
+ if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
4959
5559
  markWorkspaceDirty(
4960
5560
  { type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
4961
- true
5561
+ true,
5562
+ message.target
4962
5563
  );
4963
5564
  }
4964
5565
  return;
@@ -4978,6 +5579,7 @@ async function startWorker(options) {
4978
5579
  requestId: message.requestId,
4979
5580
  runId: message.runId
4980
5581
  });
5582
+ const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
4981
5583
  const runCommand = async () => {
4982
5584
  try {
4983
5585
  const resolvedTarget = resolveMessageTarget(message.target);
@@ -4994,7 +5596,7 @@ async function startWorker(options) {
4994
5596
  assertAdmission: assertMessageAdmission
4995
5597
  });
4996
5598
  } finally {
4997
- if (targetMayMutateVisibleWorkspace(message.target)) {
5599
+ if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
4998
5600
  markWorkspaceDirty(
4999
5601
  {
5000
5602
  type: "process_terminal",
@@ -5002,7 +5604,8 @@ async function startWorker(options) {
5002
5604
  processRunId: message.runId,
5003
5605
  detail: "process completed"
5004
5606
  },
5005
- true
5607
+ true,
5608
+ message.target
5006
5609
  );
5007
5610
  }
5008
5611
  }
@@ -5010,10 +5613,13 @@ async function startWorker(options) {
5010
5613
  const execution = (async () => {
5011
5614
  let targetReserved = false;
5012
5615
  try {
5013
- await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
5014
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
5015
- targetReserved = true;
5016
- });
5616
+ if (hasWorkspaceEffect) {
5617
+ await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
5618
+ workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
5619
+ targetReserved = true;
5620
+ if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
5621
+ });
5622
+ }
5017
5623
  await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
5018
5624
  } finally {
5019
5625
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
@@ -5031,11 +5637,20 @@ async function startWorker(options) {
5031
5637
  return;
5032
5638
  }
5033
5639
  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") {
5640
+ if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
5641
+ sendWorkerMessage(ws, {
5642
+ type: "operation_result",
5643
+ requestId: message.requestId,
5644
+ error: `Workspace operations are deferred for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}; use a canonical remediation shell`
5645
+ });
5646
+ return;
5647
+ }
5034
5648
  const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
5035
5649
  const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
5036
5650
  if (reservesVisibleWorkspace) {
5037
5651
  await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
5038
5652
  workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
5653
+ if (mutatesVisibleWorkspace) recordVisibleWorkspaceMutation(message.target);
5039
5654
  });
5040
5655
  }
5041
5656
  let dirtyTrigger;
@@ -5048,7 +5663,8 @@ async function startWorker(options) {
5048
5663
  baseUrl,
5049
5664
  token,
5050
5665
  artifactRoot,
5051
- planRoot
5666
+ planRoot,
5667
+ assertAdmission: assertMessageAdmission
5052
5668
  });
5053
5669
  });
5054
5670
  ws.send(
@@ -5065,7 +5681,7 @@ async function startWorker(options) {
5065
5681
  toolCallId: message.requestId,
5066
5682
  ...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
5067
5683
  };
5068
- markWorkspaceDirty(dirtyTrigger);
5684
+ markWorkspaceDirty(dirtyTrigger, false, message.target);
5069
5685
  }
5070
5686
  } catch (error) {
5071
5687
  ws.send(
@@ -5078,7 +5694,7 @@ async function startWorker(options) {
5078
5694
  } finally {
5079
5695
  if (reservesVisibleWorkspace) {
5080
5696
  workspaceSyncPriorityOperationTargets.delete(message.requestId);
5081
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
5697
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
5082
5698
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, dirtyTrigger ? WORKSPACE_GIT_QUIET_MS : 0);
5083
5699
  }
5084
5700
  }
@@ -5114,6 +5730,10 @@ async function startWorker(options) {
5114
5730
  clearInterval(workspacePeriodicTimer);
5115
5731
  workspacePeriodicTimer = void 0;
5116
5732
  }
5733
+ if (deferredWorkspaceConfigurationRefreshTimer) {
5734
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5735
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
5736
+ }
5117
5737
  if (terminalReplayTimer) {
5118
5738
  clearInterval(terminalReplayTimer);
5119
5739
  terminalReplayTimer = void 0;
@@ -5225,5 +5845,7 @@ export {
5225
5845
  syncSessionArtifacts,
5226
5846
  workerChildProcessEnvironment,
5227
5847
  workerGitSecurityTestHarness,
5848
+ workerPtyBridgeTestHarness,
5849
+ workerPtyTestHarness,
5228
5850
  writeWorkerTextFile
5229
5851
  };