@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/cjs/main.cjs CHANGED
@@ -50,6 +50,8 @@ __export(main_exports, {
50
50
  syncSessionArtifacts: () => syncSessionArtifacts,
51
51
  workerChildProcessEnvironment: () => workerChildProcessEnvironment,
52
52
  workerGitSecurityTestHarness: () => workerGitSecurityTestHarness,
53
+ workerPtyBridgeTestHarness: () => workerPtyBridgeTestHarness,
54
+ workerPtyTestHarness: () => workerPtyTestHarness,
53
55
  writeWorkerTextFile: () => writeWorkerTextFile
54
56
  });
55
57
  module.exports = __toCommonJS(main_exports);
@@ -95,6 +97,11 @@ const DEFAULT_READ_MAX_BYTES = 5e4;
95
97
  const MAX_LINE_LENGTH = 2e3;
96
98
  const WORKSPACE_GIT_QUIET_MS = 5e3;
97
99
  const WORKSPACE_GIT_PERIODIC_MS = 6e4;
100
+ const WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS = 6e4;
101
+ const PTY_INPUT_BUSY_GRACE_MS = 3e3;
102
+ const PTY_FOREGROUND_POLL_MS = 1e3;
103
+ const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
104
+ const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
98
105
  const activeProcesses = /* @__PURE__ */ new Map();
99
106
  const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
100
107
  const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
@@ -152,6 +159,107 @@ function assertWorkerChildAdmission(input) {
152
159
  throw new StaleWorkerAdmissionError();
153
160
  }
154
161
  }
162
+ function pathIsInsideRoot(rootPath, candidatePath) {
163
+ const relative = import_node_path.default.relative(rootPath, candidatePath);
164
+ return relative === "" || !relative.startsWith(`..${import_node_path.default.sep}`) && relative !== ".." && !import_node_path.default.isAbsolute(relative);
165
+ }
166
+ function resolvePtyEnvFilePath(requestedPath, temporaryRoot = import_node_os.default.tmpdir()) {
167
+ const canonicalTemporaryRoot = import_node_fs.default.realpathSync.native(temporaryRoot);
168
+ if (requestedPath.startsWith(PTY_TMP_PATH_PREFIX)) {
169
+ const filename = requestedPath.slice(PTY_TMP_PATH_PREFIX.length);
170
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(filename)) {
171
+ throw new Error("PTY environment file temporary token must contain one safe filename");
172
+ }
173
+ return import_node_path.default.join(canonicalTemporaryRoot, filename);
174
+ }
175
+ if (!import_node_path.default.isAbsolute(requestedPath)) {
176
+ throw new Error(`PTY environment file path must be absolute or use ${PTY_TMP_PATH_PREFIX}`);
177
+ }
178
+ const resolvedPath = import_node_path.default.resolve(requestedPath);
179
+ const canonicalParent = import_node_fs.default.realpathSync.native(import_node_path.default.dirname(resolvedPath));
180
+ const canonicalPath = import_node_path.default.join(canonicalParent, import_node_path.default.basename(resolvedPath));
181
+ if (!pathIsInsideRoot(canonicalTemporaryRoot, canonicalPath) || canonicalPath === canonicalTemporaryRoot) {
182
+ throw new Error(`PTY environment file path must be inside ${canonicalTemporaryRoot}`);
183
+ }
184
+ return canonicalPath;
185
+ }
186
+ function removePtyEnvFiles(paths) {
187
+ for (const filePath of paths) {
188
+ try {
189
+ import_node_fs.default.rmSync(filePath, { force: true });
190
+ } catch (error) {
191
+ process.stderr.write(
192
+ `[r5d-worker] failed to remove PTY environment file ${filePath}: ${error instanceof Error ? error.message : String(error)}
193
+ `
194
+ );
195
+ }
196
+ }
197
+ }
198
+ function stagePtyEnvFiles(envFiles, temporaryRoot = import_node_os.default.tmpdir()) {
199
+ const staged = { paths: [], resolvedByRequestedPath: /* @__PURE__ */ new Map() };
200
+ try {
201
+ for (const envFile of envFiles ?? []) {
202
+ if (!envFile || typeof envFile.path !== "string" || typeof envFile.content !== "string") {
203
+ throw new Error("PTY environment files require string path and content values");
204
+ }
205
+ if (envFile.mode !== void 0 && envFile.mode !== 384) {
206
+ throw new Error("PTY environment files must use mode 0600");
207
+ }
208
+ const resolvedPath = resolvePtyEnvFilePath(envFile.path, temporaryRoot);
209
+ if (staged.resolvedByRequestedPath.has(envFile.path) || staged.paths.includes(resolvedPath)) {
210
+ throw new Error(`Duplicate PTY environment file path: ${envFile.path}`);
211
+ }
212
+ const descriptor = import_node_fs.default.openSync(
213
+ resolvedPath,
214
+ import_node_fs.default.constants.O_WRONLY | import_node_fs.default.constants.O_CREAT | import_node_fs.default.constants.O_EXCL | (typeof import_node_fs.default.constants.O_NOFOLLOW === "number" ? import_node_fs.default.constants.O_NOFOLLOW : 0),
215
+ 384
216
+ );
217
+ staged.paths.push(resolvedPath);
218
+ staged.resolvedByRequestedPath.set(envFile.path, resolvedPath);
219
+ try {
220
+ import_node_fs.default.writeFileSync(descriptor, envFile.content, "utf8");
221
+ import_node_fs.default.fchmodSync(descriptor, 384);
222
+ import_node_fs.default.fsyncSync(descriptor);
223
+ } finally {
224
+ import_node_fs.default.closeSync(descriptor);
225
+ }
226
+ }
227
+ return staged;
228
+ } catch (error) {
229
+ removePtyEnvFiles(staged.paths);
230
+ throw error;
231
+ }
232
+ }
233
+ function resolvePtyEnvFileReferences(env, staged) {
234
+ return Object.fromEntries(Object.entries(env).map(([name, value]) => [name, staged.resolvedByRequestedPath.get(value) ?? value]));
235
+ }
236
+ function workerCommandHasWorkspaceEffect(message) {
237
+ return message.workspaceEffect !== "none";
238
+ }
239
+ function workerPtyIsWorkspaceBusy(pty, now = Date.now(), foregroundIdleEnabled = PTY_FOREGROUND_IDLE_ENABLED) {
240
+ return !foregroundIdleEnabled || pty.foregroundBusy || now - pty.lastInputAt < PTY_INPUT_BUSY_GRACE_MS;
241
+ }
242
+ function parseLinuxPtyForegroundBusy(stat) {
243
+ const commandEnd = stat.lastIndexOf(")");
244
+ if (commandEnd < 0) return null;
245
+ const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
246
+ const processGroup = Number(fields[2]);
247
+ const foregroundProcessGroup = Number(fields[5]);
248
+ if (!Number.isSafeInteger(processGroup) || processGroup <= 0 || !Number.isSafeInteger(foregroundProcessGroup)) return null;
249
+ return foregroundProcessGroup !== processGroup;
250
+ }
251
+ const workerPtyTestHarness = {
252
+ temporaryPathPrefix: PTY_TMP_PATH_PREFIX,
253
+ inputBusyGraceMs: PTY_INPUT_BUSY_GRACE_MS,
254
+ resolveEnvFilePath: resolvePtyEnvFilePath,
255
+ stageEnvFiles: stagePtyEnvFiles,
256
+ removeEnvFiles: removePtyEnvFiles,
257
+ resolveEnvFileReferences: resolvePtyEnvFileReferences,
258
+ commandHasWorkspaceEffect: workerCommandHasWorkspaceEffect,
259
+ canonicalSyncTerminalHead,
260
+ ptyIsWorkspaceBusy: workerPtyIsWorkspaceBusy,
261
+ parseLinuxForegroundBusy: parseLinuxPtyForegroundBusy
262
+ };
155
263
  function defaultConfigPath() {
156
264
  return import_node_path.default.join(import_node_os.default.homedir(), ".config", "r5d", "r5dctl", "config.json");
157
265
  }
@@ -1410,6 +1518,86 @@ function credentialReapContractStatus(probe) {
1410
1518
  function verifiedCredentialReapContract(probe) {
1411
1519
  return credentialReapContractStatus(probe) === "verified_systemd";
1412
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
+ }
1413
1601
  function fenceUnsafeWorkspaceSyncFailure(input) {
1414
1602
  if (input.hydrationCurrent) return false;
1415
1603
  input.invalidateExecution();
@@ -1442,7 +1630,21 @@ const workerGitSecurityTestHarness = {
1442
1630
  credentialReapContractStatus,
1443
1631
  verifiedCredentialReapContract,
1444
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,
1445
1645
  assertWorkerChildAdmission,
1646
+ executeWriteFileOperation,
1647
+ executeEditFileOperation,
1446
1648
  terminateCredentialBearingChildren,
1447
1649
  terminateCredentialBearingChildrenWithRetention,
1448
1650
  async commitCredentialGeneration(prepared, credential, children, beforeMutation, previousGenerationFenced = false) {
@@ -1593,6 +1795,15 @@ function hasProjectWorktree(checkoutPath) {
1593
1795
  return false;
1594
1796
  }
1595
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
+ }
1596
1807
  function describeWorkerSessionTarget(target) {
1597
1808
  return target.type === "project" ? `${target.projectId}/${target.branchName}` : `${target.ownerUserId}/${target.rootProfile}`;
1598
1809
  }
@@ -1897,8 +2108,10 @@ async function executeWriteFileOperation(input) {
1897
2108
  planRoot: input.planRoot,
1898
2109
  access: "write"
1899
2110
  });
2111
+ input.assertAdmission();
1900
2112
  const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
1901
2113
  return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
2114
+ input.assertAdmission();
1902
2115
  return writeWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.content, builtInPaths);
1903
2116
  });
1904
2117
  }
@@ -1914,8 +2127,10 @@ async function executeEditFileOperation(input) {
1914
2127
  planRoot: input.planRoot,
1915
2128
  access: "write"
1916
2129
  });
2130
+ input.assertAdmission();
1917
2131
  const resolved = resolveWorkerFilePath(input.resolvedTarget.rootPath, input.message.filePath, builtInPaths);
1918
2132
  return withFileMutationQueue(mutationQueueKey(input.resolvedTarget.target, resolved), async () => {
2133
+ input.assertAdmission();
1919
2134
  return editWorkerTextFile(input.resolvedTarget.rootPath, input.message.filePath, input.message.edits, builtInPaths);
1920
2135
  });
1921
2136
  }
@@ -2333,7 +2548,9 @@ async function executeCommand(input) {
2333
2548
  });
2334
2549
  spawnedProcess = subprocess;
2335
2550
  credentialBearingProcessGroups.set(subprocess.pid, subprocess);
2336
- credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
2551
+ if (workerCommandHasWorkspaceEffect(input.message)) {
2552
+ credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
2553
+ }
2337
2554
  activeProcesses.set(input.message.runId, {
2338
2555
  process: subprocess,
2339
2556
  target: input.resolvedTarget.target,
@@ -2344,7 +2561,8 @@ async function executeCommand(input) {
2344
2561
  argv: input.message.argv,
2345
2562
  command: input.message.argv.join(" "),
2346
2563
  cwd,
2347
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
2564
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2565
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2348
2566
  });
2349
2567
  if (input.message.timeoutMs) {
2350
2568
  timeout = setTimeout(() => {
@@ -2430,7 +2648,9 @@ async function executeStreamingCommand(input) {
2430
2648
  });
2431
2649
  spawnedProcess = subprocess;
2432
2650
  credentialBearingProcessGroups.set(subprocess.pid, subprocess);
2433
- credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
2651
+ if (workerCommandHasWorkspaceEffect(input.message)) {
2652
+ credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
2653
+ }
2434
2654
  activeProcesses.set(input.message.runId, {
2435
2655
  process: subprocess,
2436
2656
  target: input.resolvedTarget.target,
@@ -2443,7 +2663,8 @@ async function executeStreamingCommand(input) {
2443
2663
  command: input.message.command,
2444
2664
  cwd,
2445
2665
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2446
- ...interactive ? { interactive: true, stdin: subprocess.stdin } : {}
2666
+ ...interactive ? { interactive: true, stdin: subprocess.stdin } : {},
2667
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2447
2668
  });
2448
2669
  started = true;
2449
2670
  sendWorkerMessage(input.ws, {
@@ -2485,12 +2706,15 @@ async function executeStreamingCommand(input) {
2485
2706
  });
2486
2707
  })
2487
2708
  ]);
2709
+ const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2488
2710
  const terminal = {
2489
2711
  type: "exec_exit",
2490
2712
  runId: input.message.runId,
2491
2713
  exitCode,
2492
2714
  durationMs: Date.now() - startedAt,
2493
- ...timedOut ? { timedOut: true } : {}
2715
+ ...timedOut ? { timedOut: true } : {},
2716
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2717
+ ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2494
2718
  };
2495
2719
  pendingProcessTerminals.set(input.message.runId, terminal);
2496
2720
  sendWorkerMessage(input.ws, terminal);
@@ -2498,11 +2722,14 @@ async function executeStreamingCommand(input) {
2498
2722
  if (!started && error instanceof StaleWorkerAdmissionError) throw error;
2499
2723
  const message = error instanceof Error ? error.message : String(error);
2500
2724
  if (started) {
2725
+ const canonicalWorkspaceHead = canonicalSyncTerminalHead(input.resolvedTarget);
2501
2726
  const terminal = {
2502
2727
  type: "exec_error",
2503
2728
  runId: input.message.runId,
2504
2729
  error: message,
2505
- durationMs: Date.now() - startedAt
2730
+ durationMs: Date.now() - startedAt,
2731
+ ...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {},
2732
+ ...canonicalWorkspaceHead ? { canonicalWorkspaceHead } : {}
2506
2733
  };
2507
2734
  pendingProcessTerminals.set(input.message.runId, terminal);
2508
2735
  sendWorkerMessage(input.ws, terminal);
@@ -2583,7 +2810,8 @@ function buildActiveProcessReports() {
2583
2810
  command: active.command,
2584
2811
  ...active.cwd ? { cwd: active.cwd } : {},
2585
2812
  startedAt: active.startedAt,
2586
- ...active.interactive ? { interactive: true } : {}
2813
+ ...active.interactive ? { interactive: true } : {},
2814
+ ...active.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
2587
2815
  }));
2588
2816
  }
2589
2817
  function sendActiveProcessReport(ws) {
@@ -2595,8 +2823,13 @@ function sendActiveProcessReport(ws) {
2595
2823
  const PTY_BRIDGE_SCRIPT = String.raw`
2596
2824
  const readline = require("node:readline");
2597
2825
  const nodePty = require("node-pty");
2826
+ const fs = require("node:fs");
2827
+ const { execFile } = require("node:child_process");
2598
2828
 
2599
2829
  let ptyProcess = null;
2830
+ let foregroundTimer = null;
2831
+ let foregroundPollInFlight = false;
2832
+ let lastForegroundBusy = null;
2600
2833
 
2601
2834
  function send(message, callback) {
2602
2835
  process.stdout.write(JSON.stringify(message) + "\n", callback);
@@ -2606,6 +2839,46 @@ function decode(data) {
2606
2839
  return Buffer.from(data, "base64").toString("utf8");
2607
2840
  }
2608
2841
 
2842
+ function emitForeground(busy) {
2843
+ if (busy === lastForegroundBusy) return;
2844
+ lastForegroundBusy = busy;
2845
+ send({ type: "foreground", busy });
2846
+ }
2847
+
2848
+ function linuxForegroundBusy(pid) {
2849
+ const stat = fs.readFileSync("/proc/" + pid + "/stat", "utf8");
2850
+ const commandEnd = stat.lastIndexOf(")");
2851
+ if (commandEnd < 0) throw new Error("invalid /proc stat");
2852
+ const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
2853
+ const processGroup = Number(fields[2]);
2854
+ const foregroundProcessGroup = Number(fields[5]);
2855
+ if (!Number.isSafeInteger(processGroup) || processGroup <= 0 || !Number.isSafeInteger(foregroundProcessGroup)) {
2856
+ throw new Error("invalid process group fields");
2857
+ }
2858
+ return foregroundProcessGroup !== processGroup;
2859
+ }
2860
+
2861
+ function pollForeground() {
2862
+ if (!ptyProcess || !Number.isSafeInteger(ptyProcess.pid) || ptyProcess.pid <= 0) return;
2863
+ if (process.platform === "linux") {
2864
+ try {
2865
+ emitForeground(linuxForegroundBusy(ptyProcess.pid));
2866
+ } catch {
2867
+ emitForeground(true);
2868
+ }
2869
+ return;
2870
+ }
2871
+ if (process.platform !== "darwin" || foregroundPollInFlight) return;
2872
+ foregroundPollInFlight = true;
2873
+ const pid = ptyProcess.pid;
2874
+ execFile("/bin/ps", ["-o", "tpgid=", "-p", String(pid)], (error, stdout) => {
2875
+ foregroundPollInFlight = false;
2876
+ if (!ptyProcess || ptyProcess.pid !== pid) return;
2877
+ const foregroundProcessGroup = Number(String(stdout).trim());
2878
+ emitForeground(Boolean(error) || !Number.isSafeInteger(foregroundProcessGroup) || foregroundProcessGroup !== pid);
2879
+ });
2880
+ }
2881
+
2609
2882
  const rl = readline.createInterface({ input: process.stdin });
2610
2883
 
2611
2884
  rl.on("line", (line) => {
@@ -2624,9 +2897,14 @@ rl.on("line", (line) => {
2624
2897
  send({ type: "output", data: Buffer.from(data, "utf8").toString("base64") });
2625
2898
  });
2626
2899
  ptyProcess.onExit((event) => {
2900
+ if (foregroundTimer) clearInterval(foregroundTimer);
2901
+ foregroundTimer = null;
2627
2902
  send({ type: "exit", exitCode: event.exitCode, signal: event.signal }, () => process.exit(0));
2628
2903
  });
2629
- send({ type: "opened" });
2904
+ send({ type: "opened", pid: ptyProcess.pid });
2905
+ pollForeground();
2906
+ foregroundTimer = setInterval(pollForeground, ${PTY_FOREGROUND_POLL_MS});
2907
+ foregroundTimer.unref();
2630
2908
  return;
2631
2909
  }
2632
2910
 
@@ -2719,7 +2997,11 @@ function createNodePtyBridge(options) {
2719
2997
  };
2720
2998
  const handleEvent = (event) => {
2721
2999
  if (event.type === "opened") {
2722
- options.onOpened();
3000
+ options.onOpened(event.pid);
3001
+ return;
3002
+ }
3003
+ if (event.type === "foreground") {
3004
+ options.onForeground(event.busy);
2723
3005
  return;
2724
3006
  }
2725
3007
  if (event.type === "output") {
@@ -2800,6 +3082,9 @@ function createNodePtyBridge(options) {
2800
3082
  }
2801
3083
  };
2802
3084
  }
3085
+ const workerPtyBridgeTestHarness = {
3086
+ create: createNodePtyBridge
3087
+ };
2803
3088
  function resolveHostShell(command, platform = process.platform) {
2804
3089
  if (platform === "win32") {
2805
3090
  const file2 = process.env.COMSPEC || "powershell.exe";
@@ -2835,64 +3120,90 @@ async function openPty(input) {
2835
3120
  }
2836
3121
  });
2837
3122
  input.assertAdmission();
2838
- const ptyProcess = createNodePtyBridge({
2839
- file: shell.file,
2840
- args: shell.args,
2841
- ptyOptions: {
2842
- name: "xterm-256color",
2843
- cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
2844
- rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
2845
- cwd: input.resolvedTarget.rootPath,
2846
- env: workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, planProcessEnv, targetProcessEnv, shell.env ?? {}])
2847
- },
2848
- onOpened: () => {
2849
- sendWorkerMessage(input.ws, {
2850
- type: "pty_opened",
2851
- requestId: input.message.requestId,
2852
- ptyId: input.message.ptyId
2853
- });
2854
- },
2855
- onOutput: (data) => {
2856
- outputCoalescer.push(data);
2857
- },
2858
- onExit: (event) => {
2859
- outputCoalescer.flush();
2860
- input.releaseWorkspaceMutation?.();
2861
- activePtys.delete(input.message.ptyId);
2862
- input.onTerminal?.();
2863
- sendWorkerMessage(input.ws, {
2864
- type: "pty_exit",
2865
- ptyId: input.message.ptyId,
2866
- exitCode: event.exitCode,
2867
- signal: event.signal
2868
- });
2869
- },
2870
- onError: (error) => {
2871
- outputCoalescer.flush();
2872
- input.releaseWorkspaceMutation?.();
2873
- activePtys.delete(input.message.ptyId);
2874
- input.onTerminal?.();
2875
- sendWorkerMessage(input.ws, {
2876
- type: "pty_error",
2877
- requestId: input.message.requestId,
2878
- ptyId: input.message.ptyId,
2879
- error: error.message
2880
- });
2881
- },
2882
- onTerminationError: (error) => {
2883
- sendWorkerMessage(input.ws, {
2884
- type: "pty_error",
2885
- requestId: input.message.requestId,
2886
- ptyId: input.message.ptyId,
2887
- error: error.message
2888
- });
2889
- }
2890
- });
2891
- activePtys.set(input.message.ptyId, {
2892
- ...ptyProcess,
2893
- target: input.resolvedTarget.target,
2894
- ...input.releaseWorkspaceMutation ? { releaseWorkspaceMutation: input.releaseWorkspaceMutation } : {}
2895
- });
3123
+ const stagedEnvFiles = stagePtyEnvFiles(input.message.envFiles);
3124
+ let envFilesRemoved = false;
3125
+ const cleanupEnvFiles = () => {
3126
+ if (envFilesRemoved) return;
3127
+ envFilesRemoved = true;
3128
+ removePtyEnvFiles(stagedEnvFiles.paths);
3129
+ };
3130
+ try {
3131
+ const ptyProcess = createNodePtyBridge({
3132
+ file: shell.file,
3133
+ args: shell.args,
3134
+ ptyOptions: {
3135
+ name: "xterm-256color",
3136
+ cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
3137
+ rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
3138
+ cwd: input.resolvedTarget.rootPath,
3139
+ env: workerChildProcessEnvironment([
3140
+ githubProcessEnv(),
3141
+ resolvePtyEnvFileReferences(input.message.env ?? {}, stagedEnvFiles),
3142
+ planProcessEnv,
3143
+ targetProcessEnv,
3144
+ shell.env ?? {}
3145
+ ])
3146
+ },
3147
+ onOpened: () => {
3148
+ sendWorkerMessage(input.ws, {
3149
+ type: "pty_opened",
3150
+ requestId: input.message.requestId,
3151
+ ptyId: input.message.ptyId
3152
+ });
3153
+ },
3154
+ onForeground: (busy) => {
3155
+ const activePty = activePtys.get(input.message.ptyId);
3156
+ if (activePty && (busy || input.message.command === void 0)) activePty.foregroundBusy = busy;
3157
+ },
3158
+ onOutput: (data) => {
3159
+ outputCoalescer.push(data);
3160
+ },
3161
+ onExit: (event) => {
3162
+ outputCoalescer.flush();
3163
+ input.releaseWorkspaceMutation?.();
3164
+ activePtys.delete(input.message.ptyId);
3165
+ cleanupEnvFiles();
3166
+ input.onTerminal?.();
3167
+ sendWorkerMessage(input.ws, {
3168
+ type: "pty_exit",
3169
+ ptyId: input.message.ptyId,
3170
+ exitCode: event.exitCode,
3171
+ signal: event.signal
3172
+ });
3173
+ },
3174
+ onError: (error) => {
3175
+ outputCoalescer.flush();
3176
+ input.releaseWorkspaceMutation?.();
3177
+ activePtys.delete(input.message.ptyId);
3178
+ cleanupEnvFiles();
3179
+ input.onTerminal?.();
3180
+ sendWorkerMessage(input.ws, {
3181
+ type: "pty_error",
3182
+ requestId: input.message.requestId,
3183
+ ptyId: input.message.ptyId,
3184
+ error: error.message
3185
+ });
3186
+ },
3187
+ onTerminationError: (error) => {
3188
+ sendWorkerMessage(input.ws, {
3189
+ type: "pty_error",
3190
+ requestId: input.message.requestId,
3191
+ ptyId: input.message.ptyId,
3192
+ error: error.message
3193
+ });
3194
+ }
3195
+ });
3196
+ activePtys.set(input.message.ptyId, {
3197
+ ...ptyProcess,
3198
+ target: input.resolvedTarget.target,
3199
+ foregroundBusy: true,
3200
+ lastInputAt: 0,
3201
+ ...input.releaseWorkspaceMutation ? { releaseWorkspaceMutation: input.releaseWorkspaceMutation } : {}
3202
+ });
3203
+ } catch (error) {
3204
+ cleanupEnvFiles();
3205
+ throw error;
3206
+ }
2896
3207
  }
2897
3208
  function writePty(ws, message) {
2898
3209
  const ptyProcess = activePtys.get(message.ptyId);
@@ -2904,6 +3215,7 @@ function writePty(ws, message) {
2904
3215
  });
2905
3216
  return;
2906
3217
  }
3218
+ ptyProcess.lastInputAt = Date.now();
2907
3219
  ptyProcess.write(message.data);
2908
3220
  }
2909
3221
  function resizePty(message) {
@@ -3072,6 +3384,9 @@ async function startWorker(options) {
3072
3384
  let workspaceGitIdentity = null;
3073
3385
  let workspaceConfigured = false;
3074
3386
  let activeWorkspaceIncidentId = null;
3387
+ let deferredWorkspaceConfiguration = null;
3388
+ let deferredWorkspaceConfigurationRefreshTimer;
3389
+ let workspaceConfigurationReceiptGeneration = 0;
3075
3390
  let workspaceAutomaticTimer;
3076
3391
  let workspacePeriodicTimer;
3077
3392
  let pendingAutomaticTrigger;
@@ -3341,13 +3656,24 @@ async function startWorker(options) {
3341
3656
  });
3342
3657
  };
3343
3658
  const activeWorkspaceMutationTargets = () => [
3344
- ...Array.from(activeProcesses.values(), ({ target }) => target),
3659
+ ...Array.from(activeProcesses.values()).filter(workerCommandHasWorkspaceEffect).map(({ target }) => target),
3345
3660
  ...credentialBearingProcessGroupTargets.values(),
3346
3661
  ...workspaceSyncPriorityProcessTargets.values(),
3347
3662
  ...workspaceSyncPriorityOperationTargets.values(),
3348
- ...Array.from(activePtys.values(), ({ target }) => target),
3663
+ ...Array.from(activePtys.values()).filter((pty) => workerPtyIsWorkspaceBusy(pty)).map(({ target }) => target),
3349
3664
  ...workspaceSyncPriorityPtyTargets.values()
3350
3665
  ];
3666
+ let visibleWorkspaceMutationEpoch = 0;
3667
+ const projectWorkspaceMutationEpochs = /* @__PURE__ */ new Map();
3668
+ const recordVisibleWorkspaceMutation = (target) => {
3669
+ if (target.type === "workspace") {
3670
+ if (target.rootProfile === "visible_projects") visibleWorkspaceMutationEpoch += 1;
3671
+ return;
3672
+ }
3673
+ const key = projectBranchKey(target.projectId, target.branchName);
3674
+ projectWorkspaceMutationEpochs.set(key, (projectWorkspaceMutationEpochs.get(key) ?? 0) + 1);
3675
+ };
3676
+ const projectWorkspaceMutationToken = (projectId, branchName) => `${visibleWorkspaceMutationEpoch}:${projectWorkspaceMutationEpochs.get(projectBranchKey(projectId, branchName)) ?? 0}`;
3351
3677
  const canonicalWorkspaceMutationIsActive = (targets) => targets.some((target) => target.type === "workspace" && target.rootProfile === "canonical_sync");
3352
3678
  const projectBranchMountActivityBusy = (projectId, branchName, branchPath) => {
3353
3679
  const activeTargets = activeWorkspaceMutationTargets();
@@ -3384,6 +3710,7 @@ async function startWorker(options) {
3384
3710
  preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3385
3711
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3386
3712
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3713
+ mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3387
3714
  busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3388
3715
  });
3389
3716
  mounts.push({
@@ -3397,6 +3724,7 @@ async function startWorker(options) {
3397
3724
  preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3398
3725
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3399
3726
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3727
+ mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
3400
3728
  busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
3401
3729
  });
3402
3730
  }
@@ -3417,6 +3745,7 @@ async function startWorker(options) {
3417
3745
  const activeTargets = activeWorkspaceMutationTargets();
3418
3746
  return canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.hasActiveVisibleProjectsWorkspaceTarget)(activeTargets);
3419
3747
  },
3748
+ mutationToken: () => String(visibleWorkspaceMutationEpoch),
3420
3749
  busyForRecovery: () => {
3421
3750
  const activeTargets = activeWorkspaceMutationTargets();
3422
3751
  return canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.hasActiveVisibleProjectsWorkspaceTarget)(activeTargets);
@@ -3774,6 +4103,8 @@ async function startWorker(options) {
3774
4103
  localChangesDiscarded: false,
3775
4104
  ...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
3776
4105
  ...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
4106
+ ...result.conflictKind ? { conflictKind: result.conflictKind } : {},
4107
+ ...result.verifiedAncestorHeads ? { verifiedAncestorHeads: result.verifiedAncestorHeads } : {},
3777
4108
  ...result.error ? { error: result.error } : {}
3778
4109
  };
3779
4110
  };
@@ -3840,10 +4171,7 @@ async function startWorker(options) {
3840
4171
  };
3841
4172
  }
3842
4173
  const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
3843
- const ordinaryCycleHasBusyLiveMount = mounts.some(
3844
- (mount) => !(mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath)) && mount.busy?.()
3845
- );
3846
- const observedProjectHeads = outerRemediation || ordinaryCycleHasBusyLiveMount ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
4174
+ const observedProjectHeads = outerRemediation ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
3847
4175
  const inboundMoveMountIds = new Set(
3848
4176
  observedProjectHeads.flatMap(
3849
4177
  ({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
@@ -3862,10 +4190,13 @@ async function startWorker(options) {
3862
4190
  commitDetail: input.confirmationReason ?? input.trigger.detail,
3863
4191
  allowLargeDiff: input.confirmedLargeDiff,
3864
4192
  skipMountMirror: outerRemediation,
4193
+ requiredAncestorHeads: input.requiredAncestorHeads,
4194
+ assertStillAdmitted: input.assertStillAdmitted,
3865
4195
  afterWorkspacePublished: outerRemediation ? void 0 : ({ activeMountIds, publishedHead }) => {
3866
4196
  pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, inboundMoveMountIds);
3867
4197
  }
3868
4198
  });
4199
+ input.assertStillAdmitted?.();
3869
4200
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
3870
4201
  applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads, result.activeMountIds, false);
3871
4202
  }
@@ -3888,6 +4219,28 @@ async function startWorker(options) {
3888
4219
  const runWorkspaceSync = async (input) => {
3889
4220
  const attemptId = input.attemptId ?? crypto.randomUUID();
3890
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
+ }
3891
4244
  if (!input.requestId && input.sendResult !== false) {
3892
4245
  if (currentWorkerSocket !== ws || ws.readyState !== WebSocket.OPEN) {
3893
4246
  throw new Error("Cannot start autonomous workspace synchronization without an open worker control socket");
@@ -3902,23 +4255,32 @@ async function startWorker(options) {
3902
4255
  result = await workspaceSyncSingleFlight.runExclusive(async () => {
3903
4256
  queueEnteredAt = Date.now();
3904
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
+ }
3905
4267
  try {
4268
+ assertStillAdmitted();
3906
4269
  return await performWorkspaceSync({
3907
4270
  attemptId,
3908
4271
  trigger: input.trigger,
3909
4272
  confirmedLargeDiff: input.confirmedLargeDiff,
3910
4273
  confirmationReason: input.confirmationReason,
3911
- resetToCanonical: input.resetToCanonical
4274
+ resetToCanonical: input.resetToCanonical,
4275
+ requiredAncestorHeads: input.requiredAncestorHeads,
4276
+ assertStillAdmitted
3912
4277
  });
3913
4278
  } catch (error) {
3914
- let hydrationCurrent = input.resetToCanonical !== true;
3915
- if (hydrationCurrent) {
3916
- try {
3917
- hydrationCurrent = (0, import_workspace_git_sync.workspaceGitHydrationIsCurrent)(workspaceShadowRoot, buildWorkspaceMounts());
3918
- } catch {
3919
- hydrationCurrent = false;
3920
- }
3921
- }
4279
+ const hydrationCurrent = workspaceSyncFailureHydrationIsSafe({
4280
+ error,
4281
+ resetToCanonical: input.resetToCanonical === true,
4282
+ inspectCurrentHydration: () => (0, import_workspace_git_sync.workspaceGitHydrationIsCurrent)(workspaceShadowRoot, buildWorkspaceMounts())
4283
+ });
3922
4284
  if (!hydrationCurrent) {
3923
4285
  process.stderr.write(
3924
4286
  `[r5d-worker] workspace synchronization failed with an incompletely hydrated visible tree; exiting for exact recovery: ${error instanceof Error ? error.message : String(error)}
@@ -3960,7 +4322,7 @@ async function startWorker(options) {
3960
4322
  pendingCreatedBranchPublicationNotBefore
3961
4323
  );
3962
4324
  const initialDeferral = pendingBranchDeferral();
3963
- 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") {
3964
4326
  return;
3965
4327
  }
3966
4328
  if (workspaceAutomaticTimer) clearTimeout(workspaceAutomaticTimer);
@@ -3968,7 +4330,8 @@ async function startWorker(options) {
3968
4330
  () => {
3969
4331
  workspaceAutomaticTimer = void 0;
3970
4332
  const scheduledTrigger = pendingAutomaticTrigger;
3971
- if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || currentWorkerSocket !== ws) return;
4333
+ if (!scheduledTrigger || !workspaceConfigured || activeWorkspaceIncidentId || deferredWorkspaceConfiguration || currentWorkerSocket !== ws)
4334
+ return;
3972
4335
  const currentDeferral = pendingBranchDeferral();
3973
4336
  if (currentDeferral?.kind === "active_target") return;
3974
4337
  if (currentDeferral?.kind === "creation_grace") {
@@ -3990,7 +4353,7 @@ async function startWorker(options) {
3990
4353
  }).finally(() => {
3991
4354
  automaticSyncInFlight = false;
3992
4355
  heartbeatBusyGrace = (0, import_heartbeat.grantWorkerHeartbeatBusyGrace)(lastServerHeartbeatAt, heartbeatBusyGrace);
3993
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
4356
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
3994
4357
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, WORKSPACE_GIT_QUIET_MS);
3995
4358
  }
3996
4359
  });
@@ -3999,12 +4362,18 @@ async function startWorker(options) {
3999
4362
  );
4000
4363
  workspaceAutomaticTimer.unref();
4001
4364
  };
4002
- const markWorkspaceDirty = (trigger, force = false) => {
4365
+ const markWorkspaceDirty = (trigger, force = false, target) => {
4366
+ if (target) recordVisibleWorkspaceMutation(target);
4003
4367
  scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
4004
4368
  };
4005
4369
  const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
4006
4370
  const resolveMessageTarget = (target) => {
4007
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
+ }
4008
4377
  if (target.type === "project" && !readyProjectIds.has(target.projectId)) {
4009
4378
  throw new Error(`Project ${target.projectId} is not ready on this worker`);
4010
4379
  }
@@ -4018,7 +4387,26 @@ async function startWorker(options) {
4018
4387
  projectConfigById
4019
4388
  });
4020
4389
  };
4021
- 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
+ }
4022
4410
  const incomingCredentialGenerationFingerprint = credentialGenerationFingerprint({
4023
4411
  ...message,
4024
4412
  workerBaseUrl: baseUrl,
@@ -4029,6 +4417,22 @@ async function startWorker(options) {
4029
4417
  pendingFingerprintAtProcessStart: pendingCredentialGenerationIntentAtProcessStart?.fingerprint ?? null,
4030
4418
  incomingFingerprint: incomingCredentialGenerationFingerprint
4031
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
+ }
4032
4436
  const credentialReapStatus = credentialReapContractStatus();
4033
4437
  const verifiedCredentialReapContractNow = credentialReapStatus === "verified_systemd";
4034
4438
  const credentialRestartIsContained = credentialGenerationRestartIsContained({
@@ -4142,6 +4546,12 @@ async function startWorker(options) {
4142
4546
  workspaceSyncRequestsInFlight += 1;
4143
4547
  try {
4144
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
+ }
4145
4555
  for (const project of message.projects) (0, import_repository_transition_policy.assertRepositoryTransitionState)(project);
4146
4556
  const busyConfigurationChanges = (0, import_workspace_project_config_policy.busyProjectConfigurationChangeIds)({
4147
4557
  currentProjects: [...projectConfigById.values()],
@@ -4163,10 +4573,12 @@ async function startWorker(options) {
4163
4573
  const preserveOnlyBranches = message.projects.flatMap(
4164
4574
  (project) => project.preserveOnlyBranches.map(({ branchId, branchName }) => ({ branchId, projectId: project.projectId, branchName }))
4165
4575
  );
4166
- projectWorkspaceState = projectWorkspaceStateStore.reconcile({
4167
- desiredProjects: message.projects,
4168
- preserveOnlyBranches
4169
- });
4576
+ if (!incidentDeferral.incidentId) {
4577
+ projectWorkspaceState = projectWorkspaceStateStore.reconcile({
4578
+ desiredProjects: message.projects,
4579
+ preserveOnlyBranches
4580
+ });
4581
+ }
4170
4582
  const stillPendingCreatedBranches = new Map(
4171
4583
  projectWorkspaceState.locallyPendingCreatedBranches.map((branch) => [
4172
4584
  (0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(branch.projectId, branch.branchName),
@@ -4226,6 +4638,12 @@ async function startWorker(options) {
4226
4638
  void 0,
4227
4639
  credentialPublicationPreauthorized
4228
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
+ }
4229
4647
  configuredCredentialGenerationFingerprint = incomingCredentialGenerationFingerprint;
4230
4648
  pendingCredentialGenerationIntentAtProcessStart = null;
4231
4649
  bootstrapCredentialGenerationFingerprintAtProcessStart = null;
@@ -4240,6 +4658,34 @@ async function startWorker(options) {
4240
4658
  branches: project.branches.filter(({ branchName }) => !pendingMirrorDeletes.has(projectBranchKey(project.projectId, branchName)))
4241
4659
  }));
4242
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
+ }
4243
4689
  (0, import_workspace_git_sync.ensureWorkspaceGitClone)({
4244
4690
  workspacePath: workspaceShadowRoot,
4245
4691
  remoteUrl: message.workspaceRemoteUrl,
@@ -4260,7 +4706,7 @@ async function startWorker(options) {
4260
4706
  sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
4261
4707
  busy: mount.busyForRecovery ?? mount.busy
4262
4708
  }));
4263
- (0, import_workspace_git_sync.recoverWorkspaceGitHydration)(workspaceShadowRoot, recoveryMounts);
4709
+ (0, import_workspace_git_sync.recoverWorkspaceGitHydration)(workspaceShadowRoot, recoveryMounts, { preserveStaleBases: true });
4264
4710
  for (const project of message.projects) {
4265
4711
  for (const { branchName } of project.preserveOnlyBranches) {
4266
4712
  if (stillPendingCreatedBranches.has((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(project.projectId, branchName))) {
@@ -4307,10 +4753,24 @@ async function startWorker(options) {
4307
4753
  { ignoreBusy: true }
4308
4754
  );
4309
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
+ };
4310
4762
  const result = await performWorkspaceSync({
4311
4763
  attemptId: crypto.randomUUID(),
4312
- trigger: { type: "connect" }
4764
+ trigger: { type: "connect" },
4765
+ assertStillAdmitted: assertConfigurationSyncStillAdmitted
4313
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
+ }
4314
4774
  const publishedHead = result.publishedHead ?? result.localHead ?? result.startingHead;
4315
4775
  if (publishedHead && ["no_change", "published", "updated", "conflict_reset", "reset"].includes(result.outcome)) {
4316
4776
  const activeMountIds = new Set(result.activeMountIds ?? []);
@@ -4327,6 +4787,11 @@ async function startWorker(options) {
4327
4787
  });
4328
4788
  }
4329
4789
  }
4790
+ if (deferredWorkspaceConfigurationRefreshTimer) {
4791
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
4792
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
4793
+ }
4794
+ deferredWorkspaceConfiguration = null;
4330
4795
  workspaceConfigured = true;
4331
4796
  const pending = [...pendingCheckouts.values()].sort(
4332
4797
  (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
@@ -4334,6 +4799,13 @@ async function startWorker(options) {
4334
4799
  return { result, pending, aheadOfOriginBranches: collectAheadOfOriginBranches() };
4335
4800
  });
4336
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
+ }
4337
4809
  if (error instanceof import_registry_auth.RegistryAuthConfigurationError) {
4338
4810
  workspaceConfigured = false;
4339
4811
  githubCredential = null;
@@ -4388,7 +4860,7 @@ async function startWorker(options) {
4388
4860
  workspaceAutomaticTimer = void 0;
4389
4861
  }
4390
4862
  void (async () => {
4391
- if (workspaceConfigured && !activeWorkspaceIncidentId) {
4863
+ if (workspaceConfigured && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
4392
4864
  await runWorkspaceSync({
4393
4865
  trigger: { type: "manual", detail: "graceful worker shutdown" }
4394
4866
  });
@@ -4436,7 +4908,10 @@ async function startWorker(options) {
4436
4908
  capabilities: {
4437
4909
  updateClis: true,
4438
4910
  browserPortForwarding: true,
4439
- execStdinV1: true
4911
+ execStdinV1: true,
4912
+ ptyEnvFilesV1: true,
4913
+ workspaceRemediationAncestorGuardV1: true,
4914
+ workspaceIncidentConfigDeferralV1: true
4440
4915
  },
4441
4916
  projectRoot: projectsRoot,
4442
4917
  artifactRoot,
@@ -4472,26 +4947,35 @@ async function startWorker(options) {
4472
4947
  return;
4473
4948
  }
4474
4949
  if (message.type === "workspace_config") {
4950
+ const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
4951
+ const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
4475
4952
  advanceWorkerAdmissionGeneration();
4476
- const configured = await configureWorkerWorkspace(message);
4953
+ const configured = await configureWorkerWorkspace(message, receiptGeneration);
4477
4954
  sendWorkerMessageFromCurrentSource(ws, {
4478
4955
  type: "workspace_configured",
4479
4956
  requestId: message.requestId,
4480
4957
  result: configured.result,
4481
4958
  pendingCheckouts: configured.pending,
4482
- aheadOfOriginBranches: configured.aheadOfOriginBranches
4959
+ aheadOfOriginBranches: configured.aheadOfOriginBranches,
4960
+ ...configured.deferredWorkspaceSyncForIncidentId ? { deferredWorkspaceSyncForIncidentId: configured.deferredWorkspaceSyncForIncidentId } : {}
4483
4961
  });
4484
4962
  process.stdout.write(
4485
4963
  `[r5d-worker] workspace configured: ${message.projects.length} project(s), ${configured.pending.length} pending checkout(s)
4486
4964
  `
4487
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
+ }
4488
4972
  if (!workspacePeriodicTimer) {
4489
4973
  workspacePeriodicTimer = setInterval(() => {
4490
4974
  scheduleAutomaticWorkspaceSync({ type: "periodic", detail: "periodic workspace reconciliation" }, 0);
4491
4975
  }, WORKSPACE_GIT_PERIODIC_MS);
4492
4976
  workspacePeriodicTimer.unref();
4493
4977
  }
4494
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
4978
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
4495
4979
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, 0);
4496
4980
  }
4497
4981
  return;
@@ -4503,11 +4987,20 @@ async function startWorker(options) {
4503
4987
  trigger: message.trigger,
4504
4988
  confirmedLargeDiff: message.confirmedLargeDiff,
4505
4989
  confirmationReason: message.confirmationReason,
4506
- resetToCanonical: message.resetToCanonical
4990
+ resetToCanonical: message.resetToCanonical,
4991
+ requiredAncestorHeads: message.requiredAncestorHeads
4507
4992
  });
4508
4993
  return;
4509
4994
  }
4510
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
+ }
4511
5004
  try {
4512
5005
  const pendingBranch = {
4513
5006
  branchId: message.branchId,
@@ -4515,6 +5008,11 @@ async function startWorker(options) {
4515
5008
  branchName: message.targetBranch
4516
5009
  };
4517
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
+ }
4518
5016
  const project = projectConfigById.get(message.projectId);
4519
5017
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
4520
5018
  (0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(project);
@@ -4598,9 +5096,22 @@ async function startWorker(options) {
4598
5096
  return;
4599
5097
  }
4600
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
+ }
4601
5107
  try {
4602
5108
  const deletionKey = projectBranchKey(message.projectId, message.branchName);
4603
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
+ }
4604
5115
  const project = projectConfigById.get(message.projectId);
4605
5116
  if (!project) throw new Error(`Project ${message.projectId} is missing from the worker workspace configuration`);
4606
5117
  (0, import_repository_transition_policy.assertRepositoryExecutionEnabled)(project);
@@ -4700,8 +5211,38 @@ async function startWorker(options) {
4700
5211
  }
4701
5212
  if (message.type === "workspace_incident_updated") {
4702
5213
  const previousIncidentId = activeWorkspaceIncidentId;
4703
- 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;
4704
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
+ }
4705
5246
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger ?? { type: "periodic", detail: "resume after workspace incident" }, 0);
4706
5247
  }
4707
5248
  return;
@@ -4805,12 +5346,21 @@ async function startWorker(options) {
4805
5346
  sendAck({ error: `Process run ${message.runId} is not active on this worker` });
4806
5347
  return;
4807
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
+ }
4808
5355
  if (!active.interactive || !active.stdin) {
4809
5356
  sendAck({
4810
5357
  error: `Process run ${message.runId} has no open stdin. Start a new shell command with "interactive": true to write to its stdin.`
4811
5358
  });
4812
5359
  return;
4813
5360
  }
5361
+ if (active.workspaceEffect !== "none" && targetMayMutateVisibleWorkspace(active.target)) {
5362
+ recordVisibleWorkspaceMutation(active.target);
5363
+ }
4814
5364
  try {
4815
5365
  let bytesWritten = 0;
4816
5366
  if (message.data !== void 0 && message.data.length > 0) {
@@ -4821,8 +5371,8 @@ async function startWorker(options) {
4821
5371
  await active.stdin.end();
4822
5372
  active.stdin = void 0;
4823
5373
  }
4824
- if (targetMayMutateVisibleWorkspace(active.target)) {
4825
- markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` });
5374
+ if (active.workspaceEffect !== "none" && targetMayMutateVisibleWorkspace(active.target)) {
5375
+ markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` }, false, active.target);
4826
5376
  }
4827
5377
  sendAck({
4828
5378
  result: {
@@ -4847,13 +5397,28 @@ async function startWorker(options) {
4847
5397
  });
4848
5398
  return;
4849
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
+ }
4850
5409
  await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
4851
5410
  workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
5411
+ if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
4852
5412
  });
4853
5413
  let releaseWorkspaceMutation;
4854
5414
  let mutationLeaseTransferred = false;
4855
5415
  try {
4856
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
+ }
4857
5422
  const resolvedTarget = resolveMessageTarget(message.target);
4858
5423
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${describeWorkerSessionTarget(message.target)}
4859
5424
  `);
@@ -4866,7 +5431,7 @@ async function startWorker(options) {
4866
5431
  ...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
4867
5432
  ...targetMayMutateVisibleWorkspace(message.target) ? {
4868
5433
  onTerminal: () => {
4869
- markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true);
5434
+ markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true, message.target);
4870
5435
  }
4871
5436
  } : {}
4872
5437
  });
@@ -4886,8 +5451,16 @@ async function startWorker(options) {
4886
5451
  }
4887
5452
  if (message.type === "pty_input") {
4888
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
+ }
4889
5462
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
4890
- markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` });
5463
+ markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
4891
5464
  }
4892
5465
  writePty(ws, message);
4893
5466
  return;
@@ -4900,7 +5473,7 @@ async function startWorker(options) {
4900
5473
  const activePty = activePtys.get(message.ptyId);
4901
5474
  closePty(message);
4902
5475
  if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
4903
- markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true);
5476
+ markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
4904
5477
  }
4905
5478
  return;
4906
5479
  }
@@ -4915,14 +5488,34 @@ async function startWorker(options) {
4915
5488
  });
4916
5489
  return;
4917
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
+ }
4918
5502
  let result;
4919
5503
  let targetReserved = false;
5504
+ const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
4920
5505
  try {
4921
- await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
4922
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
4923
- targetReserved = true;
4924
- });
5506
+ if (hasWorkspaceEffect) {
5507
+ await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
5508
+ workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
5509
+ targetReserved = true;
5510
+ if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
5511
+ });
5512
+ }
4925
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
+ }
4926
5519
  const resolvedTarget = resolveMessageTarget(message.target);
4927
5520
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
4928
5521
  `);
@@ -4933,7 +5526,14 @@ async function startWorker(options) {
4933
5526
  token,
4934
5527
  artifactRoot,
4935
5528
  planRoot,
4936
- 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
+ }
4937
5537
  });
4938
5538
  };
4939
5539
  result = await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
@@ -4951,10 +5551,11 @@ async function startWorker(options) {
4951
5551
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
4952
5552
  }
4953
5553
  ws.send(JSON.stringify(result));
4954
- if (targetMayMutateVisibleWorkspace(message.target)) {
5554
+ if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
4955
5555
  markWorkspaceDirty(
4956
5556
  { type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
4957
- true
5557
+ true,
5558
+ message.target
4958
5559
  );
4959
5560
  }
4960
5561
  return;
@@ -4974,6 +5575,7 @@ async function startWorker(options) {
4974
5575
  requestId: message.requestId,
4975
5576
  runId: message.runId
4976
5577
  });
5578
+ const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
4977
5579
  const runCommand = async () => {
4978
5580
  try {
4979
5581
  const resolvedTarget = resolveMessageTarget(message.target);
@@ -4990,7 +5592,7 @@ async function startWorker(options) {
4990
5592
  assertAdmission: assertMessageAdmission
4991
5593
  });
4992
5594
  } finally {
4993
- if (targetMayMutateVisibleWorkspace(message.target)) {
5595
+ if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
4994
5596
  markWorkspaceDirty(
4995
5597
  {
4996
5598
  type: "process_terminal",
@@ -4998,7 +5600,8 @@ async function startWorker(options) {
4998
5600
  processRunId: message.runId,
4999
5601
  detail: "process completed"
5000
5602
  },
5001
- true
5603
+ true,
5604
+ message.target
5002
5605
  );
5003
5606
  }
5004
5607
  }
@@ -5006,10 +5609,13 @@ async function startWorker(options) {
5006
5609
  const execution = (async () => {
5007
5610
  let targetReserved = false;
5008
5611
  try {
5009
- await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
5010
- workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
5011
- targetReserved = true;
5012
- });
5612
+ if (hasWorkspaceEffect) {
5613
+ await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
5614
+ workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
5615
+ targetReserved = true;
5616
+ if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
5617
+ });
5618
+ }
5013
5619
  await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
5014
5620
  } finally {
5015
5621
  if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
@@ -5027,11 +5633,20 @@ async function startWorker(options) {
5027
5633
  return;
5028
5634
  }
5029
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
+ }
5030
5644
  const reservesVisibleWorkspace = targetMayMutateVisibleWorkspace(message.target);
5031
5645
  const mutatesVisibleWorkspace = (message.type === "write" || message.type === "edit") && targetMayMutateVisibleWorkspace(message.target);
5032
5646
  if (reservesVisibleWorkspace) {
5033
5647
  await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
5034
5648
  workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
5649
+ if (mutatesVisibleWorkspace) recordVisibleWorkspaceMutation(message.target);
5035
5650
  });
5036
5651
  }
5037
5652
  let dirtyTrigger;
@@ -5044,7 +5659,8 @@ async function startWorker(options) {
5044
5659
  baseUrl,
5045
5660
  token,
5046
5661
  artifactRoot,
5047
- planRoot
5662
+ planRoot,
5663
+ assertAdmission: assertMessageAdmission
5048
5664
  });
5049
5665
  });
5050
5666
  ws.send(
@@ -5061,7 +5677,7 @@ async function startWorker(options) {
5061
5677
  toolCallId: message.requestId,
5062
5678
  ...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
5063
5679
  };
5064
- markWorkspaceDirty(dirtyTrigger);
5680
+ markWorkspaceDirty(dirtyTrigger, false, message.target);
5065
5681
  }
5066
5682
  } catch (error) {
5067
5683
  ws.send(
@@ -5074,7 +5690,7 @@ async function startWorker(options) {
5074
5690
  } finally {
5075
5691
  if (reservesVisibleWorkspace) {
5076
5692
  workspaceSyncPriorityOperationTargets.delete(message.requestId);
5077
- if (pendingAutomaticTrigger && !activeWorkspaceIncidentId) {
5693
+ if (pendingAutomaticTrigger && !activeWorkspaceIncidentId && !deferredWorkspaceConfiguration) {
5078
5694
  scheduleAutomaticWorkspaceSync(pendingAutomaticTrigger, dirtyTrigger ? WORKSPACE_GIT_QUIET_MS : 0);
5079
5695
  }
5080
5696
  }
@@ -5110,6 +5726,10 @@ async function startWorker(options) {
5110
5726
  clearInterval(workspacePeriodicTimer);
5111
5727
  workspacePeriodicTimer = void 0;
5112
5728
  }
5729
+ if (deferredWorkspaceConfigurationRefreshTimer) {
5730
+ clearTimeout(deferredWorkspaceConfigurationRefreshTimer);
5731
+ deferredWorkspaceConfigurationRefreshTimer = void 0;
5732
+ }
5113
5733
  if (terminalReplayTimer) {
5114
5734
  clearInterval(terminalReplayTimer);
5115
5735
  terminalReplayTimer = void 0;
@@ -5222,5 +5842,7 @@ if (isCliEntrypoint()) {
5222
5842
  syncSessionArtifacts,
5223
5843
  workerChildProcessEnvironment,
5224
5844
  workerGitSecurityTestHarness,
5845
+ workerPtyBridgeTestHarness,
5846
+ workerPtyTestHarness,
5225
5847
  writeWorkerTextFile
5226
5848
  });