@ricsam/r5d-worker 0.0.81 → 0.0.82
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 +324 -94
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-command-sync-policy.cjs +3 -3
- package/dist/cjs/workspace-git-sync.cjs +377 -122
- package/dist/cjs/workspace-merge-projection.cjs +392 -0
- package/dist/mjs/main.mjs +322 -94
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-command-sync-policy.mjs +3 -3
- package/dist/mjs/workspace-git-sync.mjs +381 -122
- package/dist/mjs/workspace-merge-projection.mjs +355 -0
- package/dist/types/main.d.ts +63 -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 +6 -0
- package/dist/types/workspace-merge-projection.d.ts +42 -0
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -99,6 +99,10 @@ const DEFAULT_READ_MAX_BYTES = 5e4;
|
|
|
99
99
|
const MAX_LINE_LENGTH = 2e3;
|
|
100
100
|
const WORKSPACE_GIT_QUIET_MS = 5e3;
|
|
101
101
|
const WORKSPACE_GIT_PERIODIC_MS = 6e4;
|
|
102
|
+
const PTY_INPUT_BUSY_GRACE_MS = 3e3;
|
|
103
|
+
const PTY_FOREGROUND_POLL_MS = 1e3;
|
|
104
|
+
const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
|
|
105
|
+
const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
|
|
102
106
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
103
107
|
const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
|
|
104
108
|
const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
|
|
@@ -156,6 +160,106 @@ function assertWorkerChildAdmission(input) {
|
|
|
156
160
|
throw new StaleWorkerAdmissionError();
|
|
157
161
|
}
|
|
158
162
|
}
|
|
163
|
+
function pathIsInsideRoot(rootPath, candidatePath) {
|
|
164
|
+
const relative = path.relative(rootPath, candidatePath);
|
|
165
|
+
return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
166
|
+
}
|
|
167
|
+
function resolvePtyEnvFilePath(requestedPath, temporaryRoot = os.tmpdir()) {
|
|
168
|
+
const canonicalTemporaryRoot = fs.realpathSync.native(temporaryRoot);
|
|
169
|
+
if (requestedPath.startsWith(PTY_TMP_PATH_PREFIX)) {
|
|
170
|
+
const filename = requestedPath.slice(PTY_TMP_PATH_PREFIX.length);
|
|
171
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(filename)) {
|
|
172
|
+
throw new Error("PTY environment file temporary token must contain one safe filename");
|
|
173
|
+
}
|
|
174
|
+
return path.join(canonicalTemporaryRoot, filename);
|
|
175
|
+
}
|
|
176
|
+
if (!path.isAbsolute(requestedPath)) {
|
|
177
|
+
throw new Error(`PTY environment file path must be absolute or use ${PTY_TMP_PATH_PREFIX}`);
|
|
178
|
+
}
|
|
179
|
+
const resolvedPath = path.resolve(requestedPath);
|
|
180
|
+
const canonicalParent = fs.realpathSync.native(path.dirname(resolvedPath));
|
|
181
|
+
const canonicalPath = path.join(canonicalParent, path.basename(resolvedPath));
|
|
182
|
+
if (!pathIsInsideRoot(canonicalTemporaryRoot, canonicalPath) || canonicalPath === canonicalTemporaryRoot) {
|
|
183
|
+
throw new Error(`PTY environment file path must be inside ${canonicalTemporaryRoot}`);
|
|
184
|
+
}
|
|
185
|
+
return canonicalPath;
|
|
186
|
+
}
|
|
187
|
+
function removePtyEnvFiles(paths) {
|
|
188
|
+
for (const filePath of paths) {
|
|
189
|
+
try {
|
|
190
|
+
fs.rmSync(filePath, { force: true });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
process.stderr.write(
|
|
193
|
+
`[r5d-worker] failed to remove PTY environment file ${filePath}: ${error instanceof Error ? error.message : String(error)}
|
|
194
|
+
`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function stagePtyEnvFiles(envFiles, temporaryRoot = os.tmpdir()) {
|
|
200
|
+
const staged = { paths: [], resolvedByRequestedPath: /* @__PURE__ */ new Map() };
|
|
201
|
+
try {
|
|
202
|
+
for (const envFile of envFiles ?? []) {
|
|
203
|
+
if (!envFile || typeof envFile.path !== "string" || typeof envFile.content !== "string") {
|
|
204
|
+
throw new Error("PTY environment files require string path and content values");
|
|
205
|
+
}
|
|
206
|
+
if (envFile.mode !== void 0 && envFile.mode !== 384) {
|
|
207
|
+
throw new Error("PTY environment files must use mode 0600");
|
|
208
|
+
}
|
|
209
|
+
const resolvedPath = resolvePtyEnvFilePath(envFile.path, temporaryRoot);
|
|
210
|
+
if (staged.resolvedByRequestedPath.has(envFile.path) || staged.paths.includes(resolvedPath)) {
|
|
211
|
+
throw new Error(`Duplicate PTY environment file path: ${envFile.path}`);
|
|
212
|
+
}
|
|
213
|
+
const descriptor = fs.openSync(
|
|
214
|
+
resolvedPath,
|
|
215
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0),
|
|
216
|
+
384
|
|
217
|
+
);
|
|
218
|
+
staged.paths.push(resolvedPath);
|
|
219
|
+
staged.resolvedByRequestedPath.set(envFile.path, resolvedPath);
|
|
220
|
+
try {
|
|
221
|
+
fs.writeFileSync(descriptor, envFile.content, "utf8");
|
|
222
|
+
fs.fchmodSync(descriptor, 384);
|
|
223
|
+
fs.fsyncSync(descriptor);
|
|
224
|
+
} finally {
|
|
225
|
+
fs.closeSync(descriptor);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return staged;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
removePtyEnvFiles(staged.paths);
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function resolvePtyEnvFileReferences(env, staged) {
|
|
235
|
+
return Object.fromEntries(Object.entries(env).map(([name, value]) => [name, staged.resolvedByRequestedPath.get(value) ?? value]));
|
|
236
|
+
}
|
|
237
|
+
function workerCommandHasWorkspaceEffect(message) {
|
|
238
|
+
return message.workspaceEffect !== "none";
|
|
239
|
+
}
|
|
240
|
+
function workerPtyIsWorkspaceBusy(pty, now = Date.now(), foregroundIdleEnabled = PTY_FOREGROUND_IDLE_ENABLED) {
|
|
241
|
+
return !foregroundIdleEnabled || pty.foregroundBusy || now - pty.lastInputAt < PTY_INPUT_BUSY_GRACE_MS;
|
|
242
|
+
}
|
|
243
|
+
function parseLinuxPtyForegroundBusy(stat) {
|
|
244
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
245
|
+
if (commandEnd < 0) return null;
|
|
246
|
+
const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
|
|
247
|
+
const processGroup = Number(fields[2]);
|
|
248
|
+
const foregroundProcessGroup = Number(fields[5]);
|
|
249
|
+
if (!Number.isSafeInteger(processGroup) || processGroup <= 0 || !Number.isSafeInteger(foregroundProcessGroup)) return null;
|
|
250
|
+
return foregroundProcessGroup !== processGroup;
|
|
251
|
+
}
|
|
252
|
+
const workerPtyTestHarness = {
|
|
253
|
+
temporaryPathPrefix: PTY_TMP_PATH_PREFIX,
|
|
254
|
+
inputBusyGraceMs: PTY_INPUT_BUSY_GRACE_MS,
|
|
255
|
+
resolveEnvFilePath: resolvePtyEnvFilePath,
|
|
256
|
+
stageEnvFiles: stagePtyEnvFiles,
|
|
257
|
+
removeEnvFiles: removePtyEnvFiles,
|
|
258
|
+
resolveEnvFileReferences: resolvePtyEnvFileReferences,
|
|
259
|
+
commandHasWorkspaceEffect: workerCommandHasWorkspaceEffect,
|
|
260
|
+
ptyIsWorkspaceBusy: workerPtyIsWorkspaceBusy,
|
|
261
|
+
parseLinuxForegroundBusy: parseLinuxPtyForegroundBusy
|
|
262
|
+
};
|
|
159
263
|
function defaultConfigPath() {
|
|
160
264
|
return path.join(os.homedir(), ".config", "r5d", "r5dctl", "config.json");
|
|
161
265
|
}
|
|
@@ -2337,7 +2441,9 @@ async function executeCommand(input) {
|
|
|
2337
2441
|
});
|
|
2338
2442
|
spawnedProcess = subprocess;
|
|
2339
2443
|
credentialBearingProcessGroups.set(subprocess.pid, subprocess);
|
|
2340
|
-
|
|
2444
|
+
if (workerCommandHasWorkspaceEffect(input.message)) {
|
|
2445
|
+
credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
|
|
2446
|
+
}
|
|
2341
2447
|
activeProcesses.set(input.message.runId, {
|
|
2342
2448
|
process: subprocess,
|
|
2343
2449
|
target: input.resolvedTarget.target,
|
|
@@ -2348,7 +2454,8 @@ async function executeCommand(input) {
|
|
|
2348
2454
|
argv: input.message.argv,
|
|
2349
2455
|
command: input.message.argv.join(" "),
|
|
2350
2456
|
cwd,
|
|
2351
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2457
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2458
|
+
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2352
2459
|
});
|
|
2353
2460
|
if (input.message.timeoutMs) {
|
|
2354
2461
|
timeout = setTimeout(() => {
|
|
@@ -2434,7 +2541,9 @@ async function executeStreamingCommand(input) {
|
|
|
2434
2541
|
});
|
|
2435
2542
|
spawnedProcess = subprocess;
|
|
2436
2543
|
credentialBearingProcessGroups.set(subprocess.pid, subprocess);
|
|
2437
|
-
|
|
2544
|
+
if (workerCommandHasWorkspaceEffect(input.message)) {
|
|
2545
|
+
credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
|
|
2546
|
+
}
|
|
2438
2547
|
activeProcesses.set(input.message.runId, {
|
|
2439
2548
|
process: subprocess,
|
|
2440
2549
|
target: input.resolvedTarget.target,
|
|
@@ -2447,7 +2556,8 @@ async function executeStreamingCommand(input) {
|
|
|
2447
2556
|
command: input.message.command,
|
|
2448
2557
|
cwd,
|
|
2449
2558
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2450
|
-
...interactive ? { interactive: true, stdin: subprocess.stdin } : {}
|
|
2559
|
+
...interactive ? { interactive: true, stdin: subprocess.stdin } : {},
|
|
2560
|
+
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2451
2561
|
});
|
|
2452
2562
|
started = true;
|
|
2453
2563
|
sendWorkerMessage(input.ws, {
|
|
@@ -2494,7 +2604,8 @@ async function executeStreamingCommand(input) {
|
|
|
2494
2604
|
runId: input.message.runId,
|
|
2495
2605
|
exitCode,
|
|
2496
2606
|
durationMs: Date.now() - startedAt,
|
|
2497
|
-
...timedOut ? { timedOut: true } : {}
|
|
2607
|
+
...timedOut ? { timedOut: true } : {},
|
|
2608
|
+
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2498
2609
|
};
|
|
2499
2610
|
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
2500
2611
|
sendWorkerMessage(input.ws, terminal);
|
|
@@ -2506,7 +2617,8 @@ async function executeStreamingCommand(input) {
|
|
|
2506
2617
|
type: "exec_error",
|
|
2507
2618
|
runId: input.message.runId,
|
|
2508
2619
|
error: message,
|
|
2509
|
-
durationMs: Date.now() - startedAt
|
|
2620
|
+
durationMs: Date.now() - startedAt,
|
|
2621
|
+
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2510
2622
|
};
|
|
2511
2623
|
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
2512
2624
|
sendWorkerMessage(input.ws, terminal);
|
|
@@ -2587,7 +2699,8 @@ function buildActiveProcessReports() {
|
|
|
2587
2699
|
command: active.command,
|
|
2588
2700
|
...active.cwd ? { cwd: active.cwd } : {},
|
|
2589
2701
|
startedAt: active.startedAt,
|
|
2590
|
-
...active.interactive ? { interactive: true } : {}
|
|
2702
|
+
...active.interactive ? { interactive: true } : {},
|
|
2703
|
+
...active.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2591
2704
|
}));
|
|
2592
2705
|
}
|
|
2593
2706
|
function sendActiveProcessReport(ws) {
|
|
@@ -2599,8 +2712,13 @@ function sendActiveProcessReport(ws) {
|
|
|
2599
2712
|
const PTY_BRIDGE_SCRIPT = String.raw`
|
|
2600
2713
|
const readline = require("node:readline");
|
|
2601
2714
|
const nodePty = require("node-pty");
|
|
2715
|
+
const fs = require("node:fs");
|
|
2716
|
+
const { execFile } = require("node:child_process");
|
|
2602
2717
|
|
|
2603
2718
|
let ptyProcess = null;
|
|
2719
|
+
let foregroundTimer = null;
|
|
2720
|
+
let foregroundPollInFlight = false;
|
|
2721
|
+
let lastForegroundBusy = null;
|
|
2604
2722
|
|
|
2605
2723
|
function send(message, callback) {
|
|
2606
2724
|
process.stdout.write(JSON.stringify(message) + "\n", callback);
|
|
@@ -2610,6 +2728,46 @@ function decode(data) {
|
|
|
2610
2728
|
return Buffer.from(data, "base64").toString("utf8");
|
|
2611
2729
|
}
|
|
2612
2730
|
|
|
2731
|
+
function emitForeground(busy) {
|
|
2732
|
+
if (busy === lastForegroundBusy) return;
|
|
2733
|
+
lastForegroundBusy = busy;
|
|
2734
|
+
send({ type: "foreground", busy });
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
function linuxForegroundBusy(pid) {
|
|
2738
|
+
const stat = fs.readFileSync("/proc/" + pid + "/stat", "utf8");
|
|
2739
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
2740
|
+
if (commandEnd < 0) throw new Error("invalid /proc stat");
|
|
2741
|
+
const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
|
|
2742
|
+
const processGroup = Number(fields[2]);
|
|
2743
|
+
const foregroundProcessGroup = Number(fields[5]);
|
|
2744
|
+
if (!Number.isSafeInteger(processGroup) || processGroup <= 0 || !Number.isSafeInteger(foregroundProcessGroup)) {
|
|
2745
|
+
throw new Error("invalid process group fields");
|
|
2746
|
+
}
|
|
2747
|
+
return foregroundProcessGroup !== processGroup;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
function pollForeground() {
|
|
2751
|
+
if (!ptyProcess || !Number.isSafeInteger(ptyProcess.pid) || ptyProcess.pid <= 0) return;
|
|
2752
|
+
if (process.platform === "linux") {
|
|
2753
|
+
try {
|
|
2754
|
+
emitForeground(linuxForegroundBusy(ptyProcess.pid));
|
|
2755
|
+
} catch {
|
|
2756
|
+
emitForeground(true);
|
|
2757
|
+
}
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
if (process.platform !== "darwin" || foregroundPollInFlight) return;
|
|
2761
|
+
foregroundPollInFlight = true;
|
|
2762
|
+
const pid = ptyProcess.pid;
|
|
2763
|
+
execFile("/bin/ps", ["-o", "tpgid=", "-p", String(pid)], (error, stdout) => {
|
|
2764
|
+
foregroundPollInFlight = false;
|
|
2765
|
+
if (!ptyProcess || ptyProcess.pid !== pid) return;
|
|
2766
|
+
const foregroundProcessGroup = Number(String(stdout).trim());
|
|
2767
|
+
emitForeground(Boolean(error) || !Number.isSafeInteger(foregroundProcessGroup) || foregroundProcessGroup !== pid);
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2613
2771
|
const rl = readline.createInterface({ input: process.stdin });
|
|
2614
2772
|
|
|
2615
2773
|
rl.on("line", (line) => {
|
|
@@ -2628,9 +2786,14 @@ rl.on("line", (line) => {
|
|
|
2628
2786
|
send({ type: "output", data: Buffer.from(data, "utf8").toString("base64") });
|
|
2629
2787
|
});
|
|
2630
2788
|
ptyProcess.onExit((event) => {
|
|
2789
|
+
if (foregroundTimer) clearInterval(foregroundTimer);
|
|
2790
|
+
foregroundTimer = null;
|
|
2631
2791
|
send({ type: "exit", exitCode: event.exitCode, signal: event.signal }, () => process.exit(0));
|
|
2632
2792
|
});
|
|
2633
|
-
send({ type: "opened" });
|
|
2793
|
+
send({ type: "opened", pid: ptyProcess.pid });
|
|
2794
|
+
pollForeground();
|
|
2795
|
+
foregroundTimer = setInterval(pollForeground, ${PTY_FOREGROUND_POLL_MS});
|
|
2796
|
+
foregroundTimer.unref();
|
|
2634
2797
|
return;
|
|
2635
2798
|
}
|
|
2636
2799
|
|
|
@@ -2723,7 +2886,11 @@ function createNodePtyBridge(options) {
|
|
|
2723
2886
|
};
|
|
2724
2887
|
const handleEvent = (event) => {
|
|
2725
2888
|
if (event.type === "opened") {
|
|
2726
|
-
options.onOpened();
|
|
2889
|
+
options.onOpened(event.pid);
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
if (event.type === "foreground") {
|
|
2893
|
+
options.onForeground(event.busy);
|
|
2727
2894
|
return;
|
|
2728
2895
|
}
|
|
2729
2896
|
if (event.type === "output") {
|
|
@@ -2804,6 +2971,9 @@ function createNodePtyBridge(options) {
|
|
|
2804
2971
|
}
|
|
2805
2972
|
};
|
|
2806
2973
|
}
|
|
2974
|
+
const workerPtyBridgeTestHarness = {
|
|
2975
|
+
create: createNodePtyBridge
|
|
2976
|
+
};
|
|
2807
2977
|
function resolveHostShell(command, platform = process.platform) {
|
|
2808
2978
|
if (platform === "win32") {
|
|
2809
2979
|
const file2 = process.env.COMSPEC || "powershell.exe";
|
|
@@ -2839,64 +3009,90 @@ async function openPty(input) {
|
|
|
2839
3009
|
}
|
|
2840
3010
|
});
|
|
2841
3011
|
input.assertAdmission();
|
|
2842
|
-
const
|
|
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
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3012
|
+
const stagedEnvFiles = stagePtyEnvFiles(input.message.envFiles);
|
|
3013
|
+
let envFilesRemoved = false;
|
|
3014
|
+
const cleanupEnvFiles = () => {
|
|
3015
|
+
if (envFilesRemoved) return;
|
|
3016
|
+
envFilesRemoved = true;
|
|
3017
|
+
removePtyEnvFiles(stagedEnvFiles.paths);
|
|
3018
|
+
};
|
|
3019
|
+
try {
|
|
3020
|
+
const ptyProcess = createNodePtyBridge({
|
|
3021
|
+
file: shell.file,
|
|
3022
|
+
args: shell.args,
|
|
3023
|
+
ptyOptions: {
|
|
3024
|
+
name: "xterm-256color",
|
|
3025
|
+
cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
|
|
3026
|
+
rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
|
|
3027
|
+
cwd: input.resolvedTarget.rootPath,
|
|
3028
|
+
env: workerChildProcessEnvironment([
|
|
3029
|
+
githubProcessEnv(),
|
|
3030
|
+
resolvePtyEnvFileReferences(input.message.env ?? {}, stagedEnvFiles),
|
|
3031
|
+
planProcessEnv,
|
|
3032
|
+
targetProcessEnv,
|
|
3033
|
+
shell.env ?? {}
|
|
3034
|
+
])
|
|
3035
|
+
},
|
|
3036
|
+
onOpened: () => {
|
|
3037
|
+
sendWorkerMessage(input.ws, {
|
|
3038
|
+
type: "pty_opened",
|
|
3039
|
+
requestId: input.message.requestId,
|
|
3040
|
+
ptyId: input.message.ptyId
|
|
3041
|
+
});
|
|
3042
|
+
},
|
|
3043
|
+
onForeground: (busy) => {
|
|
3044
|
+
const activePty = activePtys.get(input.message.ptyId);
|
|
3045
|
+
if (activePty && (busy || input.message.command === void 0)) activePty.foregroundBusy = busy;
|
|
3046
|
+
},
|
|
3047
|
+
onOutput: (data) => {
|
|
3048
|
+
outputCoalescer.push(data);
|
|
3049
|
+
},
|
|
3050
|
+
onExit: (event) => {
|
|
3051
|
+
outputCoalescer.flush();
|
|
3052
|
+
input.releaseWorkspaceMutation?.();
|
|
3053
|
+
activePtys.delete(input.message.ptyId);
|
|
3054
|
+
cleanupEnvFiles();
|
|
3055
|
+
input.onTerminal?.();
|
|
3056
|
+
sendWorkerMessage(input.ws, {
|
|
3057
|
+
type: "pty_exit",
|
|
3058
|
+
ptyId: input.message.ptyId,
|
|
3059
|
+
exitCode: event.exitCode,
|
|
3060
|
+
signal: event.signal
|
|
3061
|
+
});
|
|
3062
|
+
},
|
|
3063
|
+
onError: (error) => {
|
|
3064
|
+
outputCoalescer.flush();
|
|
3065
|
+
input.releaseWorkspaceMutation?.();
|
|
3066
|
+
activePtys.delete(input.message.ptyId);
|
|
3067
|
+
cleanupEnvFiles();
|
|
3068
|
+
input.onTerminal?.();
|
|
3069
|
+
sendWorkerMessage(input.ws, {
|
|
3070
|
+
type: "pty_error",
|
|
3071
|
+
requestId: input.message.requestId,
|
|
3072
|
+
ptyId: input.message.ptyId,
|
|
3073
|
+
error: error.message
|
|
3074
|
+
});
|
|
3075
|
+
},
|
|
3076
|
+
onTerminationError: (error) => {
|
|
3077
|
+
sendWorkerMessage(input.ws, {
|
|
3078
|
+
type: "pty_error",
|
|
3079
|
+
requestId: input.message.requestId,
|
|
3080
|
+
ptyId: input.message.ptyId,
|
|
3081
|
+
error: error.message
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
});
|
|
3085
|
+
activePtys.set(input.message.ptyId, {
|
|
3086
|
+
...ptyProcess,
|
|
3087
|
+
target: input.resolvedTarget.target,
|
|
3088
|
+
foregroundBusy: true,
|
|
3089
|
+
lastInputAt: 0,
|
|
3090
|
+
...input.releaseWorkspaceMutation ? { releaseWorkspaceMutation: input.releaseWorkspaceMutation } : {}
|
|
3091
|
+
});
|
|
3092
|
+
} catch (error) {
|
|
3093
|
+
cleanupEnvFiles();
|
|
3094
|
+
throw error;
|
|
3095
|
+
}
|
|
2900
3096
|
}
|
|
2901
3097
|
function writePty(ws, message) {
|
|
2902
3098
|
const ptyProcess = activePtys.get(message.ptyId);
|
|
@@ -2908,6 +3104,7 @@ function writePty(ws, message) {
|
|
|
2908
3104
|
});
|
|
2909
3105
|
return;
|
|
2910
3106
|
}
|
|
3107
|
+
ptyProcess.lastInputAt = Date.now();
|
|
2911
3108
|
ptyProcess.write(message.data);
|
|
2912
3109
|
}
|
|
2913
3110
|
function resizePty(message) {
|
|
@@ -3345,13 +3542,24 @@ async function startWorker(options) {
|
|
|
3345
3542
|
});
|
|
3346
3543
|
};
|
|
3347
3544
|
const activeWorkspaceMutationTargets = () => [
|
|
3348
|
-
...Array.from(activeProcesses.values()
|
|
3545
|
+
...Array.from(activeProcesses.values()).filter(workerCommandHasWorkspaceEffect).map(({ target }) => target),
|
|
3349
3546
|
...credentialBearingProcessGroupTargets.values(),
|
|
3350
3547
|
...workspaceSyncPriorityProcessTargets.values(),
|
|
3351
3548
|
...workspaceSyncPriorityOperationTargets.values(),
|
|
3352
|
-
...Array.from(activePtys.values()
|
|
3549
|
+
...Array.from(activePtys.values()).filter((pty) => workerPtyIsWorkspaceBusy(pty)).map(({ target }) => target),
|
|
3353
3550
|
...workspaceSyncPriorityPtyTargets.values()
|
|
3354
3551
|
];
|
|
3552
|
+
let visibleWorkspaceMutationEpoch = 0;
|
|
3553
|
+
const projectWorkspaceMutationEpochs = /* @__PURE__ */ new Map();
|
|
3554
|
+
const recordVisibleWorkspaceMutation = (target) => {
|
|
3555
|
+
if (target.type === "workspace") {
|
|
3556
|
+
if (target.rootProfile === "visible_projects") visibleWorkspaceMutationEpoch += 1;
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
const key = projectBranchKey(target.projectId, target.branchName);
|
|
3560
|
+
projectWorkspaceMutationEpochs.set(key, (projectWorkspaceMutationEpochs.get(key) ?? 0) + 1);
|
|
3561
|
+
};
|
|
3562
|
+
const projectWorkspaceMutationToken = (projectId, branchName) => `${visibleWorkspaceMutationEpoch}:${projectWorkspaceMutationEpochs.get(projectBranchKey(projectId, branchName)) ?? 0}`;
|
|
3355
3563
|
const canonicalWorkspaceMutationIsActive = (targets) => targets.some((target) => target.type === "workspace" && target.rootProfile === "canonical_sync");
|
|
3356
3564
|
const projectBranchMountActivityBusy = (projectId, branchName, branchPath) => {
|
|
3357
3565
|
const activeTargets = activeWorkspaceMutationTargets();
|
|
@@ -3388,6 +3596,7 @@ async function startWorker(options) {
|
|
|
3388
3596
|
preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
|
|
3389
3597
|
preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
|
|
3390
3598
|
busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
|
|
3599
|
+
mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
|
|
3391
3600
|
busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
|
|
3392
3601
|
});
|
|
3393
3602
|
mounts.push({
|
|
@@ -3401,6 +3610,7 @@ async function startWorker(options) {
|
|
|
3401
3610
|
preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
|
|
3402
3611
|
preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
|
|
3403
3612
|
busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
|
|
3613
|
+
mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
|
|
3404
3614
|
busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
|
|
3405
3615
|
});
|
|
3406
3616
|
}
|
|
@@ -3421,6 +3631,7 @@ async function startWorker(options) {
|
|
|
3421
3631
|
const activeTargets = activeWorkspaceMutationTargets();
|
|
3422
3632
|
return canonicalWorkspaceMutationIsActive(activeTargets) || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
|
|
3423
3633
|
},
|
|
3634
|
+
mutationToken: () => String(visibleWorkspaceMutationEpoch),
|
|
3424
3635
|
busyForRecovery: () => {
|
|
3425
3636
|
const activeTargets = activeWorkspaceMutationTargets();
|
|
3426
3637
|
return canonicalWorkspaceMutationIsActive(activeTargets) || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
|
|
@@ -3778,6 +3989,7 @@ async function startWorker(options) {
|
|
|
3778
3989
|
localChangesDiscarded: false,
|
|
3779
3990
|
...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
|
|
3780
3991
|
...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
|
|
3992
|
+
...result.conflictKind ? { conflictKind: result.conflictKind } : {},
|
|
3781
3993
|
...result.error ? { error: result.error } : {}
|
|
3782
3994
|
};
|
|
3783
3995
|
};
|
|
@@ -3844,10 +4056,7 @@ async function startWorker(options) {
|
|
|
3844
4056
|
};
|
|
3845
4057
|
}
|
|
3846
4058
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
3847
|
-
const
|
|
3848
|
-
(mount) => !(mount.deleteWhenSourceMissing && !fs.existsSync(mount.sourcePath)) && mount.busy?.()
|
|
3849
|
-
);
|
|
3850
|
-
const observedProjectHeads = outerRemediation || ordinaryCycleHasBusyLiveMount ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
4059
|
+
const observedProjectHeads = outerRemediation ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
3851
4060
|
const inboundMoveMountIds = new Set(
|
|
3852
4061
|
observedProjectHeads.flatMap(
|
|
3853
4062
|
({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
|
|
@@ -4003,7 +4212,8 @@ async function startWorker(options) {
|
|
|
4003
4212
|
);
|
|
4004
4213
|
workspaceAutomaticTimer.unref();
|
|
4005
4214
|
};
|
|
4006
|
-
const markWorkspaceDirty = (trigger, force = false) => {
|
|
4215
|
+
const markWorkspaceDirty = (trigger, force = false, target) => {
|
|
4216
|
+
if (target) recordVisibleWorkspaceMutation(target);
|
|
4007
4217
|
scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
|
|
4008
4218
|
};
|
|
4009
4219
|
const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
|
|
@@ -4264,7 +4474,7 @@ async function startWorker(options) {
|
|
|
4264
4474
|
sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
|
|
4265
4475
|
busy: mount.busyForRecovery ?? mount.busy
|
|
4266
4476
|
}));
|
|
4267
|
-
recoverWorkspaceGitHydration(workspaceShadowRoot, recoveryMounts);
|
|
4477
|
+
recoverWorkspaceGitHydration(workspaceShadowRoot, recoveryMounts, { preserveStaleBases: true });
|
|
4268
4478
|
for (const project of message.projects) {
|
|
4269
4479
|
for (const { branchName } of project.preserveOnlyBranches) {
|
|
4270
4480
|
if (stillPendingCreatedBranches.has(pendingCreatedBranchKey(project.projectId, branchName))) {
|
|
@@ -4440,7 +4650,8 @@ async function startWorker(options) {
|
|
|
4440
4650
|
capabilities: {
|
|
4441
4651
|
updateClis: true,
|
|
4442
4652
|
browserPortForwarding: true,
|
|
4443
|
-
execStdinV1: true
|
|
4653
|
+
execStdinV1: true,
|
|
4654
|
+
ptyEnvFilesV1: true
|
|
4444
4655
|
},
|
|
4445
4656
|
projectRoot: projectsRoot,
|
|
4446
4657
|
artifactRoot,
|
|
@@ -4815,6 +5026,9 @@ async function startWorker(options) {
|
|
|
4815
5026
|
});
|
|
4816
5027
|
return;
|
|
4817
5028
|
}
|
|
5029
|
+
if (active.workspaceEffect !== "none" && targetMayMutateVisibleWorkspace(active.target)) {
|
|
5030
|
+
recordVisibleWorkspaceMutation(active.target);
|
|
5031
|
+
}
|
|
4818
5032
|
try {
|
|
4819
5033
|
let bytesWritten = 0;
|
|
4820
5034
|
if (message.data !== void 0 && message.data.length > 0) {
|
|
@@ -4825,8 +5039,8 @@ async function startWorker(options) {
|
|
|
4825
5039
|
await active.stdin.end();
|
|
4826
5040
|
active.stdin = void 0;
|
|
4827
5041
|
}
|
|
4828
|
-
if (targetMayMutateVisibleWorkspace(active.target)) {
|
|
4829
|
-
markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` });
|
|
5042
|
+
if (active.workspaceEffect !== "none" && targetMayMutateVisibleWorkspace(active.target)) {
|
|
5043
|
+
markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` }, false, active.target);
|
|
4830
5044
|
}
|
|
4831
5045
|
sendAck({
|
|
4832
5046
|
result: {
|
|
@@ -4853,6 +5067,7 @@ async function startWorker(options) {
|
|
|
4853
5067
|
}
|
|
4854
5068
|
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
4855
5069
|
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
5070
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
4856
5071
|
});
|
|
4857
5072
|
let releaseWorkspaceMutation;
|
|
4858
5073
|
let mutationLeaseTransferred = false;
|
|
@@ -4870,7 +5085,7 @@ async function startWorker(options) {
|
|
|
4870
5085
|
...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
|
|
4871
5086
|
...targetMayMutateVisibleWorkspace(message.target) ? {
|
|
4872
5087
|
onTerminal: () => {
|
|
4873
|
-
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true);
|
|
5088
|
+
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true, message.target);
|
|
4874
5089
|
}
|
|
4875
5090
|
} : {}
|
|
4876
5091
|
});
|
|
@@ -4891,7 +5106,7 @@ async function startWorker(options) {
|
|
|
4891
5106
|
if (message.type === "pty_input") {
|
|
4892
5107
|
const activePty = activePtys.get(message.ptyId);
|
|
4893
5108
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
4894
|
-
markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` });
|
|
5109
|
+
markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
|
|
4895
5110
|
}
|
|
4896
5111
|
writePty(ws, message);
|
|
4897
5112
|
return;
|
|
@@ -4904,7 +5119,7 @@ async function startWorker(options) {
|
|
|
4904
5119
|
const activePty = activePtys.get(message.ptyId);
|
|
4905
5120
|
closePty(message);
|
|
4906
5121
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
4907
|
-
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true);
|
|
5122
|
+
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
4908
5123
|
}
|
|
4909
5124
|
return;
|
|
4910
5125
|
}
|
|
@@ -4921,11 +5136,15 @@ async function startWorker(options) {
|
|
|
4921
5136
|
}
|
|
4922
5137
|
let result;
|
|
4923
5138
|
let targetReserved = false;
|
|
5139
|
+
const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
|
|
4924
5140
|
try {
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
5141
|
+
if (hasWorkspaceEffect) {
|
|
5142
|
+
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
5143
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
5144
|
+
targetReserved = true;
|
|
5145
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
5146
|
+
});
|
|
5147
|
+
}
|
|
4929
5148
|
const runCommand = async () => {
|
|
4930
5149
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
4931
5150
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
@@ -4955,10 +5174,11 @@ async function startWorker(options) {
|
|
|
4955
5174
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
4956
5175
|
}
|
|
4957
5176
|
ws.send(JSON.stringify(result));
|
|
4958
|
-
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
5177
|
+
if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
|
|
4959
5178
|
markWorkspaceDirty(
|
|
4960
5179
|
{ type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
|
|
4961
|
-
true
|
|
5180
|
+
true,
|
|
5181
|
+
message.target
|
|
4962
5182
|
);
|
|
4963
5183
|
}
|
|
4964
5184
|
return;
|
|
@@ -4978,6 +5198,7 @@ async function startWorker(options) {
|
|
|
4978
5198
|
requestId: message.requestId,
|
|
4979
5199
|
runId: message.runId
|
|
4980
5200
|
});
|
|
5201
|
+
const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
|
|
4981
5202
|
const runCommand = async () => {
|
|
4982
5203
|
try {
|
|
4983
5204
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
@@ -4994,7 +5215,7 @@ async function startWorker(options) {
|
|
|
4994
5215
|
assertAdmission: assertMessageAdmission
|
|
4995
5216
|
});
|
|
4996
5217
|
} finally {
|
|
4997
|
-
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
5218
|
+
if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
|
|
4998
5219
|
markWorkspaceDirty(
|
|
4999
5220
|
{
|
|
5000
5221
|
type: "process_terminal",
|
|
@@ -5002,7 +5223,8 @@ async function startWorker(options) {
|
|
|
5002
5223
|
processRunId: message.runId,
|
|
5003
5224
|
detail: "process completed"
|
|
5004
5225
|
},
|
|
5005
|
-
true
|
|
5226
|
+
true,
|
|
5227
|
+
message.target
|
|
5006
5228
|
);
|
|
5007
5229
|
}
|
|
5008
5230
|
}
|
|
@@ -5010,10 +5232,13 @@ async function startWorker(options) {
|
|
|
5010
5232
|
const execution = (async () => {
|
|
5011
5233
|
let targetReserved = false;
|
|
5012
5234
|
try {
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5235
|
+
if (hasWorkspaceEffect) {
|
|
5236
|
+
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
5237
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
5238
|
+
targetReserved = true;
|
|
5239
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
5240
|
+
});
|
|
5241
|
+
}
|
|
5017
5242
|
await runWorkspaceCommand(message.target, workspaceSyncSingleFlight, runCommand);
|
|
5018
5243
|
} finally {
|
|
5019
5244
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
@@ -5036,6 +5261,7 @@ async function startWorker(options) {
|
|
|
5036
5261
|
if (reservesVisibleWorkspace) {
|
|
5037
5262
|
await reserveWorkspaceCommandAfterCurrentSync(message.target, workspaceSyncSingleFlight, () => {
|
|
5038
5263
|
workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
|
|
5264
|
+
if (mutatesVisibleWorkspace) recordVisibleWorkspaceMutation(message.target);
|
|
5039
5265
|
});
|
|
5040
5266
|
}
|
|
5041
5267
|
let dirtyTrigger;
|
|
@@ -5065,7 +5291,7 @@ async function startWorker(options) {
|
|
|
5065
5291
|
toolCallId: message.requestId,
|
|
5066
5292
|
...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
|
|
5067
5293
|
};
|
|
5068
|
-
markWorkspaceDirty(dirtyTrigger);
|
|
5294
|
+
markWorkspaceDirty(dirtyTrigger, false, message.target);
|
|
5069
5295
|
}
|
|
5070
5296
|
} catch (error) {
|
|
5071
5297
|
ws.send(
|
|
@@ -5225,5 +5451,7 @@ export {
|
|
|
5225
5451
|
syncSessionArtifacts,
|
|
5226
5452
|
workerChildProcessEnvironment,
|
|
5227
5453
|
workerGitSecurityTestHarness,
|
|
5454
|
+
workerPtyBridgeTestHarness,
|
|
5455
|
+
workerPtyTestHarness,
|
|
5228
5456
|
writeWorkerTextFile
|
|
5229
5457
|
};
|