@mono-agent/agent-runtime 0.15.2 → 0.15.4

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.
Files changed (39) hide show
  1. package/README.md +55 -7
  2. package/package.json +5 -1
  3. package/src/agent/tools/agent-tool.js +859 -0
  4. package/src/agent/tools/bash.js +241 -123
  5. package/src/agent/tools/exec.js +238 -0
  6. package/src/agent/tools/index.js +10 -3
  7. package/src/agent/tools/node-repl.js +231 -95
  8. package/src/agent/tools/pi-bridge.js +115 -24
  9. package/src/agent/tools/shared/process-runner.js +162 -0
  10. package/src/agent/tools/shared/semaphore.js +73 -0
  11. package/src/agent/tools/web-browser-render.js +221 -0
  12. package/src/agent/tools/web-controller.js +160 -0
  13. package/src/agent/tools/web-fetch.js +653 -68
  14. package/src/agent/tools/web-search.js +568 -16
  15. package/src/ai/providers/codex-app.js +18 -0
  16. package/src/ai/providers/pi-native/stream-subscriber.js +37 -0
  17. package/src/ai/providers/pi-native/turn-runner.js +60 -5
  18. package/src/ai/providers/pi-native.js +49 -5
  19. package/src/ai/runtime/router.js +302 -166
  20. package/src/ai/types.js +52 -1
  21. package/src/runtime.js +51 -1
  22. package/types/agent/tools/agent-tool.d.ts +60 -0
  23. package/types/agent/tools/bash.d.ts +55 -7
  24. package/types/agent/tools/exec.d.ts +53 -0
  25. package/types/agent/tools/index.d.ts +5 -3
  26. package/types/agent/tools/node-repl.d.ts +28 -3
  27. package/types/agent/tools/pi-bridge.d.ts +6 -2
  28. package/types/agent/tools/shared/process-runner.d.ts +33 -0
  29. package/types/agent/tools/shared/semaphore.d.ts +29 -0
  30. package/types/agent/tools/web-browser-render.d.ts +16 -0
  31. package/types/agent/tools/web-controller.d.ts +20 -0
  32. package/types/agent/tools/web-fetch.d.ts +74 -5
  33. package/types/agent/tools/web-search.d.ts +81 -5
  34. package/types/ai/backend.d.ts +57 -0
  35. package/types/ai/providers/pi-native/turn-runner.d.ts +34 -2
  36. package/types/ai/providers/pi-native.d.ts +12 -0
  37. package/types/ai/registry.d.ts +1 -0
  38. package/types/ai/runtime/router.d.ts +23 -3
  39. package/types/ai/types.d.ts +163 -1
@@ -1,24 +1,30 @@
1
1
  // @ts-check
2
2
 
3
3
  import { spawn } from "node:child_process";
4
+ import { randomBytes } from "node:crypto";
4
5
  import { resolve } from "node:path";
5
6
  import { passthroughSandbox } from "../sandbox-seam.js";
6
7
  import { capChars } from "./shared/output-truncation.js";
8
+ import { killProcessGroup } from "./shared/process-runner.js";
7
9
  import { readToolRuntime } from "./shared/runtime-context.js";
8
10
  import { resolveSandboxPolicy } from "./shared/tool-context.js";
9
11
 
10
12
  const DEFAULT_NODE_REPL_TIMEOUT_MS = 120_000;
11
13
  const NODE_REPL_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
14
+ const NODE_REPL_MAX_FRAME_BYTES = NODE_REPL_MAX_BUFFER_BYTES * 3;
12
15
  const KILL_GRACE_MS = 1_000;
13
16
 
14
17
  // Kept self-contained so the sandboxed child can start from `node --eval`
15
18
  // without needing read access to agent-runtime's installed package directory.
16
- function nodeReplWorkerMain() {
19
+ function nodeReplWorkerMain(frameToken) {
17
20
  const repl = require("node:repl");
18
21
  const { PassThrough } = require("node:stream");
19
22
  const MAX_BUFFER_BYTES = 8 * 1024 * 1024;
23
+ const MAX_FRAME_BYTES = MAX_BUFFER_BYTES * 3;
20
24
  const input = new PassThrough();
21
25
  const output = new PassThrough();
26
+ const protocolWrite = process.stdout.write.bind(process.stdout);
27
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
22
28
  const server = repl.start({
23
29
  input,
24
30
  output,
@@ -28,10 +34,19 @@ function nodeReplWorkerMain() {
28
34
  });
29
35
  const replServer = /** @type {any} */ (server);
30
36
  let active = null;
37
+ let protocolBuffer = Buffer.alloc(0);
38
+
39
+ function encodeFrame(message) {
40
+ const payload = Buffer.from(JSON.stringify(message), "utf8");
41
+ return Buffer.concat([
42
+ Buffer.from(`${frameToken}:${payload.length}\n`, "utf8"),
43
+ payload,
44
+ ]);
45
+ }
31
46
 
32
47
  function send(message) {
33
48
  try {
34
- process.send?.(message);
49
+ protocolWrite(encodeFrame(message));
35
50
  } catch {
36
51
  process.exit(1);
37
52
  }
@@ -57,7 +72,7 @@ function nodeReplWorkerMain() {
57
72
  ok: false,
58
73
  reset: true,
59
74
  text: `Node REPL output exceeded ${MAX_BUFFER_BYTES} bytes.`,
60
- stdout: request.stdout,
75
+ stdout: request.stdout.slice(0, MAX_BUFFER_BYTES),
61
76
  stderr: request.stderr,
62
77
  });
63
78
  setImmediate(() => process.exit(1));
@@ -94,8 +109,8 @@ function nodeReplWorkerMain() {
94
109
  };
95
110
  }
96
111
 
97
- process.stdout.write = captureProcessWrite("stdout", process.stdout.write.bind(process.stdout));
98
- process.stderr.write = captureProcessWrite("stderr", process.stderr.write.bind(process.stderr));
112
+ process.stdout.write = captureProcessWrite("stdout", protocolWrite);
113
+ process.stderr.write = captureProcessWrite("stderr", originalStderrWrite);
99
114
 
100
115
  output.on("data", (chunk) => {
101
116
  if (!active) return;
@@ -117,8 +132,7 @@ function nodeReplWorkerMain() {
117
132
  finish(false, active.output.trimEnd() || "Node REPL evaluation failed.");
118
133
  };
119
134
 
120
- process.on("message", (message) => {
121
- const requestMessage = /** @type {any} */ (message);
135
+ function evaluate(requestMessage) {
122
136
  if (!requestMessage || requestMessage.type !== "evaluate") return;
123
137
  if (active) {
124
138
  send({ type: "result", id: requestMessage.id, ok: false, text: "Node REPL is already evaluating code." });
@@ -150,42 +164,69 @@ function nodeReplWorkerMain() {
150
164
  } catch (error) {
151
165
  finish(false, [active.output.trimEnd(), errorText(error)].filter(Boolean).join("\n"));
152
166
  }
153
- });
167
+ }
168
+
169
+ function consumeFrames(chunk) {
170
+ protocolBuffer = Buffer.concat([protocolBuffer, chunk]);
171
+ if (protocolBuffer.length > MAX_FRAME_BYTES) process.exit(1);
172
+ while (protocolBuffer.length > 0) {
173
+ const newline = protocolBuffer.indexOf(0x0a);
174
+ if (newline < 0) return;
175
+ const header = protocolBuffer.subarray(0, newline).toString("utf8");
176
+ const prefix = `${frameToken}:`;
177
+ if (!header.startsWith(prefix)) process.exit(1);
178
+ const lengthText = header.slice(prefix.length);
179
+ if (!/^\d+$/.test(lengthText)) process.exit(1);
180
+ const length = Number(lengthText);
181
+ if (!Number.isSafeInteger(length) || length < 0 || length > MAX_FRAME_BYTES) process.exit(1);
182
+ const frameEnd = newline + 1 + length;
183
+ if (protocolBuffer.length < frameEnd) return;
184
+ const payload = protocolBuffer.subarray(newline + 1, frameEnd);
185
+ protocolBuffer = protocolBuffer.subarray(frameEnd);
186
+ try {
187
+ evaluate(JSON.parse(payload.toString("utf8")));
188
+ } catch {
189
+ process.exit(1);
190
+ }
191
+ }
192
+ }
154
193
 
155
- process.on("disconnect", () => {
194
+ process.stdin.on("data", consumeFrames);
195
+ process.stdin.on("end", () => {
156
196
  server.close();
157
197
  process.exit(0);
158
198
  });
159
199
  }
160
200
 
161
- const NODE_REPL_WORKER_SOURCE = `(${nodeReplWorkerMain.toString()})();`;
201
+ function workerSource(frameToken) {
202
+ return `(${nodeReplWorkerMain.toString()})(${JSON.stringify(frameToken)});`;
203
+ }
162
204
 
163
- function killProcessGroup(child, signal) {
164
- if (!child?.pid) return;
165
- try {
166
- process.kill(process.platform === "win32" ? child.pid : -child.pid, signal);
167
- } catch {
168
- try { process.kill(child.pid, signal); } catch { /* already gone */ }
169
- }
205
+ function encodeFrame(frameToken, message) {
206
+ const payload = Buffer.from(JSON.stringify(message), "utf8");
207
+ return Buffer.concat([
208
+ Buffer.from(`${frameToken}:${payload.length}\n`, "utf8"),
209
+ payload,
210
+ ]);
170
211
  }
171
212
 
172
213
  function errorMessage(error) {
173
214
  return error instanceof Error ? error.message : String(error);
174
215
  }
175
216
 
176
- function appendChunk(record, target, chunk) {
217
+ function appendStderr(record, chunk) {
177
218
  record.directOutputBytes += chunk.length;
178
219
  if (record.directOutputBytes > NODE_REPL_MAX_BUFFER_BYTES) {
179
220
  record.failureReason = `Node REPL output exceeded ${NODE_REPL_MAX_BUFFER_BYTES} bytes.`;
180
221
  void terminateRecord(record);
181
222
  return;
182
223
  }
183
- target.push(chunk);
224
+ record.stderr.push(chunk);
184
225
  }
185
226
 
186
227
  function directOutput(record, capturedStdout = "", capturedStderr = "") {
187
228
  const sections = [];
188
- const stdout = `${capturedStdout}${Buffer.concat(record.stdout).toString("utf8")}`.trimEnd();
229
+ const stdout = String(capturedStdout || "").trimEnd();
189
230
  const stderr = `${capturedStderr}${Buffer.concat(record.stderr).toString("utf8")}`.trimEnd();
190
231
  if (stdout) sections.push(`STDOUT:\n${stdout}`);
191
232
  if (stderr) sections.push(`STDERR:\n${stderr}`);
@@ -193,7 +234,6 @@ function directOutput(record, capturedStdout = "", capturedStderr = "") {
193
234
  }
194
235
 
195
236
  function clearRequestOutput(record) {
196
- record.stdout = [];
197
237
  record.stderr = [];
198
238
  record.directOutputBytes = 0;
199
239
  record.failureReason = null;
@@ -222,6 +262,55 @@ async function terminateRecord(record) {
222
262
  await record.done;
223
263
  }
224
264
 
265
+ function consumeWorkerFrames(record, chunk, onMessage) {
266
+ record.protocolBuffer = Buffer.concat([record.protocolBuffer, chunk]);
267
+ if (record.protocolBuffer.length > NODE_REPL_MAX_FRAME_BYTES) {
268
+ record.failureReason = "Node REPL protocol frame exceeded its byte limit.";
269
+ void terminateRecord(record);
270
+ return;
271
+ }
272
+ while (record.protocolBuffer.length > 0) {
273
+ const newline = record.protocolBuffer.indexOf(0x0a);
274
+ if (newline < 0) return;
275
+ const header = record.protocolBuffer.subarray(0, newline).toString("utf8");
276
+ const prefix = `${record.frameToken}:`;
277
+ if (!header.startsWith(prefix)) {
278
+ record.failureReason = "Node REPL protocol framing was corrupted.";
279
+ void terminateRecord(record);
280
+ return;
281
+ }
282
+ const lengthText = header.slice(prefix.length);
283
+ if (!/^\d+$/.test(lengthText)) {
284
+ record.failureReason = "Node REPL protocol frame length was invalid.";
285
+ void terminateRecord(record);
286
+ return;
287
+ }
288
+ const length = Number(lengthText);
289
+ if (!Number.isSafeInteger(length) || length < 0 || length > NODE_REPL_MAX_FRAME_BYTES) {
290
+ record.failureReason = "Node REPL protocol frame length was out of range.";
291
+ void terminateRecord(record);
292
+ return;
293
+ }
294
+ const frameEnd = newline + 1 + length;
295
+ if (record.protocolBuffer.length < frameEnd) return;
296
+ const payload = record.protocolBuffer.subarray(newline + 1, frameEnd);
297
+ record.protocolBuffer = record.protocolBuffer.subarray(frameEnd);
298
+ try {
299
+ onMessage(JSON.parse(payload.toString("utf8")));
300
+ } catch {
301
+ record.failureReason = "Node REPL protocol payload was invalid JSON.";
302
+ void terminateRecord(record);
303
+ return;
304
+ }
305
+ }
306
+ }
307
+
308
+ function codedError(message, code) {
309
+ const error = /** @type {Error & {code?: string}} */ (new Error(message));
310
+ error.code = code;
311
+ return error;
312
+ }
313
+
225
314
  /**
226
315
  * One lazy Node REPL process owned by a single Pi run.
227
316
  * @param {{cwd?: string, maxOutputChars?: number, sandboxPolicy?: any, sandboxEngine?: any, ctx?: any}} [options]
@@ -243,27 +332,28 @@ export function createNodeReplController({
243
332
  let nextRequestId = 0;
244
333
 
245
334
  async function startChild() {
335
+ const frameToken = randomBytes(24).toString("hex");
246
336
  const prepared = await sandbox.prepareCommand({
247
337
  policy,
248
338
  engine: sandboxEngine ?? resolvedCtx.sandboxEngine ?? undefined,
249
339
  command: {
250
340
  command: process.execPath,
251
- args: ["--eval", NODE_REPL_WORKER_SOURCE],
341
+ args: ["--eval", workerSource(frameToken)],
252
342
  cwd: workdir,
253
343
  },
254
344
  });
255
345
  if (permanentlyClosed) {
256
346
  await prepared.cleanup?.();
257
- throw new Error("Node REPL run has already ended.");
347
+ throw codedError("Node REPL run has already ended.", "closed");
258
348
  }
259
349
 
260
350
  let child;
261
351
  try {
262
352
  child = spawn(prepared.command, prepared.args || [], {
263
353
  cwd: prepared.cwd,
264
- detached: true,
354
+ detached: process.platform !== "win32",
265
355
  env: prepared.env ? { ...process.env, ...prepared.env } : process.env,
266
- stdio: ["ignore", "pipe", "pipe", "ipc"],
356
+ stdio: ["pipe", "pipe", "pipe"],
267
357
  });
268
358
  } catch (error) {
269
359
  await prepared.cleanup?.();
@@ -277,43 +367,24 @@ export function createNodeReplController({
277
367
  prepared,
278
368
  done,
279
369
  resolveDone,
370
+ frameToken,
371
+ protocolBuffer: Buffer.alloc(0),
280
372
  closed: false,
281
373
  cleaned: false,
282
374
  killTimer: null,
283
375
  spawnError: null,
284
376
  failureReason: null,
285
377
  pending: null,
286
- stdout: [],
287
378
  stderr: [],
288
379
  directOutputBytes: 0,
289
380
  };
290
381
  current = record;
291
382
 
292
- child.stdout?.on("data", (chunk) => appendChunk(record, record.stdout, chunk));
293
- child.stderr?.on("data", (chunk) => appendChunk(record, record.stderr, chunk));
294
- child.once("error", (error) => { record.spawnError = error; });
295
- child.on("message", (message) => {
296
- const result = /** @type {any} */ (message);
297
- const pending = record.pending;
298
- if (!pending || !result || result.type !== "result" || result.id !== pending.id) return;
299
- record.pending = null;
300
- clearTimeout(pending.timeoutTimer);
301
- pending.signal?.removeEventListener?.("abort", pending.onAbort);
302
- setImmediate(async () => {
303
- const text = resultText(record, result.text, result.stdout, result.stderr);
304
- if (result.reset) await terminateRecord(record);
305
- if (result.ok) {
306
- pending.resolve(capChars(text || "(no output)", {
307
- label: "NodeRepl",
308
- maxChars: maxOutputChars,
309
- strategy: "head_tail",
310
- ctx: resolvedCtx,
311
- }));
312
- } else {
313
- pending.reject(new Error(text || "Node REPL evaluation failed."));
314
- }
315
- });
383
+ child.stdout?.on("data", (chunk) => {
384
+ consumeWorkerFrames(record, chunk, (message) => handleWorkerMessage(record, message));
316
385
  });
386
+ child.stderr?.on("data", (chunk) => appendStderr(record, chunk));
387
+ child.once("error", (error) => { record.spawnError = error; });
317
388
  child.once("close", (code, closeSignal) => {
318
389
  record.closed = true;
319
390
  if (record.killTimer) clearTimeout(record.killTimer);
@@ -326,72 +397,137 @@ export function createNodeReplController({
326
397
  const reason = record.failureReason
327
398
  || (record.spawnError ? errorMessage(record.spawnError) : null)
328
399
  || `Node REPL process exited before evaluation completed${closeSignal ? ` (${closeSignal})` : ` (code ${code ?? "unknown"})`}.`;
329
- pending.reject(new Error(`${reason} Session state was reset.`));
400
+ pending.reject(codedError(`${reason} Session state was reset.`, "process_exit"));
330
401
  }
331
402
  void cleanupPrepared(record).finally(() => record.resolveDone());
332
403
  });
333
404
  return record;
334
405
  }
335
406
 
407
+ function handleWorkerMessage(record, result) {
408
+ const pending = record.pending;
409
+ if (!pending || !result || result.type !== "result" || result.id !== pending.id) return;
410
+ record.pending = null;
411
+ clearTimeout(pending.timeoutTimer);
412
+ pending.signal?.removeEventListener?.("abort", pending.onAbort);
413
+ setImmediate(async () => {
414
+ const text = resultText(record, result.text, result.stdout, result.stderr);
415
+ if (result.reset) await terminateRecord(record);
416
+ if (result.ok) {
417
+ pending.resolve(capChars(text || "(no output)", {
418
+ label: "NodeRepl",
419
+ maxChars: maxOutputChars,
420
+ strategy: "head_tail",
421
+ ctx: resolvedCtx,
422
+ }));
423
+ } else {
424
+ pending.reject(codedError(text || "Node REPL evaluation failed.", result.reset ? "output_limit" : "evaluation_error"));
425
+ }
426
+ });
427
+ }
428
+
336
429
  async function ensureChild() {
337
- if (permanentlyClosed) throw new Error("Node REPL run has already ended.");
430
+ if (permanentlyClosed) throw codedError("Node REPL run has already ended.", "closed");
338
431
  if (current && !current.closed) return current;
339
432
  starting ??= startChild().finally(() => { starting = null; });
340
433
  return await starting;
341
434
  }
342
435
 
343
- async function resetForFailure(record, pending, message) {
436
+ async function resetForFailure(record, pending, message, code) {
344
437
  if (record.pending === pending) record.pending = null;
345
438
  clearTimeout(pending.timeoutTimer);
346
439
  pending.signal?.removeEventListener?.("abort", pending.onAbort);
347
440
  await terminateRecord(record);
348
- pending.reject(new Error(`${message} Session state was reset.`));
441
+ pending.reject(codedError(`${message} Session state was reset.`, code));
442
+ }
443
+
444
+ /**
445
+ * @param {{code: string}} params
446
+ * @param {{signal?: AbortSignal}} [execution]
447
+ */
448
+ async function execute({ code }, { signal } = {}) {
449
+ if (typeof code !== "string" || code.trim().length === 0) {
450
+ throw codedError("Node REPL code must not be empty.", "invalid_code");
451
+ }
452
+ if (signal?.aborted) throw codedError("Node REPL execution aborted.", "aborted");
453
+ const record = await ensureChild();
454
+ if (signal?.aborted) {
455
+ await terminateRecord(record);
456
+ throw codedError("Node REPL execution aborted. Session state was reset.", "aborted");
457
+ }
458
+ if (record.pending) throw codedError("Node REPL is already evaluating code.", "busy");
459
+ clearRequestOutput(record);
460
+ const id = `node-repl-${++nextRequestId}`;
461
+
462
+ return await new Promise((resolveResult, rejectResult) => {
463
+ const pending = {
464
+ id,
465
+ resolve: resolveResult,
466
+ reject: rejectResult,
467
+ signal,
468
+ onAbort: null,
469
+ timeoutTimer: null,
470
+ };
471
+ pending.onAbort = () => {
472
+ void resetForFailure(record, pending, "Node REPL execution aborted.", "aborted");
473
+ };
474
+ pending.timeoutTimer = setTimeout(() => {
475
+ void resetForFailure(
476
+ record,
477
+ pending,
478
+ `Node REPL execution timed out after ${DEFAULT_NODE_REPL_TIMEOUT_MS}ms.`,
479
+ "timeout",
480
+ );
481
+ }, DEFAULT_NODE_REPL_TIMEOUT_MS);
482
+ pending.timeoutTimer.unref?.();
483
+ record.pending = pending;
484
+ signal?.addEventListener?.("abort", pending.onAbort, { once: true });
485
+ const frame = encodeFrame(record.frameToken, { type: "evaluate", id, code });
486
+ record.child.stdin?.write(frame, (error) => {
487
+ if (error && record.pending === pending) {
488
+ void resetForFailure(record, pending, `Node REPL stream protocol failed: ${errorMessage(error)}.`, "protocol_error");
489
+ }
490
+ });
491
+ });
349
492
  }
350
493
 
351
494
  return {
352
- /** @param {{code: string}} params @param {{signal?: AbortSignal}} [execution] */
353
- async execute({ code }, { signal } = {}) {
354
- if (typeof code !== "string" || code.trim().length === 0) {
355
- throw new Error("Node REPL code must not be empty.");
356
- }
357
- if (signal?.aborted) throw new Error("Node REPL execution aborted.");
358
- const record = await ensureChild();
359
- if (signal?.aborted) {
360
- await terminateRecord(record);
361
- throw new Error("Node REPL execution aborted. Session state was reset.");
362
- }
363
- if (record.pending) throw new Error("Node REPL is already evaluating code.");
364
- clearRequestOutput(record);
365
- const id = `node-repl-${++nextRequestId}`;
366
-
367
- return await new Promise((resolveResult, rejectResult) => {
368
- const pending = {
369
- id,
370
- resolve: resolveResult,
371
- reject: rejectResult,
372
- signal,
373
- onAbort: null,
374
- timeoutTimer: null,
495
+ execute,
496
+
497
+ /** Structured result used by the Pi bridge so telemetry does not depend on text prefixes. */
498
+ async executeDetailed(params, execution = {}) {
499
+ const startedAt = Date.now();
500
+ try {
501
+ const text = await execute(params, execution);
502
+ return {
503
+ text,
504
+ outcome: {
505
+ status: "ok",
506
+ code: "ok",
507
+ retryable: false,
508
+ attempts: 1,
509
+ durationMs: Date.now() - startedAt,
510
+ bytes: Buffer.byteLength(text, "utf8"),
511
+ truncated: String(text).includes("[truncated NodeRepl output"),
512
+ },
513
+ error: false,
375
514
  };
376
- pending.onAbort = () => {
377
- void resetForFailure(record, pending, "Node REPL execution aborted.");
515
+ } catch (error) {
516
+ const text = errorMessage(error);
517
+ return {
518
+ text,
519
+ outcome: {
520
+ status: "error",
521
+ code: typeof error?.code === "string" ? error.code : "evaluation_error",
522
+ retryable: false,
523
+ attempts: 1,
524
+ durationMs: Date.now() - startedAt,
525
+ bytes: Buffer.byteLength(text, "utf8"),
526
+ truncated: false,
527
+ },
528
+ error: true,
378
529
  };
379
- pending.timeoutTimer = setTimeout(() => {
380
- void resetForFailure(
381
- record,
382
- pending,
383
- `Node REPL execution timed out after ${DEFAULT_NODE_REPL_TIMEOUT_MS}ms.`,
384
- );
385
- }, DEFAULT_NODE_REPL_TIMEOUT_MS);
386
- pending.timeoutTimer.unref?.();
387
- record.pending = pending;
388
- signal?.addEventListener?.("abort", pending.onAbort, { once: true });
389
- record.child.send({ type: "evaluate", id, code }, (error) => {
390
- if (error && record.pending === pending) {
391
- void resetForFailure(record, pending, `Node REPL IPC failed: ${errorMessage(error)}.`);
392
- }
393
- });
394
- });
530
+ }
395
531
  },
396
532
 
397
533
  async close() {