@sema-agent/server 7.125.1 → 7.126.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 (248) hide show
  1. package/MIGRATION.md +195 -2
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +263 -140
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +47 -0
  7. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +5 -1
  8. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  9. package/deploy/sema-up/chart/templates/server.yaml +1 -1
  10. package/deploy/sema-up/chart/values.yaml +12 -0
  11. package/deploy/sema-up/cluster-up.sh +10 -10
  12. package/deploy/sema-up/kube-up.sh +11 -10
  13. package/deploy/sema-up/lib/dataroot.sh +1 -1
  14. package/deploy/sema-up/lib/envf.sh +5 -5
  15. package/deploy/sema-up/lib/flagshape.sh +5 -5
  16. package/deploy/sema-up/lib/lanes.py +1 -1
  17. package/deploy/sema-up/lib/parse-machines.py +2 -2
  18. package/deploy/sema-up/lib/shell-access.sh +1 -1
  19. package/deploy/sema-up/preflight.sh +4 -4
  20. package/deploy/sema-up/sema-up.sh +33 -33
  21. package/deploy/sema-up/smoke.sh +3 -3
  22. package/deploy/sema-up/stack/compose.yaml +1 -1
  23. package/deploy/sema-up/stack/sema-stack-cluster.yaml +6 -2
  24. package/dist/approval-card.d.ts +20 -4
  25. package/dist/approval-content-kind.d.ts +38 -22
  26. package/dist/approval-content-kind.js +20 -4
  27. package/dist/audit.js +2 -0
  28. package/dist/bench/s1/arms.d.ts +9 -37
  29. package/dist/bench/s1/arms.js +4 -61
  30. package/dist/bench/s1/live-deps.d.ts +4 -10
  31. package/dist/bench/s1/live-deps.js +1 -222
  32. package/dist/bench/s1/oracle.d.ts +1 -1
  33. package/dist/bench/s1/reviewer.d.ts +1 -1
  34. package/dist/bench/s1/row.d.ts +5 -7
  35. package/dist/bench/s1/row.js +0 -2
  36. package/dist/bench/s1/run-firm.d.ts +2 -2
  37. package/dist/bench/s1/run-firm.js +1 -4
  38. package/dist/bench/s1/runner-ctx.d.ts +17 -29
  39. package/dist/bench/s1/runner-ctx.js +3 -7
  40. package/dist/bench/s1/tasks.d.ts +9 -46
  41. package/dist/bench/s1/tasks.js +4 -63
  42. package/dist/boot/execution-env.d.ts +38 -26
  43. package/dist/boot/execution-env.js +71 -38
  44. package/dist/boot/memory-boundary.d.ts +1 -1
  45. package/dist/boot/parked-revive-gate.d.ts +0 -3
  46. package/dist/boot/parked-revive-gate.js +2 -2
  47. package/dist/boot/resolve-spec.d.ts +0 -2
  48. package/dist/boot/resolve-spec.js +12 -17
  49. package/dist/boot/retention-lane.d.ts +12 -1
  50. package/dist/boot/retention-lane.js +2 -0
  51. package/dist/boot/runner-deps.d.ts +5 -5
  52. package/dist/boot/runner-deps.js +5 -16
  53. package/dist/boot/session-faces.d.ts +5 -1
  54. package/dist/boot/session-faces.js +37 -1
  55. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  56. package/dist/boot/stage-03-store-backend.js +2 -2
  57. package/dist/boot/stage-04-stores.d.ts +1 -0
  58. package/dist/boot/stage-05-execution-env.d.ts +2 -2
  59. package/dist/boot/stage-05-execution-env.js +7 -4
  60. package/dist/boot/stage-06-runners.d.ts +2 -3
  61. package/dist/boot/stage-06-runners.js +8 -7
  62. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  63. package/dist/boot/stage-07-capability-layer.js +3 -4
  64. package/dist/boot/stage-08-reapers.d.ts +2 -3
  65. package/dist/boot/stage-08-reapers.js +2 -2
  66. package/dist/boot/stage-09-leader.d.ts +2 -3
  67. package/dist/boot/stage-09-leader.js +2 -1
  68. package/dist/boot/stage-10-http-server.d.ts +2 -3
  69. package/dist/boot/stage-10-http-server.js +2 -2
  70. package/dist/boot/stores.d.ts +1 -0
  71. package/dist/boot/stores.js +4 -3
  72. package/dist/boot/web-admin.d.ts +5 -0
  73. package/dist/boot/web-admin.js +28 -14
  74. package/dist/brain.d.ts +2 -2
  75. package/dist/brain.js +1 -1
  76. package/dist/capabilities/builtin-tools.d.ts +2 -1
  77. package/dist/capabilities/composite-members.d.ts +1 -1
  78. package/dist/capabilities/composite-members.js +1 -1
  79. package/dist/capabilities/hands-lane.d.ts +9 -6
  80. package/dist/capabilities/hands-lane.js +2 -2
  81. package/dist/capabilities/memory-notice.d.ts +27 -22
  82. package/dist/capabilities/memory-notice.js +15 -17
  83. package/dist/capabilities/scenarios.d.ts +9 -14
  84. package/dist/capabilities/scenarios.js +5 -50
  85. package/dist/capability-facts.d.ts +22 -11
  86. package/dist/capability-facts.js +13 -6
  87. package/dist/config-catalog.js +11 -4
  88. package/dist/config-provider.d.ts +5 -1
  89. package/dist/config-provider.js +23 -4
  90. package/dist/config-types.d.ts +14 -1
  91. package/dist/config.js +39 -6
  92. package/dist/deployment-governance.d.ts +19 -21
  93. package/dist/deployment-governance.js +7 -8
  94. package/dist/env-name-allowlist-knobs.d.ts +5 -5
  95. package/dist/env-name-allowlist-knobs.js +1 -0
  96. package/dist/execution-lane-caps.js +2 -2
  97. package/dist/hooks/hook-runner.d.ts +84 -30
  98. package/dist/hooks/hook-runner.js +74 -41
  99. package/dist/http/admission.js +12 -9
  100. package/dist/http/durable-run-leg.d.ts +5 -4
  101. package/dist/http/durable-run-leg.js +1 -1
  102. package/dist/http/idempotency.d.ts +28 -6
  103. package/dist/http/idempotency.js +10 -2
  104. package/dist/http/principal-gate.d.ts +14 -0
  105. package/dist/http/principal-gate.js +7 -0
  106. package/dist/http/request-guards.d.ts +2 -1
  107. package/dist/http/request-guards.js +14 -14
  108. package/dist/http/resume-legs.d.ts +2 -2
  109. package/dist/http/resume-legs.js +5 -4
  110. package/dist/http/route-ctx.d.ts +8 -6
  111. package/dist/http/routes/a2a-serve.js +11 -7
  112. package/dist/http/routes/approvals-assistant.js +18 -3
  113. package/dist/http/routes/background.d.ts +1 -1
  114. package/dist/http/routes/background.js +1 -1
  115. package/dist/http/routes/capabilities.js +10 -2
  116. package/dist/http/routes/mcp-reconnect.js +1 -1
  117. package/dist/http/routes/runs.d.ts +4 -3
  118. package/dist/http/routes/runs.js +12 -13
  119. package/dist/http/routes/session-sync.js +14 -11
  120. package/dist/http/routes/sessions.js +9 -9
  121. package/dist/http/routes/side-query.js +6 -19
  122. package/dist/http/routes/tasks.js +12 -11
  123. package/dist/http/server.d.ts +4 -0
  124. package/dist/http/server.js +2 -2
  125. package/dist/http/submit-replay.d.ts +15 -8
  126. package/dist/http/submit-replay.js +6 -0
  127. package/dist/http/wire-types.d.ts +7 -9
  128. package/dist/index.d.ts +0 -1
  129. package/dist/index.js +0 -1
  130. package/dist/internal-run-env.d.ts +30 -0
  131. package/dist/internal-run-env.js +11 -0
  132. package/dist/leader/wire.js +3 -2
  133. package/dist/memory-scope.d.ts +19 -7
  134. package/dist/memory-scope.js +25 -8
  135. package/dist/observability/fail-open.d.ts +15 -3
  136. package/dist/observability/fail-open.js +15 -3
  137. package/dist/observability/metrics.d.ts +0 -1
  138. package/dist/observability/metrics.js +0 -3
  139. package/dist/observability/run-terminal.js +69 -4
  140. package/dist/permission-rule-vocab.d.ts +3 -2
  141. package/dist/permission-rule-vocab.js +1 -1
  142. package/dist/plan-cache-probe.d.ts +3 -2
  143. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  144. package/dist/plugins/caching-session-store.d.ts +3 -0
  145. package/dist/plugins/caching-session-store.js +3 -0
  146. package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
  147. package/dist/plugins/checkpoint-store-sql.js +57 -11
  148. package/dist/plugins/file-run-store.d.ts +25 -21
  149. package/dist/plugins/file-run-store.js +70 -34
  150. package/dist/plugins/local-checkpoint-store.d.ts +8 -5
  151. package/dist/plugins/local-checkpoint-store.js +5 -15
  152. package/dist/plugins/local-session-store.d.ts +66 -7
  153. package/dist/plugins/local-session-store.js +168 -101
  154. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
  155. package/dist/plugins/memory-run-store.d.ts +19 -3
  156. package/dist/plugins/memory-run-store.js +38 -3
  157. package/dist/plugins/one-time-migrations.d.ts +141 -39
  158. package/dist/plugins/one-time-migrations.js +32 -143
  159. package/dist/plugins/pg-pool.js +11 -1
  160. package/dist/plugins/pg-session-storage.d.ts +2 -0
  161. package/dist/plugins/pg-session-storage.js +9 -5
  162. package/dist/plugins/remote-env-adb.d.ts +3 -0
  163. package/dist/plugins/remote-env-adb.js +1 -0
  164. package/dist/plugins/remote-env-device.d.ts +3 -0
  165. package/dist/plugins/remote-env-device.js +1 -0
  166. package/dist/plugins/remote-env-e2b.d.ts +4 -0
  167. package/dist/plugins/remote-env-e2b.js +2 -1
  168. package/dist/plugins/remote-env-host.d.ts +12 -0
  169. package/dist/plugins/remote-env-host.js +11 -1
  170. package/dist/plugins/remote-env-k8s.d.ts +227 -39
  171. package/dist/plugins/remote-env-k8s.js +450 -75
  172. package/dist/plugins/remote-env-local-docker.d.ts +4 -0
  173. package/dist/plugins/remote-env-local-docker.js +2 -1
  174. package/dist/plugins/remote-env-ssh.d.ts +3 -0
  175. package/dist/plugins/remote-env-ssh.js +1 -0
  176. package/dist/plugins/retention-store-sql.d.ts +4 -1
  177. package/dist/plugins/retention-store-sql.js +11 -9
  178. package/dist/plugins/run-store-sql.d.ts +44 -6
  179. package/dist/plugins/run-store-sql.js +92 -27
  180. package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
  181. package/dist/plugins/sql-driver.d.ts +2 -1
  182. package/dist/plugins/store-backend.js +1 -1
  183. package/dist/plugins/store-contracts.d.ts +17 -3
  184. package/dist/plugins/tidb-pool.js +15 -3
  185. package/dist/plugins/tidb-session-storage.js +3 -2
  186. package/dist/plugins/tidb-session-store.d.ts +2 -0
  187. package/dist/plugins/tidb-session-store.js +6 -2
  188. package/dist/request-key-closure.d.ts +12 -0
  189. package/dist/request-key-closure.js +3 -3
  190. package/dist/router/route-orchestration.d.ts +3 -11
  191. package/dist/router/route-orchestration.js +0 -4
  192. package/dist/rule-entry-list.js +0 -4
  193. package/dist/rules-consent.js +3 -0
  194. package/dist/run-local.d.ts +5 -4
  195. package/dist/run-local.js +23 -17
  196. package/dist/runs.js +1 -0
  197. package/dist/security.d.ts +7 -0
  198. package/dist/security.js +5 -0
  199. package/dist/server-secret-env.d.ts +10 -4
  200. package/dist/server-secret-env.js +4 -1
  201. package/dist/session-incarnation.d.ts +46 -2
  202. package/dist/session-incarnation.js +14 -3
  203. package/dist/session-sync-kernel.d.ts +10 -3
  204. package/dist/spec-fields.d.ts +24 -8
  205. package/dist/spec-fields.js +21 -2
  206. package/dist/task-workflow.d.ts +6 -4
  207. package/dist/task-workflow.js +3 -2
  208. package/dist/tool-approval.d.ts +78 -6
  209. package/dist/tool-approval.js +7 -3
  210. package/dist/trace/core-keyset-guard.d.ts +4 -4
  211. package/dist/trace/engine-notice-wire.d.ts +26 -1
  212. package/dist/trace/engine-notice-wire.js +7 -2
  213. package/dist/trace/project.d.ts +8 -3
  214. package/dist/trace/project.js +15 -1
  215. package/dist/trace/self-minted-wire.d.ts +104 -2
  216. package/dist/trace/self-minted-wire.js +35 -0
  217. package/dist/web-admin-client/apply-effective.d.ts +13 -7
  218. package/dist/web-admin-client/apply-effective.js +11 -26
  219. package/dist/web-admin-client/apply-ledger.d.ts +2 -2
  220. package/dist/web-admin-client/candidate-gate.d.ts +39 -4
  221. package/dist/web-admin-client/candidate-gate.js +9 -2
  222. package/dist/web-admin-client/facade.d.ts +2 -2
  223. package/dist/web-admin-client/http-client.d.ts +3 -1
  224. package/dist/web-admin-client/http-client.js +4 -3
  225. package/dist/web-admin-client/read-warnings.d.ts +33 -5
  226. package/dist/web-admin-client/read-warnings.js +41 -6
  227. package/dist/web-admin-client/types.d.ts +11 -5
  228. package/dist/workspace-cache.d.ts +45 -0
  229. package/dist/workspace-cache.js +30 -0
  230. package/dist/workspace-space-commit.d.ts +116 -0
  231. package/dist/workspace-space-commit.js +117 -0
  232. package/dist/workspace-space.d.ts +75 -9
  233. package/dist/workspace-space.js +52 -1
  234. package/package.json +4 -4
  235. package/dist/boot/session-incarnation-seat.d.ts +0 -41
  236. package/dist/boot/session-incarnation-seat.js +0 -69
  237. package/dist/capabilities/code-review-council.d.ts +0 -49
  238. package/dist/capabilities/code-review-council.js +0 -111
  239. package/dist/capabilities/collab-wire.d.ts +0 -36
  240. package/dist/capabilities/collab-wire.js +0 -2
  241. package/dist/capabilities/collab-workflows.d.ts +0 -47
  242. package/dist/capabilities/collab-workflows.js +0 -90
  243. package/dist/capabilities/prompts/code-review.d.ts +0 -25
  244. package/dist/capabilities/prompts/code-review.js +0 -39
  245. package/dist/capabilities/prompts/team.d.ts +0 -10
  246. package/dist/capabilities/prompts/team.js +0 -6
  247. package/dist/capabilities/team.d.ts +0 -113
  248. package/dist/capabilities/team.js +0 -70
@@ -88,6 +88,9 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
88
88
  private readonly cfg;
89
89
  /** core `ExecutionEnv.homeDir`([ref]):设备上执行用户的 home。见 `DeviceEnvConfig.homeDir`。 */
90
90
  readonly homeDir?: string | undefined;
91
+ /** core 7.49.0 #1554:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在设备上(设备自己的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
92
+ * 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
93
+ readonly cdPath: "unknown";
91
94
  /**
92
95
  * 🔴 `suspendable: false` 在 core 契约里**同时断言**「工作区在目标上外部持久」(remote-env.ts 的
93
96
  * load-bearing 不变量)—— 设备工作区 = 员工机磁盘,真持久 ✅,park-only 降级腿因此合法可用。
@@ -120,6 +120,7 @@ function assertPosixDevicePlatform(os, deviceId) {
120
120
  export class RemoteDeviceExecutionEnv {
121
121
  cfg;
122
122
  homeDir;
123
+ cdPath = "unknown";
123
124
  capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true };
124
125
  externalContentTarget = true;
125
126
  hostLocalPaths = false;
@@ -100,6 +100,9 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
100
100
  cwd: string;
101
101
  /** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
102
102
  readonly homeDir?: string | undefined;
103
+ /** core 7.49.0 #1554:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在 e2b 沙箱里(模板镜像的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
104
+ * 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
105
+ readonly cdPath: "unknown";
103
106
  private readonly cfg;
104
107
  private readonly sandboxApi;
105
108
  private sbx?;
@@ -129,6 +132,7 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
129
132
  readonly capabilities: {
130
133
  isolation: boolean;
131
134
  suspendable: boolean;
135
+ canonicalPathAuthoritative: boolean;
132
136
  };
133
137
  workspaceHandle(): WorkspaceHandle;
134
138
  /**
@@ -19,6 +19,7 @@ const ok = (value) => ({ ok: true, value });
19
19
  export class RemoteContainerExecutionEnv {
20
20
  cwd;
21
21
  homeDir;
22
+ cdPath = "unknown";
22
23
  cfg;
23
24
  sandboxApi;
24
25
  sbx;
@@ -42,7 +43,7 @@ export class RemoteContainerExecutionEnv {
42
43
  this.homeDir = config.homeDir;
43
44
  this.sandboxApi = deps.sandboxApi ?? Sandbox;
44
45
  }
45
- capabilities = { isolation: true, suspendable: true };
46
+ capabilities = { isolation: true, suspendable: true, canonicalPathAuthoritative: true };
46
47
  workspaceHandle() {
47
48
  if (!this.handle) {
48
49
  throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
@@ -94,6 +94,12 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
94
94
  cwd: string;
95
95
  /** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
96
96
  readonly homeDir?: string | undefined;
97
+ /**
98
+ * core 7.49.0 #1554:shell 子进程**真拿到的** `CDPATH` 条目(`[]` = 没有)—— 删除守卫按它解裸 `cd <dir>`。本车道的命令就是本机子进程,
99
+ * env 是本类自己组合的(继承姿态 + `cfg.env`),所以说得出:读同一只组合({@link childEnv},不披露),切法与 core `NodeExecutionEnv` 同
100
+ * (缺席 / 空串 ⇒ `[]`,否则按 `:` 切)。逐命令的 `options.env` 由引擎自己交,它知道。
101
+ */
102
+ get cdPath(): readonly string[];
97
103
  private readonly workspaceDir;
98
104
  private readonly cfg;
99
105
  /** Workspace created on disk (lazily, on first use). */
@@ -406,7 +412,13 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
406
412
  * source of the shape rule) minus the config catalog's `secret`-typed names (`scrubServerSecrets`, 7.99.0 F7). `"all"` = full inherit (opt-out); a `string[]` = strict allowlist. Legacy `inheritProcessEnv`
407
413
  * maps `true`→`"all"` / `false`→`[]`. */
408
414
  private inheritMode;
415
+ /** 每次 spawn 交给子进程的 env(组合 + 剥除披露)。 */
409
416
  private mergeEnv;
417
+ /**
418
+ * 子进程 env 的**唯一组合**。`disclose` = 这一次是不是真要交给子进程(是 ⇒ scrub 腿经 `hostSubprocessEnv` 记剥除;否 ⇒ 只读组合,
419
+ * 不计数 —— {@link cdPath} 这类「子进程会拿到什么」的读者走这一臂,一次没发生的 spawn 不进剥除账)。
420
+ */
421
+ private childEnv;
410
422
  private fileError;
411
423
  }
412
424
  /**
@@ -10,7 +10,7 @@ import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
10
10
  import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
11
11
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
12
12
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
13
- import { allowlistedEnv, hostSubprocessEnv } from "../server-secret-env.js";
13
+ import { allowlistedEnv, composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
14
14
  import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
15
15
  const PROVIDER = "host";
16
16
  const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
@@ -57,6 +57,10 @@ export class RemoteHostExecutionEnv {
57
57
  }
58
58
  cwd;
59
59
  homeDir;
60
+ get cdPath() {
61
+ const value = this.childEnv(undefined, false).CDPATH;
62
+ return value === undefined || value === "" ? [] : value.split(":");
63
+ }
60
64
  workspaceDir;
61
65
  cfg;
62
66
  prepared = false;
@@ -1308,6 +1312,9 @@ export class RemoteHostExecutionEnv {
1308
1312
  return "scrub";
1309
1313
  }
1310
1314
  mergeEnv(perCommand) {
1315
+ return this.childEnv(perCommand, true);
1316
+ }
1317
+ childEnv(perCommand, disclose) {
1311
1318
  const mode = this.inheritMode();
1312
1319
  let base;
1313
1320
  if (mode === "all")
@@ -1315,6 +1322,9 @@ export class RemoteHostExecutionEnv {
1315
1322
  else if (Array.isArray(mode)) {
1316
1323
  base = allowlistedEnv(mode, process.env);
1317
1324
  }
1325
+ else if (!disclose) {
1326
+ base = composeHostSubprocessEnv([]);
1327
+ }
1318
1328
  else {
1319
1329
  base = hostSubprocessEnv({ site: "server.remote-env-host.inherit-scrub", allow: [] });
1320
1330
  }
@@ -1,4 +1,6 @@
1
- import type { WorkspaceSyncCapability } from "../workspace-space.js";
1
+ import { type SpaceReclaimer, type WorkspaceSpacePolicy, type CacheReclaimReason } from "../workspace-space.js";
2
+ import { type SpaceCommitFence } from "../workspace-space-commit.js";
3
+ import { type WorkspaceCacheCapability } from "../workspace-cache.js";
2
4
  import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll } from "@sema-agent/core";
3
5
  import type { PodSpecPatch } from "./k8s-exec-protocol.js";
4
6
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
@@ -37,8 +39,15 @@ export interface K8sEnvConfig {
37
39
  rpcTimeoutMs?: number;
38
40
  /** Zero-progress liveness bound for commands (no output AND not finished ⇒ hang). */
39
41
  livenessMs?: number;
40
- /** File-transfer bound. */
42
+ /** The ONE data bound (`WORKSPACE_SNAPSHOT_TIMEOUT_MS`; default = `WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT`, owned by
43
+ * `workspace-space.ts`): the snapshot upload's and restore's wall-clock, the deadline this replica's commits write into
44
+ * their space-commit fence row (a reader judges another run's row by the deadline IN that row, never by this field), and
45
+ * a large file transfer's wall-clock. Every use reads this field — never a second number. */
41
46
  dataTimeoutMs?: number;
47
+ /** The snapshot's exclusion table (`WORKSPACE_SNAPSHOT_EXCLUDE`, items already in the closed form — `workspace-space.ts`):
48
+ * the ONE tar upload ({@link RemoteK8sExecutionEnv.uploadSnapshot}, both lanes) passes `--exclude=./<item>` per item.
49
+ * Absent / empty ⇒ the upload command is what it always was. */
50
+ snapshotExclude?: readonly string[];
42
51
  cpu?: string;
43
52
  memory?: string;
44
53
  retry?: {
@@ -58,8 +67,8 @@ export interface K8sEnvConfig {
58
67
  * trifecta. The presigned URL is used only inside an adapter-issued exec at suspend/resume time, so it never
59
68
  * reaches an agent turn.
60
69
  *
61
- * WORKSPACE SPACE (`workspacePersist`, default `off` — opt-in `space`): the same store also carries the workspace ACROSS
62
- * RUNS, in its own sub-prefix `<prefix>/<spaceIncarnation>/space/` — `space/latest` (body = the newest snapshotId) names the
70
+ * WORKSPACE SPACE (`workspacePersist`, default `space`; `off` = a fresh workspace every run): the same store also carries the workspace ACROSS
71
+ * RUNS, in its own sub-prefix `<prefix>/<incarnation>/space/` — `space/latest` (body = the newest snapshotId) names the
63
72
  * snapshot `connect()` restores; `destroy()` snapshots into `space/`, moves `latest`, keeps the newest `workspaceSnapshotKeep`
64
73
  * (the same one upload / one download as suspend / resume, see `SnapshotTarget`). The pointer read / write, the listing and the deletes are
65
74
  * WORKER-side requests to `endpoint` (the snapshot-BLOB offload dials the same `MINIO_ENDPOINT` from the worker).
@@ -74,7 +83,7 @@ export interface K8sEnvConfig {
74
83
  presignTtlSec?: number;
75
84
  /** The workspace-space policy (the capability fact `workspaceSpace`'s witness, handed in by the factory; the adapter
76
85
  * never resolves defaults itself). Absent or `persist: "off"` ⇒ no persistence across runs. */
77
- space?: WorkspaceSyncCapability;
86
+ space?: WorkspaceSpacePolicy;
78
87
  };
79
88
  /**
80
89
  * The PARK lane's partition (`<prefix>/<sessionId>/<snapshotId>.tar.gz`), set by {@link k8sExecutionEnvFactory} from
@@ -84,18 +93,22 @@ export interface K8sEnvConfig {
84
93
  * it never decides who reads. Segment shape is validated in exactly one place ({@link RemoteK8sExecutionEnv.presignSnapshot}).
85
94
  */
86
95
  sessionId?: string;
96
+ /** The SPACE lane's occupant — handed in per run by the factory binding (`boot/execution-env.ts`); see {@link SpaceOccupant}. */
97
+ spaceOccupant?: SpaceOccupant;
87
98
  /**
88
- * The SPACE lane's partition (`<prefix>/<spaceIncarnation>/space/…`): the session OCCUPANT's incarnation token
89
- * (`session-incarnation.ts`, the one owner of "is this the same occupant"), handed in per run by the factory binding
90
- * (`boot/execution-env.ts`) from the same seat the memory engine reads. Why not the session id: the space lane is read
91
- * by whoever runs next in the session, so its partition decides WHO reads — a session id deleted and registered again
92
- * (by anyone) is a new occupant and starts from an empty workspace. Absent while persistence is on ⇒ the space lane
93
- * does not open for this env (connect restores nothing, destroy saves nothing) and one `workspace_space_unavailable`
94
- * line says so — never a fallback to the session id. Never logged whole: operator lines carry `incarnationLogPrefix`.
99
+ * The build-cache volume per workspace space (`WORKSPACE_CACHE=volume`; the capability fact `workspaceCache`'s witness, handed
100
+ * in by the factory — absent for a delegated child, whose env opens no space). Keyed by the SAME space key as the space lane
101
+ * (`spaceOccupant.incarnation`): connect finds / creates the space's claim (`workspaceCacheClaimOf`) and mounts it at `mount`
102
+ * — outside the workspace mount, so no snapshot ever covers it — and layers the cache env closed table under `podEnv`
103
+ * (an explicit operator value of the same name wins). A volume that cannot be had (create refused, not bound within
104
+ * `dataTimeoutMs`, no such session ⇒ no space key) ⇒ the pod starts WITHOUT it and without the cache env: `recordFailOpen` + one
105
+ * `workspace_cache_unavailable` line. An occupant read that FAULTED is not a miss: the run is refused, as on the space lane
106
+ * ({@link SpaceOccupant}). Absent ⇒ no claim request, the pod as before.
95
107
  */
96
- spaceIncarnation?: string;
108
+ cache?: WorkspaceCacheCapability;
97
109
  /** Operator log seat (the e2b adapter's shape): `workspace_snapshot_failed` / `workspace_snapshot_prune_failed` /
98
- * `workspace_space_unavailable` / `workspace_commit_abandoned` / `workspace_commit_marker_stuck`. */
110
+ * `workspace_space_unavailable` / `workspace_space_unreadable_occupant` / `workspace_commit_abandoned` / `workspace_latest_dangling` /
111
+ * `workspace_restore_failed` / `workspace_cache_fresh` / `workspace_cache_unavailable`. */
99
112
  logger?: {
100
113
  warn(event: string, fields?: Record<string, unknown>): void;
101
114
  };
@@ -115,8 +128,10 @@ export interface K8sEnvConfig {
115
128
  podSpecPatch?: PodSpecPatch;
116
129
  /** RFC B5 (sema-internal server/docs/design/ENV-SELECT-AND-REGION-SOURCE.md 抽象点②): operator-trusted env injected into the
117
130
  * sandbox container (e.g. `SEMA_PKG_SOURCE=cn|global` — the runtime package-source selector the image's
118
- * pkg-source.sh hook materializes at start). Deployment-region config, NEVER task-controlled. Absent = the
119
- * pod is byte-identical to today. */
131
+ * pkg-source.sh hook materializes at start). Deployment config, NEVER task-controlled. This is the pod's ONE container-env
132
+ * seat: the region package-source set AND the deployer-named forwarding (`K8S_SANDBOX_ENV` keys, original values) arrive
133
+ * here already merged by the boot assembly (explicit names win). Values go into `containers[0].env[]` and nowhere else —
134
+ * never a command string, the workspace or a snapshot. Absent = the pod is byte-identical to today. */
120
135
  podEnv?: Record<string, string>;
121
136
  }
122
137
  export { type PodSpecPatch, applyPodSpecPatch } from "./k8s-exec-protocol.js";
@@ -150,6 +165,45 @@ export interface K8sEnvDeps {
150
165
  /** Worker-side object-store requests (the space pointer, listing, deletes). Default: global `fetch`. */
151
166
  fetch?: typeof fetch;
152
167
  }
168
+ /**
169
+ * One run's space occupant — the space key of both lanes keyed by it (the space lane, the cache volume) — a closed set of three
170
+ * readings of the session row's cell, read by id from the session store (`SessionStore.incarnationOf`) when the run's env is built:
171
+ * - known: `incarnation` — the partition (`<prefix>/<incarnation>/space/…`): the session OCCUPANT's incarnation token
172
+ * (`session-incarnation.ts`, the one owner of "is this the same occupant"). Why not the session id: the space lane is read by
173
+ * whoever runs next in the session, so its partition decides WHO reads — a session id deleted and registered again (by anyone)
174
+ * is a new occupant and starts from an empty workspace. Never logged whole: operator lines carry `incarnationLogPrefix`;
175
+ * `fence` — this env's seat on the space's commit fence (the run store's `workspace_space_commit` row,
176
+ * `workspace-space-commit.ts`): `connect()` waits on it, the run-end commit holds it;
177
+ * - absent: `unread: "incarnation_unknown"` — there is NO space for this run: the store answered that it has no such session
178
+ * (deleted / never registered), or this replica holds no commit fence to key one by (see `occupantOf`) ⇒ a lane keyed by it
179
+ * does not open (no restore, no save, no volume) and says so in one line;
180
+ * - faulted: `fault` — the read did not answer (it threw, or answered something that is not a token): the code of the store's
181
+ * error (never its text), `incarnation_read_failed` without one. A space we could not read is not "no space" ⇒ a lane keyed by
182
+ * it refuses this env (`RemoteK8sExecutionEnv.provision`, the one pod seat): the run never starts on a workspace it believes is its own.
183
+ * Never a fallback to the session id.
184
+ */
185
+ export type SpaceOccupant = {
186
+ readonly incarnation: string;
187
+ readonly fence: SpaceCommitFence;
188
+ } | {
189
+ readonly unread: "incarnation_unknown";
190
+ } | {
191
+ readonly fault: string;
192
+ };
193
+ /** One failed stage of {@link RemoteK8sExecutionEnv.reclaimSpaceSnapshots}: whose partition (the space's `space/` or the park lane's), listing it, deleting from it,
194
+ * or the reclaim call's deadline passing before or while on it (`budget`: the rest was not attempted). */
195
+ export interface ReclaimFailure {
196
+ readonly lane: "space" | "park";
197
+ readonly stage: "list" | "delete" | "budget";
198
+ readonly detail: string;
199
+ }
200
+ /** The failed cache-volume half of a reclaim ({@link RemoteK8sExecutionEnv.reclaimSpaceCache}); `reason` is the closed word set of
201
+ * `workspace_cache_reclaim_failed` from the reclaim seat: the delete-by-label failed or answered non-2xx (`delete_failed`), or the reclaim call's
202
+ * deadline passed before it was sent or while it was in flight (`budget` — the same word the object half's `stage` uses for the same cause). */
203
+ export interface CacheReclaimFailure {
204
+ readonly reason: Exclude<CacheReclaimReason, "occupant_unreadable">;
205
+ readonly detail: string;
206
+ }
153
207
  export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
154
208
  /** `Error`-or-event → message. Bun's ws shim emits browser-style ErrorEvents (NOT instanceof Error) whose
155
209
  * String() is "[object ErrorEvent]" — that masked the real TLS failure for a whole drill. */
@@ -185,12 +239,24 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
185
239
  private connectPromise?;
186
240
  /** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
187
241
  readonly homeDir?: string | undefined;
242
+ /** core 7.49.0 #1554:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在 pod 里(镜像与 pod spec 的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
243
+ * 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
244
+ readonly cdPath: "unknown";
188
245
  private readonly cfg;
189
246
  private readonly deps;
247
+ /** The known occupant's incarnation (`undefined` = this run has no known occupant: a delegated child, an unread session, a direct construction) — the
248
+ * ownership mark of every snapshot id this env mints, both lanes ({@link mintSnapshotId}); the space lane's partition is the same reading. */
249
+ private readonly occupantIncarnation;
190
250
  /** This env's space lane (resolved once from the policy + the occupant's incarnation; see {@link SpaceLane}). */
191
251
  private readonly spaceLane;
192
252
  /** `workspace_space_unavailable` is said once per env. */
193
253
  private spaceUnavailableSaid;
254
+ /** `workspace_space_unreadable_occupant` is said once per env (a retried lazy connect is refused again, silently). */
255
+ private unreadableOccupantSaid;
256
+ /** This env's cache volume lane (resolved once from the cache policy + the space key; see {@link CacheLane}). */
257
+ private readonly cacheLane;
258
+ /** A cache volume that could not be had is counted and said once per env (a retried connect does not count again). */
259
+ private cacheUnavailableSaid;
194
260
  private readonly logger?;
195
261
  private readonly onWorkspaceSnapshot?;
196
262
  constructor(config: K8sEnvConfig, deps?: K8sEnvDeps);
@@ -208,20 +274,47 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
208
274
  private openExec;
209
275
  /**
210
276
  * Connect = the workspace SPACE's restore point. Space lane open ⇒ first wait out a commit still in flight
211
- * ({@link awaitSpaceCommit}), then read `<prefix>/<incarnation>/space/latest`: present ⇒
212
- * restore it through {@link restoreSnapshot} (the one tar download, shared with `resumeVM`; a missing archive is
213
- * refused); absent (404) ⇒ a fresh pod; a read that FAILS ⇒ a loud
214
- * `connect_failed` — never a silent fresh workspace (a run that believes it resumed while it did not is the worst form).
215
- * Persistence off / no store ⇒ a fresh pod, exactly as before. Persistence on but the occupant unknown ⇒ a fresh pod
216
- * and one `workspace_space_unavailable` line (no restore by any other key).
277
+ * ({@link awaitSpaceCommit}: the run store's commit row), then read `<prefix>/<incarnation>/space/latest`: present ⇒
278
+ * restore it through {@link restoreSnapshot} (the one tar download, shared with `resumeVM`; an archive that is gone ⇒
279
+ * loud `workspace_latest_dangling`, the pointer deleted, an empty workspace); absent (404) ⇒ a fresh pod; a read that
280
+ * FAILS ⇒ a loud `connect_failed` — never a silent fresh workspace (a run that believes it resumed while it did not is
281
+ * the worst form). Once the pod holds the workspace, this env arms its fence seat (its destroy will commit).
282
+ * Persistence off / no store ⇒ a fresh pod, exactly as before. Persistence on but no such session (the occupant absent) ⇒ a
283
+ * fresh pod and one `workspace_space_unavailable` line (no restore by any other key). The occupant read FAULTED ⇒ the same
284
+ * loud `connect_failed` as a failed `latest` read, no pod ({@link provision} refuses it — every entry that would create one).
217
285
  */
218
286
  connect(config?: RemoteConnectConfig): Promise<ConnectResult>;
219
- /** The space lane is on but this env's occupant is unknown — said once per env (at connect, or at a destroy with a pod). */
287
+ /** This env holds a workspace its `destroy()` will commit ⇒ arm its seat on the space's commit fence (from here the
288
+ * run's terminal write fences the space; `workspace-space-commit.ts`). Each write of this run's row sets its deadline to
289
+ * now + the upload's own wall-clock bound (`dataTimeoutMs`). No-op unless the space lane is open. */
290
+ private armSpace;
291
+ /** The env no longer holds a workspace to commit (a commit ended, a park released the pod, or destroy found no pod) ⇒
292
+ * end its fence seat: the row this run wrote, if any, is deleted. A delete that fails leaves the row until its deadline —
293
+ * the next run waits at most that long and says `workspace_commit_abandoned` (the loud form of a stuck row). */
294
+ private endSpace;
295
+ /**
296
+ * One call to the space's commit fence (the run store), bounded like every other request this adapter makes
297
+ * (`rpcTimeoutMs`) and abortable: a store that never answers never hangs connect or destroy. The store call itself is
298
+ * not cancellable — a late answer is dropped; what it may have written dies at its own deadline (the fence's one rule).
299
+ * Rejects with {@link FenceCallAborted} on abort.
300
+ */
301
+ private fenceCall;
302
+ /** The space lane is on but this run's session has no occupant (no such session — {@link SpaceOccupant}) — said once per env
303
+ * (at connect, or at a destroy with a pod). `reason` = the reading's word. */
220
304
  private sayUnavailable;
305
+ /**
306
+ * A lane that is on is keyed by an occupant read that FAULTED (`unreadable`) ⇒ this env creates no pod: the refusal, in the one
307
+ * space-read failure form ({@link spaceReadFailed} — a failed `latest` / commit-row read says the same), naming the knob of every
308
+ * lane that refuses; one `workspace_space_unreadable_occupant{sessionId, code}` line per env. `code` = the store error's code,
309
+ * never its text. Not a knob: there is no honest way to run a space we could not read — a retry of the run reads it again.
310
+ */
311
+ private refuseUnreadableOccupant;
221
312
  /** Every space-lane operator line: the space is named by its incarnation's log prefix only, and free text (a curl
222
313
  * stderr, a fetch error) passes {@link redactIncarnations} — the full token never reaches a log body. */
223
314
  private spaceLine;
224
- /** A fresh pod (create → Running → workspace root), bounded retry. Shared by {@link connect} and {@link resumeVM}. */
315
+ /** A fresh pod (create → Running → workspace root), bounded retry — THE seat every pod of this env is created through
316
+ * ({@link connect} and, via {@link restoreSnapshot}, {@link resumeVM}). A lane keyed by an occupant read that faulted ⇒ refused
317
+ * here, before any request ({@link refuseUnreadableOccupant}): never retried (the reading is this env's, fixed at construction). */
225
318
  private provision;
226
319
  /**
227
320
  * Lazy-connect on first use — 🔴 core NEVER calls `connect()` (verified: the Runner only calls `resumeVM` on
@@ -230,7 +323,38 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
230
323
  * pod create; a failed connect clears the memo (next op retries); `destroyed` is terminal.
231
324
  */
232
325
  private ensureConnected;
326
+ /**
327
+ * One pod creation attempt. With the cache volume open: first the space's claim ({@link attachCache} — found, or created),
328
+ * then the pod carrying it; a pod that only waits on a claim that does not bind within the data bound is deleted and ONE pod
329
+ * without the volume is created in its place (the run starts; the miss is counted). Every other failure is the attempt's.
330
+ */
233
331
  private connectOnce;
332
+ /** The cache volume for this pod creation, or `undefined` (off, or not to be had — counted and said there). */
333
+ private attachCache;
334
+ /** Find the space's claim, or create it (labelled with the space digest; size = the policy's; the cluster's default
335
+ * StorageClass). `fresh` = this call created it. A concurrent creator (409) is a find. Any other answer is a miss. */
336
+ private ensureCacheClaim;
337
+ /** Is the claim bound? (A read that fails counts as not bound — the bind deadline still decides.) */
338
+ private cacheClaimBound;
339
+ /**
340
+ * Reclaim one ended space's cache volume: delete the claims labelled `sema.space=<its digest>` (one deletecollection —
341
+ * a claim still mounted by a terminating pod is kept by the cluster's pvc-protection until the pod is gone). None ⇒ zero
342
+ * action. The request runs under the reclaim call's `deadline` (built once per call by {@link createSpaceReclaimer}): already passed ⇒
343
+ * nothing is sent; passing while the request is in flight ⇒ the request is cut (its own `rpcTimeoutMs` stays as this control-plane request's socket-idle bound; the object half's `storeFetch` bound is a whole-request wall clock — the deadline is what bounds both). Answers the outcome; never throws. Used by {@link createSpaceReclaimer} only (a control-plane env, no pod).
344
+ */
345
+ reclaimSpaceCache(spaceKey: string, deadline: AbortSignal): Promise<{
346
+ ok: true;
347
+ deleted: number;
348
+ } | ({
349
+ ok: false;
350
+ } & CacheReclaimFailure)>;
351
+ /** The one spelling of "the reclaim call's deadline passed" — both halves of the reclaim (volume and objects) say it with this. */
352
+ private reclaimDeadlinePassed;
353
+ /** The cache volume could not be had: counted (`recordFailOpen`) and one operator line — once per env. */
354
+ private cacheUnavailable;
355
+ /** Create one pod (with the cache volume when given) and wait for it to run. A {@link CacheMiss} = a pod carrying the
356
+ * volume did not become ready (its claim unbound past the bind deadline, or bound and still not ready) — already deleted. */
357
+ private createPod;
234
358
  /**
235
359
  * Build a validated, presigned URL for a snapshot object of either lane (see {@link SnapshotTarget}) — the ONE place a
236
360
  * snapshot object key is spelled, and the one place each lane's partition is chosen (park = the session id, space = the
@@ -243,6 +367,30 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
243
367
  * snapshotKey()+s3Presign() pair so the two call sites can't diverge.
244
368
  */
245
369
  private presignSnapshot;
370
+ /** A lane's partition prefix inside the bucket — the ONE place it is spelled (`presignSnapshot` signs under it; the reclaim reads the listing against it).
371
+ * The partition segment is validated by `presignSnapshot` before a URL is signed. */
372
+ private snapshotBase;
373
+ /**
374
+ * Reclaim an ENDED space's snapshot objects (the reclaim seat's half of {@link createSpaceReclaimer}): list the whole `space/` partition
375
+ * of `spaceKey` (archives, `latest`, anything else this adapter ever wrote there) and, when this env is keyed by a session
376
+ * (`cfg.sessionId`), the park lane's `<prefix>/<sessionId>/*.tar.gz` whose names carry the ended occupant's ownership mark (see the two
377
+ * paragraphs below: unmarked / `none` objects stay) — the session is gone, so nothing can ever read either again.
378
+ * Each object is deleted over the same presigned DELETE the prune uses. Never throws: answers the failed stages (an empty answer = all
379
+ * gone, or nothing was there). An object whose name this adapter could not have written, or that sits deeper than the lane's own
380
+ * partition, is left alone. A truncated listing (more than 1000 objects) deletes what it saw and says so.
381
+ * Every LIST and DELETE of both partitions runs under the reclaim call's `deadline` — ONE absolute deadline per call of {@link createSpaceReclaimer}, built at its
382
+ * entry from `dataTimeoutMs` (the snapshot time bound; no knob of its own) and shared by every space of the call and by the cache-volume half; this method builds
383
+ * none of its own. Passed before this space is reached ⇒ nothing is sent; passing on the way ⇒ the request in flight is cut and nothing further is sent. Either
384
+ * way the answer carries one `stage: "budget"` failure for the lane it stopped on: what was not yet deleted stays for the bucket's lifecycle rule. (Each request
385
+ * also keeps its own `rpcTimeoutMs`; the deadline is what bounds their sum.)
386
+ * The park partition is keyed by the SESSION ID, which another tenant may register again once the old row is gone — and a reclaim can run
387
+ * late (a retention sweep walks many sessions). So the park lane deletes only objects whose NAME carries the ended occupant's ownership mark
388
+ * ({@link snapshotNameOwnedBy}; the mark is minted into every snapshot id by {@link mintSnapshotId}, `uploadSnapshot`): a later occupant's
389
+ * objects carry its own mark, and a name with no mark (written before marks existed, or by a run with no occupant) is nobody's to delete here —
390
+ * the bucket's lifecycle rule is its fallback. The judgment reads NO clock (not the object store's stamp, not this replica's). The `space/`
391
+ * partition needs no such guard (its partition is the incarnation, which a new occupant does not share).
392
+ */
393
+ reclaimSpaceSnapshots(spaceKey: string, deadline: AbortSignal): Promise<ReclaimFailure[]>;
246
394
  /** Worker-side object-store request, bounded by the control-plane RPC wall-clock — or a tighter caller deadline — and
247
395
  * the caller's abort. */
248
396
  private storeFetch;
@@ -256,12 +404,20 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
256
404
  * caller deadline tighter than the RPC bound. */
257
405
  private writePointer;
258
406
  /**
259
- * Wait out the previous run's space commit. `space/committing` present and younger than the upload's own wall-clock
260
- * bound (`dataTimeoutMs` — the SAME number `uploadSnapshot` gives its tar + curl exec) ⇒ re-read every
261
- * {@link SPACE_COMMIT_POLL_MS} until it is gone or has aged past that bound. A marker at / past the bound = a commit
262
- * whose worker died inside it (a live upload fails at the bound and clears its marker) ⇒ one
263
- * `workspace_commit_abandoned` line; `latest` still names the previous COMPLETE snapshot. A marker read that fails, or
264
- * a body that is not an epoch-ms timestamp, is a loud `connect_failed` (the same form as a failed `latest` read).
407
+ * Wait out the previous run's space commit — its row in the run store's `workspace_space_commit` (the ONE seat of
408
+ * "a commit of this space is in flight", `workspace-space-commit.ts`; written by that run's terminal write before the
409
+ * session's slot was released, so a next run can never start before it). The row carries its WRITER's deadline; this
410
+ * reader brings no number of its own: alive (`spaceCommitAlive`) ⇒ re-read every {@link SPACE_COMMIT_POLL_MS} until the
411
+ * row is gone. Read dead = a run whose worker died between its terminal write and its destroy (state `pending`), inside
412
+ * the upload, or while moving `latest` — or a writer whose renewal lands at this very moment. So the read alone
413
+ * decides nothing: the reader ABANDONS the row ({@link SpaceCommitFence.abandon} — one locked conditional delete, judged
414
+ * on the store's clock under the row's lock). Deleted ⇒ one `workspace_commit_abandoned{space, remainingMs, state}` line
415
+ * (`remainingMs` ≤ 0: how far past the writer's deadline, as read) and `latest` (still the previous COMPLETE snapshot)
416
+ * is restored; not deleted (renewed / taken / gone) ⇒ re-read at once. A row read or an abandon that fails is a loud
417
+ * `connect_failed` (never a guess that no commit is in flight).
418
+ * How long connect may wait here has no bound of its own (no knob, and NOT this replica's `dataTimeoutMs`): it is the
419
+ * deadline the writer keeps in its row — each of that run's at most three writes (terminal, commit start, publish) sets
420
+ * it to now + the writer's bound — plus the run's abort signal, which ends the wait at once.
265
421
  */
266
422
  private awaitSpaceCommit;
267
423
  /**
@@ -292,7 +448,9 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
292
448
  }>;
293
449
  /**
294
450
  * The ONE tar upload (both lanes): tar the workspace to a pod-local file, presigned PUT into the lane's namespace,
295
- * release the pod. `suspendVM` = lane `park`; the run-end space snapshot = lane `space` (its open space).
451
+ * release the pod. `suspendVM` = lane `park`; the run-end space snapshot = lane `space` (its open space). The exclusion
452
+ * table (`snapshotExclude`) is applied HERE and nowhere else — each item one `--exclude=./<item>` argument through the one
453
+ * `shellQuote`, anchored at the workspace root (`-C <mount> .` names every member `./…`); no table ⇒ the command as it always was.
296
454
  */
297
455
  private uploadSnapshot;
298
456
  /**
@@ -309,12 +467,25 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
309
467
  resumeVM(snapshotId: SnapshotId, options?: VmLifecycleOptions): Promise<ConnectResult>;
310
468
  /**
311
469
  * The ONE tar download (both lanes): provision a FRESH pod, download + untar the lane's archive into the workspace.
312
- * `missing: "empty"` (park: a plan-only suspend has no object) tolerates a 404 as a clean workspace; `missing:
313
- * "refuse"` (space: its pointer is only ever written after a successful upload ⇒ a 404 there is lost data) refuses it.
470
+ * A missing archive (404) starts from the clean workspace on both lanes — what differs is what it means:
471
+ * - park: a plan-only suspend has no object — nothing to say;
472
+ * - space: `latest` dangles (its pointer is only written after a successful upload, so the archive was removed later —
473
+ * by the bucket's lifecycle rule or a hand): the data is gone by an operator rule ⇒ loud and continuable —
474
+ * {@link dropDanglingLatest} (one `workspace_latest_dangling` line, the pointer deleted); the run's own run-end
475
+ * snapshot then becomes the new `latest`.
314
476
  * A restore that does not complete releases the pod it provisioned: no tool and no run-end snapshot may ever see a
315
- * torn or empty-by-accident workspace.
477
+ * torn workspace. On the space lane that failure is the archive `latest` names being THERE but not coming back
478
+ * ({@link RestoreFailureClass}): the run is refused all the same — never an empty workspace (the user's changes are still in
479
+ * the bucket) — with exactly one `workspace_restore_failed` line and a refusal naming the two ways out.
316
480
  */
317
481
  private restoreSnapshot;
482
+ /**
483
+ * `latest` names an archive that is not there (S-1438): one `workspace_latest_dangling{space, key, deleted}` line and the
484
+ * pointer is deleted (the next run must not trip on it again); the run continues on the empty workspace. A pointer
485
+ * delete that fails rides on the same line (`deleted: false` + the error) — the next run then says it once more.
486
+ * This env is the space's only writer here (the commit fence admits one per space), so the delete is a plain one.
487
+ */
488
+ private dropDanglingLatest;
318
489
  /**
319
490
  * Honest post-resume consistency ([ref] §5 council #8): the workspace FILES are restored byte-for-byte
320
491
  * from the snapshot, so read-before-edit hashing already lines up — there's nothing to re-fetch. In-VM
@@ -334,15 +505,18 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
334
505
  * Delete the sandbox pod. Idempotent, never throws. Space lane open and a pod exists ⇒ FIRST the run-end space
335
506
  * snapshot ({@link persistWorkspace}); its failure is loud and never blocks the delete (the pod lifetime is the
336
507
  * leak guard). A run that never connected has no workspace to save — `latest` stays where it was. Space lane
337
- * unavailable (occupant unknown) ⇒ nothing is saved (the once-per-env line says so).
508
+ * unavailable (no such session) ⇒ nothing is saved (the once-per-env line says so); unreadable ⇒ there is no pod
509
+ * ({@link provision} refused every creation).
338
510
  */
339
511
  destroy(): Promise<void>;
340
512
  cleanup(): Promise<void>;
341
513
  /**
342
- * Run-end space snapshot: PUT the `committing` marker → the one tar upload ({@link uploadSnapshot}, lane `space` — it
343
- * also releases the pod) → move `latest` → keep N → DELETE the marker (always, saved or failed).
344
- * Upload or pointer failure ⇒ one `workspace_snapshot_failed` line + the `failed` outcome; `latest` keeps naming the
345
- * previous snapshot, so the next run restores THAT — never an empty workspace dressed up as a restore.
514
+ * Run-end space snapshot, under the space's commit fence (the run store's row, `workspace-space-commit.ts`): the fence
515
+ * first (this run's row moves to its upload state; lost ⇒ no upload, loud) → the one tar upload ({@link uploadSnapshot},
516
+ * lane `space` — it also releases the pod) → the fence again (this run's row moves to its publishing state; lost ⇒
517
+ * `latest` is not published, loud) → move `latest` → keep N → the row is deleted (always, saved or failed).
518
+ * A lost fence, an upload or a pointer failure ⇒ one `workspace_snapshot_failed` line + the `failed` outcome; `latest`
519
+ * keeps naming the previous snapshot, so the next run restores THAT — never an empty workspace dressed up as a restore.
346
520
  */
347
521
  private persistWorkspace;
348
522
  private execUrl;
@@ -457,6 +631,20 @@ export declare class RemoteK8sExecutionEnv implements RemoteExecutionEnv, Backgr
457
631
  * 车PL:buffered `exec` 上守卫触发 ⇒ `spawn_error`)。 */
458
632
  private workingDirOf;
459
633
  }
634
+ /**
635
+ * The ONE reclaim seat for an ended space ({@link SpaceReclaimer}) — called where a space ENDS (the session-delete coordinator and the
636
+ * managed-retention sweep). For each ended space it reclaims everything the space left behind, over this deployment's k8s config (it
637
+ * never creates a pod): its build-cache volume (the claims labelled by the space key; only when the cache volume is configured),
638
+ * its snapshot objects — the `space/` partition (archives + `latest`) and the park lane's objects in `<prefix>/<sessionId>/` that carry the ended occupant's mark (only when a
639
+ * snapshot store is configured). Each part is independent: one part failing never skips another. A failed part ⇒ one operator line
640
+ * (`workspace_cache_reclaim_failed{reason}` for the volume, `workspace_space_reclaim_failed{stage}` for the objects) + `recordFailOpen`; never throws.
641
+ * ONE call = ONE absolute deadline (the snapshot bound, `dataTimeoutMs`; no knob of its own), built at the entry and nowhere else: every request of the call — each
642
+ * space's cache-volume delete and every LIST / DELETE of its objects — runs under it. A session delete calls with its one space; the managed-retention sweep calls
643
+ * once per domain per round with the spaces that round ended there. When the deadline passes the request in flight is cut and every space not yet done is still
644
+ * walked but sends nothing: its volume says `reason: "budget"`, its objects `stage: "budget"` (the volume stays for deletion by label, the objects for the
645
+ * bucket's lifecycle rule). A caller that must not wait for the seat (the session-delete answer) starts it and does not await it.
646
+ */
647
+ export declare function createSpaceReclaimer(config: K8sEnvConfig, deps?: K8sEnvDeps): SpaceReclaimer;
460
648
  /**
461
649
  * `ExecutionEnvFactory` for the k8s pod-sandbox backend — one fresh (unconnected) env per task; the pod is
462
650
  * created lazily on connect and deleted by `destroy()` (Runner-owned lifetime; `activeDeadlineSeconds` is the