@ricsam/r5d-worker 0.0.67 → 0.0.69
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/README.md +2 -0
- package/dist/cjs/main.cjs +81 -8
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/pty-output-coalescer.cjs +87 -0
- package/dist/mjs/main.mjs +81 -8
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/pty-output-coalescer.mjs +61 -0
- package/dist/types/pty-output-coalescer.d.ts +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,4 +38,6 @@ worker exec macos docker compose up -d
|
|
|
38
38
|
worker exec macos bun test
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
Agent shell commands receive no stdin by default: prompts read immediate EOF instead of hanging. A command started with `interactive: true` is spawned with a writable stdin pipe, and the agent's `shell_write` tool delivers input through the `exec_stdin` protocol message. The worker acknowledges each write with an `operation_result` carrying the delivered byte count, closes stdin on an explicit `eof`, on cancellation, on timeout, and at process exit, and advertises this support through the `execStdinV1` capability in its hello message. Servers refuse interactive runs and stdin writes for workers without the capability, so this is a protocol-compatible release that uses the ordinary worker update flow. Delivered stdin is audited server-side in the run's `process-runs/<runId>/stdin.log`.
|
|
42
|
+
|
|
41
43
|
When the connected `r5d-browser` requests a port forward, the worker opens each relayed connection only to `127.0.0.1` on the requested worker port. Browser-side and worker-side ports may differ. The worker never opens a public listener, and a disconnected worker leaves the browser's long-lived mapping unavailable until the same worker label reconnects.
|
package/dist/cjs/main.cjs
CHANGED
|
@@ -62,6 +62,7 @@ var import_cli_update = require("./cli-update.cjs");
|
|
|
62
62
|
var import_git_identity = require("./git-identity.cjs");
|
|
63
63
|
var import_heartbeat = require("./heartbeat.cjs");
|
|
64
64
|
var import_process_tree = require("./process-tree.cjs");
|
|
65
|
+
var import_pty_output_coalescer = require("./pty-output-coalescer.cjs");
|
|
65
66
|
var import_port_forward_client = require("./port-forward-client.cjs");
|
|
66
67
|
var import_workspace_incident_state = require("./workspace-incident-state.cjs");
|
|
67
68
|
var import_workspace_command_sync_policy = require("./workspace-command-sync-policy.cjs");
|
|
@@ -1960,8 +1961,12 @@ async function executeStreamingCommand(input) {
|
|
|
1960
1961
|
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1961
1962
|
});
|
|
1962
1963
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
1964
|
+
const interactive = input.message.interactive === true;
|
|
1963
1965
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1964
1966
|
cwd,
|
|
1967
|
+
// Without an explicit stdin the process reads /dev/null and interactive
|
|
1968
|
+
// prompts see immediate EOF; "pipe" keeps stdin open for exec_stdin.
|
|
1969
|
+
...interactive ? { stdin: "pipe" } : {},
|
|
1965
1970
|
stdout: "pipe",
|
|
1966
1971
|
stderr: "pipe",
|
|
1967
1972
|
detached: true,
|
|
@@ -1983,7 +1988,8 @@ async function executeStreamingCommand(input) {
|
|
|
1983
1988
|
argv: input.message.argv,
|
|
1984
1989
|
command: input.message.command,
|
|
1985
1990
|
cwd,
|
|
1986
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1991
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1992
|
+
...interactive ? { interactive: true, stdin: subprocess.stdin } : {}
|
|
1987
1993
|
});
|
|
1988
1994
|
started = true;
|
|
1989
1995
|
sendWorkerMessage(input.ws, {
|
|
@@ -1997,6 +2003,7 @@ async function executeStreamingCommand(input) {
|
|
|
1997
2003
|
if (input.message.timeoutMs) {
|
|
1998
2004
|
timeout = setTimeout(() => {
|
|
1999
2005
|
timedOut = true;
|
|
2006
|
+
closeProcessStdin(activeProcesses.get(input.message.runId));
|
|
2000
2007
|
void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
|
|
2001
2008
|
process.stderr.write(
|
|
2002
2009
|
`[r5d-worker] failed to terminate timed-out process ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -2056,10 +2063,21 @@ async function executeStreamingCommand(input) {
|
|
|
2056
2063
|
if (timeout) {
|
|
2057
2064
|
clearTimeout(timeout);
|
|
2058
2065
|
}
|
|
2066
|
+
closeProcessStdin(activeProcesses.get(input.message.runId));
|
|
2059
2067
|
activeProcesses.delete(input.message.runId);
|
|
2060
2068
|
cancelledProcessRuns.delete(input.message.runId);
|
|
2061
2069
|
}
|
|
2062
2070
|
}
|
|
2071
|
+
function closeProcessStdin(active) {
|
|
2072
|
+
if (!active?.stdin) {
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
try {
|
|
2076
|
+
void active.stdin.end();
|
|
2077
|
+
} catch {
|
|
2078
|
+
}
|
|
2079
|
+
active.stdin = void 0;
|
|
2080
|
+
}
|
|
2063
2081
|
function sendWorkerMessage(ws, message) {
|
|
2064
2082
|
const target = currentWorkerSocket ?? ws;
|
|
2065
2083
|
try {
|
|
@@ -2083,7 +2101,8 @@ function buildActiveProcessReports() {
|
|
|
2083
2101
|
argv: active.argv,
|
|
2084
2102
|
command: active.command,
|
|
2085
2103
|
...active.cwd ? { cwd: active.cwd } : {},
|
|
2086
|
-
startedAt: active.startedAt
|
|
2104
|
+
startedAt: active.startedAt,
|
|
2105
|
+
...active.interactive ? { interactive: true } : {}
|
|
2087
2106
|
}));
|
|
2088
2107
|
}
|
|
2089
2108
|
function sendActiveProcessReport(ws) {
|
|
@@ -2290,6 +2309,15 @@ async function openPty(input) {
|
|
|
2290
2309
|
}) : {};
|
|
2291
2310
|
const targetProcessEnv = targetIdentityEnv(input.resolvedTarget.target);
|
|
2292
2311
|
const shell = resolveHostShell(input.message.command);
|
|
2312
|
+
const outputCoalescer = (0, import_pty_output_coalescer.createPtyOutputCoalescer)({
|
|
2313
|
+
send: (data) => {
|
|
2314
|
+
sendWorkerMessage(input.ws, {
|
|
2315
|
+
type: "pty_output",
|
|
2316
|
+
ptyId: input.message.ptyId,
|
|
2317
|
+
data
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2293
2321
|
const ptyProcess = createNodePtyBridge({
|
|
2294
2322
|
file: shell.file,
|
|
2295
2323
|
args: shell.args,
|
|
@@ -2314,13 +2342,10 @@ async function openPty(input) {
|
|
|
2314
2342
|
});
|
|
2315
2343
|
},
|
|
2316
2344
|
onOutput: (data) => {
|
|
2317
|
-
|
|
2318
|
-
type: "pty_output",
|
|
2319
|
-
ptyId: input.message.ptyId,
|
|
2320
|
-
data
|
|
2321
|
-
});
|
|
2345
|
+
outputCoalescer.push(data);
|
|
2322
2346
|
},
|
|
2323
2347
|
onExit: (event) => {
|
|
2348
|
+
outputCoalescer.flush();
|
|
2324
2349
|
input.releaseWorkspaceMutation?.();
|
|
2325
2350
|
activePtys.delete(input.message.ptyId);
|
|
2326
2351
|
input.onTerminal?.();
|
|
@@ -2332,6 +2357,7 @@ async function openPty(input) {
|
|
|
2332
2357
|
});
|
|
2333
2358
|
},
|
|
2334
2359
|
onError: (error) => {
|
|
2360
|
+
outputCoalescer.flush();
|
|
2335
2361
|
input.releaseWorkspaceMutation?.();
|
|
2336
2362
|
activePtys.delete(input.message.ptyId);
|
|
2337
2363
|
input.onTerminal?.();
|
|
@@ -2889,7 +2915,8 @@ async function startWorker(options) {
|
|
|
2889
2915
|
browserPortForwarding: true,
|
|
2890
2916
|
globalWorkspaceSyncLeaseV1: true,
|
|
2891
2917
|
eventualWorkspaceSyncV1: true,
|
|
2892
|
-
workspaceIncidentCandidatePinV1: true
|
|
2918
|
+
workspaceIncidentCandidatePinV1: true,
|
|
2919
|
+
execStdinV1: true
|
|
2893
2920
|
},
|
|
2894
2921
|
projectRoot: projectsRoot,
|
|
2895
2922
|
artifactRoot,
|
|
@@ -3332,6 +3359,7 @@ async function startWorker(options) {
|
|
|
3332
3359
|
let cancelMessage;
|
|
3333
3360
|
if (active) {
|
|
3334
3361
|
try {
|
|
3362
|
+
closeProcessStdin(active);
|
|
3335
3363
|
await (0, import_process_tree.terminateProcessTree)(active.process);
|
|
3336
3364
|
cancelMessage = `Stopped ${active.command}`;
|
|
3337
3365
|
} catch (error) {
|
|
@@ -3353,6 +3381,51 @@ async function startWorker(options) {
|
|
|
3353
3381
|
);
|
|
3354
3382
|
return;
|
|
3355
3383
|
}
|
|
3384
|
+
if (message.type === "exec_stdin") {
|
|
3385
|
+
const sendAck = (payload) => ws.send(
|
|
3386
|
+
JSON.stringify({
|
|
3387
|
+
type: "operation_result",
|
|
3388
|
+
requestId: message.requestId,
|
|
3389
|
+
...payload
|
|
3390
|
+
})
|
|
3391
|
+
);
|
|
3392
|
+
const active = activeProcesses.get(message.runId);
|
|
3393
|
+
if (!active) {
|
|
3394
|
+
sendAck({ error: `Process run ${message.runId} is not active on this worker` });
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
if (!active.interactive || !active.stdin) {
|
|
3398
|
+
sendAck({
|
|
3399
|
+
error: `Process run ${message.runId} has no open stdin. Start a new shell command with "interactive": true to write to its stdin.`
|
|
3400
|
+
});
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
try {
|
|
3404
|
+
let bytesWritten = 0;
|
|
3405
|
+
if (message.data !== void 0 && message.data.length > 0) {
|
|
3406
|
+
bytesWritten = await active.stdin.write(message.data);
|
|
3407
|
+
await active.stdin.flush();
|
|
3408
|
+
}
|
|
3409
|
+
if (message.eof === true) {
|
|
3410
|
+
await active.stdin.end();
|
|
3411
|
+
active.stdin = void 0;
|
|
3412
|
+
}
|
|
3413
|
+
if (targetMayMutateVisibleWorkspace(active.target)) {
|
|
3414
|
+
markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` });
|
|
3415
|
+
}
|
|
3416
|
+
sendAck({
|
|
3417
|
+
result: {
|
|
3418
|
+
type: "exec_stdin",
|
|
3419
|
+
runId: message.runId,
|
|
3420
|
+
bytesWritten,
|
|
3421
|
+
eof: message.eof === true
|
|
3422
|
+
}
|
|
3423
|
+
});
|
|
3424
|
+
} catch (error) {
|
|
3425
|
+
sendAck({ error: error instanceof Error ? error.message : String(error) });
|
|
3426
|
+
}
|
|
3427
|
+
return;
|
|
3428
|
+
}
|
|
3356
3429
|
if (message.type === "pty_open") {
|
|
3357
3430
|
if (cliUpdateInProgress) {
|
|
3358
3431
|
sendWorkerMessage(ws, {
|
package/dist/cjs/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var pty_output_coalescer_exports = {};
|
|
20
|
+
__export(pty_output_coalescer_exports, {
|
|
21
|
+
DEFAULT_PTY_OUTPUT_FLUSH_INTERVAL_MS: () => DEFAULT_PTY_OUTPUT_FLUSH_INTERVAL_MS,
|
|
22
|
+
DEFAULT_PTY_OUTPUT_MAX_BUFFER_BYTES: () => DEFAULT_PTY_OUTPUT_MAX_BUFFER_BYTES,
|
|
23
|
+
createPtyOutputCoalescer: () => createPtyOutputCoalescer
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(pty_output_coalescer_exports);
|
|
26
|
+
const DEFAULT_PTY_OUTPUT_FLUSH_INTERVAL_MS = 8;
|
|
27
|
+
const DEFAULT_PTY_OUTPUT_MAX_BUFFER_BYTES = 64 * 1024;
|
|
28
|
+
function defaultSchedule(callback, delayMs) {
|
|
29
|
+
const timer = setTimeout(callback, delayMs);
|
|
30
|
+
timer.unref();
|
|
31
|
+
return () => clearTimeout(timer);
|
|
32
|
+
}
|
|
33
|
+
function createPtyOutputCoalescer(options) {
|
|
34
|
+
const flushIntervalMs = options.flushIntervalMs ?? DEFAULT_PTY_OUTPUT_FLUSH_INTERVAL_MS;
|
|
35
|
+
const maxBufferBytes = options.maxBufferBytes ?? DEFAULT_PTY_OUTPUT_MAX_BUFFER_BYTES;
|
|
36
|
+
const now = options.now ?? Date.now;
|
|
37
|
+
const schedule = options.schedule ?? defaultSchedule;
|
|
38
|
+
let buffer = "";
|
|
39
|
+
let bufferedBytes = 0;
|
|
40
|
+
let cancelScheduledFlush = null;
|
|
41
|
+
let lastSendAt = Number.NEGATIVE_INFINITY;
|
|
42
|
+
const sendNow = (data) => {
|
|
43
|
+
lastSendAt = now();
|
|
44
|
+
options.send(data);
|
|
45
|
+
};
|
|
46
|
+
const flush = () => {
|
|
47
|
+
if (cancelScheduledFlush) {
|
|
48
|
+
cancelScheduledFlush();
|
|
49
|
+
cancelScheduledFlush = null;
|
|
50
|
+
}
|
|
51
|
+
if (bufferedBytes === 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const data = buffer;
|
|
55
|
+
buffer = "";
|
|
56
|
+
bufferedBytes = 0;
|
|
57
|
+
sendNow(data);
|
|
58
|
+
};
|
|
59
|
+
const push = (data) => {
|
|
60
|
+
if (data.length === 0) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (bufferedBytes === 0 && now() - lastSendAt >= flushIntervalMs) {
|
|
64
|
+
sendNow(data);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
buffer += data;
|
|
68
|
+
bufferedBytes += Buffer.byteLength(data, "utf8");
|
|
69
|
+
if (bufferedBytes >= maxBufferBytes) {
|
|
70
|
+
flush();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!cancelScheduledFlush) {
|
|
74
|
+
cancelScheduledFlush = schedule(() => {
|
|
75
|
+
cancelScheduledFlush = null;
|
|
76
|
+
flush();
|
|
77
|
+
}, flushIntervalMs);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return { push, flush };
|
|
81
|
+
}
|
|
82
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
83
|
+
0 && (module.exports = {
|
|
84
|
+
DEFAULT_PTY_OUTPUT_FLUSH_INTERVAL_MS,
|
|
85
|
+
DEFAULT_PTY_OUTPUT_MAX_BUFFER_BYTES,
|
|
86
|
+
createPtyOutputCoalescer
|
|
87
|
+
});
|
package/dist/mjs/main.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
WORKER_HEARTBEAT_INTERVAL_MS
|
|
13
13
|
} from "./heartbeat.mjs";
|
|
14
14
|
import { terminateProcessTree } from "./process-tree.mjs";
|
|
15
|
+
import { createPtyOutputCoalescer } from "./pty-output-coalescer.mjs";
|
|
15
16
|
import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
|
|
16
17
|
import { applyWorkspaceIncidentUpdate, releasePendingWorkspaceHead, WorkspaceIncidentOrderingFence } from "./workspace-incident-state.mjs";
|
|
17
18
|
import { acquireWorkspaceCommandMutation, runWorkspaceCommand } from "./workspace-command-sync-policy.mjs";
|
|
@@ -1931,8 +1932,12 @@ async function executeStreamingCommand(input) {
|
|
|
1931
1932
|
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1932
1933
|
});
|
|
1933
1934
|
const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
|
|
1935
|
+
const interactive = input.message.interactive === true;
|
|
1934
1936
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1935
1937
|
cwd,
|
|
1938
|
+
// Without an explicit stdin the process reads /dev/null and interactive
|
|
1939
|
+
// prompts see immediate EOF; "pipe" keeps stdin open for exec_stdin.
|
|
1940
|
+
...interactive ? { stdin: "pipe" } : {},
|
|
1936
1941
|
stdout: "pipe",
|
|
1937
1942
|
stderr: "pipe",
|
|
1938
1943
|
detached: true,
|
|
@@ -1954,7 +1959,8 @@ async function executeStreamingCommand(input) {
|
|
|
1954
1959
|
argv: input.message.argv,
|
|
1955
1960
|
command: input.message.command,
|
|
1956
1961
|
cwd,
|
|
1957
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1962
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1963
|
+
...interactive ? { interactive: true, stdin: subprocess.stdin } : {}
|
|
1958
1964
|
});
|
|
1959
1965
|
started = true;
|
|
1960
1966
|
sendWorkerMessage(input.ws, {
|
|
@@ -1968,6 +1974,7 @@ async function executeStreamingCommand(input) {
|
|
|
1968
1974
|
if (input.message.timeoutMs) {
|
|
1969
1975
|
timeout = setTimeout(() => {
|
|
1970
1976
|
timedOut = true;
|
|
1977
|
+
closeProcessStdin(activeProcesses.get(input.message.runId));
|
|
1971
1978
|
void terminateProcessTree(subprocess).catch((error) => {
|
|
1972
1979
|
process.stderr.write(
|
|
1973
1980
|
`[r5d-worker] failed to terminate timed-out process ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -2027,10 +2034,21 @@ async function executeStreamingCommand(input) {
|
|
|
2027
2034
|
if (timeout) {
|
|
2028
2035
|
clearTimeout(timeout);
|
|
2029
2036
|
}
|
|
2037
|
+
closeProcessStdin(activeProcesses.get(input.message.runId));
|
|
2030
2038
|
activeProcesses.delete(input.message.runId);
|
|
2031
2039
|
cancelledProcessRuns.delete(input.message.runId);
|
|
2032
2040
|
}
|
|
2033
2041
|
}
|
|
2042
|
+
function closeProcessStdin(active) {
|
|
2043
|
+
if (!active?.stdin) {
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
try {
|
|
2047
|
+
void active.stdin.end();
|
|
2048
|
+
} catch {
|
|
2049
|
+
}
|
|
2050
|
+
active.stdin = void 0;
|
|
2051
|
+
}
|
|
2034
2052
|
function sendWorkerMessage(ws, message) {
|
|
2035
2053
|
const target = currentWorkerSocket ?? ws;
|
|
2036
2054
|
try {
|
|
@@ -2054,7 +2072,8 @@ function buildActiveProcessReports() {
|
|
|
2054
2072
|
argv: active.argv,
|
|
2055
2073
|
command: active.command,
|
|
2056
2074
|
...active.cwd ? { cwd: active.cwd } : {},
|
|
2057
|
-
startedAt: active.startedAt
|
|
2075
|
+
startedAt: active.startedAt,
|
|
2076
|
+
...active.interactive ? { interactive: true } : {}
|
|
2058
2077
|
}));
|
|
2059
2078
|
}
|
|
2060
2079
|
function sendActiveProcessReport(ws) {
|
|
@@ -2261,6 +2280,15 @@ async function openPty(input) {
|
|
|
2261
2280
|
}) : {};
|
|
2262
2281
|
const targetProcessEnv = targetIdentityEnv(input.resolvedTarget.target);
|
|
2263
2282
|
const shell = resolveHostShell(input.message.command);
|
|
2283
|
+
const outputCoalescer = createPtyOutputCoalescer({
|
|
2284
|
+
send: (data) => {
|
|
2285
|
+
sendWorkerMessage(input.ws, {
|
|
2286
|
+
type: "pty_output",
|
|
2287
|
+
ptyId: input.message.ptyId,
|
|
2288
|
+
data
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
});
|
|
2264
2292
|
const ptyProcess = createNodePtyBridge({
|
|
2265
2293
|
file: shell.file,
|
|
2266
2294
|
args: shell.args,
|
|
@@ -2285,13 +2313,10 @@ async function openPty(input) {
|
|
|
2285
2313
|
});
|
|
2286
2314
|
},
|
|
2287
2315
|
onOutput: (data) => {
|
|
2288
|
-
|
|
2289
|
-
type: "pty_output",
|
|
2290
|
-
ptyId: input.message.ptyId,
|
|
2291
|
-
data
|
|
2292
|
-
});
|
|
2316
|
+
outputCoalescer.push(data);
|
|
2293
2317
|
},
|
|
2294
2318
|
onExit: (event) => {
|
|
2319
|
+
outputCoalescer.flush();
|
|
2295
2320
|
input.releaseWorkspaceMutation?.();
|
|
2296
2321
|
activePtys.delete(input.message.ptyId);
|
|
2297
2322
|
input.onTerminal?.();
|
|
@@ -2303,6 +2328,7 @@ async function openPty(input) {
|
|
|
2303
2328
|
});
|
|
2304
2329
|
},
|
|
2305
2330
|
onError: (error) => {
|
|
2331
|
+
outputCoalescer.flush();
|
|
2306
2332
|
input.releaseWorkspaceMutation?.();
|
|
2307
2333
|
activePtys.delete(input.message.ptyId);
|
|
2308
2334
|
input.onTerminal?.();
|
|
@@ -2860,7 +2886,8 @@ async function startWorker(options) {
|
|
|
2860
2886
|
browserPortForwarding: true,
|
|
2861
2887
|
globalWorkspaceSyncLeaseV1: true,
|
|
2862
2888
|
eventualWorkspaceSyncV1: true,
|
|
2863
|
-
workspaceIncidentCandidatePinV1: true
|
|
2889
|
+
workspaceIncidentCandidatePinV1: true,
|
|
2890
|
+
execStdinV1: true
|
|
2864
2891
|
},
|
|
2865
2892
|
projectRoot: projectsRoot,
|
|
2866
2893
|
artifactRoot,
|
|
@@ -3303,6 +3330,7 @@ async function startWorker(options) {
|
|
|
3303
3330
|
let cancelMessage;
|
|
3304
3331
|
if (active) {
|
|
3305
3332
|
try {
|
|
3333
|
+
closeProcessStdin(active);
|
|
3306
3334
|
await terminateProcessTree(active.process);
|
|
3307
3335
|
cancelMessage = `Stopped ${active.command}`;
|
|
3308
3336
|
} catch (error) {
|
|
@@ -3324,6 +3352,51 @@ async function startWorker(options) {
|
|
|
3324
3352
|
);
|
|
3325
3353
|
return;
|
|
3326
3354
|
}
|
|
3355
|
+
if (message.type === "exec_stdin") {
|
|
3356
|
+
const sendAck = (payload) => ws.send(
|
|
3357
|
+
JSON.stringify({
|
|
3358
|
+
type: "operation_result",
|
|
3359
|
+
requestId: message.requestId,
|
|
3360
|
+
...payload
|
|
3361
|
+
})
|
|
3362
|
+
);
|
|
3363
|
+
const active = activeProcesses.get(message.runId);
|
|
3364
|
+
if (!active) {
|
|
3365
|
+
sendAck({ error: `Process run ${message.runId} is not active on this worker` });
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
if (!active.interactive || !active.stdin) {
|
|
3369
|
+
sendAck({
|
|
3370
|
+
error: `Process run ${message.runId} has no open stdin. Start a new shell command with "interactive": true to write to its stdin.`
|
|
3371
|
+
});
|
|
3372
|
+
return;
|
|
3373
|
+
}
|
|
3374
|
+
try {
|
|
3375
|
+
let bytesWritten = 0;
|
|
3376
|
+
if (message.data !== void 0 && message.data.length > 0) {
|
|
3377
|
+
bytesWritten = await active.stdin.write(message.data);
|
|
3378
|
+
await active.stdin.flush();
|
|
3379
|
+
}
|
|
3380
|
+
if (message.eof === true) {
|
|
3381
|
+
await active.stdin.end();
|
|
3382
|
+
active.stdin = void 0;
|
|
3383
|
+
}
|
|
3384
|
+
if (targetMayMutateVisibleWorkspace(active.target)) {
|
|
3385
|
+
markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` });
|
|
3386
|
+
}
|
|
3387
|
+
sendAck({
|
|
3388
|
+
result: {
|
|
3389
|
+
type: "exec_stdin",
|
|
3390
|
+
runId: message.runId,
|
|
3391
|
+
bytesWritten,
|
|
3392
|
+
eof: message.eof === true
|
|
3393
|
+
}
|
|
3394
|
+
});
|
|
3395
|
+
} catch (error) {
|
|
3396
|
+
sendAck({ error: error instanceof Error ? error.message : String(error) });
|
|
3397
|
+
}
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3327
3400
|
if (message.type === "pty_open") {
|
|
3328
3401
|
if (cliUpdateInProgress) {
|
|
3329
3402
|
sendWorkerMessage(ws, {
|
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const DEFAULT_PTY_OUTPUT_FLUSH_INTERVAL_MS = 8;
|
|
2
|
+
const DEFAULT_PTY_OUTPUT_MAX_BUFFER_BYTES = 64 * 1024;
|
|
3
|
+
function defaultSchedule(callback, delayMs) {
|
|
4
|
+
const timer = setTimeout(callback, delayMs);
|
|
5
|
+
timer.unref();
|
|
6
|
+
return () => clearTimeout(timer);
|
|
7
|
+
}
|
|
8
|
+
function createPtyOutputCoalescer(options) {
|
|
9
|
+
const flushIntervalMs = options.flushIntervalMs ?? DEFAULT_PTY_OUTPUT_FLUSH_INTERVAL_MS;
|
|
10
|
+
const maxBufferBytes = options.maxBufferBytes ?? DEFAULT_PTY_OUTPUT_MAX_BUFFER_BYTES;
|
|
11
|
+
const now = options.now ?? Date.now;
|
|
12
|
+
const schedule = options.schedule ?? defaultSchedule;
|
|
13
|
+
let buffer = "";
|
|
14
|
+
let bufferedBytes = 0;
|
|
15
|
+
let cancelScheduledFlush = null;
|
|
16
|
+
let lastSendAt = Number.NEGATIVE_INFINITY;
|
|
17
|
+
const sendNow = (data) => {
|
|
18
|
+
lastSendAt = now();
|
|
19
|
+
options.send(data);
|
|
20
|
+
};
|
|
21
|
+
const flush = () => {
|
|
22
|
+
if (cancelScheduledFlush) {
|
|
23
|
+
cancelScheduledFlush();
|
|
24
|
+
cancelScheduledFlush = null;
|
|
25
|
+
}
|
|
26
|
+
if (bufferedBytes === 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const data = buffer;
|
|
30
|
+
buffer = "";
|
|
31
|
+
bufferedBytes = 0;
|
|
32
|
+
sendNow(data);
|
|
33
|
+
};
|
|
34
|
+
const push = (data) => {
|
|
35
|
+
if (data.length === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (bufferedBytes === 0 && now() - lastSendAt >= flushIntervalMs) {
|
|
39
|
+
sendNow(data);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
buffer += data;
|
|
43
|
+
bufferedBytes += Buffer.byteLength(data, "utf8");
|
|
44
|
+
if (bufferedBytes >= maxBufferBytes) {
|
|
45
|
+
flush();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!cancelScheduledFlush) {
|
|
49
|
+
cancelScheduledFlush = schedule(() => {
|
|
50
|
+
cancelScheduledFlush = null;
|
|
51
|
+
flush();
|
|
52
|
+
}, flushIntervalMs);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return { push, flush };
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
DEFAULT_PTY_OUTPUT_FLUSH_INTERVAL_MS,
|
|
59
|
+
DEFAULT_PTY_OUTPUT_MAX_BUFFER_BYTES,
|
|
60
|
+
createPtyOutputCoalescer
|
|
61
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface PtyOutputCoalescerOptions {
|
|
2
|
+
send: (data: string) => void;
|
|
3
|
+
flushIntervalMs?: number;
|
|
4
|
+
maxBufferBytes?: number;
|
|
5
|
+
now?: () => number;
|
|
6
|
+
schedule?: (callback: () => void, delayMs: number) => () => void;
|
|
7
|
+
}
|
|
8
|
+
export interface PtyOutputCoalescer {
|
|
9
|
+
push(data: string): void;
|
|
10
|
+
flush(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare const DEFAULT_PTY_OUTPUT_FLUSH_INTERVAL_MS = 8;
|
|
13
|
+
export declare const DEFAULT_PTY_OUTPUT_MAX_BUFFER_BYTES: number;
|
|
14
|
+
/**
|
|
15
|
+
* Batches PTY output into fewer, larger relay messages. An isolated chunk
|
|
16
|
+
* (an interactive keystroke echo) is sent immediately so coalescing adds no
|
|
17
|
+
* input latency; only sustained bursts (build logs, `cat` of a large file)
|
|
18
|
+
* are buffered, bounded by the flush interval and the buffer byte cap.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createPtyOutputCoalescer(options: PtyOutputCoalescerOptions): PtyOutputCoalescer;
|