@sema-agent/server 7.131.0 → 7.132.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 (256) hide show
  1. package/MIGRATION.md +309 -0
  2. package/README.md +5 -1
  3. package/README.zh-CN.md +3 -1
  4. package/USAGE.md +163 -54
  5. package/dist/approval-card.d.ts +3 -1
  6. package/dist/approval-card.js +2 -3
  7. package/dist/audit.d.ts +7 -10
  8. package/dist/auth-bridge.d.ts +4 -0
  9. package/dist/auth-bridge.js +10 -11
  10. package/dist/boot/deployment-mods.d.ts +31 -0
  11. package/dist/boot/deployment-mods.js +26 -0
  12. package/dist/boot/execution-env.d.ts +1 -1
  13. package/dist/boot/execution-env.js +19 -6
  14. package/dist/boot/memory-boundary.d.ts +2 -2
  15. package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
  16. package/dist/boot/read-deny-anchor-audit.js +36 -0
  17. package/dist/boot/resolve-spec.d.ts +1 -5
  18. package/dist/boot/resolve-spec.js +19 -47
  19. package/dist/boot/runner-deps.d.ts +19 -8
  20. package/dist/boot/runner-deps.js +14 -2
  21. package/dist/boot/session-faces.d.ts +1 -7
  22. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  23. package/dist/boot/stage-03-store-backend.js +2 -2
  24. package/dist/boot/stage-04-stores.d.ts +1 -1
  25. package/dist/boot/stage-05-execution-env.d.ts +4 -4
  26. package/dist/boot/stage-05-execution-env.js +7 -10
  27. package/dist/boot/stage-06-runners.d.ts +2 -2
  28. package/dist/boot/stage-06-runners.js +5 -4
  29. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  30. package/dist/boot/stage-07-capability-layer.js +3 -3
  31. package/dist/boot/stage-08-reapers.d.ts +2 -2
  32. package/dist/boot/stage-09-leader.d.ts +3 -9
  33. package/dist/boot/stage-10-http-server.d.ts +3 -9
  34. package/dist/boot/stage-10-http-server.js +6 -7
  35. package/dist/boot/stores.d.ts +1 -1
  36. package/dist/boot/stores.js +5 -6
  37. package/dist/boot/web-admin.d.ts +0 -3
  38. package/dist/boot/web-admin.js +35 -35
  39. package/dist/brain.js +17 -22
  40. package/dist/budget.js +5 -4
  41. package/dist/caller-text-echo.d.ts +20 -0
  42. package/dist/caller-text-echo.js +24 -0
  43. package/dist/capabilities/brand-identity.d.ts +26 -0
  44. package/dist/capabilities/brand-identity.js +27 -0
  45. package/dist/capabilities/center-plugins.js +1 -1
  46. package/dist/capabilities/center-prompts.d.ts +32 -59
  47. package/dist/capabilities/center-prompts.js +18 -32
  48. package/dist/capabilities/engine-capabilities.js +2 -1
  49. package/dist/capabilities/hands-lane.d.ts +9 -6
  50. package/dist/capabilities/hands-lane.js +1 -1
  51. package/dist/capabilities/mcp-probe.d.ts +14 -1
  52. package/dist/capabilities/mcp-probe.js +22 -133
  53. package/dist/capabilities/scenarios.d.ts +34 -40
  54. package/dist/capabilities/scenarios.js +16 -33
  55. package/dist/capabilities/skills.d.ts +19 -4
  56. package/dist/capabilities/skills.js +30 -10
  57. package/dist/config-catalog.js +6 -6
  58. package/dist/config-lkg.d.ts +9 -9
  59. package/dist/config-lkg.js +20 -4
  60. package/dist/config-provider.d.ts +3 -1
  61. package/dist/config-provider.js +12 -4
  62. package/dist/config-types.d.ts +7 -9
  63. package/dist/config.js +17 -4
  64. package/dist/device-ws-protocol.d.ts +1 -1
  65. package/dist/device-ws-protocol.js +1 -0
  66. package/dist/env-name-allowlist-knobs.d.ts +12 -2
  67. package/dist/env-name-allowlist-knobs.js +5 -4
  68. package/dist/exec-bounds.d.ts +15 -3
  69. package/dist/exec-bounds.js +4 -0
  70. package/dist/execution-lane-caps.d.ts +9 -2
  71. package/dist/execution-lane-caps.js +11 -0
  72. package/dist/hooks/classic-stations.d.ts +86 -0
  73. package/dist/hooks/classic-stations.js +27 -0
  74. package/dist/hooks/hook-llm.d.ts +8 -5
  75. package/dist/hooks/hook-llm.js +78 -35
  76. package/dist/hooks/hook-runner.d.ts +232 -80
  77. package/dist/hooks/hook-runner.js +218 -312
  78. package/dist/http/admission.d.ts +8 -2
  79. package/dist/http/admission.js +101 -57
  80. package/dist/http/approval-sweeps.js +57 -16
  81. package/dist/http/checkpoint-ctx.d.ts +25 -8
  82. package/dist/http/checkpoint-ctx.js +11 -7
  83. package/dist/http/claimed-run.d.ts +13 -0
  84. package/dist/http/claimed-run.js +15 -0
  85. package/dist/http/device-placement.js +5 -10
  86. package/dist/http/dispatch.d.ts +0 -4
  87. package/dist/http/dispatch.js +5 -9
  88. package/dist/http/durable-run-leg.d.ts +2 -1
  89. package/dist/http/durable-run-leg.js +5 -3
  90. package/dist/http/health-self-report.d.ts +7 -5
  91. package/dist/http/health-self-report.js +4 -18
  92. package/dist/http/principal-gate.d.ts +23 -6
  93. package/dist/http/principal-gate.js +24 -5
  94. package/dist/http/refusal-body.d.ts +21 -0
  95. package/dist/http/refusal-body.js +4 -0
  96. package/dist/http/request-guards.d.ts +27 -0
  97. package/dist/http/request-guards.js +24 -3
  98. package/dist/http/resume-legs.d.ts +25 -0
  99. package/dist/http/resume-legs.js +43 -22
  100. package/dist/http/routes/capabilities.js +1 -1
  101. package/dist/http/routes/diagnostics.js +1 -1
  102. package/dist/http/routes/rules.js +2 -2
  103. package/dist/http/routes/runs.js +7 -7
  104. package/dist/http/routes/session-sync.js +4 -2
  105. package/dist/http/routes/sessions.js +1 -1
  106. package/dist/http/routes/shared-memory.js +2 -13
  107. package/dist/http/routes/side-query.js +2 -3
  108. package/dist/http/routes/tasks.js +19 -9
  109. package/dist/http/send.d.ts +9 -7
  110. package/dist/http/send.js +7 -2
  111. package/dist/http/server.d.ts +21 -11
  112. package/dist/http/server.js +28 -2
  113. package/dist/http/submit-ctx.d.ts +25 -0
  114. package/dist/http/submit-ctx.js +39 -0
  115. package/dist/http/wire-types.d.ts +1 -1
  116. package/dist/index.d.ts +1 -1
  117. package/dist/index.js +1 -1
  118. package/dist/lane-home-probe.d.ts +43 -0
  119. package/dist/lane-home-probe.js +35 -0
  120. package/dist/limit-refusal.d.ts +47 -0
  121. package/dist/limit-refusal.js +12 -0
  122. package/dist/main.js +1 -1
  123. package/dist/mcp-assertion-header.d.ts +1 -9
  124. package/dist/mcp-assertion-header.js +0 -6
  125. package/dist/mcp-tool-faces.d.ts +5 -0
  126. package/dist/mcp-tool-faces.js +5 -1
  127. package/dist/memory-scope.d.ts +1 -1
  128. package/dist/model-provider.d.ts +39 -12
  129. package/dist/model-provider.js +19 -2
  130. package/dist/model-route-endpoint.d.ts +1 -1
  131. package/dist/model-route-endpoint.js +5 -5
  132. package/dist/observability/cost-taxonomy.d.ts +7 -7
  133. package/dist/observability/fail-open.d.ts +42 -6
  134. package/dist/observability/fail-open.js +42 -6
  135. package/dist/observability/metrics.js +3 -1
  136. package/dist/observability/prompt-manifest.d.ts +47 -82
  137. package/dist/observability/prompt-manifest.js +119 -17
  138. package/dist/observability/run-terminal.js +14 -6
  139. package/dist/observability/secret-env-scrub.d.ts +13 -1
  140. package/dist/observability/secret-env-scrub.js +13 -1
  141. package/dist/observability/tool-trace.d.ts +16 -21
  142. package/dist/observability/tool-trace.js +51 -42
  143. package/dist/per-task-image.d.ts +7 -1
  144. package/dist/per-task-image.js +3 -3
  145. package/dist/plain-string-record.d.ts +25 -0
  146. package/dist/plain-string-record.js +32 -0
  147. package/dist/plugins/blob-backend.d.ts +68 -13
  148. package/dist/plugins/blob-backend.js +53 -10
  149. package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
  150. package/dist/plugins/checkpoint-store-sql.js +39 -26
  151. package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
  152. package/dist/plugins/k8s-bg-scripts.js +22 -0
  153. package/dist/plugins/local-checkpoint-store.js +5 -2
  154. package/dist/plugins/local-session-store.js +1 -1
  155. package/dist/plugins/one-time-migrations.d.ts +137 -90
  156. package/dist/plugins/one-time-migrations.js +356 -207
  157. package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
  158. package/dist/plugins/permission-rule-store-sql.js +4 -18
  159. package/dist/plugins/pg-session-storage.d.ts +4 -5
  160. package/dist/plugins/pg-session-storage.js +7 -17
  161. package/dist/plugins/remote-env-adb.d.ts +6 -0
  162. package/dist/plugins/remote-env-adb.js +9 -4
  163. package/dist/plugins/remote-env-device.js +3 -0
  164. package/dist/plugins/remote-env-e2b.d.ts +1 -1
  165. package/dist/plugins/remote-env-e2b.js +21 -9
  166. package/dist/plugins/remote-env-host.d.ts +9 -9
  167. package/dist/plugins/remote-env-host.js +10 -32
  168. package/dist/plugins/remote-env-k8s.d.ts +95 -26
  169. package/dist/plugins/remote-env-k8s.js +265 -90
  170. package/dist/plugins/remote-env-local-docker.d.ts +20 -1
  171. package/dist/plugins/remote-env-local-docker.js +77 -9
  172. package/dist/plugins/remote-env-ssh.d.ts +15 -0
  173. package/dist/plugins/remote-env-ssh.js +109 -3
  174. package/dist/plugins/remote-shell.d.ts +0 -1
  175. package/dist/plugins/remote-shell.js +1 -2
  176. package/dist/plugins/retention-store-sql.js +6 -2
  177. package/dist/plugins/schema-consistency.d.ts +54 -0
  178. package/dist/plugins/schema-consistency.js +52 -0
  179. package/dist/plugins/session-epoch-anchor.d.ts +13 -0
  180. package/dist/plugins/session-epoch-anchor.js +23 -0
  181. package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
  182. package/dist/plugins/shared-memory-store-sql.js +5 -16
  183. package/dist/plugins/sql-driver.d.ts +141 -0
  184. package/dist/plugins/sql-driver.js +115 -1
  185. package/dist/plugins/sql-errors.d.ts +41 -0
  186. package/dist/plugins/sql-errors.js +36 -0
  187. package/dist/plugins/store-backend.js +2 -1
  188. package/dist/plugins/tidb-session-storage.d.ts +7 -11
  189. package/dist/plugins/tidb-session-storage.js +6 -16
  190. package/dist/plugins/tidb-session-store.js +1 -1
  191. package/dist/prompts-domain-validate.d.ts +17 -61
  192. package/dist/prompts-domain-validate.js +9 -119
  193. package/dist/request-key-closure.d.ts +2 -2
  194. package/dist/request-key-closure.js +2 -4
  195. package/dist/rule-entry-list.d.ts +12 -9
  196. package/dist/rule-entry-list.js +37 -21
  197. package/dist/run-local.js +19 -19
  198. package/dist/runs.d.ts +6 -0
  199. package/dist/runs.js +16 -0
  200. package/dist/server-secret-env.d.ts +44 -18
  201. package/dist/server-secret-env.js +49 -8
  202. package/dist/session-incarnation.d.ts +15 -5
  203. package/dist/session-incarnation.js +16 -9
  204. package/dist/session-titler.js +12 -2
  205. package/dist/spec-fields.d.ts +28 -16
  206. package/dist/spec-fields.js +46 -53
  207. package/dist/sql-engine-posture.d.ts +29 -4
  208. package/dist/sql-engine-posture.js +36 -1
  209. package/dist/stored-request-tool-axes-m19.d.ts +19 -4
  210. package/dist/stored-request-tool-axes-m19.js +30 -2
  211. package/dist/task-a2a.d.ts +9 -1
  212. package/dist/task-a2a.js +73 -47
  213. package/dist/task-mcp.d.ts +46 -58
  214. package/dist/task-mcp.js +99 -65
  215. package/dist/task-settings.d.ts +7 -21
  216. package/dist/task-settings.js +7 -36
  217. package/dist/text-bidi.d.ts +3 -46
  218. package/dist/text-bidi.js +3 -3
  219. package/dist/tool-approval.d.ts +1 -1
  220. package/dist/trace/core-keyset-guard.d.ts +24 -4
  221. package/dist/trace/engine-notice-wire.d.ts +11 -3
  222. package/dist/trace/engine-notice-wire.js +8 -1
  223. package/dist/trace/ledger-events.d.ts +3 -1
  224. package/dist/trace/ledger-sink.js +4 -1
  225. package/dist/trace/model-segment.js +1 -1
  226. package/dist/trace/project.d.ts +19 -6
  227. package/dist/trace/project.js +57 -22
  228. package/dist/trace/wire-projection-faces.d.ts +79 -1
  229. package/dist/trace/wire-projection-faces.js +22 -0
  230. package/dist/web-admin-client/candidate-gate.d.ts +6 -6
  231. package/dist/web-admin-client/candidate-gate.js +3 -3
  232. package/dist/web-admin-client/http-client.js +0 -6
  233. package/dist/web-admin-client/read-warnings.d.ts +13 -0
  234. package/dist/web-admin-client/read-warnings.js +10 -0
  235. package/dist/web-admin-client/skills-mcp.d.ts +3 -0
  236. package/dist/web-admin-client/skills-mcp.js +9 -6
  237. package/dist/web-admin-client/types.d.ts +2 -3
  238. package/dist/workspace-cache.d.ts +35 -0
  239. package/dist/workspace-cache.js +36 -0
  240. package/dist/workspace-space.d.ts +3 -2
  241. package/dist/workspace-space.js +2 -0
  242. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
  243. package/package.json +4 -4
  244. package/scripts/drain-check-local.mjs +34 -1
  245. package/scripts/drain-check.mysql.sql +30 -2
  246. package/scripts/drain-check.pg.sql +29 -2
  247. package/dist/capabilities/memory-notice.d.ts +0 -138
  248. package/dist/capabilities/memory-notice.js +0 -64
  249. package/dist/capabilities/prompt.d.ts +0 -17
  250. package/dist/capabilities/prompt.js +0 -6
  251. package/dist/mcp-content-origin.d.ts +0 -34
  252. package/dist/mcp-content-origin.js +0 -10
  253. package/dist/tool-axis-words.d.ts +0 -27
  254. package/dist/tool-axis-words.js +0 -9
  255. package/dist/wire-name-bound.d.ts +0 -14
  256. package/dist/wire-name-bound.js +0 -13
@@ -11,7 +11,7 @@ import { spaceCommitAlive } from "../workspace-space-commit.js";
11
11
  import { incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
12
12
  import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
13
13
  import { recordFailOpen } from "../observability/fail-open.js";
14
- import { commandBoundsOf, laneLivenessOf, livenessCutMessage, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
14
+ import { buildLivenessCut, commandBoundsOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
15
15
  import { SANDBOX_IPV6_DEFAULT, sandboxEgressGaps, sandboxPodSysctls } from "../sandbox-egress-policy.js";
16
16
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
17
17
  import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
@@ -26,8 +26,8 @@ const K8S_BG_MAX_TIMEOUT_SEC = numEnvOr("K8S_BG_MAX_TIMEOUT_SEC", 1800, K8S_BG_D
26
26
  const K8S_BG_READ_CAP = numEnvOr("K8S_BG_READ_CAP", 1024 * 1024, 64 * 1024);
27
27
  const K8S_BG_FILE_CAP = numEnvOr("K8S_BG_FILE_CAP", 64 * 1024 * 1024, K8S_BG_READ_CAP);
28
28
  const K8S_BG_MEM_CAP = numEnvOr("K8S_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
29
- import { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
30
- export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
29
+ import { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, buildKillAllScript, KILL_ALL_RECHECK_MS, parseBgPollOutput, } from "./k8s-bg-scripts.js";
30
+ export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, buildKillAllScript, parseBgPollOutput, } from "./k8s-bg-scripts.js";
31
31
  const ok = (value) => ({ ok: true, value });
32
32
  const unsupported = (op) => ({
33
33
  ok: false,
@@ -116,6 +116,45 @@ class FenceCallAborted extends Error {
116
116
  function dataTimeoutMsOf(config) {
117
117
  return config.dataTimeoutMs ?? WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT;
118
118
  }
119
+ const RPC_TIMEOUT_MS_DEFAULT = 30_000;
120
+ export function rpcTimeoutMsOf(config) {
121
+ return config.rpcTimeoutMs ?? RPC_TIMEOUT_MS_DEFAULT;
122
+ }
123
+ export function boundedStoreCall(op, bound) {
124
+ const signal = bound.abort?.signal;
125
+ return new Promise((resolve, reject) => {
126
+ if (signal?.aborted)
127
+ return reject(bound.abort.error());
128
+ const settle = () => {
129
+ clearTimeout(timer);
130
+ signal?.removeEventListener("abort", onAbort);
131
+ };
132
+ const onAbort = () => {
133
+ settle();
134
+ reject(bound.abort.error());
135
+ };
136
+ const timer = setTimeout(() => {
137
+ settle();
138
+ reject(bound.onTimeout());
139
+ }, bound.ms);
140
+ signal?.addEventListener("abort", onAbort, { once: true });
141
+ let pending;
142
+ try {
143
+ pending = op();
144
+ }
145
+ catch (e) {
146
+ settle();
147
+ return reject(e);
148
+ }
149
+ pending.then((v) => {
150
+ settle();
151
+ resolve(v);
152
+ }, (e) => {
153
+ settle();
154
+ reject(e);
155
+ });
156
+ });
157
+ }
119
158
  import { execSocketTlsOptions, exitCodeFromStatus } from "./k8s-exec-protocol.js";
120
159
  export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
121
160
  export function errorMessageOf(e) {
@@ -134,9 +173,10 @@ export class RemoteK8sExecutionEnv {
134
173
  podIP;
135
174
  destroyed = false;
136
175
  handleSnapshotId;
137
- inFlightExecs = 0;
138
- connectPromise;
139
- restoreRefusal;
176
+ inFlight = new Set();
177
+ idleWaiters = [];
178
+ connectInFlight;
179
+ connectRefusal;
140
180
  homeDir;
141
181
  cdPath = "unknown";
142
182
  cfg;
@@ -150,6 +190,7 @@ export class RemoteK8sExecutionEnv {
150
190
  cacheUnavailableSaid = false;
151
191
  logger;
152
192
  onWorkspaceSnapshot;
193
+ cacheMisses;
153
194
  constructor(config, deps) {
154
195
  this.cfg = {
155
196
  apiUrl: config.apiUrl,
@@ -162,7 +203,7 @@ export class RemoteK8sExecutionEnv {
162
203
  mountPath: config.mountPath ?? K8S_WORKSPACE_MOUNT_DEFAULT,
163
204
  timeoutMs: config.timeoutMs ?? 30 * 60_000,
164
205
  readyTimeoutMs: config.readyTimeoutMs ?? 120_000,
165
- rpcTimeoutMs: config.rpcTimeoutMs ?? 30_000,
206
+ rpcTimeoutMs: rpcTimeoutMsOf(config),
166
207
  livenessMs: config.livenessMs ?? LIVENESS_MS_DEFAULT,
167
208
  dataTimeoutMs: dataTimeoutMsOf(config),
168
209
  cpu: config.cpu ?? "1",
@@ -185,6 +226,7 @@ export class RemoteK8sExecutionEnv {
185
226
  this.cacheLane = cacheLaneOf(config.cache, config.spaceOccupant);
186
227
  this.logger = config.logger;
187
228
  this.onWorkspaceSnapshot = config.onWorkspaceSnapshot;
229
+ this.cacheMisses = config.cacheMisses;
188
230
  this.capabilities = { isolation: true, suspendable: !!this.cfg.s3Snapshot, canonicalPathAuthoritative: true };
189
231
  const bgMax = Math.max(1, Math.min(K8S_BG_MAX_TIMEOUT_SEC, Math.floor(this.cfg.timeoutMs / 1000)));
190
232
  this.backgroundCapabilities = {
@@ -283,7 +325,15 @@ export class RemoteK8sExecutionEnv {
283
325
  ...(url.startsWith("wss:") ? execSocketTlsOptions(ca, this.cfg.insecureTls) : {}),
284
326
  });
285
327
  }
286
- async connect(config) {
328
+ connect(config) {
329
+ this.connectInFlight ??= this.establish(config).finally(() => {
330
+ this.connectInFlight = undefined;
331
+ });
332
+ return this.connectInFlight;
333
+ }
334
+ async establish(config) {
335
+ if (this.connectRefusal)
336
+ return this.connectRefusal;
287
337
  const signal = config?.abortSignal;
288
338
  const lane = this.spaceLane;
289
339
  switch (lane.state) {
@@ -295,8 +345,6 @@ export class RemoteK8sExecutionEnv {
295
345
  case "unreadable":
296
346
  return this.provision(signal);
297
347
  case "open": {
298
- if (this.restoreRefusal)
299
- return this.restoreRefusal;
300
348
  const settled = await this.awaitSpaceCommit(lane.space, signal);
301
349
  if (!settled.ok)
302
350
  return settled;
@@ -322,27 +370,10 @@ export class RemoteK8sExecutionEnv {
322
370
  await this.fenceCall(() => space.fence.end(), "delete").catch(() => undefined);
323
371
  }
324
372
  fenceCall(op, what, signal) {
325
- return new Promise((resolve, reject) => {
326
- if (signal?.aborted)
327
- return reject(new FenceCallAborted());
328
- const onAbort = () => {
329
- clearTimeout(timer);
330
- reject(new FenceCallAborted());
331
- };
332
- const timer = setTimeout(() => {
333
- signal?.removeEventListener("abort", onAbort);
334
- reject(new Error(`workspace space commit state ${what} did not answer within ${this.cfg.rpcTimeoutMs}ms`));
335
- }, this.cfg.rpcTimeoutMs);
336
- signal?.addEventListener("abort", onAbort, { once: true });
337
- op().then((v) => {
338
- clearTimeout(timer);
339
- signal?.removeEventListener("abort", onAbort);
340
- resolve(v);
341
- }, (e) => {
342
- clearTimeout(timer);
343
- signal?.removeEventListener("abort", onAbort);
344
- reject(e);
345
- });
373
+ return boundedStoreCall(op, {
374
+ ms: this.cfg.rpcTimeoutMs,
375
+ onTimeout: () => new Error(`workspace space commit state ${what} did not answer within ${this.cfg.rpcTimeoutMs}ms`),
376
+ ...(signal ? { abort: { signal, error: () => new FenceCallAborted() } } : {}),
346
377
  });
347
378
  }
348
379
  sayUnavailable(reason) {
@@ -355,6 +386,21 @@ export class RemoteK8sExecutionEnv {
355
386
  effect: "WORKSPACE_PERSIST=space, but the session store has no occupant (incarnation) for this run's session — its /workspace is neither restored nor saved (it is never keyed by the session id instead)",
356
387
  });
357
388
  }
389
+ holdRefusal(error) {
390
+ this.connectRefusal = { ok: false, error };
391
+ return this.connectRefusal;
392
+ }
393
+ refusePod(pod, phase, reason, message) {
394
+ this.logger?.warn("sandbox_pod_refused", {
395
+ pod,
396
+ phase,
397
+ reason: reason ?? "?",
398
+ ...(message ? { detail: message.slice(0, 300) } : {}),
399
+ ...(this.cfg.sessionId ? { sessionId: this.cfg.sessionId } : {}),
400
+ effect: "the cluster created this run's sandbox pod and it ended before it ran — this run's operations answer the same refusal without creating another pod; a retry of the run builds a fresh sandbox",
401
+ });
402
+ return this.holdRefusal(new RemoteExecutionError("connect_failed", `pod entered ${phase} before Running (${reason ?? "?"}) — the cluster refused this run's sandbox pod; the refusal stands for the run (no pod is created again), a retry of the run asks the cluster afresh`));
403
+ }
358
404
  refuseUnreadableOccupant(code) {
359
405
  const ways = [...(this.spaceLane.state === "unreadable" ? ["WORKSPACE_PERSIST=off"] : []), ...(this.cacheLane.state === "unreadable" ? ["WORKSPACE_CACHE=off"] : [])].join(" and ");
360
406
  if (!this.unreadableOccupantSaid) {
@@ -374,6 +420,8 @@ export class RemoteK8sExecutionEnv {
374
420
  provision(signal) {
375
421
  const space = this.spaceLane;
376
422
  const cache = this.cacheLane;
423
+ if (this.connectRefusal)
424
+ return Promise.resolve(this.connectRefusal);
377
425
  if (space.state === "unreadable")
378
426
  return Promise.resolve(this.refuseUnreadableOccupant(space.code));
379
427
  if (cache.state === "unreadable")
@@ -384,19 +432,23 @@ export class RemoteK8sExecutionEnv {
384
432
  backoffMs: (failed) => this.cfg.retry.backoffMs * failed,
385
433
  };
386
434
  const once = this.deps.connectOnce ?? ((s) => this.connectOnce(cache, s));
387
- return withRetry(() => once(signal), policy, { signal });
435
+ const refused = new AbortController();
436
+ const halt = signal ? AbortSignal.any([signal, refused.signal]) : refused.signal;
437
+ return withRetry(async () => {
438
+ const r = await once(signal);
439
+ if (!r.ok && this.connectRefusal)
440
+ refused.abort();
441
+ return r;
442
+ }, policy, { signal: halt });
388
443
  }
389
444
  async ensureConnected() {
390
- if (this.podName)
391
- return ok(undefined);
392
445
  if (this.destroyed)
393
446
  return { ok: false, error: new ExecutionError("shell_unavailable", "k8s sandbox destroyed") };
394
- this.connectPromise ??= this.connect();
395
- const r = await this.connectPromise;
396
- if (!r.ok) {
397
- this.connectPromise = undefined;
447
+ if (this.podName && this.connectInFlight === undefined)
448
+ return ok(undefined);
449
+ const r = await this.connect();
450
+ if (!r.ok)
398
451
  return { ok: false, error: new ExecutionError("shell_unavailable", `k8s sandbox connect failed: ${r.error.message}`, r.error) };
399
- }
400
452
  return ok(undefined);
401
453
  }
402
454
  async connectOnce(cacheLane, signal) {
@@ -409,6 +461,8 @@ export class RemoteK8sExecutionEnv {
409
461
  if (!("cacheMiss" in first))
410
462
  return first;
411
463
  this.cacheUnavailable(first.cacheMiss, first.detail, cache);
464
+ if (cache)
465
+ this.cacheMisses?.remember(cache.spaceKey, { reason: first.cacheMiss, detail: first.detail }, this.cfg.dataTimeoutMs);
412
466
  const second = await this.createPod(signal, undefined);
413
467
  return "cacheMiss" in second ? { ok: false, error: new RemoteExecutionError("connect_failed", "unreachable: a pod without the cache volume was given up for it") } : second;
414
468
  }
@@ -420,6 +474,11 @@ export class RemoteK8sExecutionEnv {
420
474
  this.cacheUnavailable(lane.reason, "the session store has no occupant (incarnation) for this run's session — no space key to find its cache volume by (never keyed by the session id instead)");
421
475
  return undefined;
422
476
  case "open": {
477
+ const remembered = this.cacheMisses?.recall(lane.cache.spaceKey);
478
+ if (remembered !== undefined) {
479
+ this.cacheUnavailable(remembered.reason, `${remembered.detail} — remembered from an earlier run of this space; not asked for again within ${this.cfg.dataTimeoutMs}ms`, lane.cache);
480
+ return undefined;
481
+ }
423
482
  const ensured = await this.ensureCacheClaim(lane.cache, signal);
424
483
  if (!ensured.ok) {
425
484
  this.cacheUnavailable("claim_failed", ensured.detail, lane.cache);
@@ -586,7 +645,7 @@ export class RemoteK8sExecutionEnv {
586
645
  }
587
646
  if (phase === "Failed" || phase === "Succeeded") {
588
647
  await this.deletePod(name).catch(() => { });
589
- return { ok: false, error: new RemoteExecutionError("connect_failed", `pod entered ${phase} before Running (${status?.reason ?? "?"})`) };
648
+ return this.refusePod(name, phase, status?.reason, status?.message);
590
649
  }
591
650
  const waiting = status?.containerStatuses?.[0]?.state?.waiting;
592
651
  if (waiting?.reason === "ImagePullBackOff" || waiting?.reason === "ErrImagePull") {
@@ -864,8 +923,8 @@ export class RemoteK8sExecutionEnv {
864
923
  await this.endSpace(this.spaceLane.space);
865
924
  return r;
866
925
  }
867
- async uploadSnapshot(target) {
868
- if (this.inFlightExecs > 0)
926
+ async uploadSnapshot(target, boundMs = this.cfg.dataTimeoutMs) {
927
+ if (target.lane === "park" && this.inFlight.size > 0)
869
928
  return { ok: false, error: new RemoteExecutionError("command_in_flight", "cannot suspend while a command is in flight (would snapshot a torn workspace)") };
870
929
  const snapshotId = mintSnapshotId(this.occupantIncarnation);
871
930
  if (!this.podName)
@@ -878,7 +937,7 @@ export class RemoteK8sExecutionEnv {
878
937
  const excludes = (this.cfg.snapshotExclude ?? []).map((item) => ` --exclude=${shellQuote(`./${item}`)}`).join("");
879
938
  const cmd = `tar czf ${tmp}${excludes} -C ${shellQuote(this.cfg.mountPath)} . && curl -fsS -X PUT -T ${tmp} ${shellQuote(url)}; ` +
880
939
  `rc=$?; rm -f ${tmp}; exit $rc`;
881
- const r = await this.runExec(cmd, { timeoutMs: this.cfg.dataTimeoutMs });
940
+ const r = await this.runExec(cmd, { timeoutMs: boundMs });
882
941
  if (!r.ok)
883
942
  return { ok: false, error: new RemoteExecutionError("connect_failed", `workspace snapshot upload failed: ${this.redactUrl(r.error.message, url)}`, r.error) };
884
943
  if (r.value.exitCode !== 0)
@@ -941,13 +1000,9 @@ export class RemoteK8sExecutionEnv {
941
1000
  case "space": {
942
1001
  const tries = attempt === 1 ? "1 attempt" : `${attempt} attempts`;
943
1002
  this.spaceLine("workspace_restore_failed", target.space, { snapshotId, class: failure.cls, attempts: attempt, boundMs: this.cfg.dataTimeoutMs, error: failure.detail });
944
- this.restoreRefusal = {
945
- ok: false,
946
- error: new RemoteExecutionError("connect_failed", `${failure.detail} — the workspace's latest snapshot (${snapshotId}) is in the store but did not come back (${failure.cls}, ${tries}, bound ${this.cfg.dataTimeoutMs}ms each), ` +
947
- "so this run does not start on an empty /workspace. Ways out: raise WORKSPACE_SNAPSHOT_TIMEOUT_MS if the archive is large, " +
948
- "or set WORKSPACE_PERSIST=off for one run (it starts from an empty /workspace and deletes no snapshot)", failure.cause),
949
- };
950
- return this.restoreRefusal;
1003
+ return this.holdRefusal(new RemoteExecutionError("connect_failed", `${failure.detail} — the workspace's latest snapshot (${snapshotId}) is in the store but did not come back (${failure.cls}, ${tries}, bound ${this.cfg.dataTimeoutMs}ms each), ` +
1004
+ "so this run does not start on an empty /workspace. Ways out: raise WORKSPACE_SNAPSHOT_TIMEOUT_MS if the archive is large, " +
1005
+ "or set WORKSPACE_PERSIST=off for one run (it starts from an empty /workspace and deletes no snapshot)", failure.cause));
951
1006
  }
952
1007
  default: {
953
1008
  const unreachable = target;
@@ -987,6 +1042,8 @@ export class RemoteK8sExecutionEnv {
987
1042
  return ok(undefined);
988
1043
  }
989
1044
  async reconnect(_sessionToken) {
1045
+ if (this.connectInFlight)
1046
+ return this.connectInFlight;
990
1047
  if (this.podName) {
991
1048
  const alive = await this.request("GET", `/api/v1/namespaces/${this.cfg.namespace}/pods/${this.podName}`).catch(() => undefined);
992
1049
  const phase = alive?.json?.status?.phase;
@@ -1032,6 +1089,63 @@ export class RemoteK8sExecutionEnv {
1032
1089
  async cleanup() {
1033
1090
  return this.destroy();
1034
1091
  }
1092
+ beginExec() {
1093
+ const ac = new AbortController();
1094
+ this.inFlight.add(ac);
1095
+ return ac;
1096
+ }
1097
+ endExec(ac) {
1098
+ this.inFlight.delete(ac);
1099
+ if (this.inFlight.size > 0)
1100
+ return;
1101
+ for (const wake of this.idleWaiters.splice(0))
1102
+ wake();
1103
+ }
1104
+ whenIdle(ms) {
1105
+ if (this.inFlight.size === 0)
1106
+ return Promise.resolve(true);
1107
+ return new Promise((resolve) => {
1108
+ const idle = () => {
1109
+ clearTimeout(timer);
1110
+ resolve(true);
1111
+ };
1112
+ const timer = setTimeout(() => {
1113
+ this.idleWaiters = this.idleWaiters.filter((w) => w !== idle);
1114
+ resolve(false);
1115
+ }, ms);
1116
+ this.idleWaiters.push(idle);
1117
+ });
1118
+ }
1119
+ async quiesce(space) {
1120
+ const inFlight = this.inFlight.size;
1121
+ if (inFlight === 0 && !this.podName)
1122
+ return { ok: true, waitedMs: 0 };
1123
+ const startedAt = Date.now();
1124
+ const ended = await this.whenIdle(this.cfg.rpcTimeoutMs);
1125
+ const killed = await this.killPodProcesses();
1126
+ for (const ac of [...this.inFlight])
1127
+ ac.abort();
1128
+ const waitedMs = Date.now() - startedAt;
1129
+ if (!ended) {
1130
+ this.spaceLine("workspace_snapshot_commands_cut", space, {
1131
+ inFlight,
1132
+ waitedMs,
1133
+ killed,
1134
+ effect: killed
1135
+ ? "the run is over but its commands were still running after the wait — their processes were killed (the sandbox is deleted right after) so the run-end snapshot reads a still tree; the workspace is saved as it stood"
1136
+ : "the run is over but its commands were still running after the wait and the pod's processes could not be confirmed stopped — the workspace is NOT saved (it may still be changing); `latest` keeps naming the previous snapshot",
1137
+ });
1138
+ }
1139
+ return killed
1140
+ ? { ok: true, waitedMs }
1141
+ : { ok: false, error: new RemoteExecutionError("command_in_flight", "the pod's processes could not be confirmed stopped when the run ended — not saving a workspace that may still be changing") };
1142
+ }
1143
+ async killPodProcesses() {
1144
+ if (!this.podName)
1145
+ return false;
1146
+ const r = await this.runExec(buildKillAllScript(), { timeoutMs: this.cfg.rpcTimeoutMs + KILL_ALL_RECHECK_MS });
1147
+ return r.ok && r.value.exitCode === 0;
1148
+ }
1035
1149
  async persistWorkspace(space) {
1036
1150
  const failed = (stage, error) => {
1037
1151
  this.spaceLine("workspace_snapshot_failed", space, { stage, code: error.code, error: error.message });
@@ -1052,7 +1166,10 @@ export class RemoteK8sExecutionEnv {
1052
1166
  try {
1053
1167
  let snap;
1054
1168
  try {
1055
- snap = await this.uploadSnapshot({ lane: "space", space });
1169
+ const quiet = await this.quiesce(space);
1170
+ if (!quiet.ok)
1171
+ return failed("upload", quiet.error);
1172
+ snap = await this.uploadSnapshot({ lane: "space", space }, Math.max(1, this.cfg.dataTimeoutMs - quiet.waitedMs));
1056
1173
  }
1057
1174
  catch (e) {
1058
1175
  return failed("upload", new RemoteExecutionError("unknown", errorMessageOf(e)));
@@ -1142,7 +1259,8 @@ export class RemoteK8sExecutionEnv {
1142
1259
  clearTimeout(livenessTimer);
1143
1260
  const idle = Date.now() - lastProgress;
1144
1261
  if (idle >= liveness.ms) {
1145
- finish({ ok: false, error: new ExecutionError("timeout", livenessCutMessage(liveness)) });
1262
+ const cut = buildLivenessCut(liveness);
1263
+ finish({ ok: false, error: new ExecutionError(cut.code, cut.message, undefined, undefined, cut.noOutputSec) });
1146
1264
  return;
1147
1265
  }
1148
1266
  livenessTimer = setTimeout(armLiveness, liveness.ms - idle);
@@ -1276,11 +1394,12 @@ export class RemoteK8sExecutionEnv {
1276
1394
  if (!c.ok)
1277
1395
  return c;
1278
1396
  const detachSignal = options?.detachSignal;
1279
- this.inFlightExecs++;
1397
+ const inflight = this.beginExec();
1398
+ const abortSignal = options?.abortSignal ? AbortSignal.any([options.abortSignal, inflight.signal]) : inflight.signal;
1280
1399
  try {
1281
1400
  if (!detachSignal) {
1282
1401
  return call.settle(await this.runExec(call.command, {
1283
- signal: options?.abortSignal,
1402
+ signal: abortSignal,
1284
1403
  timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
1285
1404
  boundOutput: true,
1286
1405
  onStdout: options?.onStdout,
@@ -1292,7 +1411,7 @@ export class RemoteK8sExecutionEnv {
1292
1411
  const dir = `${K8S_BG_DIR_ROOT}/exec-${adoptId}`;
1293
1412
  const tee = { out: seedMemStream(""), err: seedMemStream("") };
1294
1413
  return call.settle(await this.runExec(buildDetachCapableExec(dir, call.command), {
1295
- signal: options?.abortSignal,
1414
+ signal: abortSignal,
1296
1415
  timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
1297
1416
  boundOutput: true,
1298
1417
  onStdout: options?.onStdout,
@@ -1316,7 +1435,7 @@ export class RemoteK8sExecutionEnv {
1316
1435
  }));
1317
1436
  }
1318
1437
  finally {
1319
- this.inFlightExecs--;
1438
+ this.endExec(inflight);
1320
1439
  }
1321
1440
  }
1322
1441
  async getHost(port) {
@@ -1337,7 +1456,7 @@ export class RemoteK8sExecutionEnv {
1337
1456
  const c = await this.ensureConnected();
1338
1457
  if (!c.ok)
1339
1458
  throw new RemoteExecutionError("connect_failed", c.error.message, c.error);
1340
- this.inFlightExecs++;
1459
+ const ac = this.beginExec();
1341
1460
  const queue = [];
1342
1461
  let finished = false;
1343
1462
  let failure;
@@ -1350,7 +1469,6 @@ export class RemoteK8sExecutionEnv {
1350
1469
  };
1351
1470
  const cap = options?.maxOutputBytes;
1352
1471
  let bytes = 0;
1353
- const ac = new AbortController();
1354
1472
  const signal = options?.signal ? AbortSignal.any([options.signal, ac.signal]) : ac.signal;
1355
1473
  const done = this.runExec(buildCwdEnvCommand(command, this.workingDirOf(options?.cwd), options?.env, "raw").command, {
1356
1474
  signal,
@@ -1375,9 +1493,11 @@ export class RemoteK8sExecutionEnv {
1375
1493
  ? "transport_lost"
1376
1494
  : r.error.code === "timeout"
1377
1495
  ? "timeout"
1378
- : r.error.code === "spawn_error" || r.error.code === "shell_unavailable"
1379
- ? "unknown"
1380
- : "connect_failed";
1496
+ : r.error.code === "liveness_no_output"
1497
+ ? "liveness_no_output"
1498
+ : r.error.code === "spawn_error" || r.error.code === "shell_unavailable"
1499
+ ? "unknown"
1500
+ : "connect_failed";
1381
1501
  failure = new RemoteExecutionError(code, r.error.message, r.error);
1382
1502
  }
1383
1503
  finished = true;
@@ -1409,7 +1529,7 @@ export class RemoteK8sExecutionEnv {
1409
1529
  finally {
1410
1530
  ac.abort();
1411
1531
  await done.catch(() => { });
1412
- this.inFlightExecs--;
1532
+ this.endExec(ac);
1413
1533
  }
1414
1534
  }
1415
1535
  backgroundCapabilities;
@@ -1626,42 +1746,97 @@ export class RemoteK8sExecutionEnv {
1626
1746
  return dir ? this.resolve(dir) : undefined;
1627
1747
  }
1628
1748
  }
1749
+ const SPACE_RECLAIM_CONCURRENCY = 4;
1750
+ function createGate(limit) {
1751
+ let active = 0;
1752
+ const waiting = [];
1753
+ const release = () => {
1754
+ const next = waiting.shift();
1755
+ if (next)
1756
+ next();
1757
+ else
1758
+ active--;
1759
+ };
1760
+ return async (task) => {
1761
+ if (active < limit)
1762
+ active++;
1763
+ else
1764
+ await new Promise((resolve) => waiting.push(resolve));
1765
+ try {
1766
+ return await task();
1767
+ }
1768
+ finally {
1769
+ release();
1770
+ }
1771
+ };
1772
+ }
1773
+ function tallyFailure(classes, key, at, pos, sample) {
1774
+ const seen = classes.get(key);
1775
+ if (seen === undefined)
1776
+ classes.set(key, { count: 1, at, pos, sample });
1777
+ else {
1778
+ seen.count++;
1779
+ if (at < seen.at || (at === seen.at && pos < seen.pos)) {
1780
+ seen.at = at;
1781
+ seen.pos = pos;
1782
+ seen.sample = sample;
1783
+ }
1784
+ }
1785
+ }
1786
+ function failureClassesInOrder(classes) {
1787
+ return [...classes.values()].sort((a, b) => a.at - b.at || a.pos - b.pos);
1788
+ }
1629
1789
  export function createSpaceReclaimer(config, deps) {
1790
+ const gate = createGate(SPACE_RECLAIM_CONCURRENCY);
1630
1791
  return {
1631
1792
  async reclaim(spaces) {
1632
1793
  const deadline = AbortSignal.timeout(dataTimeoutMsOf(config));
1633
- for (const { sessionId, spaceKey } of spaces) {
1634
- if (spaceKey === "")
1635
- continue;
1636
- const env = new RemoteK8sExecutionEnv({ ...config, sessionId }, deps);
1637
- if (config.cache !== undefined) {
1638
- const r = await env.reclaimSpaceCache(spaceKey, deadline).catch((e) => ({ ok: false, reason: "delete_failed", detail: errorMessageOf(e) }));
1639
- if (!r.ok) {
1640
- recordFailOpen("server.workspace-cache.reclaim-failed", `reason=${r.reason} label=${workspaceCacheClaimOf(spaceKey).label}`);
1641
- config.logger?.warn("workspace_cache_reclaim_failed", {
1642
- reason: r.reason,
1643
- space: incarnationLogPrefix(spaceKey),
1644
- label: workspaceCacheClaimOf(spaceKey).label,
1645
- error: redactIncarnations(r.detail),
1646
- effect: `the ended space's cache volume stays — delete it by label ${WORKSPACE_CACHE_SPACE_LABEL}=<label> (the deletion it belongs to went ahead)`,
1647
- });
1794
+ const volumeFailures = new Map();
1795
+ const objectFailures = new Map();
1796
+ const settled = await Promise.allSettled(spaces.map(({ sessionId, spaceKey }, at) => spaceKey === ""
1797
+ ? Promise.resolve()
1798
+ : gate(async () => {
1799
+ const env = new RemoteK8sExecutionEnv({ ...config, sessionId }, deps);
1800
+ if (config.cache !== undefined) {
1801
+ const r = await env.reclaimSpaceCache(spaceKey, deadline).catch((e) => ({ ok: false, reason: "delete_failed", detail: errorMessageOf(e) }));
1802
+ if (!r.ok) {
1803
+ recordFailOpen("server.workspace-cache.reclaim-failed", `reason=${r.reason} label=${workspaceCacheClaimOf(spaceKey).label}`);
1804
+ tallyFailure(volumeFailures, r.reason, at, 0, { spaceKey, failure: r });
1805
+ }
1648
1806
  }
1649
- }
1650
- if (config.s3Snapshot !== undefined) {
1651
- const failures = await env.reclaimSpaceSnapshots(spaceKey, deadline).catch((e) => [{ lane: "space", stage: "delete", detail: errorMessageOf(e) }]);
1652
- for (const f of failures) {
1653
- recordFailOpen("server.workspace-space.reclaim-failed", `lane=${f.lane} stage=${f.stage}`);
1654
- config.logger?.warn("workspace_space_reclaim_failed", {
1655
- space: incarnationLogPrefix(spaceKey),
1656
- sessionId,
1657
- lane: f.lane,
1658
- stage: f.stage,
1659
- error: redactIncarnations(f.detail),
1660
- effect: `the ended space's ${f.lane === "space" ? "workspace snapshots" : "park snapshots"} may stay in the bucket (unreadable through the API; the bucket's lifecycle rule is the fallback) — the deletion it belongs to went ahead`,
1807
+ if (config.s3Snapshot !== undefined) {
1808
+ const failures = await env.reclaimSpaceSnapshots(spaceKey, deadline).catch((e) => [{ lane: "space", stage: "delete", detail: errorMessageOf(e) }]);
1809
+ failures.forEach((f, pos) => {
1810
+ recordFailOpen("server.workspace-space.reclaim-failed", `lane=${f.lane} stage=${f.stage}`);
1811
+ tallyFailure(objectFailures, `${f.lane}/${f.stage}`, at, pos, { sessionId, spaceKey, failure: f });
1661
1812
  });
1662
1813
  }
1663
- }
1814
+ })));
1815
+ for (const { count, sample } of failureClassesInOrder(volumeFailures)) {
1816
+ config.logger?.warn("workspace_cache_reclaim_failed", {
1817
+ reason: sample.failure.reason,
1818
+ count,
1819
+ space: incarnationLogPrefix(sample.spaceKey),
1820
+ label: workspaceCacheClaimOf(sample.spaceKey).label,
1821
+ error: redactIncarnations(sample.failure.detail),
1822
+ effect: `the ended space's cache volume stays — delete it by label ${WORKSPACE_CACHE_SPACE_LABEL}=<label> (the deletion it belongs to went ahead)${count > 1 ? `; ${count} spaces of this call met the same failure, the line names the first` : ""}`,
1823
+ });
1824
+ }
1825
+ for (const { count, sample } of failureClassesInOrder(objectFailures)) {
1826
+ const { sessionId, spaceKey, failure: f } = sample;
1827
+ config.logger?.warn("workspace_space_reclaim_failed", {
1828
+ space: incarnationLogPrefix(spaceKey),
1829
+ sessionId,
1830
+ lane: f.lane,
1831
+ stage: f.stage,
1832
+ count,
1833
+ error: redactIncarnations(f.detail),
1834
+ effect: `the ended space's ${f.lane === "space" ? "workspace snapshots" : "park snapshots"} may stay in the bucket (unreadable through the API; the bucket's lifecycle rule is the fallback) — the deletion it belongs to went ahead${count > 1 ? `; ${count} spaces of this call met the same failure, the line names the first` : ""}`,
1835
+ });
1664
1836
  }
1837
+ const threw = settled.find((r) => r.status === "rejected");
1838
+ if (threw)
1839
+ throw threw.reason;
1665
1840
  },
1666
1841
  };
1667
1842
  }
@@ -93,6 +93,8 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
93
93
  private destroyed;
94
94
  /** In-flight connect, memoized so concurrent lazy first-use starts exactly one container (no double-run). */
95
95
  private connecting?;
96
+ /** 家目录:环境答了而与声明不一致 ⇒ 这只环境的拒,记一次、此后每次连接原样回它(同一镜像同一答案:不再起容器、不再问)。 */
97
+ private homeRefusal?;
96
98
  constructor(config: LocalDockerEnvConfig, deps?: LocalDockerEnvDeps);
97
99
  private resolve;
98
100
  workspaceHandle(): WorkspaceHandle;
@@ -104,6 +106,12 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
104
106
  error: RemoteExecutionError;
105
107
  }>;
106
108
  private doConnect;
109
+ /**
110
+ * 家目录:声明在场(`homeDir`,只来自 `REMOTE_EXEC_HOME_DIR` —— 本车道没有自己的缺省)⇒ 在本容器里问一次 `$HOME`(与命令同一条 `docker exec` 通道、
111
+ * 同一份 `-e` env),交唯一判官 `judgeLaneHome`。环境答了而不一致 / 不是绝对路径 ⇒ 回拒并记在环境上(同一镜像同一答案:此后每次连接原样回它,不再起容器、不再问);
112
+ * 没答上(exec 失败 / 超时 / 非零退出)⇒ 这一次同样不开,但不记(与任何一次起容器失败同:下一次连接重问);调用方中止 ⇒ `aborted`、不记。没声明 ⇒ 不问(`undefined`)。容器由调用方收。
113
+ */
114
+ private homeVerdict;
107
115
  suspendVM(_options?: VmLifecycleOptions): Promise<{
108
116
  ok: true;
109
117
  value: SnapshotId;
@@ -183,6 +191,12 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
183
191
  suffix?: string;
184
192
  abortSignal?: AbortSignal;
185
193
  }): Promise<Result<string, FileError>>;
194
+ /**
195
+ * 交给宿主 `docker` CLI 的 env —— 宿主子进程 env 的唯一组合(`server-secret-env.ts` `hostSubprocessEnv`),继承词 `"all"`:本服务自有凭据(起服声明的名与值)
196
+ * 任何词都不交,CLI 与它拉起的凭据助手 / CLI 插件要读的变量(`DOCKER_*` / `AWS_*` …)照交 —— 形状剥会剥 `AWS_SECRET_ACCESS_KEY` 一类,ECR / GCR 凭据助手因此断。
197
+ * 命令本身跑在容器里,容器的 env 只由 `execArgv` 的 `-e K=V` 显式给,本 env 到不了容器。每次起进程现算一次(与 host shell 同)。
198
+ */
199
+ private cliEnv;
186
200
  /** Prepend `-H <dockerHost>` to a docker argv when configured (per-invocation, not the process env). */
187
201
  private withHostFlag;
188
202
  /** Build the `docker exec` argv for a command, applying cwd (`-w`) and per-command env (`-e K=V`).
@@ -198,8 +212,13 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
198
212
  * ⇒ 保 sh(修前字节形,诚实降级);探测自身失败同。容器**创建**引导命令(mkdir && tail)不走
199
213
  * 本 shell——无 bash 语法且探测尚不可能。 */
200
214
  private containerShell;
201
- private containerShellProbed;
215
+ /** shell 探测的**一只在飞承诺**:并发的调用方(首用 exec / fs、connect、reconnect)都 join 它 —— 修前是先置真的布尔,第二个调用方在探测落定之前就拿 /bin/sh 往下走,
216
+ * 探测随后才把命令切到 bash(家目录对证因此可能问的是另一只 shell)。 */
217
+ private containerShellReady?;
218
+ /** 等一只共享的承诺,调用方的取消只退**它自己的**等待(承诺照常落定、照常服务其余等待者)。`false` = 取消了。 */
219
+ private untilOrAborted;
202
220
  private probeContainerShell;
221
+ private detectContainerShell;
203
222
  private ensureConnectedExec;
204
223
  /** Lazy connect for a FileSystem path → FileError on failure (fail closed, never throws). */
205
224
  private fsReady;