char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Sommcp: add streaming fetch on conversation-local and remote environmentse5ec380

main
26.2 KiB775 linesraw
1// berry's side of an environment on another machine: the worker it starts there, and the ExecutionEnv that speaks to
2// it. Nothing is retried: a call the connection lost may or may not have happened, so it fails.
3
4import { Buffer } from "node:buffer";
5import { dirname, fromFileUrl } from "@std/path";
6import type { Context } from "@earendil-works/chord";
7import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
8import {
9  type BinaryReader,
10  type DirReader,
11  ExecutionError,
12  FileError,
13  type FileInfo,
14  type FileWatcher,
15  type Result,
16  type ShellExecOptions,
17  type ShellExecResult,
18  type ShellOutputInfo,
19  type TextLineReader,
20  type WatchChange,
21  type WatchTarget,
22} from "@earendil-works/pi-durable/env";
23import type {
24  Environment,
25  Process,
26  Pty,
27  PtyOptions,
28  Socket,
29  SpawnOptions,
30} from "@berry/api/environment.ts";
31import { denoExecutable } from "../deno.ts";
32import {
33  decode,
34  encode,
35  type FetchOptions,
36  type FetchResponse,
37  type Hello,
38  lines,
39  PROTOCOL,
40  type Request,
41  type Response,
42} from "./codec.ts";
43
44/**
45 * Reads a length-prefixed module off stdin and imports it, handing it the rest of stdin. Remote shells (sh, cmd, and
46 * powershell alike) pass it through double quotes untouched, as it holds none of `"$\`\\%!`.
47 */
48export const BOOTSTRAP = [
49  "const r=Deno.stdin.readable.getReader();let b=new Uint8Array(0);",
50  "const take=async(n)=>{while(b.length<n){const c=await r.read();if(c.done)Deno.exit(70);",
51  "const m=new Uint8Array(b.length+c.value.length);m.set(b);m.set(c.value,b.length);b=m}",
52  "const o=b.slice(0,n);b=b.slice(n);return o};",
53  "const n=new DataView((await take(4)).buffer).getUint32(0);",
54  "const s=new TextDecoder().decode(await take(n));",
55  "globalThis.berryStdin={reader:r,buffered:b};",
56  "await import('data:text/javascript;base64,'+s)",
57].join("");
58
59/** How berry starts the worker on `destination`, with `deno` as the far side calls it. */
60export function sshCommand(destination: string, deno = "deno"): string[] {
61  return [
62    "ssh",
63    "-T",
64    "-o",
65    "BatchMode=yes",
66    "-o",
67    "ConnectTimeout=15",
68    "-o",
69    "ServerAliveInterval=15",
70    "-o",
71    "ServerAliveCountMax=3",
72    "--",
73    destination,
74    `${deno} eval --quiet "${BOOTSTRAP}"`,
75  ];
76}
77
78let bundled: Promise<string> | undefined;
79
80/** The worker as a single module that needs nothing but deno, in base64 for the bootstrap. */
81function workerSource(): Promise<string> {
82  bundled ??= (async () => {
83    const worker = fromFileUrl(new URL("./worker.ts", import.meta.url));
84    const out = await Deno.makeTempDir({ prefix: "berry-worker-" });
85    try {
86      const { success, stderr } = await new Deno.Command(denoExecutable(), {
87        args: ["bundle", "--quiet", "--platform", "deno", "--output", `${out}/worker.js`, worker],
88        cwd: dirname(worker),
89        env: { NO_COLOR: "1" },
90        stdout: "null",
91        stderr: "piped",
92      }).output();
93      if (!success) {
94        throw new Error(`could not bundle the worker:\n${new TextDecoder().decode(stderr)}`);
95      }
96      return Buffer.from(await Deno.readFile(`${out}/worker.js`)).toString("base64");
97    } finally {
98      await Deno.remove(out, { recursive: true });
99    }
100  })();
101  bundled.catch(() => (bundled = undefined));
102  return bundled;
103}
104
105const HELLO_TIMEOUT_MS = 60_000;
106const STDERR_KEPT = 4096;
107
108const messageOf = (error: unknown) => (error instanceof Error ? error.message : String(error));
109
110async function settleWithin(promise: Promise<unknown>, ms: number) {
111  let timer: ReturnType<typeof setTimeout> | undefined;
112  try {
113    await Promise.race([
114      promise.catch(() => {}),
115      new Promise<void>((resolve) => timer = setTimeout(resolve, ms)),
116    ]);
117  } finally {
118    clearTimeout(timer);
119  }
120}
121
122function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
123  return new Promise((resolve, reject) => {
124    const abort = () => {
125      signal.removeEventListener("abort", abort);
126      reject(signal.reason);
127    };
128    signal.addEventListener("abort", abort, { once: true });
129    void promise.then(
130      (value) => {
131        signal.removeEventListener("abort", abort);
132        resolve(value);
133      },
134      (error) => {
135        signal.removeEventListener("abort", abort);
136        reject(error);
137      },
138    );
139    if (signal.aborted) abort();
140  });
141}
142
143type CallOptions = {
144  readonly cwd?: string;
145  readonly context: Context;
146  /** Receives what the call streams, until it settles or `until` does. */
147  readonly listen?: (event: unknown) => void;
148  readonly until?: Promise<void>;
149};
150
151/** One worker, and the calls in flight to it. */
152export class Connection {
153  readonly #name: string;
154  readonly #process: Deno.ChildProcess;
155  readonly #writer: WritableStreamDefaultWriter<Uint8Array>;
156  readonly #pending = new Map<number, PromiseWithResolvers<unknown>>();
157  readonly #listeners = new Map<number, (event: unknown) => void>();
158  readonly #gone = Promise.withResolvers<Error>();
159  readonly #lifetime = new AbortController();
160  #next = 0;
161  #lost: Error | undefined;
162
163  private constructor(
164    name: string,
165    child: Deno.ChildProcess,
166    writer: WritableStreamDefaultWriter<Uint8Array>,
167    readonly hello: Hello,
168  ) {
169    this.#name = name;
170    this.#process = child;
171    this.#writer = writer;
172  }
173
174  /** Start the worker with `argv`, which runs the bootstrap on the far side. */
175  static async open(name: string, argv: readonly string[]): Promise<Connection> {
176    const source = await workerSource();
177    const child = new Deno.Command(argv[0], {
178      args: argv.slice(1),
179      stdin: "piped",
180      stdout: "piped",
181      stderr: "piped",
182    }).spawn();
183    let stderr = "";
184    const stderrReader = child.stderr.getReader();
185    const errors = (async () => {
186      const decoder = new TextDecoder();
187      try {
188        while (true) {
189          const { done, value } = await stderrReader.read();
190          if (done) break;
191          stderr = (stderr + decoder.decode(value, { stream: true })).slice(-STDERR_KEPT);
192        }
193        stderr = (stderr + decoder.decode()).slice(-STDERR_KEPT);
194      } finally {
195        stderrReader.releaseLock();
196      }
197    })().catch(() => {});
198    const stopErrors = async () => {
199      await settleWithin(errors, 100);
200      await settleWithin(Promise.allSettled([stderrReader.cancel().catch(() => {}), errors]), 100);
201    };
202    const failed = (why: string) =>
203      new Error(`${name}: ${why}${stderr.trim() ? `\n${stderr.trim()}` : ""}`);
204
205    const writer = child.stdin.getWriter();
206    const length = new Uint8Array(4);
207    new DataView(length.buffer).setUint32(0, source.length);
208    const incoming = lines(child.stdout.getReader());
209    let timer: ReturnType<typeof setTimeout> | undefined;
210    try {
211      await writer.write(length);
212      await writer.write(new TextEncoder().encode(source));
213      const first = await Promise.race([
214        incoming.next(),
215        new Promise<never>((_, reject) => {
216          timer = setTimeout(() => reject(new Error("timed out")), HELLO_TIMEOUT_MS);
217        }),
218      ]);
219      if (first.done) throw new Error("closed");
220      const hello = (decode(first.value) as { hello?: Hello }).hello;
221      if (hello?.protocol !== PROTOCOL) {
222        throw new Error(`speaks protocol ${hello?.protocol}, not ${PROTOCOL}`);
223      }
224      const connection = new Connection(name, child, writer, hello);
225      void connection.#receive(incoming, stopErrors, failed);
226      return connection;
227    } catch (error) {
228      try {
229        child.kill();
230      } catch {
231        // already gone
232      }
233      await child.status;
234      await stopErrors();
235      throw failed(`could not start its worker: ${messageOf(error)}`);
236    } finally {
237      clearTimeout(timer);
238    }
239  }
240
241  /** Why calls fail, once the worker cannot be reached. */
242  get lost(): Error | undefined {
243    return this.#lost;
244  }
245
246  /** Settles with why, once the worker cannot be reached. */
247  get gone(): Promise<Error> {
248    return this.#gone.promise;
249  }
250
251  get lifetime(): AbortSignal {
252    return this.#lifetime.signal;
253  }
254
255  async #receive(
256    incoming: AsyncGenerator<string>,
257    stopErrors: () => Promise<void>,
258    failed: (why: string) => Error,
259  ) {
260    try {
261      for await (const line of incoming) {
262        const message = decode(line) as Response;
263        if ("hello" in message) continue;
264        if ("o" in message) {
265          this.#listeners.get(message.i)?.(message.o);
266          continue;
267        }
268        const pending = this.#pending.get(message.i);
269        this.#pending.delete(message.i);
270        if ("e" in message) pending?.reject(new Error(`${this.#name}: ${message.e}`));
271        else pending?.resolve(message.r);
272      }
273    } catch (error) {
274      this.#lost = failed(`lost: ${messageOf(error)}`);
275    }
276    await this.#process.status;
277    await stopErrors();
278    this.#lost ??= failed("lost");
279    this.#lifetime.abort(this.#lost);
280    this.#gone.resolve(this.#lost);
281    for (const pending of this.#pending.values()) pending.reject(this.#lost);
282    this.#pending.clear();
283    this.#listeners.clear();
284  }
285
286  #send(request: Request) {
287    // a failed write means the worker is gone, which #receive reports
288    void this.#writer.write(new TextEncoder().encode(`${encode(request)}\n`)).catch(() => {});
289  }
290
291  async call(method: string, args: readonly unknown[], options: CallOptions): Promise<unknown> {
292    if (this.#lost) throw this.#lost;
293    const i = ++this.#next;
294    const pending = Promise.withResolvers<unknown>();
295    this.#pending.set(i, pending);
296    if (options.listen) {
297      const listen = options.listen;
298      this.#listeners.set(i, listen);
299      void (options.until ?? pending.promise).catch(() => {}).finally(() => {
300        if (this.#listeners.get(i) === listen) this.#listeners.delete(i);
301      });
302    }
303    const signal = options.context.abortSignal;
304    const abort = () => this.#send({ c: i });
305    signal?.addEventListener("abort", abort, { once: true });
306    this.#send({
307      i,
308      m: method,
309      a: args,
310      ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
311    });
312    if (signal?.aborted) abort();
313    try {
314      return await pending.promise;
315    } finally {
316      signal?.removeEventListener("abort", abort);
317    }
318  }
319
320  /** End the worker, and with it everything it runs. */
321  async close(): Promise<void> {
322    await this.#writer.close().catch(() => {});
323    const timer = setTimeout(() => {
324      try {
325        this.#process.kill();
326      } catch {
327        // already gone
328      }
329    }, 5_000);
330    await this.#process.status;
331    clearTimeout(timer);
332    await this.#gone.promise;
333  }
334}
335
336type Calls = (method: string, args: readonly unknown[], options: CallOptions) => Promise<unknown>;
337
338const fileFailure = (error: unknown) => ({
339  ok: false as const,
340  error: new FileError("unknown", messageOf(error)),
341});
342// a call already aborted must not happen at all, which only this side can be sure of
343const fileAborted = { ok: false as const, error: new FileError("aborted", "aborted") };
344
345/** A machine berry reaches through a worker there, at `cwd`. */
346export class RemoteEnvironment implements Environment {
347  readonly id: string;
348  readonly platform: string;
349  readonly home: string;
350  readonly variables: Readonly<Record<string, string>>;
351  readonly lifetime: AbortSignal;
352  readonly #connection: Connection;
353  /** What `cleanup` stops: the calls made through this object that still run. */
354  readonly #running = new Set<AbortController>();
355
356  constructor(readonly name: string, connection: Connection, public cwd: string) {
357    this.id = `ssh:${name}`;
358    this.platform = connection.hello.platform;
359    this.home = connection.hello.home;
360    this.variables = connection.hello.variables;
361    this.lifetime = connection.lifetime;
362    this.#connection = connection;
363  }
364
365  /** A call that `cleanup` can stop, failing as the environment's calls fail. */
366  #call: Calls = async (method, args, options) => {
367    const stop = new AbortController();
368    this.#running.add(stop);
369    const signals = [
370      stop.signal,
371      ...(options.context.abortSignal ? [options.context.abortSignal] : []),
372    ];
373    try {
374      return await this.#connection.call(method, args, {
375        ...options,
376        context: withAbortSignal(AbortSignal.any(signals), options.context),
377      });
378    } finally {
379      this.#running.delete(stop);
380    }
381  };
382
383  async #file<T>(method: string, args: readonly unknown[], context: Context) {
384    if (context.abortSignal?.aborted) return fileAborted;
385    try {
386      return await this.#call(method, args, { cwd: this.cwd, context }) as Result<T, FileError>;
387    } catch (error) {
388      return fileFailure(error);
389    }
390  }
391
392  /** Calls on something the worker holds open for this one. */
393  #held(handle: number) {
394    return async <T>(method: string, args: readonly unknown[], context: Context) => {
395      if (context.abortSignal?.aborted) return fileAborted as T;
396      try {
397        return await this.#call("handle", [handle, method, ...args], { context }) as T;
398      } catch (error) {
399        return fileFailure(error) as T;
400      }
401    };
402  }
403
404  absolutePath = (path: string, context: Context) =>
405    this.#file<string>("absolutePath", [path], context);
406  joinPath = (parts: string[], context: Context) =>
407    this.#file<string>("joinPath", [parts], context);
408  readTextFile = (path: string, context: Context) =>
409    this.#file<string>("readTextFile", [path], context);
410  readTextLines = (path: string, options: { maxLines?: number } | undefined, context: Context) =>
411    this.#file<string[]>("readTextLines", [path, options], context);
412  readBinaryFile = (path: string, context: Context) =>
413    this.#file<Uint8Array>("readBinaryFile", [path], context);
414  writeFile = (path: string, content: string | Uint8Array, context: Context) =>
415    this.#file<void>("writeFile", [path, content], context);
416  appendFile = (path: string, content: string | Uint8Array, context: Context) =>
417    this.#file<void>("appendFile", [path, content], context);
418  truncateFile = (path: string, size: number, context: Context) =>
419    this.#file<void>("truncateFile", [path, size], context);
420  flushFile = (path: string, context: Context) => this.#file<void>("flushFile", [path], context);
421  renameFile = (from: string, to: string, context: Context) =>
422    this.#file<void>("renameFile", [from, to], context);
423  fileInfo = (path: string, context: Context) => this.#file<FileInfo>("fileInfo", [path], context);
424  listDir = (path: string, context: Context) => this.#file<FileInfo[]>("listDir", [path], context);
425  canonicalPath = (path: string, context: Context) =>
426    this.#file<string>("canonicalPath", [path], context);
427  exists = (path: string, context: Context) => this.#file<boolean>("exists", [path], context);
428  createDir = (path: string, options: { recursive?: boolean } | undefined, context: Context) =>
429    this.#file<void>("createDir", [path, options], context);
430  remove = (
431    path: string,
432    options: { recursive?: boolean; force?: boolean } | undefined,
433    context: Context,
434  ) => this.#file<void>("remove", [path, options], context);
435  createTempDir = (prefix: string | undefined, context: Context) =>
436    this.#file<string>("createTempDir", [prefix], context);
437  createTempFile = (
438    options: { prefix?: string; suffix?: string } | undefined,
439    context: Context,
440  ) => this.#file<string>("createTempFile", [options], context);
441
442  async openTextLineReader(
443    path: string,
444    context: Context,
445  ): Promise<Result<TextLineReader, FileError>> {
446    const opened = await this.#file<{ handle: number }>("openTextLineReader", [path], context);
447    if (!opened.ok) return opened;
448    const held = this.#held(opened.value.handle);
449    return {
450      ok: true,
451      value: {
452        readLine: (context) => held("readLine", [], context),
453        close: (context) => held("close", [], context),
454      },
455    };
456  }
457
458  async openBinaryReader(
459    path: string,
460    options: { noFollow?: boolean } | undefined,
461    context: Context,
462  ): Promise<Result<BinaryReader, FileError>> {
463    const opened = await this.#file<{ handle: number }>(
464      "openBinaryReader",
465      [path, options],
466      context,
467    );
468    if (!opened.ok) return opened;
469    const held = this.#held(opened.value.handle);
470    return {
471      ok: true,
472      value: {
473        info: (context) => held("info", [], context),
474        read: (offset, length, context) => held("read", [offset, length], context),
475        scanLines: (options, context) => held("scanLines", [options], context),
476        close: (context) => held("close", [], context),
477      },
478    };
479  }
480
481  async openDirReader(path: string, context: Context): Promise<Result<DirReader, FileError>> {
482    const opened = await this.#file<{ handle: number }>("openDirReader", [path], context);
483    if (!opened.ok) return opened;
484    const held = this.#held(opened.value.handle);
485    return {
486      ok: true,
487      value: {
488        next: (count, context) => held("next", [count], context),
489        close: (context) => held("close", [], context),
490      },
491    };
492  }
493
494  async watch(
495    targets: readonly WatchTarget[],
496    onChange: (change: WatchChange) => void,
497    context: Context,
498  ): Promise<Result<FileWatcher, FileError>> {
499    const closed = Promise.withResolvers<void>();
500    let opened;
501    try {
502      opened = await this.#connection.call("watch", [targets], {
503        cwd: this.cwd,
504        context,
505        listen: (change) => onChange(change as WatchChange),
506        until: closed.promise,
507      }) as Result<{ handle: number; mode: FileWatcher["mode"] }, FileError>;
508    } catch (error) {
509      opened = fileFailure(error);
510    }
511    if (!opened.ok) {
512      closed.resolve();
513      return opened;
514    }
515    const held = this.#held(opened.value.handle);
516    return {
517      ok: true,
518      value: {
519        mode: opened.value.mode,
520        async close(context) {
521          await held("close", [], context);
522          closed.resolve();
523        },
524      },
525    };
526  }
527
528  async exec(
529    command: string | readonly string[],
530    options: ShellExecOptions | undefined,
531    context: Context,
532  ): Promise<Result<ShellExecResult, ExecutionError>> {
533    if (context.abortSignal?.aborted) {
534      return { ok: false, error: new ExecutionError("aborted", "aborted") };
535    }
536    const { onOutput, ...rest } = options ?? {};
537    // a throwing callback stops the command, as it would here
538    const failing = new AbortController();
539    let failure: unknown;
540    const listen = onOutput &&
541      ((event: unknown) => {
542        const [text, info] = event as [string, ShellOutputInfo];
543        try {
544          onOutput(text, context, info);
545        } catch (error) {
546          failure ??= error;
547          failing.abort();
548        }
549      });
550    try {
551      const result = await this.#call("exec", [command, { ...rest, output: !!onOutput }], {
552        cwd: this.cwd,
553        context: withAbortSignal(
554          AbortSignal.any([failing.signal, ...(context.abortSignal ? [context.abortSignal] : [])]),
555          context,
556        ),
557        ...(listen ? { listen } : {}),
558      }) as Result<ShellExecResult, ExecutionError>;
559      if (failure === undefined) return result;
560      return { ok: false, error: new ExecutionError("callback_error", messageOf(failure)) };
561    } catch (error) {
562      return { ok: false, error: new ExecutionError("unknown", messageOf(error)) };
563    }
564  }
565
566  async fetch(input: string | URL, init?: RequestInit): Promise<globalThis.Response> {
567    init?.signal?.throwIfAborted();
568    const request = new globalThis.Request(input, init);
569    const stop = new AbortController();
570    this.#running.add(stop);
571    const signal = AbortSignal.any([stop.signal, request.signal]);
572    const during = withAbortSignal(signal, BACKGROUND_CONTEXT);
573    let opened: FetchResponse | undefined;
574    let controller: ReadableStreamDefaultController<Uint8Array> | undefined;
575    let ended = false;
576    const finish = () => {
577      ended = true;
578      this.#running.delete(stop);
579      signal.removeEventListener("abort", abort);
580    };
581    const release = async (handle: number | undefined) => {
582      if (handle !== undefined) {
583        await abortable(
584          this.#connection.call("handle", [handle, "close"], {
585            context: BACKGROUND_CONTEXT,
586          }),
587          AbortSignal.timeout(1000),
588        ).catch(() => {});
589      }
590    };
591    const close = async () => {
592      if (ended) return;
593      finish();
594      await release(opened?.handle);
595    };
596    const abort = () => {
597      if (ended) return;
598      controller?.error(signal.reason);
599      void close();
600    };
601    try {
602      const options: FetchOptions = {
603        method: request.method,
604        headers: [...request.headers],
605        ...(request.body
606          ? { body: new Uint8Array(await abortable(request.arrayBuffer(), signal)) }
607          : {}),
608        redirect: request.redirect,
609        credentials: request.credentials,
610        cache: request.cache,
611        referrer: request.referrer,
612        referrerPolicy: request.referrerPolicy,
613        integrity: request.integrity,
614      };
615      signal.throwIfAborted();
616      const opening = this.#connection.call("fetch", [request.url, options], {
617        context: during,
618      }).then((value) => {
619        opened = value as FetchResponse;
620        // a canceled caller must still release a handle returned late by the worker
621        if (ended) void release(opened.handle);
622        return opened;
623      });
624      opened = await abortable(opening, signal);
625      signal.throwIfAborted();
626      const body = opened.handle === undefined ? null : new ReadableStream<Uint8Array>({
627        start: (stream) => {
628          controller = stream;
629          signal.addEventListener("abort", abort, { once: true });
630        },
631        pull: async (stream) => {
632          try {
633            const next = await abortable(
634              this.#connection.call("handle", [opened!.handle, "read"], {
635                context: during,
636              }),
637              signal,
638            ) as ReadableStreamReadResult<Uint8Array>;
639            if (ended) return;
640            if (next.done) {
641              finish();
642              stream.close();
643            } else stream.enqueue(next.value);
644          } catch (error) {
645            if (!ended) {
646              stream.error(signal.aborted ? signal.reason : error);
647              void close();
648            }
649          }
650        },
651        cancel: close,
652      });
653      if (body === null) finish();
654      const response = new globalThis.Response(body, opened);
655      Object.defineProperties(response, {
656        url: { value: opened.url },
657        redirected: { value: opened.redirected },
658      });
659      return response;
660    } catch (error) {
661      void close();
662      if (signal.aborted) throw signal.reason;
663      throw new TypeError(`Fetch failed on ${this.name}: ${messageOf(error)}`, { cause: error });
664    }
665  }
666
667  async spawn(argv: readonly string[], options: SpawnOptions, context: Context): Promise<Process> {
668    return (await this.#pipe("spawn", [argv, options], context)).process;
669  }
670
671  async pty(argv: readonly string[], options: PtyOptions, context: Context): Promise<Pty> {
672    const { process, operate } = await this.#pipe("pty", [argv, options], context);
673    return {
674      readable: process.stdout,
675      writable: process.stdin,
676      resize: (cols, rows) => void operate("resize", cols, rows).catch(() => {}),
677      exited: process.exited,
678      kill: process.kill,
679    };
680  }
681
682  async connect(hostname: string, port: number, context: Context): Promise<Socket> {
683    const { stdin, stdout, kill } =
684      (await this.#pipe("connect", [hostname, port], context)).process;
685    return { readable: stdout, writable: stdin, close: kill };
686  }
687
688  /** Something the worker runs or holds open, spoken to through its pipes until it exits. */
689  async #pipe(
690    method: string,
691    args: readonly unknown[],
692    context: Context,
693  ): Promise<{ process: Process; operate(operation: string, ...rest: unknown[]): Promise<void> }> {
694    context.abortSignal?.throwIfAborted();
695    const streams: Record<string, ReadableStreamDefaultController<Uint8Array>> = {};
696    const readable = (name: string) =>
697      new ReadableStream<Uint8Array>({
698        start: (controller) => void (streams[name] = controller),
699        cancel: () => void delete streams[name],
700      });
701    const stdout = readable("stdout");
702    const stderr = readable("stderr");
703    const exited = Promise.withResolvers<number | null>();
704    const finished = Promise.withResolvers<void>();
705    const end = (error?: Error) => {
706      for (const stream of Object.values(streams)) {
707        try {
708          if (error) stream.error(error);
709          else stream.close();
710        } catch {
711          // already ended
712        }
713      }
714    };
715    const listen = (event: unknown) => {
716      const [name, chunk] = event as [string, Uint8Array | number | null];
717      if (name === "exit") exited.resolve(chunk as number | null);
718      else if (name === "end") {
719        end();
720        finished.resolve();
721      } else if (chunk === null) {
722        streams[name]?.close();
723        delete streams[name];
724      } else streams[name]?.enqueue(chunk as Uint8Array);
725    };
726    let opened: { pipe: number };
727    try {
728      opened = await this.#connection.call(method, args, {
729        cwd: this.cwd,
730        context,
731        listen,
732        until: finished.promise,
733      }) as { pipe: number };
734    } catch (error) {
735      exited.resolve(null);
736      finished.resolve();
737      throw error;
738    }
739    // what berry cannot reach any more has ended, as far as anyone here can tell
740    void Promise.race([this.#connection.gone, finished.promise]).then((outcome) => {
741      if (outcome instanceof Error) {
742        end(outcome);
743        exited.resolve(null);
744        finished.resolve();
745      }
746    });
747    const operate = (operation: string, ...rest: unknown[]) =>
748      this.#connection.call("pipe", [opened.pipe, operation, ...rest], {
749        context: BACKGROUND_CONTEXT,
750      }).then(() => {});
751    const kill = (signal: "SIGTERM" | "SIGKILL" = "SIGTERM") =>
752      void operate("kill", signal).catch(() => {});
753    const abort = () => kill();
754    context.abortSignal?.addEventListener("abort", abort, { once: true });
755    if (context.abortSignal?.aborted) abort();
756    void exited.promise.then(() => context.abortSignal?.removeEventListener("abort", abort));
757    const process = {
758      stdin: new WritableStream<Uint8Array>({
759        write: (chunk) => operate("write", chunk),
760        close: () => operate("end"),
761        abort: () => operate("kill"),
762      }),
763      stdout,
764      stderr,
765      exited: exited.promise,
766      kill,
767    };
768    return { process, operate };
769  }
770
771  cleanup(_context: Context): Promise<void> {
772    for (const stop of this.#running) stop.abort();
773    return Promise.resolve();
774  }
775}