@sema-agent/server 7.138.0 → 7.139.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 (215) hide show
  1. package/MIGRATION.md +276 -7
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +73 -29
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/wire.d.ts +3 -3
  8. package/dist/approval-card.d.ts +20 -20
  9. package/dist/bench/s1/arms.d.ts +1 -1
  10. package/dist/bench/s1/arms.js +1 -3
  11. package/dist/bench/s1/run-firm.js +2 -2
  12. package/dist/boot/device-lane.d.ts +8 -3
  13. package/dist/boot/device-lane.js +7 -2
  14. package/dist/boot/execution-env.d.ts +4 -2
  15. package/dist/boot/execution-env.js +16 -24
  16. package/dist/boot/execution-lane.d.ts +1 -1
  17. package/dist/boot/governance-seams.d.ts +3 -2
  18. package/dist/boot/governance-seams.js +9 -11
  19. package/dist/boot/memory-consolidation.js +2 -2
  20. package/dist/boot/resolve-spec.d.ts +1 -1
  21. package/dist/boot/resolve-spec.js +8 -9
  22. package/dist/boot/retention-lane.d.ts +5 -5
  23. package/dist/boot/retention-lane.js +0 -6
  24. package/dist/boot/runner-deps.d.ts +3 -1
  25. package/dist/boot/runner-deps.js +2 -1
  26. package/dist/boot/session-faces.d.ts +0 -3
  27. package/dist/boot/session-faces.js +15 -8
  28. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  29. package/dist/boot/stage-05-execution-env.js +2 -2
  30. package/dist/boot/stage-06-runners.js +0 -1
  31. package/dist/boot/stage-09-leader.js +1 -2
  32. package/dist/boot/stage-10-http-server.js +2 -2
  33. package/dist/boot/stores.js +4 -4
  34. package/dist/boot/web-admin.js +2 -2
  35. package/dist/capabilities/mcp-probe.js +1 -1
  36. package/dist/capabilities/scenarios.d.ts +1 -1
  37. package/dist/capability-facts.d.ts +15 -7
  38. package/dist/capability-facts.js +8 -3
  39. package/dist/config-catalog.js +7 -7
  40. package/dist/config-invariants.d.ts +23 -3
  41. package/dist/config-invariants.js +42 -0
  42. package/dist/config-provider.d.ts +19 -24
  43. package/dist/config-provider.js +27 -12
  44. package/dist/config-types.d.ts +10 -5
  45. package/dist/config.d.ts +33 -0
  46. package/dist/config.js +44 -15
  47. package/dist/delegated-session-owner.d.ts +2 -2
  48. package/dist/deployment-shape.d.ts +1 -1
  49. package/dist/device-enrollment.d.ts +41 -2
  50. package/dist/device-enrollment.js +27 -12
  51. package/dist/device-store.d.ts +58 -85
  52. package/dist/device-store.js +19 -49
  53. package/dist/device-ws-hub.js +2 -1
  54. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  55. package/dist/exec-cut.d.ts +60 -0
  56. package/dist/exec-cut.js +36 -0
  57. package/dist/execution-lane-caps.d.ts +19 -0
  58. package/dist/execution-lane-caps.js +10 -0
  59. package/dist/execution-placement.d.ts +259 -0
  60. package/dist/execution-placement.js +199 -0
  61. package/dist/field-conflict.d.ts +3 -2
  62. package/dist/field-conflict.js +1 -0
  63. package/dist/hosted-posture.d.ts +2 -2
  64. package/dist/http/admission.js +8 -19
  65. package/dist/http/checkpoint-ctx.d.ts +21 -12
  66. package/dist/http/checkpoint-ctx.js +4 -2
  67. package/dist/http/claimed-run.d.ts +1 -1
  68. package/dist/http/durable-run-leg.js +9 -5
  69. package/dist/http/execution-context.d.ts +218 -0
  70. package/dist/http/execution-context.js +330 -0
  71. package/dist/http/resume-legs.js +50 -20
  72. package/dist/http/route-ctx.d.ts +14 -8
  73. package/dist/http/routes/background.d.ts +29 -1
  74. package/dist/http/routes/background.js +29 -2
  75. package/dist/http/routes/devices.d.ts +13 -19
  76. package/dist/http/routes/devices.js +21 -26
  77. package/dist/http/routes/session-sync.js +110 -15
  78. package/dist/http/routes/sessions.d.ts +6 -0
  79. package/dist/http/routes/sessions.js +115 -11
  80. package/dist/http/routes/side-query.d.ts +12 -4
  81. package/dist/http/routes/side-query.js +4 -5
  82. package/dist/http/routes/tasks.js +37 -31
  83. package/dist/http/server.d.ts +35 -19
  84. package/dist/http/server.js +11 -0
  85. package/dist/http/session-inherit-env.d.ts +3 -4
  86. package/dist/http/session-inherit-env.js +1 -1
  87. package/dist/http/wire-types.d.ts +8 -12
  88. package/dist/index.d.ts +1 -0
  89. package/dist/memory-sync-client.d.ts +53 -14
  90. package/dist/memory-sync-client.js +196 -116
  91. package/dist/memory-sync.d.ts +1 -1
  92. package/dist/memory-sync.js +3 -2
  93. package/dist/observability/cost-taxonomy.d.ts +2 -2
  94. package/dist/observability/fail-open.d.ts +12 -0
  95. package/dist/observability/fail-open.js +12 -0
  96. package/dist/observability/run-terminal.js +0 -1
  97. package/dist/orchestration/workflow-notify-journal.js +4 -4
  98. package/dist/org-memory-admission.d.ts +4 -3
  99. package/dist/org-memory-admission.js +6 -6
  100. package/dist/parked-decide.d.ts +34 -9
  101. package/dist/parked-decide.js +12 -19
  102. package/dist/parked-revive-surface.d.ts +12 -15
  103. package/dist/parked-revive-surface.js +3 -6
  104. package/dist/peer-seat.d.ts +13 -7
  105. package/dist/peer-seat.js +37 -3
  106. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  107. package/dist/plugins/approval-ask-store-file.js +11 -0
  108. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  109. package/dist/plugins/approval-ask-store-memory.js +12 -0
  110. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  111. package/dist/plugins/approval-ask-store-sql.js +30 -12
  112. package/dist/plugins/caching-session-store.d.ts +6 -2
  113. package/dist/plugins/caching-session-store.js +9 -3
  114. package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
  115. package/dist/plugins/checkpoint-store-sql.js +5 -7
  116. package/dist/plugins/device-store-sql.d.ts +25 -51
  117. package/dist/plugins/device-store-sql.js +49 -103
  118. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  119. package/dist/plugins/execution-placement-sql.js +223 -0
  120. package/dist/plugins/file-history-store-sql.d.ts +7 -6
  121. package/dist/plugins/file-history-store-sql.js +5 -5
  122. package/dist/plugins/file-run-store.d.ts +6 -1
  123. package/dist/plugins/file-run-store.js +4 -0
  124. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  125. package/dist/plugins/fork-routing-session-store.js +16 -0
  126. package/dist/plugins/local-session-store.d.ts +149 -50
  127. package/dist/plugins/local-session-store.js +382 -107
  128. package/dist/plugins/memory-key-guards.d.ts +4 -0
  129. package/dist/plugins/memory-key-guards.js +1 -0
  130. package/dist/plugins/memory-run-store.d.ts +1 -1
  131. package/dist/plugins/one-time-migrations.d.ts +111 -3
  132. package/dist/plugins/one-time-migrations.js +380 -10
  133. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  134. package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
  135. package/dist/plugins/permission-rule-store-sql.js +13 -16
  136. package/dist/plugins/pg-pool.d.ts +7 -3
  137. package/dist/plugins/pg-session-storage.d.ts +31 -20
  138. package/dist/plugins/pg-session-storage.js +120 -52
  139. package/dist/plugins/remote-env-adb.d.ts +20 -4
  140. package/dist/plugins/remote-env-adb.js +70 -62
  141. package/dist/plugins/remote-env-device.d.ts +30 -12
  142. package/dist/plugins/remote-env-device.js +18 -17
  143. package/dist/plugins/remote-env-e2b.d.ts +18 -4
  144. package/dist/plugins/remote-env-e2b.js +41 -42
  145. package/dist/plugins/remote-env-file-error.d.ts +2 -0
  146. package/dist/plugins/remote-env-file-error.js +1 -1
  147. package/dist/plugins/remote-env-host.d.ts +21 -8
  148. package/dist/plugins/remote-env-host.js +66 -70
  149. package/dist/plugins/remote-env-k8s.d.ts +11 -16
  150. package/dist/plugins/remote-env-k8s.js +61 -54
  151. package/dist/plugins/remote-env-local-docker.d.ts +29 -6
  152. package/dist/plugins/remote-env-local-docker.js +46 -59
  153. package/dist/plugins/remote-env-ssh.d.ts +16 -4
  154. package/dist/plugins/remote-env-ssh.js +72 -56
  155. package/dist/plugins/remote-shell.d.ts +100 -3
  156. package/dist/plugins/remote-shell.js +166 -2
  157. package/dist/plugins/retention-store-sql.d.ts +9 -6
  158. package/dist/plugins/retention-store-sql.js +7 -1
  159. package/dist/plugins/run-store-sql.d.ts +13 -3
  160. package/dist/plugins/run-store-sql.js +5 -0
  161. package/dist/plugins/schema-consistency.d.ts +15 -5
  162. package/dist/plugins/schema-consistency.js +14 -8
  163. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  164. package/dist/plugins/schema-drift-sentence.js +38 -0
  165. package/dist/plugins/session-fork.d.ts +54 -0
  166. package/dist/plugins/session-fork.js +38 -1
  167. package/dist/plugins/session-row-sql.d.ts +22 -0
  168. package/dist/plugins/session-row-sql.js +81 -0
  169. package/dist/plugins/session-store.d.ts +1 -1
  170. package/dist/plugins/store-backend.d.ts +1 -1
  171. package/dist/plugins/store-backend.js +19 -7
  172. package/dist/plugins/store-contracts.d.ts +25 -5
  173. package/dist/plugins/tidb-pool.d.ts +12 -7
  174. package/dist/plugins/tidb-pool.js +9 -3
  175. package/dist/plugins/tidb-session-store.d.ts +57 -26
  176. package/dist/plugins/tidb-session-store.js +118 -50
  177. package/dist/plugins/tool-result-store-sql.d.ts +3 -2
  178. package/dist/plugins/tool-result-store-sql.js +18 -10
  179. package/dist/project-memory.d.ts +9 -15
  180. package/dist/project-memory.js +5 -4
  181. package/dist/request-key-closure.js +1 -1
  182. package/dist/request-value-closure.d.ts +6 -7
  183. package/dist/request-value-closure.js +1 -3
  184. package/dist/run-local.d.ts +6 -0
  185. package/dist/run-local.js +24 -20
  186. package/dist/runs.d.ts +2 -1
  187. package/dist/runs.js +3 -1
  188. package/dist/security.d.ts +38 -12
  189. package/dist/security.js +8 -2
  190. package/dist/server-secret-env.d.ts +9 -5
  191. package/dist/session-children-retire.d.ts +27 -9
  192. package/dist/session-children-retire.js +9 -5
  193. package/dist/session-incarnation.d.ts +19 -9
  194. package/dist/session-incarnation.js +4 -2
  195. package/dist/session-row.d.ts +114 -5
  196. package/dist/session-row.js +55 -5
  197. package/dist/session-titler.d.ts +2 -2
  198. package/dist/task-a2a.d.ts +1 -1
  199. package/dist/task-cwd.d.ts +6 -1
  200. package/dist/task-mcp.d.ts +3 -3
  201. package/dist/tool-approval.d.ts +6 -0
  202. package/dist/tool-approval.js +8 -1
  203. package/dist/trace/project.d.ts +1 -0
  204. package/dist/trace/project.js +1 -0
  205. package/dist/trace/self-minted-wire.d.ts +111 -2
  206. package/dist/trace/self-minted-wire.js +38 -1
  207. package/dist/url-refusal.d.ts +1 -1
  208. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  209. package/dist/web-admin-client/apply-effective.js +20 -10
  210. package/dist/web-admin-client/skills-mcp.js +3 -2
  211. package/dist/web-admin-client/types.d.ts +6 -0
  212. package/package.json +3 -3
  213. package/wire-changes/server@7.139.0.json +348 -0
  214. package/dist/http/device-placement.d.ts +0 -175
  215. package/dist/http/device-placement.js +0 -145
@@ -4,9 +4,10 @@ import fs from "node:fs/promises";
4
4
  import { spawn } from "node:child_process";
5
5
  import { hostSubprocessEnv } from "../server-secret-env.js";
6
6
  import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
7
- import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, armPipeDestroyGrace } from "./remote-shell.js";
7
+ import { appendFileStaged, rewriteAppend, buildCwdEnvCommand, createCwdGuardedCall, shellQuote, armPipeDestroyGrace, writeFileStaged } from "./remote-shell.js";
8
8
  import { signalNumber } from "./host-platform.js";
9
9
  import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
10
+ import { buildCapturedOutput, createCutError } from "../exec-cut.js";
10
11
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
11
12
  import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
12
13
  import { createPosixShellFs } from "./posix-shell-fs.js";
@@ -27,7 +28,7 @@ export function isAdbTransportLost(stderr) {
27
28
  return /device offline|device '.*' not found|no devices\/emulators found|connection reset|protocol fault|device disconnected/i.test(stderr);
28
29
  }
29
30
  export class RemoteAdbExecutionEnv {
30
- capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false };
31
+ capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false };
31
32
  cwd;
32
33
  connected = false;
33
34
  destroyed = false;
@@ -341,57 +342,46 @@ export class RemoteAdbExecutionEnv {
341
342
  lines = lines.slice(0, options.maxLines);
342
343
  return ok(lines);
343
344
  }
345
+ stagedLane(content) {
346
+ return { upload: (dest, signal) => this.push(content, dest, signal), run: (cmd, signal) => this.fsExec(cmd, signal) };
347
+ }
344
348
  async writeFile(p, content, abortSignal) {
349
+ const gated = await this.fsGate(p);
350
+ if (gated)
351
+ return gated;
352
+ return writeFileStaged(this.stagedLane(content), this.resolve(p), abortSignal);
353
+ }
354
+ async appendFile(p, content, abortSignal) {
345
355
  const gated = await this.fsGate(p);
346
356
  if (gated)
347
357
  return gated;
348
358
  const abs = this.resolve(p);
349
- const dir = path.posix.dirname(abs);
350
- if (dir && dir !== "." && dir !== "/") {
351
- const mk = await this.fsExec(`mkdir -p ${shellQuote(dir)}`, abortSignal);
352
- if (!mk.ok)
353
- return { ok: false, error: fileErrorFromExec(mk.error, dir) };
354
- if (mk.value.exitCode !== 0)
355
- return { ok: false, error: new FileError("unknown", mk.value.stderr.trim() || `mkdir -p failed for ${dir}`, dir) };
356
- }
357
- let local;
359
+ return appendFileStaged(this.stagedLane(content), abs, abortSignal, (ctx) => rewriteAppend((signal) => this.readBinaryFile(abs, signal), (bytes, signal) => this.push(bytes, abs, signal), content, ctx));
360
+ }
361
+ async push(content, dest, abortSignal) {
362
+ let localDir;
358
363
  try {
359
- local = path.join(os.tmpdir(), `adb-push-${Date.now()}-${Math.random().toString(36).slice(2)}`);
364
+ localDir = await fs.mkdtemp(path.join(os.tmpdir(), "sema-adb-push-"));
365
+ const local = path.join(localDir, "data");
360
366
  await fs.writeFile(local, typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content));
361
- const r = await this.adb(["-s", this.cfg.serial, "push", local, abs], { signal: abortSignal, timeoutMs: this.cfg.dataTimeoutMs });
367
+ const r = await this.adb(["-s", this.cfg.serial, "push", local, dest], { signal: abortSignal, timeoutMs: this.cfg.dataTimeoutMs });
362
368
  if (!r.ok)
363
- return { ok: false, error: fileErrorFromExec(r.error, abs) };
369
+ return { ok: false, error: fileErrorFromExec(r.error, dest) };
364
370
  const out = `${r.value.stdout} ${r.value.stderr}`;
365
371
  if (/permission denied/i.test(out))
366
- return { ok: false, error: new FileError("permission_denied", out.trim(), abs) };
372
+ return { ok: false, error: new FileError("permission_denied", out.trim(), dest) };
367
373
  if (r.value.exitCode !== 0)
368
- return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `adb push failed for ${abs}`, abs) };
374
+ return { ok: false, error: new FileError("unknown", r.value.stderr.trim() || `adb push failed for ${dest}`, dest) };
369
375
  return ok(undefined);
370
376
  }
371
377
  catch (e) {
372
- return { ok: false, error: new FileError("unknown", e instanceof Error ? e.message : String(e), abs) };
378
+ return { ok: false, error: new FileError("unknown", e instanceof Error ? e.message : String(e), dest) };
373
379
  }
374
380
  finally {
375
- if (local)
376
- await fs.rm(local, { force: true }).catch(() => { });
381
+ if (localDir)
382
+ await fs.rm(localDir, { recursive: true, force: true }).catch(() => { });
377
383
  }
378
384
  }
379
- async appendFile(p, content, abortSignal) {
380
- const abs = this.resolve(p);
381
- const existing = await this.readBinaryFile(abs, abortSignal);
382
- let base;
383
- if (existing.ok)
384
- base = existing.value;
385
- else if (existing.error.code === "not_found")
386
- base = new Uint8Array(0);
387
- else
388
- return existing;
389
- const add = typeof content === "string" ? new TextEncoder().encode(content) : content;
390
- const merged = new Uint8Array(base.length + add.length);
391
- merged.set(base, 0);
392
- merged.set(add, base.length);
393
- return this.writeFile(abs, merged, abortSignal);
394
- }
395
385
  posixFsInst;
396
386
  get posixFs() {
397
387
  return (this.posixFsInst ??= createPosixShellFs({
@@ -451,23 +441,44 @@ export class RemoteAdbExecutionEnv {
451
441
  const outBufs = [];
452
442
  const outTail = binary ? undefined : new RollingTailBuffer();
453
443
  const errBuf = new RollingTailBuffer();
454
- const finish = (r) => {
444
+ const recordStderr = (bytes) => {
445
+ if (bytes.length > 0)
446
+ errBuf.push(bytes);
447
+ };
448
+ const emitStderr = (bytes) => {
449
+ if (bytes.length > 0)
450
+ opts?.onStderr?.(bytes.toString());
451
+ };
452
+ const takeStderr = (bytes) => {
453
+ recordStderr(bytes);
454
+ emitStderr(bytes);
455
+ };
456
+ const heldStderr = () => opts?.stderrFilter?.endStderr();
457
+ const captured = () => {
458
+ const held = heldStderr();
459
+ if (held)
460
+ recordStderr(held);
461
+ return buildCapturedOutput(outTail, errBuf);
462
+ };
463
+ const finish = (outcome) => {
455
464
  if (settled)
456
- return;
465
+ return false;
457
466
  settled = true;
467
+ const r = outcome();
458
468
  clearTimeout(timer);
459
469
  opts?.signal?.removeEventListener("abort", onAbort);
460
470
  resolve(r);
471
+ return true;
461
472
  };
462
473
  const timeoutMs = opts?.timeoutMs ?? this.cfg.commandTimeoutMs;
463
- const timer = setTimeout(() => {
464
- killGrace();
465
- finish({ ok: false, error: new ExecutionError("timeout", `adb ${args[0]} exceeded ${timeoutMs}ms`) });
466
- }, timeoutMs);
467
- const onAbort = () => {
468
- killGrace();
469
- finish({ ok: false, error: new ExecutionError("aborted", "adb command aborted") });
474
+ const cut = (c) => {
475
+ finish(() => {
476
+ killGrace();
477
+ return { ok: false, error: createCutError(c, captured()) };
478
+ });
470
479
  };
480
+ const timer = setTimeout(() => cut({ code: "timeout", message: `adb ${args[0]} exceeded ${timeoutMs}ms` }), timeoutMs);
481
+ const onAbort = () => cut({ code: "aborted", message: "adb command aborted" });
471
482
  if (opts?.signal) {
472
483
  if (opts.signal.aborted)
473
484
  return onAbort();
@@ -480,30 +491,27 @@ export class RemoteAdbExecutionEnv {
480
491
  outTail.push(d);
481
492
  opts?.onStdout?.(d.toString());
482
493
  });
483
- const takeStderr = (bytes) => {
484
- if (bytes.length === 0)
485
- return;
486
- errBuf.push(bytes);
487
- opts?.onStderr?.(bytes.toString());
488
- };
489
494
  child.stderr.on("data", (d) => takeStderr(opts?.stderrFilter ? opts.stderrFilter.filterStderr(d) : d));
490
- child.on("error", (e) => finish({ ok: false, error: new ExecutionError("spawn_error", `adb spawn failed (is '${this.cfg.adbPath}' installed?): ${e.message}`, e) }));
495
+ child.on("error", (e) => finish(() => ({ ok: false, error: new ExecutionError("spawn_error", `adb spawn failed (is '${this.cfg.adbPath}' installed?): ${e.message}`, e) })));
491
496
  child.on("close", (code, signal) => {
492
497
  if (forceSettleTimer)
493
498
  clearTimeout(forceSettleTimer);
494
- if (opts?.stderrFilter)
495
- takeStderr(opts.stderrFilter.endStderr());
496
- const err = errBuf.result();
497
- const stderr = markTruncated(err.text, err.droppedBytes);
498
499
  const exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 9) : 0);
499
- if (binary) {
500
- const stdoutBytes = Buffer.concat(outBufs);
501
- finish(ok({ stdout: stdoutBytes.toString("utf8"), stderr, exitCode, stdoutBytes: new Uint8Array(stdoutBytes) }));
502
- }
503
- else {
500
+ const held = heldStderr();
501
+ const won = finish(() => {
502
+ if (held)
503
+ recordStderr(held);
504
+ const err = errBuf.result();
505
+ const stderr = markTruncated(err.text, err.droppedBytes);
506
+ if (binary) {
507
+ const stdoutBytes = Buffer.concat(outBufs);
508
+ return ok({ stdout: stdoutBytes.toString("utf8"), stderr, exitCode, stdoutBytes: new Uint8Array(stdoutBytes) });
509
+ }
504
510
  const out = outTail.result();
505
- finish(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode }));
506
- }
511
+ return ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr, exitCode });
512
+ });
513
+ if (won && held)
514
+ emitStderr(held);
507
515
  });
508
516
  });
509
517
  }
@@ -1,6 +1,7 @@
1
1
  import { ExecutionError, FileError, RemoteExecutionError, type ExecutionEnv, type ExecutionEnvFactory, type ExecStreamOptions, type FileInfo, type OutputChunk, type RemoteConnectConfig, type RemoteExecutionEnv, type Result, type SessionToken, type SnapshotId, type VmLifecycleOptions, type WorkspaceHandle, type WriteExpectation, type WriteReceipt } from "@sema-agent/core";
2
2
  import { type DeviceOwner, type DeviceRejectCode, type DeviceRow, type DeviceStore } from "../device-store.js";
3
3
  import type { DeviceEnrollment } from "../device-enrollment.js";
4
+ import { type ExecutionPlacementSeat } from "../execution-placement.js";
4
5
  import type { DeviceWsHub } from "../device-ws-hub.js";
5
6
  import type { DeviceInstructionKind, DeviceOutcomeErrorCode } from "../device-ws-protocol.js";
6
7
  /** `WorkspaceHandle.provider` 与 `REMOTE_EXEC` 词表的那一个词,单点。 */
@@ -57,6 +58,8 @@ export interface DeviceEnvFactoryDeps {
57
58
  homeDir?: string;
58
59
  hub: DeviceWsHub;
59
60
  store: DeviceStore;
61
+ /** 会话店的执行席(绑定的唯一载体与唯一读;装配点交会话店的 `executionPlacement`,与准入链同一只)。工厂只调它的 `read(root)`。 */
62
+ seat: Pick<ExecutionPlacementSeat, "read">;
60
63
  enrollment: DeviceEnrollment;
61
64
  /** 指令级默认超时(ms;`DEVICE_EXEC_TIMEOUT_MS`,§4.7)。core 的 per-command `timeout` 可收紧它。 */
62
65
  execTimeoutMs?: number;
@@ -109,15 +112,17 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
109
112
  * 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
110
113
  */
111
114
  /** `sharesProcessTable`(core 7.57.0,必填):命令跑在注册设备上(设备执行器),不在本进程所在的进程表。 */
115
+ /** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
112
116
  readonly capabilities: {
113
- isolation: boolean;
114
- suspendable: boolean;
115
- canonicalPathAuthoritative: boolean;
116
- sharesProcessTable: boolean;
117
+ isolation: false;
118
+ suspendable: false;
119
+ canonicalPathAuthoritative: true;
120
+ sharesProcessTable: false;
121
+ hostLocalPaths: false;
122
+ externalContentTarget: true;
123
+ io: true;
124
+ keepsReadFloor: false;
117
125
  };
118
- readonly externalContentTarget = true;
119
- /** [ref]:设备路径不在 worker 宿主文件系统上 —— host-path 回执消费方据此拒绝把它当宿主路径呈现。 */
120
- readonly hostLocalPaths = false;
121
126
  cwd: string;
122
127
  /** 可选面(presence-typed,见文件头「铁律」段):构造期按设备声明装配,缺席就是缺席。 */
123
128
  readLink?: (path: string, abortSignal?: AbortSignal) => Promise<Result<string, FileError>>;
@@ -332,11 +337,24 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
332
337
  * `DEVICE_ERROR_CODES` 闭集(装配 / 数据错误取 {@link DEVICE_FACTORY_CONFIG_CODES}:placement 根为空 · 有效工作目录非绝对路径),句子里的 `[码]` 与 `.code` 同值。修前抛裸 `Error`,码只在句子里
333
338
  * (有效工作目录那一只直到 7.135.0 仍是裸 `Error`;钉:本工厂与 {@link effectiveDeviceCwd} 的函数体里没有 `new Error(`)。
334
339
  *
335
- * 🔴 **续跑腿的投递围栏**(core 7.54.0 [ref] r3 / [ref],`ExecutionEnvFactoryContext.resumeWorkspace`):行记下了设备(`resumeWorkspace.deviceId`)⇒
336
- * 本工厂**只**在那台设备上铸 —— placement 根此刻绑的不是它(换绑过)⇒ 抛 typed `device.identity_mismatch`(换绑后赎回的同一个词,零新码),
337
- * 句子点名两台设备;不连另一台、不改道(新设备没有那个工作区)。读当前绑定只为判出这一拒与取属主,铸的设备恒是行记下的那一台。
338
- * 「绑的就是它但现在不在线」与新跑同形:构造成功、`connect()` 回 typed `device.offline`(从不连别的设备)。
339
- * 行没记设备(非设备车道 / 早于该字段的行)⇒ 不声明要兑现的设备,按普通路由走。句柄(`workspaceHandle()`)恒报连接真到达的设备,不抄 `resumeWorkspace`。
340
+ * 🔴 **工厂认设备不认住客**(读口一只 = 会话店执行席 `seat.read(root)`,与解析器 / 换绑门 / 执行放置读面 / 准入链同一只读):按 placement 根读**当前**格,
341
+ * 与这条腿带来的设备句柄 `honor` 比,不同即 typed 拒。判序(一次行读):
342
+ * root = ctx.placementRootSessionId // 空 ⇒ config.placement_root_invalid
343
+ * honor = ctx.resumeWorkspace?.deviceId
344
+ * cell = seat.read(root).cell // 行不在 / 格缺 / mirror / writer(device, ∅) ⇒ device.not_bound(唯一读法 `boundDeviceOf`)
345
+ * honor ≠ undefined ∧ 格上设备 ≠ honor ⇒ device.identity_mismatch(句点名两台,零连接)
346
+ * readDeviceForAdmission(格上设备, 格上属主)
347
+ * 工厂**不**读 `task_run`、不读子代分区行、不按 `ctx.taskId`、不比化身、不比属主(住客谓词只用于写与 judged 复核,不是工厂的读者)。
348
+ * 续跑腿(core 7.54.0 [ref] r3 / [ref],`ExecutionEnvFactoryContext.resumeWorkspace`):行记下了设备 ⇒ 本工厂**只**在那台设备上铸 —— 根此刻绑的不是它(换绑过)
349
+ * ⇒ `device.identity_mismatch`(换绑后赎回的同一个词,零新码);不连另一台、不改道(新设备没有那个工作区)。「绑的就是它但现在不在线」与新跑同形:
350
+ * 构造成功、`connect()` 回 typed `device.offline`(从不连别的设备)。行没记设备(早于该字段的行)⇒ 引擎已命名的「行未记设备 ⇒ 普通路由」臂,按根读当前格。
351
+ * 句柄(`workspaceHandle()`)恒报连接真到达的设备,不抄 `resumeWorkspace`。
352
+ *
353
+ * 🔴 **过渡期(条件句)**:新派后代腿的放置句柄(引擎在 `ExecutionEnvFactoryContext` 上的放置工作区字段,`honor` 的第二来源)**只在本仓所基的引擎版本带它时**
354
+ * 进判序;本仓今天所基的引擎上下文没有这个字段 ⇒ `honor` 只取续跑腿的 `resumeWorkspace`,新派后代腿按根读当前格。本仓**不**注入任何替代见证、不造任何行
355
+ * (修在源头)。敞口(在那个字段到货之前成立):活过父 turn 的后台子代,在根换绑之后新派的腿会落到根当前设备 —— 换绑半边由换绑门的子代活性判关闭(归换绑门那一车),
356
+ * 删除半边另有票。字段到货后的形:`honor = ctx.resumeWorkspace?.deviceId ?? <放置句柄>.deviceId`,放置句柄的 provider 不是本车道 ⇒ 装配拒(`config.placement_root_invalid` 同级)。
357
+ * 本仓任何文本不得宣称工厂「一律按住客定址」。
340
358
  */
341
359
  export declare function deviceExecutionEnvFactory(deps: DeviceEnvFactoryDeps): ExecutionEnvFactory;
342
360
  /**
@@ -1,6 +1,8 @@
1
1
  import { posix } from "node:path";
2
2
  import { ExecutionError, FileError, RemoteExecutionError, } from "@sema-agent/core";
3
3
  import { DEVICE_ERROR_CODES, isDeviceAbsolutePath, } from "../device-store.js";
4
+ import { boundDeviceOf } from "../execution-placement.js";
5
+ import { createCutError, createExecFailure } from "../exec-cut.js";
4
6
  export const DEVICE_PROVIDER = "device";
5
7
  const ok = (value) => ({ ok: true, value });
6
8
  const STREAM_PENDING_MAX_BYTES = 2 * 1024 * 1024;
@@ -126,9 +128,7 @@ export class RemoteDeviceExecutionEnv {
126
128
  homeDir;
127
129
  cdPath = "unknown";
128
130
  withheldEnv = [];
129
- capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false };
130
- externalContentTarget = true;
131
- hostLocalPaths = false;
131
+ capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: true, io: true, keepsReadFloor: false };
132
132
  cwd;
133
133
  readLink;
134
134
  writeFileExclusive;
@@ -227,7 +227,8 @@ export class RemoteDeviceExecutionEnv {
227
227
  }
228
228
  let admitted;
229
229
  try {
230
- admitted = await this.cfg.enrollment.admitSession({ rootSessionId: this.cfg.rootSessionId, requester: this.cfg.owner });
230
+ const placement = await this.cfg.enrollment.readPlacement(this.cfg.rootSessionId);
231
+ admitted = await this.cfg.enrollment.admitSession({ rootSessionId: this.cfg.rootSessionId, placement, requester: this.cfg.owner });
231
232
  }
232
233
  catch (err) {
233
234
  return {
@@ -302,7 +303,7 @@ export class RemoteDeviceExecutionEnv {
302
303
  return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
303
304
  const failure = await this.ensureConnected();
304
305
  if (failure)
305
- return { ok: false, error: new ExecutionError(failure.exec, failure.message) };
306
+ return { ok: false, error: createExecFailure(failure.exec, failure.message) };
306
307
  return undefined;
307
308
  }
308
309
  async fsGate(p) {
@@ -381,15 +382,14 @@ export class RemoteDeviceExecutionEnv {
381
382
  case "env_terminal":
382
383
  return { ok: false, error: new ExecutionError("shell_unavailable", ENV_TERMINAL_MESSAGE) };
383
384
  case "rejected":
384
- return { ok: false, error: new ExecutionError(DEVICE_REJECT_TO_EXEC_ERROR[res.reject.code], rejectText(res.reject)) };
385
+ return { ok: false, error: createExecFailure(DEVICE_REJECT_TO_EXEC_ERROR[res.reject.code], rejectText(res.reject)) };
385
386
  case "never_started":
386
387
  case "settled": {
387
388
  const o = res.outcome;
388
- if (!o.ok) {
389
- return { ok: false, error: new ExecutionError(DEVICE_OUTCOME_TO_EXEC_ERROR[o.errorCode], o.message, undefined, { stdout, stderr }) };
390
- }
389
+ if (!o.ok)
390
+ return { ok: false, error: createExecFailure(DEVICE_OUTCOME_TO_EXEC_ERROR[o.errorCode], o.message, res.kind === "settled" ? { stdout, stderr } : undefined) };
391
391
  if (o.kind === "cancelled") {
392
- return { ok: false, error: new ExecutionError("aborted", `the device reported the command as ${o.state} after a cancel`, undefined, { stdout, stderr }) };
392
+ return { ok: false, error: createCutError({ code: "aborted", message: `the device reported the command as ${o.state} after a cancel` }, { stdout, stderr }) };
393
393
  }
394
394
  if (o.kind !== "exec")
395
395
  return { ok: false, error: new ExecutionError("unknown", `device returned a "${o.kind}" outcome for an exec — protocol violation`) };
@@ -799,17 +799,18 @@ export function deviceExecutionEnvFactory(deps) {
799
799
  throw deviceFactoryRefusal(DEVICE_FACTORY_CONFIG_CODES.PLACEMENT_ROOT_INVALID, `device lane: ExecutionEnvFactoryContext.placementRootSessionId is empty [${DEVICE_FACTORY_CONFIG_CODES.PLACEMENT_ROOT_INVALID}] — ` +
800
800
  "the device placement is keyed on the run tree's placement root; defaulting to the child sessionId would read as a NEW placement (v2 §4.3.2 / core design/380 O1①)");
801
801
  }
802
- const binding = await deps.store.getSessionBinding(rootSessionId);
802
+ const placement = await deps.seat.read(rootSessionId);
803
+ const binding = placement === undefined ? undefined : boundDeviceOf(placement.cell);
803
804
  if (!binding) {
804
805
  throw deviceFactoryRefusal(DEVICE_ERROR_CODES.NOT_BOUND, `device lane: no device binding for placement root ${rootSessionId} [${DEVICE_ERROR_CODES.NOT_BOUND}] — ` +
805
806
  "the submit-time admission chain must bind the root session to a device before a run reaches the factory (v2 §4.3.2)");
806
807
  }
807
- const recordedDevice = ctx.resumeWorkspace?.deviceId;
808
- if (recordedDevice !== undefined && recordedDevice !== binding.deviceId) {
809
- throw deviceFactoryRefusal(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `device lane: this resume rebuilds the workspace on device ${recordedDevice}, but placement root ${rootSessionId} is now bound to ${binding.deviceId} [${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] — ` +
808
+ const honor = ctx.resumeWorkspace?.deviceId;
809
+ if (honor !== undefined && honor !== binding.id) {
810
+ throw deviceFactoryRefusal(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `device lane: this resume rebuilds the workspace on device ${honor}, but placement root ${rootSessionId} is now bound to ${binding.id} [${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] — ` +
810
811
  "a parked call does not follow a rebind (the new device has no copy of that workspace); finish or cancel the parked task, then start it again on the device now bound");
811
812
  }
812
- const view = await deps.store.readDeviceForAdmission(binding.deviceId, binding.owner);
813
+ const view = await deps.store.readDeviceForAdmission(binding.id, binding.owner);
813
814
  if (!view) {
814
815
  throw deviceFactoryRefusal(DEVICE_ERROR_CODES.NOT_FOUND, `device lane: the bound device is gone or not owned by the binding's principal [${DEVICE_ERROR_CODES.NOT_FOUND}]`);
815
816
  }
@@ -821,13 +822,13 @@ export function deviceExecutionEnvFactory(deps) {
821
822
  hub: deps.hub,
822
823
  enrollment: deps.enrollment,
823
824
  rootSessionId,
824
- deviceId: binding.deviceId,
825
+ deviceId: binding.id,
825
826
  owner: binding.owner,
826
827
  cwd: effectiveDeviceCwd(deps.perSessionCwd?.get(ctx.sessionId), ctx.parentCwd, view.row),
827
828
  ...(deps.homeDir === undefined ? {} : { homeDir: deps.homeDir }),
828
829
  ...(deps.perSessionShellEnv?.get(ctx.sessionId) === undefined ? {} : { shellEnv: deps.perSessionShellEnv.get(ctx.sessionId) }),
829
830
  execTimeoutMs,
830
- supports: deps.hub.declaredSupports(binding.deviceId),
831
+ supports: deps.hub.declaredSupports(binding.id),
831
832
  ...(deps.logger === undefined ? {} : { logger: deps.logger }),
832
833
  });
833
834
  };
@@ -136,11 +136,16 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
136
136
  /** [ref] §9: E2B is a FULL RemoteExecutionEnv — an isolated, snapshot-suspendable microVM. (SSH/ADB
137
137
  * adapters declare {isolation:false, suspendable:false} — real machines/devices, not snapshotable.) */
138
138
  /** `sharesProcessTable`(core 7.57.0,必填):命令跑在云沙箱里,不在本进程所在的进程表。 */
139
+ /** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
139
140
  readonly capabilities: {
140
- isolation: boolean;
141
- suspendable: boolean;
142
- canonicalPathAuthoritative: boolean;
143
- sharesProcessTable: boolean;
141
+ isolation: true;
142
+ suspendable: true;
143
+ canonicalPathAuthoritative: true;
144
+ sharesProcessTable: false;
145
+ hostLocalPaths: false;
146
+ externalContentTarget: false;
147
+ io: true;
148
+ keepsReadFloor: false;
144
149
  };
145
150
  workspaceHandle(): WorkspaceHandle;
146
151
  /**
@@ -254,7 +259,14 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
254
259
  abortSignal?: AbortSignal;
255
260
  }): Promise<Result<string[], FileError>>;
256
261
  readBinaryFile(p: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>>;
262
+ /** 暂存写的两只面(规则与提交脚本的唯一属主 = `remote-shell.ts` `writeFileStaged` / `appendFileStaged`;本腿是原生 SDK 腿,与四条
263
+ * shell 腿共用的是这只编排,不是 `posix-shell-fs` 基座;父目录与私有暂存目录由编排的准备那一步建):上传 = `files.write` 整份写到私有目录里的
264
+ * 暂存件;提交 = 一次 `exec`。两者同一身份(`files.write` 与 `commands.run` 都不带 `user`,沙箱侧取同一个缺省用户)。 */
265
+ private stagedLane;
266
+ /** 整旧或整新(暂存 → 一次 `mv -f`)。修前 `files.write` 直落目标:上传中途断 / 取消时目标剩什么取决于沙箱侧的落地实现,本适配器不依赖它。 */
257
267
  writeFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
268
+ /** 追加(暂存要加的字节 → 一次 `cat >>`;inode 不换、已有字节不重写)。修前 = 读旧 + 拼 + 整份 `files.write`(O(文件);中途断 ⇒
269
+ * 半份)。目录不收新名字 / 私有目录收不成私有时的退路 = 旧形(`rewriteAppend`:读旧 + 拼 + 整份 `files.write` 到目标;只有写那一步经 `ctx.inPlace`)。 */
258
270
  appendFile(p: string, content: string | Uint8Array, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
259
271
  fileInfo(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>>;
260
272
  listDir(p: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
@@ -340,6 +352,8 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
340
352
  private tempResult;
341
353
  private toFileInfo;
342
354
  private fileError;
355
+ /** A thrown provider error → this exec's error. The two cuts (aborted, the provider's own command deadline) are a command that RAN: they carry what it had
356
+ * written (`captured`, `createCutError`); an unclassified fault does not (its outcome is unknown, the engine reads no partial output from it). */
343
357
  private execError;
344
358
  private remoteError;
345
359
  }
@@ -2,8 +2,9 @@ import path from "node:path";
2
2
  import { Sandbox, CommandExitError, FileType } from "e2b";
3
3
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
4
4
  import { fileErrorFromExec } from "./remote-env-file-error.js";
5
- import { numEnvOr, runSymlinkChain, shellQuote } from "./remote-shell.js";
5
+ import { appendFileStaged, rewriteAppend, numEnvOr, runSymlinkChain, shellQuote, writeFileStaged } from "./remote-shell.js";
6
6
  import { buildLivenessCut, commandBoundsOf, firstBoundOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
7
+ import { createCutError } from "../exec-cut.js";
7
8
  import { BackgroundShellManager } from "./background-shell-support.js";
8
9
  const PROVIDER = "e2b";
9
10
  const DEFAULT_MOUNT_PATH = "/home/user";
@@ -46,7 +47,7 @@ export class RemoteContainerExecutionEnv {
46
47
  this.homeDir = config.homeDir;
47
48
  this.sandboxApi = deps.sandboxApi ?? Sandbox;
48
49
  }
49
- capabilities = { isolation: true, suspendable: true, canonicalPathAuthoritative: true, sharesProcessTable: false };
50
+ capabilities = { isolation: true, suspendable: true, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false };
50
51
  workspaceHandle() {
51
52
  if (!this.handle) {
52
53
  throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
@@ -251,6 +252,8 @@ export class RemoteContainerExecutionEnv {
251
252
  stderr: { tail: new RollingTailBuffer(), total: 0, cursor: 0 },
252
253
  }
253
254
  : undefined;
255
+ let started;
256
+ const captured = () => ({ stdout: started?.stdout ?? "", stderr: started?.stderr ?? "" });
254
257
  let detached = false;
255
258
  const teePush = (s, d) => {
256
259
  const b = Buffer.from(d, "utf8");
@@ -299,10 +302,10 @@ export class RemoteContainerExecutionEnv {
299
302
  this.settleAbandonedStart(starting);
300
303
  const cut = e instanceof RemoteExecutionError && e.code === "timeout" && !options?.abortSignal?.aborted;
301
304
  if (!cut)
302
- return { ok: false, error: this.execError(e, options?.abortSignal) };
303
- const bc = boundCut(first);
304
- return { ok: false, error: new ExecutionError(bc.code, bc.message, undefined, undefined, bc.noOutputSec) };
305
+ return { ok: false, error: this.execError(e, options?.abortSignal, captured()) };
306
+ return { ok: false, error: createCutError(boundCut(first), captured()) };
305
307
  }
308
+ started = handle;
306
309
  const onAbort = () => { if (!detached)
307
310
  void handle.kill().catch(() => { }); };
308
311
  if (options?.abortSignal) {
@@ -352,6 +355,7 @@ export class RemoteContainerExecutionEnv {
352
355
  disarmed: () => detached,
353
356
  ...(tee ? { wall: { totalMs: this.commandTimeoutMs(options?.timeout), leftMs: Math.max(0, this.commandTimeoutMs(options?.timeout) - (Date.now() - startedAt)) } } : {}),
354
357
  liveness: bounds.liveness,
358
+ captured,
355
359
  });
356
360
  const winner = await (detachedP ? Promise.race([fg, detachedP]) : fg);
357
361
  if (winner === "detached") {
@@ -365,18 +369,18 @@ export class RemoteContainerExecutionEnv {
365
369
  });
366
370
  }
367
371
  if (options?.abortSignal?.aborted && !detached) {
368
- return { ok: false, error: new ExecutionError("aborted", "exec aborted (remote process killed)") };
372
+ return { ok: false, error: createCutError({ code: "aborted", message: "exec aborted (remote process killed)" }, captured()) };
369
373
  }
370
374
  return ok({ stdout: winner.stdout, stderr: winner.stderr, exitCode: winner.exitCode });
371
375
  }
372
376
  catch (e) {
373
377
  if (options?.abortSignal?.aborted && !detached) {
374
- return { ok: false, error: this.execError(e, options.abortSignal) };
378
+ return { ok: false, error: this.execError(e, options.abortSignal, captured()) };
375
379
  }
376
380
  if (e instanceof CommandExitError) {
377
381
  return ok({ stdout: e.stdout, stderr: e.stderr, exitCode: e.exitCode });
378
382
  }
379
- return { ok: false, error: this.execError(e, options?.abortSignal) };
383
+ return { ok: false, error: this.execError(e, options?.abortSignal, captured()) };
380
384
  }
381
385
  finally {
382
386
  fgSettled = true;
@@ -659,37 +663,33 @@ export class RemoteContainerExecutionEnv {
659
663
  return { ok: false, error: this.fileError(e, this.resolve(p), abortSignal) };
660
664
  }
661
665
  }
666
+ stagedLane(sbx, content) {
667
+ const data = typeof content === "string" ? content : toArrayBuffer(content);
668
+ return {
669
+ upload: async (dest, signal) => {
670
+ try {
671
+ await this.rpc("files.write", (s) => sbx.files.write(dest, data, { signal: s }), signal, this.cfg.dataTimeoutMs);
672
+ return ok(undefined);
673
+ }
674
+ catch (e) {
675
+ return { ok: false, error: this.fileError(e, dest, signal) };
676
+ }
677
+ },
678
+ run: (cmd, signal) => this.exec(cmd, { cwd: "/", ...(signal !== undefined ? { abortSignal: signal } : {}) }),
679
+ };
680
+ }
662
681
  async writeFile(p, content, abortSignal) {
663
682
  const sbx = await this.fsReady(p);
664
683
  if (!sbx.ok)
665
684
  return sbx;
666
- try {
667
- const data = typeof content === "string" ? content : toArrayBuffer(content);
668
- await this.rpc("files.write", (signal) => sbx.value.files.write(this.resolve(p), data, { signal }), abortSignal, this.cfg.dataTimeoutMs);
669
- return ok(undefined);
670
- }
671
- catch (e) {
672
- return { ok: false, error: this.fileError(e, this.resolve(p), abortSignal) };
673
- }
685
+ return writeFileStaged(this.stagedLane(sbx.value, content), this.resolve(p), abortSignal);
674
686
  }
675
687
  async appendFile(p, content, abortSignal) {
688
+ const sbx = await this.fsReady(p);
689
+ if (!sbx.ok)
690
+ return sbx;
676
691
  const abs = this.resolve(p);
677
- const existing = await this.readBinaryFile(abs, abortSignal);
678
- let base;
679
- if (existing.ok) {
680
- base = existing.value;
681
- }
682
- else if (existing.error.code === "not_found") {
683
- base = new Uint8Array(0);
684
- }
685
- else {
686
- return existing;
687
- }
688
- const add = typeof content === "string" ? new TextEncoder().encode(content) : content;
689
- const merged = new Uint8Array(base.length + add.length);
690
- merged.set(base, 0);
691
- merged.set(add, base.length);
692
- return this.writeFile(abs, merged, abortSignal);
692
+ return appendFileStaged(this.stagedLane(sbx.value, content), abs, abortSignal, (ctx) => rewriteAppend((signal) => this.readBinaryFile(abs, signal), (bytes, signal) => this.stagedLane(sbx.value, bytes).upload(abs, signal), content, ctx));
693
693
  }
694
694
  async fileInfo(p, abortSignal) {
695
695
  const sbx = await this.fsReady(p);
@@ -851,16 +851,15 @@ export class RemoteContainerExecutionEnv {
851
851
  async waitWithLiveness(handle, lastProgress, opts) {
852
852
  let timer;
853
853
  let wallTimer;
854
- const liveness = opts?.liveness;
854
+ const liveness = opts.liveness;
855
855
  const idle = new Promise((_, reject) => {
856
856
  const arm = (bound) => {
857
857
  timer = setTimeout(() => {
858
- if (opts?.disarmed?.())
858
+ if (opts.disarmed?.())
859
859
  return;
860
860
  const idleFor = Date.now() - lastProgress();
861
861
  if (idleFor >= bound.ms) {
862
- const cut = buildLivenessCut(bound);
863
- reject(new ExecutionError(cut.code, cut.message, undefined, undefined, cut.noOutputSec));
862
+ reject(createCutError(buildLivenessCut(bound), opts.captured()));
864
863
  void handle.kill().catch(() => { });
865
864
  this.cfg.metrics?.inc("remote_env_exec_idle_timeout_total", { provider: PROVIDER });
866
865
  }
@@ -871,18 +870,18 @@ export class RemoteContainerExecutionEnv {
871
870
  };
872
871
  if (liveness !== undefined)
873
872
  arm(liveness);
874
- if (opts?.wall !== undefined) {
873
+ if (opts.wall !== undefined) {
875
874
  const wall = opts.wall;
876
875
  wallTimer = setTimeout(() => {
877
876
  if (opts.disarmed?.())
878
877
  return;
879
- reject(new ExecutionError("timeout", wallCutMessage(wall.totalMs)));
878
+ reject(createCutError({ code: "timeout", message: wallCutMessage(wall.totalMs) }, opts.captured()));
880
879
  void handle.kill().catch(() => { });
881
880
  }, wall.leftMs);
882
881
  }
883
882
  });
884
883
  try {
885
- return await Promise.race([opts?.waitP ?? handle.wait(), idle]);
884
+ return await Promise.race([opts.waitP ?? handle.wait(), idle]);
886
885
  }
887
886
  finally {
888
887
  if (timer)
@@ -953,15 +952,15 @@ export class RemoteContainerExecutionEnv {
953
952
  return new FileError("is_directory", msg, p);
954
953
  return new FileError("unknown", msg, p);
955
954
  }
956
- execError(e, abortSignal) {
955
+ execError(e, abortSignal, captured) {
957
956
  if (e instanceof ExecutionError)
958
957
  return e;
959
958
  const msg = errMsg(e);
960
959
  const name = e?.name;
961
960
  if (abortSignal?.aborted || name === "AbortError")
962
- return new ExecutionError("aborted", msg);
961
+ return createCutError({ code: "aborted", message: msg }, captured);
963
962
  if (providerTimedOut(e))
964
- return new ExecutionError("timeout", msg);
963
+ return createCutError({ code: "timeout", message: msg }, captured);
965
964
  return new ExecutionError("unknown", msg, e instanceof Error ? e : undefined);
966
965
  }
967
966
  remoteError(e, code = "unknown") {
@@ -18,6 +18,8 @@
18
18
  import { FileError, type ExecutionError, type RemoteExecutionError } from "@sema-agent/core";
19
19
  /** exec 层的错误码 → 文件层的码。只映射**有对应语义**的那些,其余落 `unknown`(诚实降级)。 */
20
20
  export declare function fileErrorFromExec(e: ExecutionError | RemoteExecutionError, path?: string): FileError;
21
+ /** POSIX 的一个名字段上限(NAME_MAX,字节;Linux / macOS / 常见文件系统同值)。也是远端写的私有暂存目录名的上限(`remote-shell.ts` `buildStagingPaths`)。 */
22
+ export declare const NAME_MAX_BYTES = 255;
21
23
  /**
22
24
  * core 7.56.0 [ref]:`ENAMETOOLONG` 的**一只读法**(本文件的 stderr 分类器与 host 车道的 errno 映射同调)—— 路径有一段超过 NAME_MAX(按 UTF-8 字节量)⇒
23
25
  * `name_too_long`:没有目录装得下这个名字,引擎读站把它读作缺席(与 `not_found` 同读);段都短、只是整条超 PATH_MAX ⇒ `unknown`(那种文件可能存在,从下层目录够得着)。
@@ -3,7 +3,7 @@ export function fileErrorFromExec(e, path) {
3
3
  const code = e.code === "aborted" ? "aborted" : "unknown";
4
4
  return new FileError(code, e.message, path, e);
5
5
  }
6
- const NAME_MAX_BYTES = 255;
6
+ export const NAME_MAX_BYTES = 255;
7
7
  export function nameTooLongCode(p) {
8
8
  return p !== undefined && p.split(/[\\/]/).some((part) => Buffer.byteLength(part, "utf8") > NAME_MAX_BYTES) ? "name_too_long" : "unknown";
9
9
  }