@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 +743 -121
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-command-sync-policy.cjs +3 -3
- package/dist/cjs/workspace-git-sync.cjs +446 -123
- package/dist/cjs/workspace-merge-projection.cjs +392 -0
- package/dist/mjs/main.mjs +744 -122
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-command-sync-policy.mjs +3 -3
- package/dist/mjs/workspace-git-sync.mjs +448 -123
- package/dist/mjs/workspace-merge-projection.mjs +355 -0
- package/dist/types/main.d.ts +395 -0
- package/dist/types/working-tree-mirror.d.ts +1 -2
- package/dist/types/workspace-command-sync-policy.d.ts +4 -3
- package/dist/types/workspace-git-sync.d.ts +34 -0
- package/dist/types/workspace-merge-projection.d.ts +42 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
}
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
}
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
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()
|
|
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()
|
|
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
|
|
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
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
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)
|
|
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
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
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:
|
|
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
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
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
|
});
|