@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 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
- sendWorkerMessage(input.ws, {
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, {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.67",
3
+ "version": "0.0.69",
4
4
  "type": "commonjs"
5
5
  }
@@ -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
- sendWorkerMessage(input.ws, {
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, {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.67",
3
+ "version": "0.0.69",
4
4
  "type": "module"
5
5
  }
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.67",
3
+ "version": "0.0.69",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",