@sema-agent/server 1.316.0 → 1.318.0

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 (62) hide show
  1. package/README.md +9 -0
  2. package/README.zh-CN.md +7 -0
  3. package/dist/config-types.d.ts +1 -1
  4. package/dist/config.d.ts +14 -0
  5. package/dist/config.js +148 -45
  6. package/dist/elicitation.js +2 -2
  7. package/dist/http/route-ctx.d.ts +51 -2
  8. package/dist/http/routes/approvals-assistant.d.ts +11 -0
  9. package/dist/http/routes/approvals-assistant.js +530 -0
  10. package/dist/http/routes/attachments.js +7 -7
  11. package/dist/http/routes/fleet.d.ts +4 -0
  12. package/dist/http/routes/fleet.js +147 -0
  13. package/dist/http/routes/images.js +29 -29
  14. package/dist/http/routes/leader.d.ts +4 -0
  15. package/dist/http/routes/leader.js +48 -0
  16. package/dist/http/routes/memory-policy.js +10 -10
  17. package/dist/http/routes/notify-wake.d.ts +4 -0
  18. package/dist/http/routes/notify-wake.js +133 -0
  19. package/dist/http/routes/observability.js +5 -5
  20. package/dist/http/routes/runs.d.ts +19 -0
  21. package/dist/http/routes/runs.js +967 -0
  22. package/dist/http/routes/session-sync.js +28 -28
  23. package/dist/http/routes/sessions-list.js +8 -8
  24. package/dist/http/routes/sessions.js +47 -47
  25. package/dist/http/routes/side-query.d.ts +4 -0
  26. package/dist/http/routes/side-query.js +88 -0
  27. package/dist/http/routes/tasks.d.ts +4 -0
  28. package/dist/http/routes/tasks.js +632 -0
  29. package/dist/http/routes/trace-usage.d.ts +4 -0
  30. package/dist/http/routes/trace-usage.js +239 -0
  31. package/dist/http/routes/workflows.d.ts +5 -0
  32. package/dist/http/routes/workflows.js +337 -0
  33. package/dist/http/run-meta.d.ts +11 -0
  34. package/dist/http/run-meta.js +16 -0
  35. package/dist/http/send.d.ts +1 -0
  36. package/dist/http/send.js +15 -0
  37. package/dist/http/server.d.ts +6 -5
  38. package/dist/http/server.js +241 -3166
  39. package/dist/http/sse-log.js +2 -2
  40. package/dist/http/wire-types.d.ts +6 -0
  41. package/dist/leader/endpoint.js +4 -4
  42. package/dist/main.js +5 -5
  43. package/dist/plugins/posix-shell-fs.d.ts +35 -0
  44. package/dist/plugins/posix-shell-fs.js +122 -0
  45. package/dist/plugins/remote-env-adb.d.ts +2 -1
  46. package/dist/plugins/remote-env-adb.js +19 -108
  47. package/dist/plugins/remote-env-file-error.d.ts +1 -0
  48. package/dist/plugins/remote-env-file-error.js +16 -0
  49. package/dist/plugins/remote-env-host.js +4 -3
  50. package/dist/plugins/remote-env-k8s.d.ts +2 -2
  51. package/dist/plugins/remote-env-k8s.js +22 -97
  52. package/dist/plugins/remote-env-local-docker.d.ts +2 -2
  53. package/dist/plugins/remote-env-local-docker.js +32 -109
  54. package/dist/plugins/remote-env-ssh.d.ts +2 -1
  55. package/dist/plugins/remote-env-ssh.js +15 -61
  56. package/dist/question.js +2 -2
  57. package/dist/run-local.js +1 -1
  58. package/dist/tool-approval.js +2 -2
  59. package/dist/trace/ledger-sink.js +1 -1
  60. package/dist/trace/project.d.ts +1 -0
  61. package/dist/trace/project.js +3 -0
  62. package/package.json +1 -1
@@ -7,7 +7,8 @@ import WebSocket from "ws";
7
7
  import { shellQuote, kindFromMode } from "./remote-shell.js";
8
8
  import { presignS3Url } from "./s3-presign.js";
9
9
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
10
- import { fileErrorFromExec } from "./remote-env-file-error.js";
10
+ import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
11
+ import { createPosixShellFs } from "./posix-shell-fs.js";
11
12
  import { randomUUID } from "node:crypto";
12
13
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
13
14
  const PROVIDER = "k8s";
@@ -833,7 +834,7 @@ export class RemoteK8sExecutionEnv {
833
834
  if (!r.ok)
834
835
  return { ok: false, error: fileErrorFromExec(r.error, abs) };
835
836
  if (r.value.exitCode !== 0)
836
- return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs, "not_found") };
837
+ return { ok: false, error: classifyFsStderr(r.value.stderr, abs, "not_found") };
837
838
  return ok(new Uint8Array(Buffer.from(r.value.stdout.replace(/\s+/g, ""), "base64")));
838
839
  }
839
840
  async readTextFile(p, abortSignal) {
@@ -859,7 +860,7 @@ export class RemoteK8sExecutionEnv {
859
860
  if (!mk.ok)
860
861
  return { ok: false, error: fileErrorFromExec(mk.error, dir) };
861
862
  if (mk.value.exitCode !== 0)
862
- return { ok: false, error: this.fileErrorFrom(mk.value.stderr, dir) };
863
+ return { ok: false, error: classifyFsStderr(mk.value.stderr, dir) };
863
864
  return undefined;
864
865
  }
865
866
  async writeFile(p, content, abortSignal) {
@@ -885,99 +886,44 @@ export class RemoteK8sExecutionEnv {
885
886
  if (!r.ok)
886
887
  return { ok: false, error: fileErrorFromExec(r.error, abs) };
887
888
  if (r.value.exitCode !== 0)
888
- return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs) };
889
+ return { ok: false, error: classifyFsStderr(r.value.stderr, abs) };
889
890
  }
890
891
  return ok(undefined);
891
892
  }
893
+ posixFsInst;
894
+ get posixFs() {
895
+ return (this.posixFsInst ??= createPosixShellFs({
896
+ run: (cmd, abortSignal) => this.exec(cmd, { abortSignal }),
897
+ resolve: (p) => this.resolve(p),
898
+ tempDir: () => this.cwd,
899
+ }));
900
+ }
892
901
  async fileInfo(p, abortSignal) {
893
- const abs = this.resolve(p);
894
- const r = await this.exec(`stat -c '%s %Y %f' ${shellQuote(abs)}`, { abortSignal });
895
- if (!r.ok)
896
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
897
- if (r.value.exitCode !== 0)
898
- return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs, "not_found") };
899
- const [sizeS, mtimeS, modeHex] = r.value.stdout.trim().split(/\s+/);
900
- return ok({ name: path.posix.basename(abs), path: abs, kind: kindFromMode(parseInt(modeHex ?? "0", 16)), size: Number(sizeS) || 0, mtimeMs: (Number(mtimeS) || 0) * 1000 });
902
+ return this.posixFs.statFileInfo(p, abortSignal);
901
903
  }
902
904
  async listDir(p, abortSignal) {
903
- const abs = this.resolve(p);
904
- const r = await this.exec(`ls -1A ${shellQuote(abs)}`, { abortSignal });
905
- if (!r.ok)
906
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
907
- if (r.value.exitCode !== 0)
908
- return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs) };
909
- const names = r.value.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
910
- const out = [];
911
- for (const name of names) {
912
- const info = await this.fileInfo(path.posix.join(abs, name), abortSignal);
913
- out.push(info.ok ? info.value : { name, path: path.posix.join(abs, name), kind: "file", size: 0, mtimeMs: 0 });
914
- }
915
- return ok(out);
905
+ return this.posixFs.listDir(p, abortSignal);
916
906
  }
917
907
  async readLink(p, abortSignal) {
918
- const abs = this.resolve(p);
919
- const r = await this.exec(`readlink ${shellQuote(abs)}`, { abortSignal });
920
- if (!r.ok)
921
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
922
- if (r.value.exitCode !== 0)
923
- return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs, "not_found") };
924
- return ok(r.value.stdout.trim());
908
+ return this.posixFs.readLink(p, abortSignal);
925
909
  }
926
910
  async canonicalPath(p, abortSignal) {
927
- const abs = this.resolve(p);
928
- const r = await this.exec(`realpath ${shellQuote(abs)}`, { abortSignal });
929
- if (!r.ok)
930
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
931
- if (r.value.exitCode !== 0)
932
- return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs, "not_found") };
933
- return ok(r.value.stdout.trim());
911
+ return this.posixFs.canonicalPath(p, abortSignal);
934
912
  }
935
913
  async exists(p, abortSignal) {
936
- const abs = this.resolve(p);
937
- const r = await this.exec(`{ [ -e ${shellQuote(abs)} ] || [ -L ${shellQuote(abs)} ]; } && printf yes || printf no`, { abortSignal });
938
- if (!r.ok)
939
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
940
- const out = r.value.stdout.trim();
941
- if (out === "yes")
942
- return ok(true);
943
- if (out === "no")
944
- return ok(false);
945
- return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs) };
914
+ return this.posixFs.exists(p, abortSignal);
946
915
  }
947
916
  async createDir(p, options) {
948
- const abs = this.resolve(p);
949
- const r = await this.exec(`mkdir ${options?.recursive === false ? "" : "-p "}${shellQuote(abs)}`, { abortSignal: options?.abortSignal });
950
- if (!r.ok)
951
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
952
- if (r.value.exitCode !== 0)
953
- return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs) };
954
- return ok(undefined);
917
+ return this.posixFs.createDir(p, options);
955
918
  }
956
919
  async remove(p, options) {
957
- const abs = this.resolve(p);
958
- const flags = `${options?.recursive ? "r" : ""}${options?.force ? "f" : ""}`;
959
- const r = await this.exec(`rm ${flags ? `-${flags}` : ""} ${shellQuote(abs)}`, { abortSignal: options?.abortSignal });
960
- if (!r.ok)
961
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
962
- if (r.value.exitCode !== 0) {
963
- if (options?.force && /no such file/i.test(r.value.stderr))
964
- return ok(undefined);
965
- return { ok: false, error: this.fileErrorFrom(r.value.stderr, abs) };
966
- }
967
- return ok(undefined);
920
+ return this.posixFs.remove(p, options);
968
921
  }
969
922
  async createTempDir(prefix = "tmp-", abortSignal) {
970
- const r = await this.exec(`mktemp -d -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}`, { abortSignal });
971
- return this.tempResult(r, "mktemp -d");
923
+ return this.posixFs.createTempDir(prefix, abortSignal);
972
924
  }
973
925
  async createTempFile(options) {
974
- const prefix = options?.prefix ?? "";
975
- const sfx = options?.suffix ?? "";
976
- const cmd = sfx
977
- ? `f=$(mktemp -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}) && mv -- "$f" "$f"${shellQuote(sfx)} && printf '%s' "$f"${shellQuote(sfx)}`
978
- : `mktemp -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}`;
979
- const r = await this.exec(cmd, { abortSignal: options?.abortSignal });
980
- return this.tempResult(r, "mktemp");
926
+ return this.posixFs.createTempFile(options);
981
927
  }
982
928
  async execData(command, abortSignal) {
983
929
  const c = await this.ensureConnected();
@@ -996,27 +942,6 @@ export class RemoteK8sExecutionEnv {
996
942
  parts.push(command);
997
943
  return parts.join(" ");
998
944
  }
999
- fileErrorFrom(stderr, p, fallback = "unknown") {
1000
- const msg = stderr.trim();
1001
- if (/no such file/i.test(msg))
1002
- return new FileError("not_found", msg, p);
1003
- if (/permission denied/i.test(msg))
1004
- return new FileError("permission_denied", msg, p);
1005
- if (/is a directory/i.test(msg))
1006
- return new FileError("is_directory", msg, p);
1007
- if (/not a directory/i.test(msg))
1008
- return new FileError("not_directory", msg, p);
1009
- if (/input\/output error|too many levels|file name too long|no space left/i.test(msg))
1010
- return new FileError("unknown", msg, p);
1011
- return new FileError(fallback, msg || "command failed", p);
1012
- }
1013
- tempResult(r, label) {
1014
- if (!r.ok)
1015
- return { ok: false, error: fileErrorFromExec(r.error, undefined) };
1016
- if (r.value.exitCode !== 0)
1017
- return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `${label} failed`) };
1018
- return ok(r.value.stdout.trim());
1019
- }
1020
945
  }
1021
946
  export function k8sExecutionEnvFactory(config) {
1022
947
  return (ctx) => new RemoteK8sExecutionEnv({ ...config, sessionId: ctx.sessionId });
@@ -90,6 +90,8 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
90
90
  }): Promise<Result<string[], FileError>>;
91
91
  writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
92
92
  appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
93
+ private posixFsInst?;
94
+ private get posixFs();
93
95
  fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
94
96
  listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
95
97
  readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
@@ -119,8 +121,6 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
119
121
  private probeContainerShell;
120
122
  private ensureConnectedExec;
121
123
  private fsReady;
122
- private tempResult;
123
- private fsErrorFromStderr;
124
124
  private dockerToRemoteError;
125
125
  }
126
126
  export declare function localDockerExecutionEnvFactory(config: LocalDockerEnvConfig, deps?: LocalDockerEnvDeps): ExecutionEnvFactory;
@@ -3,9 +3,10 @@ import os from "node:os";
3
3
  import fs from "node:fs/promises";
4
4
  import { spawn } from "node:child_process";
5
5
  import { randomBytes } from "node:crypto";
6
- import { shellQuote, kindFromMode, armPipeDestroyGrace } from "./remote-shell.js";
6
+ import { shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
7
7
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
8
- import { fileErrorFromExec } from "./remote-env-file-error.js";
8
+ import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
9
+ import { createPosixShellFs } from "./posix-shell-fs.js";
9
10
  const PROVIDER = "local-docker";
10
11
  const DEFAULT_MOUNT_PATH = "/workspace";
11
12
  const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
@@ -301,7 +302,7 @@ export class RemoteLocalDockerExecutionEnv {
301
302
  if (!r.ok)
302
303
  return { ok: false, error: fileErrorFromExec(r.error, abs) };
303
304
  if (r.value.exitCode !== 0)
304
- return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, abs) };
305
+ return { ok: false, error: classifyFsStderr(r.value.stderr, abs) };
305
306
  return ok(r.value.stdoutBytes ?? new Uint8Array());
306
307
  }
307
308
  async readTextFile(p, abortSignal) {
@@ -330,7 +331,7 @@ export class RemoteLocalDockerExecutionEnv {
330
331
  if (!mk.ok)
331
332
  return { ok: false, error: fileErrorFromExec(mk.error, dir) };
332
333
  if (mk.value.exitCode !== 0)
333
- return { ok: false, error: this.fsErrorFromStderr(mk.value.stderr, dir) };
334
+ return { ok: false, error: classifyFsStderr(mk.value.stderr, dir) };
334
335
  }
335
336
  let local;
336
337
  try {
@@ -340,7 +341,7 @@ export class RemoteLocalDockerExecutionEnv {
340
341
  if (!cp.ok)
341
342
  return { ok: false, error: fileErrorFromExec(cp.error, abs) };
342
343
  if (cp.value.exitCode !== 0)
343
- return { ok: false, error: this.fsErrorFromStderr(cp.value.stderr, abs) };
344
+ return { ok: false, error: classifyFsStderr(cp.value.stderr, abs) };
344
345
  return ok(undefined);
345
346
  }
346
347
  catch (e) {
@@ -367,108 +368,49 @@ export class RemoteLocalDockerExecutionEnv {
367
368
  merged.set(add, base.length);
368
369
  return this.writeFile(abs, merged, abortSignal);
369
370
  }
371
+ posixFsInst;
372
+ get posixFs() {
373
+ return (this.posixFsInst ??= createPosixShellFs({
374
+ run: async (cmd, abortSignal) => {
375
+ if (this.destroyed)
376
+ return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
377
+ if (!this.containerId) {
378
+ const c = await this.connect();
379
+ if (!c.ok)
380
+ return { ok: false, error: new ExecutionError("shell_unavailable", `local-docker connect failed: ${c.error.message}`) };
381
+ }
382
+ return this.docker(this.execRaw(cmd), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
383
+ },
384
+ resolve: (p) => this.resolve(p),
385
+ tempDir: () => this.cwd,
386
+ }));
387
+ }
370
388
  async fileInfo(p, abortSignal) {
371
- const ready = await this.fsReady(p);
372
- if (!ready.ok)
373
- return ready;
374
- const abs = this.resolve(p);
375
- const r = await this.docker(this.execRaw(`stat -c '%s %Y %f' ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
376
- if (!r.ok)
377
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
378
- if (r.value.exitCode !== 0)
379
- return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, abs) };
380
- const [sizeS, mtimeS, modeHex] = r.value.stdout.trim().split(/\s+/);
381
- return ok({ name: path.posix.basename(abs), path: abs, kind: kindFromMode(parseInt(modeHex ?? "0", 16)), size: Number(sizeS) || 0, mtimeMs: (Number(mtimeS) || 0) * 1000 });
389
+ return this.posixFs.statFileInfo(p, abortSignal);
382
390
  }
383
391
  async listDir(p, abortSignal) {
384
- const ready = await this.fsReady(p);
385
- if (!ready.ok)
386
- return ready;
387
- const abs = this.resolve(p);
388
- const r = await this.docker(this.execRaw(`ls -1A ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
389
- if (!r.ok)
390
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
391
- if (r.value.exitCode !== 0)
392
- return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, abs) };
393
- const names = r.value.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
394
- const out = [];
395
- for (const name of names) {
396
- const info = await this.fileInfo(path.posix.join(abs, name), abortSignal);
397
- if (info.ok)
398
- out.push(info.value);
399
- else
400
- out.push({ name, path: path.posix.join(abs, name), kind: "file", size: 0, mtimeMs: 0 });
401
- }
402
- return ok(out);
392
+ return this.posixFs.listDir(p, abortSignal);
403
393
  }
404
394
  async readLink(p, abortSignal) {
405
- const abs = this.resolve(p);
406
- const r = await this.docker(this.execRaw(`readlink ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
407
- if (!r.ok)
408
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
409
- if (r.value.exitCode !== 0)
410
- return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, abs) };
411
- return ok(r.value.stdout.trim());
395
+ return this.posixFs.readLink(p, abortSignal);
412
396
  }
413
397
  async canonicalPath(p, abortSignal) {
414
- const abs = this.resolve(p);
415
- const r = await this.docker(this.execRaw(`realpath ${shellQuote(abs)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
416
- if (!r.ok)
417
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
418
- if (r.value.exitCode !== 0)
419
- return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, abs) };
420
- return ok(r.value.stdout.trim());
398
+ return this.posixFs.canonicalPath(p, abortSignal);
421
399
  }
422
400
  async exists(p, abortSignal) {
423
- const info = await this.fileInfo(p, abortSignal);
424
- if (info.ok)
425
- return ok(true);
426
- if (info.error.code === "not_found")
427
- return ok(false);
428
- return { ok: false, error: info.error };
401
+ return this.posixFs.exists(p, abortSignal);
429
402
  }
430
403
  async createDir(p, options) {
431
- const ready = await this.fsReady(p);
432
- if (!ready.ok)
433
- return ready;
434
- const abs = this.resolve(p);
435
- const recursive = options?.recursive ?? true;
436
- const r = await this.docker(this.execRaw(`mkdir ${recursive ? "-p " : ""}${shellQuote(abs)}`), { signal: options?.abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
437
- if (!r.ok)
438
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
439
- if (r.value.exitCode !== 0)
440
- return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, abs) };
441
- return ok(undefined);
404
+ return this.posixFs.createDir(p, options);
442
405
  }
443
406
  async remove(p, options) {
444
- const ready = await this.fsReady(p);
445
- if (!ready.ok)
446
- return ready;
447
- const abs = this.resolve(p);
448
- const flags = `${options?.recursive ? "r" : ""}${options?.force ? "f" : ""}`;
449
- const r = await this.docker(this.execRaw(`rm ${flags ? `-${flags} ` : ""}-- ${shellQuote(abs)}`), { signal: options?.abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
450
- if (!r.ok)
451
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
452
- if (r.value.exitCode !== 0) {
453
- const fe = this.fsErrorFromStderr(r.value.stderr, abs);
454
- if (options?.force && fe.code === "not_found")
455
- return ok(undefined);
456
- return { ok: false, error: fe };
457
- }
458
- return ok(undefined);
407
+ return this.posixFs.remove(p, options);
459
408
  }
460
409
  async createTempDir(prefix = "tmp-", abortSignal) {
461
- const r = await this.docker(this.execRaw(`mktemp -d -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}`), { signal: abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
462
- return this.tempResult(r);
410
+ return this.posixFs.createTempDir(prefix, abortSignal);
463
411
  }
464
412
  async createTempFile(options) {
465
- const prefix = options?.prefix ?? "";
466
- const sfx = options?.suffix ?? "";
467
- const cmd = sfx
468
- ? `f=$(mktemp -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}) && mv -- "$f" "$f"${shellQuote(sfx)} && printf '%s' "$f"${shellQuote(sfx)}`
469
- : `mktemp -p ${shellQuote(this.cwd)} ${shellQuote(`${prefix}XXXXXX`)}`;
470
- const r = await this.docker(this.execRaw(cmd), { signal: options?.abortSignal, timeoutMs: this.cfg.commandTimeoutMs });
471
- return this.tempResult(r);
413
+ return this.posixFs.createTempFile(options);
472
414
  }
473
415
  withHostFlag(args) {
474
416
  return this.cfg.dockerHost ? ["-H", this.cfg.dockerHost, ...args] : args;
@@ -584,25 +526,6 @@ export class RemoteLocalDockerExecutionEnv {
584
526
  return { ok: false, error: new FileError("unknown", `local-docker connect failed: ${c.error.message}`, this.resolve(p), c.error) };
585
527
  return ok(undefined);
586
528
  }
587
- tempResult(r) {
588
- if (!r.ok)
589
- return { ok: false, error: fileErrorFromExec(r.error, undefined) };
590
- if (r.value.exitCode !== 0)
591
- return { ok: false, error: this.fsErrorFromStderr(r.value.stderr, undefined) };
592
- return ok(r.value.stdout.trim());
593
- }
594
- fsErrorFromStderr(stderr, p) {
595
- const msg = stderr.trim();
596
- if (/no such file|not found/i.test(msg))
597
- return new FileError("not_found", msg || "no such file", p);
598
- if (/permission denied/i.test(msg))
599
- return new FileError("permission_denied", msg, p);
600
- if (/is a directory/i.test(msg))
601
- return new FileError("is_directory", msg, p);
602
- if (/not a directory/i.test(msg))
603
- return new FileError("not_directory", msg, p);
604
- return new FileError("unknown", msg || "command failed", p);
605
- }
606
529
  dockerToRemoteError(e, fallback) {
607
530
  if (e.code === "timeout")
608
531
  return new RemoteExecutionError("timeout", e.message, e);
@@ -101,6 +101,8 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
101
101
  appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
102
102
  fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
103
103
  listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
104
+ private posixFsInst?;
105
+ private get posixFs();
104
106
  readLink(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
105
107
  canonicalPath(p: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
106
108
  exists(p: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>>;
@@ -120,7 +122,6 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
120
122
  abortSignal?: AbortSignal;
121
123
  }): Promise<Result<string, FileError>>;
122
124
  private withCwdEnv;
123
- private tempResult;
124
125
  private connErr;
125
126
  private fileError;
126
127
  }
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { Client } from "ssh2";
3
3
  import { shellQuote, kindFromMode } from "./remote-shell.js";
4
+ import { createPosixShellFs } from "./posix-shell-fs.js";
4
5
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
5
6
  import { fileErrorFromExec } from "./remote-env-file-error.js";
6
7
  const PROVIDER = "ssh";
@@ -503,29 +504,19 @@ export class RemoteSshExecutionEnv {
503
504
  return { ok: false, error: this.fileError(e, abs) };
504
505
  }
505
506
  }
507
+ posixFsInst;
508
+ get posixFs() {
509
+ return (this.posixFsInst ??= createPosixShellFs({
510
+ run: (cmd, abortSignal) => this.exec(cmd, { abortSignal }),
511
+ resolve: (p) => this.resolve(p),
512
+ tempDir: () => null,
513
+ }));
514
+ }
506
515
  async readLink(p, abortSignal) {
507
- const abs = this.resolve(p);
508
- const r = await this.exec(`readlink -- ${shellQuote(abs)}`, { abortSignal });
509
- if (!r.ok)
510
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
511
- if (r.value.exitCode !== 0)
512
- return { ok: false, error: new FileError("not_found", r.value.stderr.trim() || `readlink failed for ${abs}`, abs) };
513
- return ok(r.value.stdout.trim());
516
+ return this.posixFs.readLink(p, abortSignal);
514
517
  }
515
518
  async canonicalPath(p, abortSignal) {
516
- const abs = this.resolve(p);
517
- const r = await this.exec(`realpath -- ${shellQuote(abs)}`, { abortSignal });
518
- if (!r.ok)
519
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
520
- if (r.value.exitCode !== 0) {
521
- const msg = r.value.stderr.trim();
522
- if (/permission denied|access denied/i.test(msg))
523
- return { ok: false, error: new FileError("permission_denied", msg, abs) };
524
- if (/not a directory/i.test(msg))
525
- return { ok: false, error: new FileError("not_directory", msg, abs) };
526
- return { ok: false, error: new FileError("not_found", msg || `realpath failed for ${abs}`, abs) };
527
- }
528
- return ok(r.value.stdout.trim());
519
+ return this.posixFs.canonicalPath(p, abortSignal);
529
520
  }
530
521
  async exists(p, abortSignal) {
531
522
  const info = await this.fileInfo(p, abortSignal);
@@ -536,46 +527,16 @@ export class RemoteSshExecutionEnv {
536
527
  return { ok: false, error: info.error };
537
528
  }
538
529
  async createDir(p, options) {
539
- const abs = this.resolve(p);
540
- const r = await this.exec(`mkdir -p -- ${shellQuote(abs)}`, { abortSignal: options?.abortSignal });
541
- if (!r.ok)
542
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
543
- if (r.value.exitCode !== 0) {
544
- const msg = r.value.stderr.trim();
545
- if (/permission denied/i.test(msg))
546
- return { ok: false, error: new FileError("permission_denied", msg, abs) };
547
- return { ok: false, error: new FileError("unknown", msg || `mkdir failed for ${abs}`, abs) };
548
- }
549
- return ok(undefined);
530
+ return this.posixFs.createDir(p, options);
550
531
  }
551
532
  async remove(p, options) {
552
- const abs = this.resolve(p);
553
- const flags = `${options?.recursive ? "r" : ""}${options?.force ? "f" : ""}`;
554
- const r = await this.exec(`rm ${flags ? `-${flags}` : ""} -- ${shellQuote(abs)}`, { abortSignal: options?.abortSignal });
555
- if (!r.ok)
556
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
557
- if (r.value.exitCode !== 0) {
558
- const msg = r.value.stderr.trim();
559
- if (options?.force && /no such file/i.test(msg))
560
- return ok(undefined);
561
- if (/no such file/i.test(msg))
562
- return { ok: false, error: new FileError("not_found", msg, abs) };
563
- if (/permission denied/i.test(msg))
564
- return { ok: false, error: new FileError("permission_denied", msg, abs) };
565
- return { ok: false, error: new FileError("unknown", msg || `rm failed for ${abs}`, abs) };
566
- }
567
- return ok(undefined);
533
+ return this.posixFs.remove(p, options);
568
534
  }
569
535
  async createTempDir(prefix = "tmp-", abortSignal) {
570
- const r = await this.exec(`mktemp -d -t ${shellQuote(`${prefix}XXXXXX`)}`, { abortSignal });
571
- return this.tempResult(r, "mktemp -d");
536
+ return this.posixFs.createTempDir(prefix, abortSignal);
572
537
  }
573
538
  async createTempFile(options) {
574
- const prefix = options?.prefix ?? "";
575
- const suffix = options?.suffix ?? "";
576
- const suffixArg = suffix ? `--suffix=${shellQuote(suffix)} -t` : "-t";
577
- const r = await this.exec(`mktemp ${suffixArg} ${shellQuote(`${prefix}XXXXXX`)}`, { abortSignal: options?.abortSignal });
578
- return this.tempResult(r, "mktemp");
539
+ return this.posixFs.createTempFile(options);
579
540
  }
580
541
  withCwdEnv(command, cwd, env) {
581
542
  const parts = [];
@@ -588,13 +549,6 @@ export class RemoteSshExecutionEnv {
588
549
  parts.push(command);
589
550
  return parts.join(" ");
590
551
  }
591
- tempResult(r, label) {
592
- if (!r.ok)
593
- return { ok: false, error: fileErrorFromExec(r.error, undefined) };
594
- if (r.value.exitCode !== 0)
595
- return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `${label} failed`) };
596
- return ok(r.value.stdout.trim());
597
- }
598
552
  connErr(e) {
599
553
  const msg = e.message || String(e);
600
554
  if (e.level === "client-authentication" || /authentication|all configured authentication methods failed/i.test(msg)) {
package/dist/question.js CHANGED
@@ -118,10 +118,10 @@ export class QuestionCoordinator {
118
118
  respond(id, principal, body) {
119
119
  const parsed = parseQuestionResponse(body);
120
120
  if (!parsed.ok)
121
- return { status: 400, body: { error: parsed.error } };
121
+ return { status: 400, body: { error: parsed.error, errorCode: "request.body_shape" } };
122
122
  const entry = this.pending.get(id);
123
123
  if (!entry || (entry.owner !== null && entry.owner !== principal)) {
124
- return { status: 404, body: { error: "no pending question for this id (answered, expired, or not on this replica)" } };
124
+ return { status: 404, body: { error: "no pending question for this id (answered, expired, or not on this replica)", errorCode: "question.not_pending" } };
125
125
  }
126
126
  entry.settle(parsed.value, "answered");
127
127
  return { status: 200, body: { questionId: id, delivery: "applied" } };
package/dist/run-local.js CHANGED
@@ -254,7 +254,7 @@ export async function runLocal(argv, deps = {}) {
254
254
  toolResultStore: fileBackend.toolResultStore,
255
255
  ...(executionEnvFactory ? { executionEnvFactory } : {}),
256
256
  ...(lspManager ? { lspManager } : {}),
257
- ...(!config.projectMemoryDisabled
257
+ ...(config.projectMemoryEnabled
258
258
  ? { loadProjectMemory: makeLoadProjectMemory({ logger }), probeInstructionSources: makeProbeInstructionSources() }
259
259
  : {}),
260
260
  });
@@ -211,10 +211,10 @@ export class ToolApprovalCoordinator {
211
211
  respond(id, principal, body) {
212
212
  const parsed = parseToolApprovalResponse(body);
213
213
  if (!parsed.ok)
214
- return { status: 400, body: { error: parsed.error } };
214
+ return { status: 400, body: { error: parsed.error, errorCode: "request.body_shape" } };
215
215
  const entry = this.pending.get(id);
216
216
  if (!entry || (entry.owner !== null && entry.owner !== principal)) {
217
- return { status: 404, body: { error: "no pending tool approval for this id (settled, expired, or not on this replica)" } };
217
+ return { status: 404, body: { error: "no pending tool approval for this id (settled, expired, or not on this replica)", errorCode: "tool_approval.not_pending" } };
218
218
  }
219
219
  let rememberApplied;
220
220
  if (parsed.value === "allow_session")
@@ -45,7 +45,7 @@ export function createLedgerSink(opts) {
45
45
  break;
46
46
  case "status":
47
47
  await flush();
48
- await append("brain_status", brainStatusEventData(ev));
48
+ await append("status", brainStatusEventData(ev));
49
49
  break;
50
50
  case "context_usage":
51
51
  await flush();
@@ -221,6 +221,7 @@ export interface TaskSummary {
221
221
  };
222
222
  }
223
223
  export declare function runSummary(run: RunRecord, latestEventType?: string): TaskSummary;
224
+ export declare function normalizeRunEventType(type: string): string;
224
225
  export declare function mapTraceEvent(type: string, seq: number, data: Record<string, unknown>): {
225
226
  event: string;
226
227
  data: Record<string, unknown>;
@@ -299,6 +299,9 @@ export function runSummary(run, latestEventType) {
299
299
  lastStep: lastStep(latestEventType, run.status),
300
300
  };
301
301
  }
302
+ export function normalizeRunEventType(type) {
303
+ return type === "brain_status" ? "status" : type;
304
+ }
302
305
  export function mapTraceEvent(type, seq, data) {
303
306
  switch (type) {
304
307
  case "reasoning":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "1.316.0",
3
+ "version": "1.318.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",