@sema-agent/server 7.43.0 → 7.44.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 (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -3,28 +3,16 @@ import { pgSafeJsonStringify, pgSanitizeText } from "./pg-safe-json.js";
3
3
  import { isMysqlDupKeyError } from "./sql-errors.js";
4
4
  export const BACKGROUND_AGENT_TABLE = "background_agent";
5
5
  const SUMMARY_COLS = "handle, owner, session_scoped, session_id, parent_session_id, root_session_id, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json";
6
- /** put/update 共用的投影列(scope_key/handle 键列另拼;record_json 恒最后)。core 1.383([1565] 提货单)
7
- * `updateIf` 的 CAS 守卫需要 `parked_checkpoint_token`/`park_claim_id` 是**专用列**——守卫条件必须在
8
- * 数据库层面对当前行状态原子求值,不能只靠 record_json 这个 blob(那要求先读后写,读写之间就是
9
- * CAS 本要堵的竞态窗)。`writerEpoch` 不在此列:updateIf 的 guard 参数集里没有它([1565] 原文/core
10
- * 类型签名逐字核对过),只是 record_json 全量对象序列化里随手带上的普通字段,不需要专用列——
11
- * record_json 的写法是 `json(stored)`(stored = `{...record, handle, scope, rev}` 整个对象展开),
12
- * 任何 record 上有的字段(含 writerEpoch)天然随之落盘/读回,无需逐字段维护投影列表。 */
13
6
  const PROJ_COLS = "scope, owner, session_scoped, session_id, parent_session_id, root_session_id, session_anchor, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json, rev, parked_checkpoint_token, park_claim_id, admitted_org_scopes, admitted_org_write_scope, record_json";
14
- const PROJ_N = PROJ_COLS.split(", ").length; // F5: WHERE 占位符序号从列数派生,列序变更单点同步
7
+ const PROJ_N = PROJ_COLS.split(", ").length;
15
8
  function assertScope(record) {
16
9
  if (record.scope === undefined || record.scope === "") {
17
- // core InMemory 同文案(default-deny 谓词下无 scope 行=经 fallback 面公共可读,写入即拒)
18
10
  throw new Error("BackgroundAgentStore.put: refusing a row without a scope (world-readable through the fallback)");
19
11
  }
20
12
  }
21
- /** stored 形归一(core put/update 语义:handle/scope/rev 以参数为准,record_json 存归一后的整行)。 */
22
13
  function storedForm(record, handle, scope, rev) {
23
14
  return { ...record, handle, scope, rev };
24
15
  }
25
- /** 投影参数(键列除外,与 PROJ_COLS 同序)。json 序列化器/展示文本清洗器按方言注入([1439]:PG
26
- * lossy 标记,TiDB verbatim)。展示列截到列宽(#3:超长 name 不该让整行 durable 写失败——真源在
27
- * record_json,摘要投影截断是诚实形)。 */
28
16
  function projParams(stored, json, text) {
29
17
  return [
30
18
  stored.scope,
@@ -44,20 +32,11 @@ function projParams(stored, json, text) {
44
32
  stored.rev,
45
33
  stored.parkedCheckpointToken ?? null,
46
34
  stored.parkClaimId ?? null,
47
- // core 5.14.0 #22([2854] 提货单⑤):每条腿在 injector-ready 栅上写下自己被裁定的 org 准入结果。
48
- // 🔴 三态必须区分,这是 core 契约试剂盒硬钉的那条:**缺席(undefined)≠ 空数组**。
49
- // · 字段缺席 ⇒ 列 NULL(「没有记录」)。老行免回填,读空即零准入 fail-closed。
50
- // · `[]` / `null` ⇒ 列上是 JSON "[]" / 真 NULL 写域 —— 「裁定过,结果是什么都没有」。
51
- // 两列是**写侧投影**(真源恒是 record_json,`get()` 只读它)——语义上不可能漂移:缺席的键根本
52
- // 不进 JSON.stringify,所以「缺席读回缺席」由真源结构保证,不靠这两列。
53
- // 📌 记账(诚实清单):本仓今天**没有**读这两列的消费者;它们存在是为了让 org 治理审计能按列
54
- // 查询(「哪些后台子代拿到过 org:acme 的写权」),不必全表扫 JSON。谁要接,先上黑板对表。
55
35
  stored.admittedOrgScopes !== undefined ? json(stored.admittedOrgScopes) : null,
56
36
  stored.admittedOrgWriteScope !== undefined ? stored.admittedOrgWriteScope : null,
57
37
  json(stored),
58
38
  ];
59
39
  }
60
- /** 摘要行 → core BackgroundAgentRowSummary(缺席键省略,core toSummary 形逐字;usage 从 usage_json)。 */
61
40
  function rowToSummary(r) {
62
41
  return {
63
42
  handle: String(r.handle),
@@ -77,11 +56,6 @@ function rowToSummary(r) {
77
56
  : {}),
78
57
  };
79
58
  }
80
- // 共享归因句:core 5.4.0 起上包根([2436] 请求→[2437] 兑现),值拷贝退役——同一常量,双生与
81
- // InMemory 语义源头彻底同源;试剂盒条目仍逐字断言它(真双库),但漂移自此在编译期就不可能发生。
82
- /** core reap 的 staleRunning 翻转形(逐字段照抄 InMemory,background-agent-store.js:266-272):
83
- * summary **和 error** 都只在缺席时补共享归因句——error 半边 5.2.0 起就该有,双生此前只写 summary
84
- * 且是手写旧句(core 5.3.0 新试剂盒首跑两方言一致揪红,[2425] bg-agent B 族同源),真失败文本永不覆写。 */
85
59
  function flippedForm(live, now) {
86
60
  return {
87
61
  ...live,
@@ -93,7 +67,6 @@ function flippedForm(live, now) {
93
67
  updatedAt: now,
94
68
  };
95
69
  }
96
- /** terminal doomed 集合(core reap 的 maxAge + keep 两策略;rows 已按 spawnedAt DESC)。 */
97
70
  function doomedHandles(rows, now, opts) {
98
71
  const doomed = new Set();
99
72
  if (opts.maxAgeMs !== undefined) {
@@ -108,11 +81,6 @@ function doomedHandles(rows, now, opts) {
108
81
  }
109
82
  return doomed;
110
83
  }
111
- /** core 1.383([1565] 提货单,design/153 件3a)`updateIf` 的三个可选守卫条件——dialect-neutral 描述,
112
- * 各方言自行渲染成 `?`(TiDB)或 `$N`(PG)占位符。core InMemoryBackgroundAgentStore.updateIf 逐字同源
113
- * (background-agent-store.js:175-187)语义:`value === null` = 该列当前**必须缺席**(SQL `IS NULL`,
114
- * 对应 core `live.X !== undefined` 的取反);`value` 是字符串 = 必须**字面相等**;字段整体缺席
115
- * (expect.X === undefined,不在返回数组里)= 不设该条守卫,与该列当前值无关。 */
116
84
  function parkGuardConditions(expect) {
117
85
  const out = [];
118
86
  if (expect.status !== undefined)
@@ -127,38 +95,12 @@ function reapInert(opts) {
127
95
  return !opts || (opts.maxAgeMs === undefined && opts.keep === undefined && opts.staleRunningMaxAgeMs === undefined);
128
96
  }
129
97
  function limitClause(limit) {
130
- // #2 病态边界与 core slice 语义对齐:Infinity=全量(无 LIMIT),NaN/-Infinity=空(LIMIT 0)
131
98
  if (limit === undefined || limit === Infinity)
132
99
  return "";
133
100
  const n = Math.floor(limit);
134
101
  return ` LIMIT ${Number.isFinite(n) ? Math.max(0, n) : 0}`;
135
102
  }
136
- /* ┌─ 历史:曾经的运行期 ADD COLUMN 迁移腿(已折进 CREATE,2026-07-26 clay 裁「删库重建」)──────────┐
137
- * 两个 ensure 曾在每次 boot 给 `background_agent` 补三列;现已全部折叠为 CREATE TABLE 里的列定义。
138
- * 保留由来,因为它解释了这些列**为什么存在**,以及当年为什么 TiDB 腿用探针而不是 `IF NOT EXISTS`:
139
- *
140
- * ① root_session_id(core 1.367 δ,additive):1.248 建的存量表没有这列,靠迁移补。
141
- * ② parked_checkpoint_token / park_claim_id(core 1.383,[1565] 提货单 / design/153 件3a,ε,additive):
142
- * 1.382 前建的表没有这两列。它们**必须是专用列**(不能只躺在 record_json 里)的理由见 {@link PROJ_COLS}
143
- * 顶注:`updateIf` 的 CAS 守卫要在数据库层面对当前行状态原子求值。
144
- *
145
- * ③ 为什么 TiDB 腿曾是 `information_schema.COLUMNS` **探针 + ALTER** 两步,而不是一条
146
- * `ADD COLUMN IF NOT EXISTS`(复审 F2):**MySQL 8 没有该语法**(TiDB/MariaDB 才有),而本仓
147
- * MySQL-protocol 口径要求可移植——tidb-pool.ts COLUMN_MIGRATIONS 同姿势。PG 腿则本来就用 IF NOT EXISTS。
148
- * ⚠️ 这个探针姿势有一个真实缺陷([1522] LOW5,当年如实记并判定可接受):探针→ALTER **不是原子的**,
149
- * 多副本并发首 boot 存在竞态窗——两个副本双双探到缺列、一方 ALTER 赢、另一方 ALTER 撞
150
- * 「column exists」砖**当次** boot;下次 boot 探到列在即自愈。
151
- * → 折叠后该窗**消失**:新库列由 CREATE TABLE 一次建全,不存在「探到缺列再补」的第二步;
152
- * `CREATE TABLE IF NOT EXISTS` 本身在服务端是单条原子 DDL,并发首 boot 只有「建了」/「已存在」两态。
153
- *
154
- * ④ 语义位:当年 `root_session_id IS NULL` 兼有「pre-δ 行」一义。删库重建后**不再产生**这类行——NULL
155
- * 只剩「该记录没有 root 锚」一义。读侧因此**仍然可达**、一律不动:{@link rowToSummary} 的
156
- * root_session_id 条件展开、listBySession 的 `root_session_id = ?` 第二臂(NULL 行恒不命中)、
157
- * 以及 parked 两列的 `IS NULL` 守卫({@link parkGuardConditions}:那是 core 的 CAS 语义,不是迁移遗留)。
158
- * └────────────────────────────────────────────────────────────────────────────────────────────┘ */
159
103
  export async function ensureTiDBBackgroundAgentSchema(pool) {
160
- // VARBINARY 键列(roster F2 同案):scope/handle 的 '=' 必须是纯字节——utf8mb4_bin 仍 PAD SPACE,
161
- // 'x' 与 'x ' 会同键。record_json LONGTEXT:终报快照(finalOutput/recentSteps)可超 64K TEXT。
162
104
  await pool.query(`CREATE TABLE IF NOT EXISTS ${BACKGROUND_AGENT_TABLE} (
163
105
  scope_key VARBINARY(190) NOT NULL,
164
106
  handle VARBINARY(190) NOT NULL,
@@ -195,7 +137,6 @@ export async function ensureTiDBBackgroundAgentSchema(pool) {
195
137
  ) COLLATE utf8mb4_bin`);
196
138
  }
197
139
  export async function ensurePgBackgroundAgentSchema(q) {
198
- // COLLATE "C"(roster F2 双方言对齐):隔离键列的 '=' 必须字节等价。
199
140
  await q(`CREATE TABLE IF NOT EXISTS ${BACKGROUND_AGENT_TABLE} (
200
141
  scope_key VARCHAR(190) COLLATE "C" NOT NULL,
201
142
  handle VARCHAR(190) COLLATE "C" NOT NULL,
@@ -239,8 +180,6 @@ export class TiDBBackgroundAgentStore {
239
180
  await this.pool.query(`INSERT INTO ${BACKGROUND_AGENT_TABLE} (scope_key, handle, ${PROJ_COLS}) VALUES (${Array.from({ length: PROJ_N + 2 }, () => "?").join(", ")})`, [stored.scope, stored.handle, ...projParams(stored, JSON.stringify, (t) => t)]);
240
181
  }
241
182
  catch (e) {
242
- // 判据属主 = `sql-errors.ts`(A-032 P1-①:旧形只认 `code`,只带 errno 的驱动错误会让
243
- // `agent_record.already_exists` 这条 typed 分流腿静默失效)。
244
183
  if (isMysqlDupKeyError(e)) {
245
184
  throw new BackgroundAgentStoreError("agent_record.already_exists", "agent record already exists");
246
185
  }
@@ -257,9 +196,6 @@ export class TiDBBackgroundAgentStore {
257
196
  WHERE scope_key = ? AND handle = ? AND rev = ?`, [...projParams(stored, JSON.stringify, (t) => t), scope, handle, expect.rev]));
258
197
  return res.affectedRows === 1;
259
198
  }
260
- /** core 1.383([1565] 提货单)REQUIRED 新方法——原子守卫 CAS(claim 的 reservation/rollback/finalize
261
- * 绑定字段态非仅 rev:rev 可因无关写移动而字段义已变)。语义逐字对齐 core InMemoryBackgroundAgentStore
262
- * ({@link parkGuardConditions} 顶注)。 */
263
199
  async updateIf(handle, scope, record, expect) {
264
200
  const stored = storedForm(record, handle, scope, expect.rev + 1);
265
201
  const guards = parkGuardConditions(expect);
@@ -283,18 +219,6 @@ export class TiDBBackgroundAgentStore {
283
219
  const [res] = (await this.pool.query(`DELETE FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND handle = ?${expect !== undefined ? " AND rev = ?" : ""}`, expect !== undefined ? [scope, handle, expect.rev] : [scope, handle]));
284
220
  return res.affectedRows === 1;
285
221
  }
286
- /** 🔴 返回值语义 = **受影响的不同行数**,不是「翻转数 + 删除数」的和(core 1.411 起)。
287
- *
288
- * core 1.411 把自己 InMemory 的 `return touched + doomed.size` 改成
289
- * `return new Set([...flippedKeys, ...doomed]).size` —— 因为一行可能在**同一轮** reap 里既被翻转
290
- * (stale running → failed)、又因为翻转后变成终态而落进 maxAge/keep 的删除集,旧写法把它算了两次。
291
- * 这条被真双库对拆套件当场抓到(升 core 1.411 后 `tidb: reaped 计数: expected 4 to be 3`,core 1.404
292
- * 下同文件全绿 —— A/B 判明是 core 的语义修,我们跟上)。
293
- *
294
- * ⚠️ 两个引擎**机制不同但语义一致**:PG 有 `DELETE … RETURNING handle`,可以单语句拿到真被删的 handle
295
- * 直接并入同一个 Set;TiDB/MySQL **没有 RETURNING**,`affectedRows` 只给数量不给身份,所以按「是否已在
296
- * 翻转集里」把 doomed 拆两批删 —— 重叠那批照样真删、只是不再计数。两边都不能用「doomed.size 当删除数」
297
- * 近似:并发写者(真心跳复活)会让 `status <> 'running'` 守卫少删几行,那时 doomed.size 会虚高。 */
298
222
  async reap(scope, now, opts) {
299
223
  if (reapInert(opts))
300
224
  return 0;
@@ -303,22 +227,14 @@ export class TiDBBackgroundAgentStore {
303
227
  const [stale] = (await this.pool.query(`SELECT handle, record_json FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND status = 'running' AND updated_at_ms < ?`, [scope, now - opts.staleRunningMaxAgeMs]));
304
228
  for (const row of stale) {
305
229
  const live = JSON.parse(String(row.record_json));
306
- // rev-CAS 翻转:并发写者(真心跳复活/别副本先翻)赢了就跳过不计——core 单进程无此窗,SQL 侧诚实
307
230
  if (await this.update(String(row.handle), scope, flippedForm(live, now), { rev: live.rev }))
308
231
  flipped.add(String(row.handle));
309
232
  }
310
233
  }
311
- // core 1.382([1561] 提货单③;InMemoryBackgroundAgentStore.reap 逐字同源,background-agent-store.js:206
312
- // `r.status !== "running" && r.status !== "parked"`):doomed 候选池排除 parked——parked 是非活非终态
313
- // (durably suspended pending approval),`doomedHandles` 只按 spawnedAt/settledAt 判定年龄/keep 排名,
314
- // 完全不看 status,所以排除必须在这条 SQL WHERE 里做到,不能指望 JS 侧再筛一次(它收到的行里根本
315
- // 没带 status 字段)。
316
234
  const [term] = (await this.pool.query(`SELECT handle, spawned_at_ms, settled_at_ms FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND status <> 'running' AND status <> 'parked' ORDER BY spawned_at_ms DESC, handle DESC`, [scope]));
317
235
  const doomed = doomedHandles(term.map((r) => ({ handle: String(r.handle), spawnedAt: Number(r.spawned_at_ms), settledAt: r.settled_at_ms === null ? null : Number(r.settled_at_ms) })), now, opts);
318
236
  if (doomed.size === 0)
319
237
  return flipped.size;
320
- // 去重(见方法头注):本轮已翻转过的 handle 若同时落进 doomed,删它但**不再计数**。
321
- // 无 RETURNING ⇒ 拆两批发,靠「哪一批」而不是靠返回的身份来分辨。
322
238
  const alreadyCounted = [...doomed].filter((h) => flipped.has(h));
323
239
  const newlyAffected = [...doomed].filter((h) => !flipped.has(h));
324
240
  const del = async (handles) => {
@@ -327,16 +243,10 @@ export class TiDBBackgroundAgentStore {
327
243
  };
328
244
  const deletedNew = newlyAffected.length > 0 ? await del(newlyAffected) : 0;
329
245
  if (alreadyCounted.length > 0)
330
- await del(alreadyCounted); // 真删,计数已由 flipped 承担
246
+ await del(alreadyCounted);
331
247
  return flipped.size + deletedNew;
332
248
  }
333
- /** core 1.368([1516]②)可选接口成员落位(原 server 扩展 listReapScopes 更名对齐):当前持行
334
- * scope 枚举——reaper 的 per-scope 联合 reap 入口。JS 侧 sort 与 core InMemory 逐字节同序
335
- * (SQL ORDER BY 按 collation,非码元序)。 */
336
249
  async listScopes() {
337
- // [1522] MED1(两路复审撞车坐实):枚举必须走 **scope_key 字节列**——utf8mb4_bin 的 VARCHAR DISTINCT
338
- // 仍 PAD SPACE,尾空格近撞 scope 被塌缩成一个,塌缩掉的分区永不被 reap(行/转录无界)。键纪律
339
- // 贯彻到枚举面;VARBINARY 读回 Buffer → utf8 归一。
340
250
  const [rows] = (await this.pool.query(`SELECT DISTINCT scope_key FROM ${BACKGROUND_AGENT_TABLE}`));
341
251
  return rows.map((r) => (Buffer.isBuffer(r.scope_key) ? r.scope_key.toString("utf8") : String(r.scope_key))).sort();
342
252
  }
@@ -349,8 +259,6 @@ export class PgBackgroundAgentStore {
349
259
  async put(record) {
350
260
  assertScope(record);
351
261
  const stored = storedForm(record, record.handle, record.scope, record.rev ?? 0);
352
- // create-once:ON CONFLICT DO NOTHING + RETURNING——空 rows = 键已存在(PgQueryFn 无 rowCount,
353
- // RETURNING 是双方言里 PG 侧的 affected 判别;错误码路径留给真障碍)。
354
262
  const { rows } = await this.q(`INSERT INTO ${BACKGROUND_AGENT_TABLE} (scope_key, handle, ${PROJ_COLS})
355
263
  VALUES (${Array.from({ length: PROJ_N + 2 }, (_, i) => `$${i + 1}`).join(", ")})
356
264
  ON CONFLICT (scope_key, handle) DO NOTHING RETURNING handle`, [stored.scope, stored.handle, ...projParams(stored, pgSafeJsonStringify, pgSanitizeText)]);
@@ -368,13 +276,11 @@ export class PgBackgroundAgentStore {
368
276
  WHERE scope_key = $${PROJ_N + 1} AND handle = $${PROJ_N + 2} AND rev = $${PROJ_N + 3} RETURNING handle`, [...projParams(stored, pgSafeJsonStringify, pgSanitizeText), scope, handle, expect.rev]);
369
277
  return rows.length === 1;
370
278
  }
371
- /** core 1.383([1565] 提货单)REQUIRED 新方法(TiDB twin 同案注释)。PG 侧占位符是显式编号 `$N`
372
- * (不像 TiDB `?` 靠数组顺序)——`n` 是跑动计数器,只在真正消耗一个占位符(非 IS NULL 分支)时前增。 */
373
279
  async updateIf(handle, scope, record, expect) {
374
280
  const stored = storedForm(record, handle, scope, expect.rev + 1);
375
281
  const setParams = projParams(stored, pgSafeJsonStringify, pgSanitizeText);
376
282
  const guards = parkGuardConditions(expect);
377
- let n = setParams.length + 3; // + scope_key/handle/rev,三条基础守卫已固定占用 setParams.length+1..+3
283
+ let n = setParams.length + 3;
378
284
  const guardSql = guards.map((g) => (g.value === null ? `${g.column} IS NULL` : `${g.column} = $${++n}`)).join(" AND ");
379
285
  const guardParams = guards.filter((g) => g.value !== null).map((g) => g.value);
380
286
  const { rows } = await this.q(`UPDATE ${BACKGROUND_AGENT_TABLE} SET ${PROJ_COLS.split(", ").map((c, i) => `${c} = $${i + 1}`).join(", ")}
@@ -395,9 +301,6 @@ export class PgBackgroundAgentStore {
395
301
  const { rows } = await this.q(`DELETE FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1 AND handle = $2${expect !== undefined ? " AND rev = $3" : ""} RETURNING handle`, expect !== undefined ? [scope, handle, expect.rev] : [scope, handle]);
396
302
  return rows.length === 1;
397
303
  }
398
- /** 返回值 = 受影响的**不同行数**(core 1.411 语义;TiDB twin 的方法头注写了完整由来 + A/B 取证)。
399
- * PG 这半有 `DELETE … RETURNING handle`,所以真被删的 handle 直接并进同一个 Set —— 不需要 TiDB 那边的
400
- * 「拆两批发」变通,但两边的**结果**必须逐个相等(真双库对拆套件按 core InMemory 的计数逐字比)。 */
401
304
  async reap(scope, now, opts) {
402
305
  if (reapInert(opts))
403
306
  return 0;
@@ -410,20 +313,16 @@ export class PgBackgroundAgentStore {
410
313
  affected.add(String(row.handle));
411
314
  }
412
315
  }
413
- // core 1.382([1561] 提货单③;TiDB twin 同案注释)——doomed 候选池排除 parked。
414
316
  const { rows: term } = await this.q(`SELECT handle, spawned_at_ms, settled_at_ms FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1 AND status <> 'running' AND status <> 'parked' ORDER BY spawned_at_ms DESC, handle DESC`, [scope]);
415
317
  const doomed = doomedHandles(term.map((r) => ({ handle: String(r.handle), spawnedAt: Number(r.spawned_at_ms), settledAt: r.settled_at_ms === null ? null : Number(r.settled_at_ms) })), now, opts);
416
318
  if (doomed.size === 0)
417
319
  return affected.size;
418
320
  const { rows: deleted } = await this.q(`DELETE FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1 AND status <> 'running' AND status <> 'parked' AND handle IN (${[...doomed].map((_, i) => `$${i + 2}`).join(", ")}) RETURNING handle`, [scope, ...doomed]);
419
321
  for (const r of deleted)
420
- affected.add(String(r.handle)); // 同一 Set ⇒ 翻转+删除的同一行只算一次
322
+ affected.add(String(r.handle));
421
323
  return affected.size;
422
324
  }
423
- /** core 1.368 可选接口成员(TiDB twin 同案注释)。 */
424
325
  async listScopes() {
425
- // [1522] MED1(TiDB twin 同案注):枚举走 COLLATE "C" 的 scope_key 键列——非确定性缺省 collation
426
- // 部署下 VARCHAR DISTINCT 同构塌缩,键列字节确定。
427
326
  const { rows } = await this.q(`SELECT DISTINCT scope_key FROM ${BACKGROUND_AGENT_TABLE}`);
428
327
  return rows.map((r) => String(r.scope_key)).sort();
429
328
  }
@@ -1,44 +1,16 @@
1
- /**
2
- * design/103 — shared support for the remote (TOB) `BackgroundShellCapability` implementations.
3
- *
4
- * core defines the seam (`@sema-agent/core` `BackgroundShellCapability` + `hasBackgroundShell`) and ships the TOC
5
- * reference (`NodeExecutionEnv`). This module centralises the **security + lifecycle red-lines** that EVERY remote
6
- * adapter (E2B, kata/k8s, …) must enforce identically, so a transport author only writes the 4 transport
7
- * primitives and cannot accidentally re-implement (or skip) an invariant:
8
- *
9
- * - **opaque shellId** — a fresh `bg_<n>_<uuid>` brand, NEVER derived from the provider's job/pid (越权红线,
10
- * design/103 §3.8). The provider id lives only inside the driver state.
11
- * - **owned-id isolation** — `poll`/`kill` look the shellId up in THIS manager's registry; an id from another env
12
- * (or a forged one) is `not_found`, never resolved against the provider (design/103 §3.8 / interface 越权契约).
13
- * - **maxConcurrent** — counts only `running` shells (terminal entries are kept for residual polling but free their
14
- * slot, so "TaskStop one first" is actionable — matches the TOC `maxConcurrent 终态占槽` review fix).
15
- * - **timeout hard wall** — a control-plane timer kills the shell and flips it to `killed` at the bounded BG
16
- * timeout (design/103 §3.6); the driver may set an additional provider-level backstop ≥ this wall.
17
- * - **dispose** — kills + cleans up EVERY shell, best-effort, MUST NOT throw, idempotent (design/103 §3.7).
18
- *
19
- * The two transports legitimately take different "按-id-可重读" paths (design/103 §5.2): E2B holds a resident
20
- * `CommandHandle` reader feeding a control-plane tail buffer (path ②); kata/k8s buffers into pod-side files
21
- * re-read by byte cursor on each poll (path ①). Both are expressed through {@link BackgroundShellDriver}.
22
- */
23
1
  import { BackgroundShellError } from "@sema-agent/core";
24
2
  import { randomUUID } from "node:crypto";
25
3
  import { StringDecoder } from "node:string_decoder";
26
4
  const ok = (value) => ({ ok: true, value });
27
5
  const fail = (error) => ({ ok: false, error });
28
- /** Fresh drain buffer pre-seeded with the output captured before adoption (rides the first poll). `initialDropped` =
29
- * bytes the fg segment's rolling-tail already evicted (surfaced as `truncated` on the first drain — the seed text is
30
- * RAW, so the truncation marker is NOT baked into the stream content: review LOW, avoids double-marking + accounting
31
- * pollution). */
32
6
  export function seedMemStream(text, initialDropped = 0) {
33
7
  const n = Buffer.byteLength(text, "utf8");
34
8
  return { chunks: text ? [text] : [], pendingBytes: n, totalBytes: n, droppedBytes: initialDropped };
35
9
  }
36
- /** Append a live chunk, evicting the OLDEST queued chunks beyond `cap` (counted → surfaced as `truncated`). A Buffer
37
- * goes through the per-stream StringDecoder (multibyte-safe across chunk boundaries); a string passes through. */
38
10
  export function feedMemStream(s, d, cap) {
39
11
  const text = typeof d === "string" ? d : (s.decoder ??= new StringDecoder("utf8")).write(d);
40
12
  if (!text)
41
- return; // decoder buffered an incomplete-codepoint tail → nothing to enqueue this frame
13
+ return;
42
14
  const n = Buffer.byteLength(text, "utf8");
43
15
  s.chunks.push(text);
44
16
  s.pendingBytes += n;
@@ -49,21 +21,14 @@ export function feedMemStream(s, d, cap) {
49
21
  s.pendingBytes -= db;
50
22
  s.droppedBytes += db;
51
23
  }
52
- // A SINGLE chunk larger than the cap (a giant WS frame / one enormous SDK string) is not covered by the
53
- // drop-oldest loop above (it never evicts the last chunk) — trim its HEAD so the retained bytes stay ~cap
54
- // instead of max(cap, largestChunk) (the cap comment otherwise promised more than the
55
- // code delivered). Byte-slice like RollingTailBuffer's partial head-drop: a multibyte codepoint cut at the trim
56
- // point decodes to replacement char(s) at the very start of the retained tail (may exceed cap by a few bytes on
57
- // re-measure) — harmless, the stream is already marked truncated.
58
24
  if (s.pendingBytes > cap && s.chunks.length === 1) {
59
25
  const only = Buffer.from(s.chunks[0], "utf8");
60
26
  const over = only.length - cap;
61
27
  s.chunks[0] = only.subarray(over).toString("utf8");
62
- s.pendingBytes = Buffer.byteLength(s.chunks[0], "utf8"); // re-measure: the cut may add a replacement char
28
+ s.pendingBytes = Buffer.byteLength(s.chunks[0], "utf8");
63
29
  s.droppedBytes += over;
64
30
  }
65
31
  }
66
- /** Drain everything queued since the last read (a driver `read`); resets the pending queue + dropped counter. */
67
32
  export function drainMemStream(s) {
68
33
  const text = s.chunks.join("");
69
34
  s.chunks = [];
@@ -72,17 +37,6 @@ export function drainMemStream(s) {
72
37
  s.droppedBytes = 0;
73
38
  return { text, dropped };
74
39
  }
75
- /**
76
- * Drives the {@link BackgroundShellCapability} contract over a {@link BackgroundShellDriver}. One instance per env
77
- * (its registry == that env's owned shells), so cross-env / forged ids never resolve.
78
- */
79
- /**
80
- * Recent terminal (exited/killed/failed) shells are kept pollable for residual output until dispose (design/103),
81
- * but an unbounded run that spawns thousands of short background shells would otherwise accumulate them — and each
82
- * E2B entry pins an 8MB tail buffer — until the task ends (workflow MED, worker-heap bound). Cap how many terminal
83
- * entries linger: evicting the OLDEST → a later poll of it is `not_found` ("already reaped"), which the BashOutput
84
- * tool already handles gracefully.
85
- */
86
40
  const RETAIN_TERMINAL = 32;
87
41
  export class BackgroundShellManager {
88
42
  driver;
@@ -93,12 +47,9 @@ export class BackgroundShellManager {
93
47
  this.driver = driver;
94
48
  this.caps = caps;
95
49
  }
96
- /** [1116] 六轮复审:destroy 端判「还有 shell 依赖 spool 文件吗」——running(驻留 keep-alive)与
97
- * terminal 残余(dispose 前仍可轮询)都算;有=env 不得整树删 spool 根(逐 shell 清理归 dispose 面)。 */
98
50
  hasShells() {
99
51
  return this.shells.size > 0;
100
52
  }
101
- /** Count live shells; KillShell/timeout flip a shell out of `running` and free its slot. */
102
53
  liveCount() {
103
54
  let n = 0;
104
55
  for (const e of this.shells.values())
@@ -106,7 +57,6 @@ export class BackgroundShellManager {
106
57
  n++;
107
58
  return n;
108
59
  }
109
- /** Bound heap: evict the OLDEST terminal entries (never a running one) once the registry exceeds the retention cap. */
110
60
  evictOldTerminal() {
111
61
  const max = this.caps.maxConcurrent + RETAIN_TERMINAL;
112
62
  if (this.shells.size <= max)
@@ -115,7 +65,7 @@ export class BackgroundShellManager {
115
65
  if (this.shells.size <= max)
116
66
  break;
117
67
  if (e.status === "running")
118
- continue; // never evict a live shell
68
+ continue;
119
69
  this.shells.delete(id);
120
70
  if (e.timer)
121
71
  clearTimeout(e.timer);
@@ -125,44 +75,17 @@ export class BackgroundShellManager {
125
75
  async spawn(command, options) {
126
76
  return this.register((ctx) => this.driver.launch(command, options, ctx), options?.timeout);
127
77
  }
128
- /**
129
- * design/116 detach (clay 排,1.81): adopt an EXTERNALLY-created RUNNING process as a background shell — the
130
- * mid-flight ctrl+b path, where the process was spawned by a FOREGROUND `exec` and must move under this
131
- * manager's ownership instead of being killed. The `builder` receives the same {@link LaunchCtx} a driver
132
- * `launch` would (terminal notification + the bounded BG timeout it may use as a backstop) and returns the
133
- * driver state the normal `read`/`kill`/`disposeOne` primitives operate on — after adoption the shell is
134
- * indistinguishable from a spawned one (poll/kill/dispose/timeout wall all apply). Returns `undefined` when
135
- * the live-shell limit is hit OR the builder throws: detach REFUSED, the caller keeps the process running in
136
- * the foreground (core NodeExecutionEnv semantics — never a silently dropped process).
137
- *
138
- * 🔴 SYNCHRONOUS by contract (double-review of the first async cut, 3 confirmed findings): the builder must
139
- * attach the live child's pipe listeners in the SAME synchronous tick it snapshots the seed, with NO `await`
140
- * before registration — otherwise (a) output produced during an async open/mkdir window is lost, (b) file fds
141
- * leak on natural exit, and (c) an exec-settle can race the async adoption and orphan a shell. A sync builder
142
- * (memory-buffered, no files) closes all three: no Node data/close event can interleave a synchronous block.
143
- *
144
- * 🔴 builder side-effect contract (Fable-5 review #5): the catch below returns undefined WITHOUT undoing any
145
- * side effects the builder already performed (listener attach, stream reroute). A builder must therefore be
146
- * all-or-nothing: put every operation that can throw BEFORE the first side effect, or clean up on the throw
147
- * path itself. Today's three lane builders are entirely non-throwing (listener attach + object construction).
148
- */
149
- /** #131-T0:两条注册腿共用的 BG 超时钳制。非有限值(NaN/±Infinity)当缺席回退 default——
150
- * Math.max/min 对 NaN 全塌 NaN,setTimeout(NaN) 被 Node 折成 1ms = 后台 shell 秒杀
151
- * (方向反转:想给超时变成即杀;同族判例 TASK_TIMEOUT_SEC / leader 旋钮非法值)。 */
152
78
  boundedBgTimeoutSec(timeoutSec) {
153
79
  const wanted = timeoutSec !== undefined && Number.isFinite(timeoutSec) ? timeoutSec : undefined;
154
- // caps 同座防御:default/max 由各 lane 从常数×cfg 推导(k8s 腿含 cfg.timeoutMs 除法),坏输入会把
155
- // NaN 带进 caps——任何一格非有限即回退安全常数(30min/24h,方向:保「有界但不即杀」,§3.6 不破)。
156
80
  const dflt = Number.isFinite(this.caps.defaultBgTimeoutSec) ? this.caps.defaultBgTimeoutSec : 1_800;
157
81
  const max = Number.isFinite(this.caps.maxBgTimeoutSec) ? this.caps.maxBgTimeoutSec : 86_400;
158
- // Bounded BG timeout — never unbounded (design/103 §3.6). `maxBgTimeoutSec` is the fail-closed ceiling.
159
82
  return Math.min(Math.max(1, wanted ?? dflt), max);
160
83
  }
161
84
  adoptSync(builder, timeoutSec) {
162
85
  if (this.liveCount() >= this.caps.maxConcurrent)
163
- return undefined; // refused → exec keeps the foreground
86
+ return undefined;
164
87
  const bgTimeoutSec = this.boundedBgTimeoutSec(timeoutSec);
165
- const shellId = `bg_${++this.counter}_${randomUUID()}`; // opaque — never the provider pid (§3.8)
88
+ const shellId = `bg_${++this.counter}_${randomUUID()}`;
166
89
  const applyTerminal = (e, failed, exitCode) => {
167
90
  if (e.status !== "running")
168
91
  return;
@@ -172,8 +95,6 @@ export class BackgroundShellManager {
172
95
  if (e.timer)
173
96
  clearTimeout(e.timer);
174
97
  };
175
- // terminal-latch: an already-exited adopted child fires onTerminal DURING the builder (before shells.set) —
176
- // buffer it, drain after registration (else the shell lies "running" until the timeout wall).
177
98
  let entry;
178
99
  let pending;
179
100
  const onTerminal = (failed, exitCode) => {
@@ -184,10 +105,10 @@ export class BackgroundShellManager {
184
105
  };
185
106
  let state;
186
107
  try {
187
- state = builder({ onTerminal, bgTimeoutSec }); // SYNC — no await, no interleaving event
108
+ state = builder({ onTerminal, bgTimeoutSec });
188
109
  }
189
110
  catch {
190
- return undefined; // adoption failed → exec keeps the foreground (never a dropped process)
111
+ return undefined;
191
112
  }
192
113
  const timer = setTimeout(() => {
193
114
  const e = this.shells.get(shellId);
@@ -204,28 +125,21 @@ export class BackgroundShellManager {
204
125
  this.evictOldTerminal();
205
126
  return { shellId };
206
127
  }
207
- /** The shared spawn/adopt body: limit check, opaque id, terminal-latch buffering, BG-timeout hard wall. */
208
128
  async register(launch, timeoutSec) {
209
129
  if (this.liveCount() >= this.caps.maxConcurrent) {
210
130
  return fail(new BackgroundShellError("limit_exceeded", `Too many running background shells (max ${this.caps.maxConcurrent}); TaskStop one first.`));
211
131
  }
212
- const bgTimeoutSec = this.boundedBgTimeoutSec(timeoutSec); // #131-T0:与 adoptSync 同座(NaN 当缺席)
213
- // Opaque brand — NOT derived from the provider job/pid (design/103 §3.8 越权红线).
132
+ const bgTimeoutSec = this.boundedBgTimeoutSec(timeoutSec);
214
133
  const shellId = `bg_${++this.counter}_${randomUUID()}`;
215
134
  const applyTerminal = (e, failed, exitCode) => {
216
135
  if (e.status !== "running")
217
- return; // kill / timeout already classified it (terminal-latch)
136
+ return;
218
137
  e.status = failed ? "failed" : "exited";
219
138
  if (exitCode !== undefined)
220
139
  e.exitCode = exitCode;
221
140
  if (e.timer)
222
141
  clearTimeout(e.timer);
223
142
  };
224
- // 🔴 A FAST-exiting process can settle `handle.wait()` and fire `onTerminal` (a microtask) BEFORE this method's
225
- // post-`launch` continuation registers the entry — so onTerminal MUST buffer into `pendingTerminal` when the
226
- // entry isn't live yet, and the continuation drains it after `shells.set` (else the shell would lie "running"
227
- // until the timeout, wedging a maxConcurrent slot). (An ADOPTED child can equally have already
228
- // exited between the detach request and adoption — the same latch covers it.)
229
143
  let entry;
230
144
  let pendingTerminal;
231
145
  const onTerminal = (failed, exitCode) => {
@@ -245,32 +159,21 @@ export class BackgroundShellManager {
245
159
  const e = this.shells.get(shellId);
246
160
  if (!e || e.status !== "running")
247
161
  return;
248
- e.status = "killed"; // BG timeout is a HARD wall → killed (design/103 §3.6)
162
+ e.status = "killed";
249
163
  void this.driver.kill(e.state).catch(() => { });
250
164
  }, bgTimeoutSec * 1000);
251
165
  timer.unref?.();
252
166
  entry = { state, status: "running", timer };
253
167
  this.shells.set(shellId, entry);
254
168
  if (pendingTerminal)
255
- applyTerminal(entry, pendingTerminal.failed, pendingTerminal.exitCode); // process already exited during launch
256
- this.evictOldTerminal(); // bound the heap (recent terminal shells stay pollable; old ones are reaped)
169
+ applyTerminal(entry, pendingTerminal.failed, pendingTerminal.exitCode);
170
+ this.evictOldTerminal();
257
171
  return ok({ shellId });
258
172
  }
259
173
  async poll(shellId) {
260
174
  const entry = this.shells.get(shellId);
261
- // Owned-id isolation: unknown / cross-env / forged id → not_found, never leak existence (design/103 §3.8).
262
175
  if (!entry)
263
176
  return fail(new BackgroundShellError("not_found", `Unknown background shell: ${shellId}`));
264
- // 🔴 **状态快照必须在 read 之前取**(2026-07-26,一条整天间歇红的 CI 追下来的真缺陷)。
265
- // push 模型的终态是**异步**落地的(host: `child.on("close") → ctx.onTerminal`;E2B: `wait()` resolve),
266
- // 所以「先 read 再读 entry.status」存在这个窗口:
267
- // 读完输出(尾巴还没写完)→ 进程写完尾巴并结束 → status 变终态 → 返回 `{stdout: 缺尾巴, status: 终态}`。
268
- // 而 `BashOutput` 这类消费方**看到终态就停止轮询** ⇒ 那段尾巴此后没有任何人会去读 = **静默丢输出**。
269
- // (单跑时窗口极窄——隔离跑 12/12 全绿;高并行负载下两个 await 之间被调度出去,窗口变宽就复现。)
270
- // ⇒ 只把「read **之前**就已经是终态」或「本次 read **自己**带回 terminal」这两种情况报为终态:
271
- // 前者说明这次读发生在进程结束之后(输出必然完整),后者是同一次读里的一致视图(输出与终态同源)。
272
- // 终态若是在读**之后**才到的,这一拍如实报 `running` —— 消费方多轮询一拍即可拿到尾巴 + 终态。
273
- // 代价:最多多一拍轮询。收益:「终态 ⇒ 输出已完整」这条消费方真正依赖的保证成立。
274
177
  const statusBeforeRead = entry.status;
275
178
  let r;
276
179
  try {
@@ -279,7 +182,6 @@ export class BackgroundShellManager {
279
182
  catch (e) {
280
183
  return fail(e instanceof BackgroundShellError ? e : new BackgroundShellError("io", e instanceof Error ? e.message : String(e), e));
281
184
  }
282
- // Pull-model terminal reconciliation (k8s): apply only if not already classified by kill/timeout.
283
185
  if (r.terminal && entry.status === "running") {
284
186
  entry.status = r.terminal.failed ? "failed" : "exited";
285
187
  if (r.terminal.exitCode !== undefined)
@@ -287,14 +189,12 @@ export class BackgroundShellManager {
287
189
  if (entry.timer)
288
190
  clearTimeout(entry.timer);
289
191
  }
290
- // 见上方旁注:读之后才到的终态,本拍不报(下一拍的快照就是终态,那时输出已完整)。
291
192
  const reportTerminal = statusBeforeRead !== "running" || r.terminal !== undefined;
292
193
  const reportedStatus = reportTerminal ? entry.status : "running";
293
194
  return ok({
294
195
  stdout: r.stdout,
295
196
  stderr: r.stderr,
296
197
  status: reportedStatus,
297
- // exitCode 只随终态一起报——报 running 却带 exitCode 会让消费方读出自相矛盾的一拍。
298
198
  ...(reportTerminal && entry.exitCode !== undefined ? { exitCode: entry.exitCode } : {}),
299
199
  ...(r.bytesDroppedBeforeCursor > 0 ? { truncated: true, bytesDroppedBeforeCursor: r.bytesDroppedBeforeCursor } : {}),
300
200
  bytesFromStart: r.bytesFromStart,
@@ -312,31 +212,24 @@ export class BackgroundShellManager {
312
212
  await this.driver.kill(entry.state);
313
213
  }
314
214
  catch {
315
- /* idempotent: killing an already-dead shell is a no-op success (design/103 §4.3) */
316
215
  }
317
216
  }
318
217
  return ok(undefined);
319
218
  }
320
- /** Kill + clean up EVERY shell. Best-effort, MUST NOT throw, idempotent (design/103 §3.7).
321
- *
322
- * core `except` 契约(background-shell.d.ts:core runtask 尾调 `{except: keepAlive}`):
323
- * 这些 shellId **留活**——session 驻留 persistent Monitor 的进程,其全部意义就是跨 turn 存活;run-end
324
- * 全灭会留下「registry handle 活着、进程死了」的孤儿 watch。留活条目连注册表一起保留(poll/kill 继续
325
- * 可用,timeout 墙钟 timer 不清=到点照杀)。无参=既有全灭行为(suspend/review 前的 dispose 不带 except)。 */
326
219
  async dispose(opts) {
327
220
  const keep = opts?.except?.length ? new Set(opts.except) : undefined;
328
221
  const doomed = [];
329
222
  if (keep) {
330
223
  for (const [id, entry] of [...this.shells]) {
331
224
  if (keep.has(id))
332
- continue; // 留活:不 kill、不清 timer、不出注册表
225
+ continue;
333
226
  this.shells.delete(id);
334
227
  doomed.push(entry);
335
228
  }
336
229
  }
337
230
  else {
338
231
  doomed.push(...this.shells.values());
339
- this.shells.clear(); // idempotent: a second dispose iterates nothing
232
+ this.shells.clear();
340
233
  }
341
234
  for (const entry of doomed) {
342
235
  try {
@@ -347,7 +240,6 @@ export class BackgroundShellManager {
347
240
  await this.driver.disposeOne(entry.state);
348
241
  }
349
242
  catch {
350
- /* best-effort; disposeBackgroundShells MUST never throw (design/103 §3.7) */
351
243
  }
352
244
  }
353
245
  }