@runuai/host 0.9.119 → 0.9.121

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.
@@ -67,6 +67,14 @@ export interface MachineHostTaskUpDeps {
67
67
  /** The task owner's GitHub access token, or null when GitHub is not
68
68
  * connected (public/anonymous clone fallback). */
69
69
  githubToken(userId: string): Promise<{ accessToken: string } | null>;
70
+ /** ADR-133: how this environment gets its shared files. Default = the
71
+ * point-in-time copy from the host's shared directory (machines over
72
+ * ssh); the sidecar mounts them from S3 instead. */
73
+ sharedFiles?: (
74
+ handle: TaskEnvironmentHandle<unknown>,
75
+ workspacePath: string,
76
+ task: { ownerOrgId?: string; ownerUserId: string; sharedFiles?: string },
77
+ ) => Promise<string | null>;
70
78
  }
71
79
 
72
80
  /** Mirrors src/index.ts isGithubRepo — host-side predicate only. */
@@ -430,6 +438,15 @@ async function ensureCodeServer(
430
438
  }
431
439
  }
432
440
 
441
+ /** The identity the host records for the task's runtime: the machine id for
442
+ * a machine over ssh (what recovery and orphan sweeps key on), the handle's
443
+ * durable identity for anything else (sidecar mode: `self:<task>`). */
444
+ function environmentRuntimeIdentity(handle: TaskEnvironmentHandle<unknown>): string {
445
+ return handle.descriptor.locator.provider === MACHINE_TASK_ENVIRONMENT_PROVIDER
446
+ ? parseMachineTaskEnvironmentLocator(handle.descriptor.locator).machineId
447
+ : handle.durableIdentity;
448
+ }
449
+
433
450
  /** Wrap the void-teardown machine handle so registry consumers get the
434
451
  * TaskDownResult contract the compose providers speak. */
435
452
  function withTaskDownResult(
@@ -477,7 +494,9 @@ export function createMachineHostTaskEnvironmentProvider(
477
494
  TaskDownResult
478
495
  > {
479
496
  return {
480
- kind: MACHINE_TASK_ENVIRONMENT_PROVIDER,
497
+ // The provider kind is whichever environment sits underneath: a machine
498
+ // over ssh, or this machine in sidecar mode (ADR-133).
499
+ kind: deps.environment.kind,
481
500
 
482
501
  async provision(
483
502
  request: TaskEnvironmentProvisionRequest<TaskLaunchInput, TaskUpCredentials>,
@@ -528,7 +547,7 @@ export function createMachineHostTaskEnvironmentProvider(
528
547
  }
529
548
 
530
549
  // ADR-062 shared files: point-in-time copy (see copySharedFiles).
531
- const sharedWarning = await copySharedFiles(handle, workspacePath, {
550
+ const sharedWarning = await (deps.sharedFiles ?? copySharedFiles)(handle, workspacePath, {
532
551
  ...(input.task.ownerOrgId ? { ownerOrgId: input.task.ownerOrgId } : {}),
533
552
  ownerUserId: input.task.ownerUserId,
534
553
  ...(input.task.sharedFiles ? { sharedFiles: input.task.sharedFiles } : {}),
@@ -570,13 +589,12 @@ export function createMachineHostTaskEnvironmentProvider(
570
589
  );
571
590
  });
572
591
 
573
- const machineId = parseMachineTaskEnvironmentLocator(
574
- handle.descriptor.locator,
575
- ).machineId;
576
592
  return {
577
593
  handle: withTaskDownResult(handle),
578
594
  result: {
579
- composeProject: machineId,
595
+ // Recorded as the task's runtime identity (the compose-project
596
+ // column): the machine id over ssh, `self:<task>` in sidecar mode.
597
+ composeProject: environmentRuntimeIdentity(handle),
580
598
  worktreePath: deps.taskControlDir(request.taskId),
581
599
  codeServerPort: MACHINE_CODE_SERVER_PORT,
582
600
  ...(warnings.length > 0
@@ -0,0 +1,346 @@
1
+ /**
2
+ * ADR-133 sidecar mode: the task environment IS this machine.
3
+ *
4
+ * The runner runs on the task's own EC2 machine (`uai-host run --task`), so
5
+ * there is no container to exec into and no ssh hop to make: every request
6
+ * runs as a local child process, as user `node` (the runner itself is root
7
+ * so it can drop privileges, mount S3, and own the workspace), in the same
8
+ * shape the machine provider sends over ssh. `machine-task-up.ts` (clone,
9
+ * editor, browser) and `agents/machine-durable.ts` (durable agent sessions)
10
+ * take a handle, not a transport, so they run unchanged on top of this one.
11
+ *
12
+ * What is deliberately NOT here: launch/stop/terminate. The cloud owns the
13
+ * machine's lifecycle (hostless-implementation.md §7); `stop` and `teardown`
14
+ * are no-ops because a parked or closed task ends with the cloud stopping or
15
+ * terminating the instance this process lives on.
16
+ */
17
+ import { spawn as nodeSpawn } from "node:child_process";
18
+ import { existsSync } from "node:fs";
19
+ import { readFile } from "node:fs/promises";
20
+
21
+ import { buildRemoteCommand } from "../machine-exec";
22
+ import { capturedCliExec, streamingCliExec } from "./docker";
23
+ import type { MachineTaskInput } from "./machine";
24
+ import {
25
+ assertTaskEnvironmentProcessRequest,
26
+ assertTaskEnvironmentSessionRequest,
27
+ assertWorkspaceScopedPath,
28
+ type TaskEnvironmentCopyRequest,
29
+ type TaskEnvironmentDescriptor,
30
+ type TaskEnvironmentDetachedSessionRequest,
31
+ type TaskEnvironmentExecRequest,
32
+ type TaskEnvironmentExecResult,
33
+ type TaskEnvironmentHandle,
34
+ type TaskEnvironmentLocator,
35
+ type TaskEnvironmentPort,
36
+ type TaskEnvironmentProcess,
37
+ type TaskEnvironmentProvider,
38
+ type TaskEnvironmentProvisionRequest,
39
+ type TaskEnvironmentProvisioned,
40
+ type TaskEnvironmentRecoveryContext,
41
+ type TaskEnvironmentRecoveryResult,
42
+ type TaskEnvironmentSessionRequest,
43
+ type TaskEnvironmentSpawnRequest,
44
+ type TaskEnvironmentStatus,
45
+ TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION,
46
+ } from "./types";
47
+
48
+ export const SELF_TASK_ENVIRONMENT_PROVIDER = "self";
49
+
50
+ /** The task user every agent, editor, and shell runs as. */
51
+ export const SELF_TASK_USER = "node";
52
+
53
+ const SAFE_TASK_ID = /^[a-z0-9]{1,32}$/i;
54
+
55
+ export interface SelfTaskEnvironmentLocatorValue {
56
+ taskId: string;
57
+ /** Absolute workspace path on this machine. */
58
+ workspacePath: string;
59
+ }
60
+
61
+ export interface SelfTaskEnvironmentDeps {
62
+ spawn?: typeof nodeSpawn;
63
+ /** Whether this process may drop privileges with setpriv (root). Tests and
64
+ * a non-root dev run keep everything as the current user. */
65
+ privileged?: () => boolean;
66
+ }
67
+
68
+ export function selfTaskEnvironmentLocator(
69
+ value: SelfTaskEnvironmentLocatorValue,
70
+ ): TaskEnvironmentLocator {
71
+ return {
72
+ schemaVersion: TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION,
73
+ provider: SELF_TASK_ENVIRONMENT_PROVIDER,
74
+ value: { taskId: value.taskId, workspacePath: value.workspacePath },
75
+ };
76
+ }
77
+
78
+ export function parseSelfTaskEnvironmentLocator(
79
+ locator: TaskEnvironmentLocator,
80
+ ): SelfTaskEnvironmentLocatorValue {
81
+ if (
82
+ locator.schemaVersion !== TASK_ENVIRONMENT_LOCATOR_SCHEMA_VERSION ||
83
+ locator.provider !== SELF_TASK_ENVIRONMENT_PROVIDER ||
84
+ typeof locator.value !== "object" ||
85
+ locator.value === null
86
+ ) {
87
+ throw new Error("not a self task environment locator");
88
+ }
89
+ const value = locator.value as Record<string, unknown>;
90
+ if (
91
+ typeof value.taskId !== "string" ||
92
+ !SAFE_TASK_ID.test(value.taskId) ||
93
+ typeof value.workspacePath !== "string" ||
94
+ !value.workspacePath.startsWith("/") ||
95
+ value.workspacePath.includes("\0")
96
+ ) {
97
+ throw new Error("malformed self task environment locator");
98
+ }
99
+ return { taskId: value.taskId, workspacePath: value.workspacePath };
100
+ }
101
+
102
+ /**
103
+ * The local command for a request: the same `cd … && exec env … argv` shell
104
+ * line the machine provider ships over ssh, run through setpriv when this
105
+ * process is root and the request wants a task user. Values ride argv here
106
+ * exactly as they ride the ssh command line on a machine task — the machine
107
+ * is one task's trust domain (machine-exec.ts).
108
+ */
109
+ export function selfCommand(
110
+ request: {
111
+ argv: readonly string[];
112
+ cwd?: string;
113
+ user?: string;
114
+ inheritEnv?: readonly string[];
115
+ env?: Readonly<Record<string, string>>;
116
+ },
117
+ mode: "interactive" | "detached",
118
+ privileged: boolean,
119
+ ): { command: string; args: string[] } {
120
+ const line = buildRemoteCommand({ ...request, detached: mode === "detached" });
121
+ const user = request.user ?? SELF_TASK_USER;
122
+ if (privileged && user !== "root") {
123
+ return {
124
+ command: "/usr/bin/setpriv",
125
+ args: [
126
+ `--reuid=${user}`,
127
+ `--regid=${user}`,
128
+ "--init-groups",
129
+ "--reset-env",
130
+ "--",
131
+ "/bin/sh",
132
+ "-lc",
133
+ line,
134
+ ],
135
+ };
136
+ }
137
+ return { command: "/bin/sh", args: ["-lc", line] };
138
+ }
139
+
140
+ class SelfTaskEnvironmentHandle implements TaskEnvironmentHandle {
141
+ readonly descriptor: TaskEnvironmentDescriptor;
142
+ readonly durableIdentity: string;
143
+ readonly #value: SelfTaskEnvironmentLocatorValue;
144
+ readonly #spawn: typeof nodeSpawn;
145
+ readonly #privileged: () => boolean;
146
+
147
+ constructor(locator: TaskEnvironmentLocator, deps: SelfTaskEnvironmentDeps) {
148
+ this.#value = parseSelfTaskEnvironmentLocator(locator);
149
+ this.#spawn = deps.spawn ?? nodeSpawn;
150
+ this.#privileged =
151
+ deps.privileged ?? (() => typeof process.getuid === "function" && process.getuid() === 0);
152
+ this.durableIdentity = `self:${this.#value.taskId}`;
153
+ this.descriptor = {
154
+ taskId: this.#value.taskId,
155
+ locator,
156
+ workspacePath: this.#value.workspacePath,
157
+ };
158
+ }
159
+
160
+ async readWorkspaceFile(path: string): Promise<Uint8Array | null> {
161
+ assertWorkspaceScopedPath(this.descriptor, path);
162
+ try {
163
+ return new Uint8Array(await readFile(path));
164
+ } catch (error) {
165
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
166
+ throw error;
167
+ }
168
+ }
169
+
170
+ async writeWorkspaceFile(path: string, bytes: Uint8Array): Promise<void> {
171
+ assertWorkspaceScopedPath(this.descriptor, path);
172
+ // Through the task user, so the file is owned like everything else the
173
+ // agents touch (a root-owned file inside the workspace is a trap).
174
+ const result = await this.exec({
175
+ argv: ["/bin/sh", "-c", 'mkdir -p "$(dirname "$1")" && cat > "$1"', "write", path],
176
+ stdin: bytes,
177
+ timeoutMs: 120_000,
178
+ maxOutputBytes: 1024 * 1024,
179
+ });
180
+ if (result.exitCode !== 0) {
181
+ throw new Error(
182
+ `workspace write failed: ${Buffer.from(result.stderr).toString("utf8").trim()}`,
183
+ );
184
+ }
185
+ }
186
+
187
+ async exec(request: TaskEnvironmentExecRequest): Promise<TaskEnvironmentExecResult> {
188
+ assertTaskEnvironmentProcessRequest(request);
189
+ const { command, args } = selfCommand(request, "interactive", this.#privileged());
190
+ return capturedCliExec(this.#spawn, command, args, request);
191
+ }
192
+
193
+ async spawn(request: TaskEnvironmentSpawnRequest): Promise<TaskEnvironmentProcess> {
194
+ assertTaskEnvironmentProcessRequest(request);
195
+ const { command, args } = selfCommand(request, "interactive", this.#privileged());
196
+ return streamingCliExec(this.#spawn, command, args, request);
197
+ }
198
+
199
+ async spawnSession(request: TaskEnvironmentSessionRequest): Promise<TaskEnvironmentProcess> {
200
+ assertTaskEnvironmentSessionRequest(request);
201
+ const session = { ...request, cwd: request.cwd ?? this.#value.workspacePath };
202
+ const { command, args } = selfCommand(session, "interactive", this.#privileged());
203
+ return streamingCliExec(this.#spawn, command, args, session);
204
+ }
205
+
206
+ async launchDetachedSession(
207
+ request: TaskEnvironmentDetachedSessionRequest,
208
+ ): Promise<TaskEnvironmentExecResult> {
209
+ assertTaskEnvironmentSessionRequest(request);
210
+ const session = { ...request, cwd: request.cwd ?? this.#value.workspacePath };
211
+ const { command, args } = selfCommand(session, "detached", this.#privileged());
212
+ return capturedCliExec(this.#spawn, command, args, {
213
+ ...session,
214
+ timeoutMs: session.launchTimeoutMs,
215
+ });
216
+ }
217
+
218
+ async copy(request: TaskEnvironmentCopyRequest): Promise<void> {
219
+ if (
220
+ !request.source.startsWith("/") ||
221
+ !request.destination.startsWith("/") ||
222
+ request.source.includes("\0") ||
223
+ request.destination.includes("\0")
224
+ ) {
225
+ throw new Error("self copy paths must be absolute and NUL-free");
226
+ }
227
+ // Both sides are this machine. `cp -a` keeps directory trees intact
228
+ // (engine-account homes), and the result is chowned to the task user so
229
+ // the agents can read what was copied in for them.
230
+ const script =
231
+ request.direction === "into"
232
+ ? 'mkdir -p "$(dirname "$2")" && cp -a "$1" "$2" && chown -R node:node "$2"'
233
+ : 'mkdir -p "$(dirname "$2")" && cp -a "$1" "$2"';
234
+ const result = await this.exec({
235
+ argv: ["/bin/sh", "-c", script, "copy", request.source, request.destination],
236
+ user: "root",
237
+ timeoutMs: 5 * 60_000,
238
+ maxOutputBytes: 64 * 1024,
239
+ });
240
+ if (result.exitCode !== 0) {
241
+ throw new Error(
242
+ `self copy failed: ${Buffer.from(result.stderr).toString("utf8").trim()}`,
243
+ );
244
+ }
245
+ }
246
+
247
+ async ports(): Promise<TaskEnvironmentPort[]> {
248
+ return [];
249
+ }
250
+
251
+ /** Every tunnel dials loopback: the editor and preview servers listen on
252
+ * this machine, and the runner that answers the tunnel is this process. */
253
+ async inspectRoute(): Promise<{ kind: "running"; ipv4Address: string }> {
254
+ return { kind: "running", ipv4Address: "127.0.0.1" };
255
+ }
256
+
257
+ async status(): Promise<TaskEnvironmentStatus> {
258
+ return {
259
+ state: "running",
260
+ instances: [this.durableIdentity],
261
+ workspacePresent: existsSync(this.#value.workspacePath),
262
+ };
263
+ }
264
+
265
+ async recover(
266
+ context: TaskEnvironmentRecoveryContext,
267
+ ): Promise<TaskEnvironmentRecoveryResult> {
268
+ await context.maintenanceReady;
269
+ if (!context.isCurrent()) {
270
+ return { outcome: "deferred", detail: "runtime generation superseded" };
271
+ }
272
+ context.allowConsumers?.();
273
+ await context.ensureConsumersStarted?.();
274
+ if (!context.isCurrent()) {
275
+ await context.quarantineConsumers?.();
276
+ return { outcome: "deferred", detail: "runtime generation superseded" };
277
+ }
278
+ return { outcome: "recovered", status: await this.status() };
279
+ }
280
+
281
+ /** Parking is the cloud stopping this instance; nothing to do locally. */
282
+ async stop(): Promise<void> {}
283
+
284
+ /** Closing is the cloud terminating this instance; the workspace dies
285
+ * with it. Nothing is deleted here so a close that races a late read (a
286
+ * final diff, a transcript flush) still finds the files. */
287
+ async teardown(): Promise<void> {}
288
+ }
289
+
290
+ export function createSelfTaskEnvironmentProvider(
291
+ deps: SelfTaskEnvironmentDeps = {},
292
+ ): TaskEnvironmentProvider<MachineTaskInput, void, TaskEnvironmentDescriptor> {
293
+ return {
294
+ kind: SELF_TASK_ENVIRONMENT_PROVIDER,
295
+
296
+ // Takes the machine input shape so `createMachineHostTaskEnvironmentProvider`
297
+ // can sit on top unchanged; image/cpus/memory describe a machine the cloud
298
+ // already launched and are ignored here.
299
+ async provision(
300
+ request: TaskEnvironmentProvisionRequest<MachineTaskInput, void>,
301
+ ): Promise<TaskEnvironmentProvisioned<TaskEnvironmentDescriptor>> {
302
+ const { taskId, input } = request;
303
+ if (!SAFE_TASK_ID.test(taskId)) {
304
+ throw new Error("self provisioning refused an unsafe task id");
305
+ }
306
+ const workspacePath = input.workspacePath ?? "/workspace";
307
+ const locator = selfTaskEnvironmentLocator({ taskId, workspacePath });
308
+ await request.onPrepared?.(locator);
309
+ const handle = new SelfTaskEnvironmentHandle(locator, deps);
310
+ const prepare = await handle.exec({
311
+ argv: ["/bin/sh", "-c", 'mkdir -p "$1" && chown node:node "$1"', "prepare", workspacePath],
312
+ user: "root",
313
+ timeoutMs: 30_000,
314
+ maxOutputBytes: 64 * 1024,
315
+ });
316
+ if (prepare.exitCode !== 0) {
317
+ throw new Error(
318
+ `workspace preparation failed: ${Buffer.from(prepare.stderr).toString("utf8").trim()}`,
319
+ );
320
+ }
321
+ if (input.repo) {
322
+ const clone = await handle.exec({
323
+ argv: [
324
+ "/usr/bin/git",
325
+ "clone",
326
+ ...(input.repo.branch ? ["--branch", input.repo.branch] : []),
327
+ input.repo.url,
328
+ workspacePath,
329
+ ],
330
+ timeoutMs: 10 * 60_000,
331
+ maxOutputBytes: 4 * 1024 * 1024,
332
+ });
333
+ if (clone.exitCode !== 0) {
334
+ throw new Error(
335
+ `clone failed: ${Buffer.from(clone.stderr).toString("utf8").trim()}`,
336
+ );
337
+ }
338
+ }
339
+ return { handle, result: handle.descriptor };
340
+ },
341
+
342
+ async reconstruct(locator: TaskEnvironmentLocator): Promise<TaskEnvironmentHandle> {
343
+ return new SelfTaskEnvironmentHandle(locator, deps);
344
+ },
345
+ };
346
+ }
@@ -446,3 +446,16 @@ function assertJsonValue(value: unknown, depth: number): void {
446
446
  }
447
447
  throw new Error("task environment locator is not JSON-serializable");
448
448
  }
449
+
450
+ /**
451
+ * Providers whose workspace is reached only through the environment
452
+ * transport — a machine over ssh (ADR-121) or this very machine in sidecar
453
+ * mode (ADR-133) — as opposed to a container the host can `docker exec` into
454
+ * or bind-mount. Every "is this a machine task?" branch in the host means
455
+ * exactly this, so it asks here rather than comparing one provider name.
456
+ */
457
+ export function isEnvironmentTransportProvider(
458
+ provider: string | null | undefined,
459
+ ): boolean {
460
+ return provider === "machine" || provider === "self";
461
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.119",
3
+ "version": "0.9.121",
4
4
  "description": "Uai host — runs ephemeral AI tasks in containers on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Uai Tech <team@runuai.com>",
package/src/cli.ts CHANGED
@@ -132,7 +132,7 @@ async function main(): Promise<void> {
132
132
  const follow = rest.includes("--follow") || rest.includes("-f");
133
133
  switch (cmd) {
134
134
  case "run":
135
- return cmdRun();
135
+ return cmdRun(rest);
136
136
  case "status":
137
137
  return cmdStatus();
138
138
  case "open":
@@ -190,7 +190,12 @@ function timestampServiceLogs(): void {
190
190
  }
191
191
  }
192
192
 
193
- async function cmdRun(): Promise<void> {
193
+ async function cmdRun(rest: string[] = []): Promise<void> {
194
+ // ADR-133 sidecar mode: `uai-host run --task <id>` on the task's own
195
+ // machine. The flag becomes the environment the rest of the process asks
196
+ // (lib/sidecar.ts) — set before main.ts loads, since its boot reads it.
197
+ const sidecarTask = flagValue(rest, "--task");
198
+ if (sidecarTask) process.env.UAI_SIDECAR_TASK = sidecarTask;
194
199
  // Timestamped service logs: the ADR-106 live smoke burned an hour dating
195
200
  // undated scrollback. Applies to the service process only, not one-shot CLI
196
201
  // commands.
package/src/index.ts CHANGED
@@ -184,6 +184,7 @@ export type {
184
184
  PreviewPortDeclaration,
185
185
  PreviewPortRuntime,
186
186
  } from "../lib/preview-ports";
187
+ import { isEnvironmentTransportProvider } from "../lib/task-environment/types";
187
188
 
188
189
  export const hostEvents: HostEventStream = {
189
190
  subscribe(handler) {
@@ -479,7 +480,7 @@ export const hostCommands: HostCommands = {
479
480
  }
480
481
  } else if (
481
482
  existingTask?.statusMirror === "running" &&
482
- existingTask.environmentProvider === "machine"
483
+ isEnvironmentTransportProvider(existingTask.environmentProvider)
483
484
  ) {
484
485
  // ADR-121: a machine-backed task revalidates through its own
485
486
  // provider — the compose fast path below speaks docker and would
@@ -894,7 +895,7 @@ export const hostCommands: HostCommands = {
894
895
  // the compose reconciler would probe for an app container, find none,
895
896
  // and silently skip. Reconnect re-injection for machines is a
896
897
  // follow-up alongside the machine recovery pass.
897
- if (getHostTask(input.task.id)?.environmentProvider === "machine") {
898
+ if (isEnvironmentTransportProvider(getHostTask(input.task.id)?.environmentProvider)) {
898
899
  void machineTaskGithubAuth(
899
900
  input.task.id,
900
901
  input.task.ownerUserId,
@@ -1005,7 +1006,7 @@ export const hostCommands: HostCommands = {
1005
1006
  // with no consumer (owner, 2026-08-29 — 16 parked disks after one
1006
1007
  // evening of testing). The task branch lives on the remote; the
1007
1008
  // machine's disk is not a preservation surface once the task ends.
1008
- if (environment.descriptor.locator.provider === "machine") {
1009
+ if (isEnvironmentTransportProvider(environment.descriptor.locator.provider)) {
1009
1010
  stopMachineGatewayTunnel(input.taskId);
1010
1011
  }
1011
1012
  return taskDownResultForInput(input, await environment.teardown());
@@ -1437,7 +1438,7 @@ export const hostCommands: HostCommands = {
1437
1438
  // tasks keep the direct host-FS write (their workspace is a bind
1438
1439
  // mount and the sync path is cheaper than a docker exec per message).
1439
1440
  const task = getHostTask(taskId);
1440
- if (task?.environmentProvider === "machine") {
1441
+ if (isEnvironmentTransportProvider(task?.environmentProvider)) {
1441
1442
  const environment = await reconstructHostTaskEnvironment(taskId);
1442
1443
  if (environment) {
1443
1444
  await appendTranscriptViaEnvironment(