@sema-agent/server 7.145.0 → 7.146.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 (275) hide show
  1. package/MIGRATION.md +492 -0
  2. package/USAGE.md +218 -47
  3. package/deploy/facts/CHANGES.json +25 -0
  4. package/deploy/facts/README.md +57 -0
  5. package/deploy/facts/lanes.json +54 -0
  6. package/deploy/facts/lanes.schema.json +57 -0
  7. package/deploy/facts/sandbox-rbac.json +113 -0
  8. package/deploy/facts/sandbox-rbac.schema.json +74 -0
  9. package/deploy/facts/sealed-key.json +9 -0
  10. package/deploy/facts/sealed-key.schema.json +52 -0
  11. package/deploy/sema-up/chart/templates/_helpers.tpl +19 -0
  12. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +47 -7
  13. package/deploy/sema-up/chart/templates/server-v4pro.yaml +4 -0
  14. package/deploy/sema-up/chart/templates/server.yaml +4 -0
  15. package/deploy/sema-up/chart/values.yaml +7 -0
  16. package/deploy/sema-up/stack/sema-stack-cluster.yaml +30 -14
  17. package/dist/adoption/import-limits.d.ts +6 -0
  18. package/dist/adoption/import-limits.js +7 -0
  19. package/dist/adoption/import.d.ts +62 -0
  20. package/dist/adoption/import.js +107 -0
  21. package/dist/adoption/wire.d.ts +52 -0
  22. package/dist/adoption/wire.js +9 -0
  23. package/dist/approval-ask-machine.d.ts +2 -2
  24. package/dist/approval-card.d.ts +3 -12
  25. package/dist/approval-card.js +0 -4
  26. package/dist/approval-gate-currency.d.ts +1 -1
  27. package/dist/approval-gate-currency.js +1 -1
  28. package/dist/approval-reconciler.d.ts +1 -1
  29. package/dist/boot/deployment-mods.d.ts +11 -1
  30. package/dist/boot/deployment-mods.js +15 -2
  31. package/dist/boot/device-lane.js +2 -0
  32. package/dist/boot/engine-lease.d.ts +193 -21
  33. package/dist/boot/engine-lease.js +146 -20
  34. package/dist/boot/execution-env.d.ts +4 -0
  35. package/dist/boot/execution-env.js +12 -0
  36. package/dist/boot/exit-boundary.d.ts +50 -0
  37. package/dist/boot/exit-boundary.js +32 -0
  38. package/dist/boot/exit-last.d.ts +20 -4
  39. package/dist/boot/exit-last.js +21 -4
  40. package/dist/boot/governance-seams.js +1 -0
  41. package/dist/boot/memory-consolidation.d.ts +29 -17
  42. package/dist/boot/memory-consolidation.js +26 -15
  43. package/dist/boot/reapers.d.ts +12 -4
  44. package/dist/boot/reapers.js +39 -15
  45. package/dist/boot/resolve-spec.d.ts +2 -0
  46. package/dist/boot/resolve-spec.js +38 -15
  47. package/dist/boot/runner-deps.js +1 -1
  48. package/dist/boot/session-faces.js +3 -1
  49. package/dist/boot/shutdown.js +27 -9
  50. package/dist/boot/stage-01-config.js +1 -1
  51. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  52. package/dist/boot/stage-03-store-backend.js +2 -2
  53. package/dist/boot/stage-04-stores.d.ts +1 -0
  54. package/dist/boot/stage-05-execution-env.d.ts +3 -0
  55. package/dist/boot/stage-05-execution-env.js +3 -3
  56. package/dist/boot/stage-06-runners.d.ts +3 -0
  57. package/dist/boot/stage-06-runners.js +11 -10
  58. package/dist/boot/stage-07-capability-layer.d.ts +3 -0
  59. package/dist/boot/stage-07-capability-layer.js +7 -2
  60. package/dist/boot/stage-08-reapers.d.ts +3 -0
  61. package/dist/boot/stage-08-reapers.js +3 -3
  62. package/dist/boot/stage-09-leader.d.ts +3 -0
  63. package/dist/boot/stage-10-http-server.d.ts +3 -0
  64. package/dist/boot/stage-10-http-server.js +3 -2
  65. package/dist/boot/stores.d.ts +1 -0
  66. package/dist/boot/stores.js +35 -19
  67. package/dist/boot/workflow-orchestration.d.ts +1 -1
  68. package/dist/boot/workflow-orchestration.js +2 -1
  69. package/dist/capabilities/composite-members.d.ts +3 -1
  70. package/dist/capabilities/composite-members.js +3 -1
  71. package/dist/capabilities/scenarios.d.ts +2 -2
  72. package/dist/capability-facts.d.ts +18 -1
  73. package/dist/capability-facts.js +25 -7
  74. package/dist/checkpoint-ctx-seal-m28.d.ts +1 -1
  75. package/dist/config-catalog.js +13 -4
  76. package/dist/config-provider.d.ts +2 -2
  77. package/dist/config-types.d.ts +43 -4
  78. package/dist/config.d.ts +6 -1
  79. package/dist/config.js +67 -11
  80. package/dist/deleted-session-ids.d.ts +19 -0
  81. package/dist/deleted-session-ids.js +26 -0
  82. package/dist/deploy-facts.d.ts +155 -0
  83. package/dist/deploy-facts.js +94 -0
  84. package/dist/device-ws-hub.d.ts +48 -3
  85. package/dist/device-ws-hub.js +48 -2
  86. package/dist/error-code-family.d.ts +35 -0
  87. package/dist/error-code-family.js +26 -0
  88. package/dist/fleet/drain-entry.d.ts +39 -3
  89. package/dist/fleet/drain-entry.js +6 -0
  90. package/dist/fleet/fleet-bus.d.ts +2 -1
  91. package/dist/hooks/branch-transcript.d.ts +1 -1
  92. package/dist/hooks/branch-transcript.js +1 -0
  93. package/dist/hooks/classic-stations.d.ts +25 -8
  94. package/dist/hooks/classic-stations.js +29 -3
  95. package/dist/hooks/hook-runner.d.ts +33 -16
  96. package/dist/hooks/hook-runner.js +166 -73
  97. package/dist/hooks/hook-source.d.ts +14 -2
  98. package/dist/hooks/hook-source.js +8 -0
  99. package/dist/http/active-run-conflict.d.ts +1 -1
  100. package/dist/http/active-run-conflict.js +1 -1
  101. package/dist/http/admission.js +11 -1
  102. package/dist/http/checkpoint-ctx.d.ts +59 -71
  103. package/dist/http/checkpoint-ctx.js +34 -40
  104. package/dist/http/dispatch.js +18 -2
  105. package/dist/http/durable-run-leg.d.ts +5 -0
  106. package/dist/http/durable-run-leg.js +1 -0
  107. package/dist/http/execution-context.d.ts +4 -0
  108. package/dist/http/execution-context.js +9 -0
  109. package/dist/http/health-self-report.d.ts +6 -2
  110. package/dist/http/health-self-report.js +3 -2
  111. package/dist/http/memory-compliance-failure-reply.d.ts +1 -1
  112. package/dist/http/memory-compliance-failure-reply.js +2 -15
  113. package/dist/http/memory-control-plane-corrupt-reply.d.ts +25 -0
  114. package/dist/http/memory-control-plane-corrupt-reply.js +19 -0
  115. package/dist/http/principal-gate.d.ts +35 -12
  116. package/dist/http/principal-gate.js +16 -12
  117. package/dist/http/request-guards.d.ts +1 -1
  118. package/dist/http/request-guards.js +5 -5
  119. package/dist/http/resume-legs.d.ts +22 -0
  120. package/dist/http/resume-legs.js +26 -17
  121. package/dist/http/route-ctx.d.ts +11 -10
  122. package/dist/http/route-table.js +2 -0
  123. package/dist/http/routes/adoption.d.ts +11 -0
  124. package/dist/http/routes/adoption.js +80 -6
  125. package/dist/http/routes/agents-roster.js +1 -0
  126. package/dist/http/routes/approvals-assistant.js +2 -1
  127. package/dist/http/routes/background.js +1 -0
  128. package/dist/http/routes/capabilities.js +4 -1
  129. package/dist/http/routes/diagnostics.d.ts +9 -14
  130. package/dist/http/routes/diagnostics.js +10 -20
  131. package/dist/http/routes/engine.d.ts +31 -0
  132. package/dist/http/routes/engine.js +81 -0
  133. package/dist/http/routes/memory-bundle.js +3 -0
  134. package/dist/http/routes/memory-consolidation.js +3 -5
  135. package/dist/http/routes/memory-origin.d.ts +1 -1
  136. package/dist/http/routes/memory-origin.js +2 -4
  137. package/dist/http/routes/memory-policy.js +5 -1
  138. package/dist/http/routes/observability.js +3 -6
  139. package/dist/http/routes/runs.js +7 -3
  140. package/dist/http/routes/session-sync.js +12 -25
  141. package/dist/http/routes/sessions-list.d.ts +1 -1
  142. package/dist/http/routes/sessions.js +4 -0
  143. package/dist/http/routes/tasks.js +23 -5
  144. package/dist/http/routes/trace-usage.js +1 -1
  145. package/dist/http/run-concurrency-cap.d.ts +86 -0
  146. package/dist/http/run-concurrency-cap.js +54 -0
  147. package/dist/http/server.d.ts +15 -12
  148. package/dist/http/server.js +18 -3
  149. package/dist/http/stored-body-replay.d.ts +9 -6
  150. package/dist/http/stored-body-replay.js +19 -5
  151. package/dist/http/wire-types.d.ts +8 -3
  152. package/dist/json-body-limit.d.ts +8 -0
  153. package/dist/json-body-limit.js +2 -0
  154. package/dist/leader/diffout.d.ts +1 -1
  155. package/dist/local-data-root.d.ts +2 -2
  156. package/dist/memory-operator-faces.d.ts +9 -4
  157. package/dist/memory-scope-stores.d.ts +9 -7
  158. package/dist/memory-scope-stores.js +3 -14
  159. package/dist/memory-scope.d.ts +36 -0
  160. package/dist/memory-scope.js +32 -1
  161. package/dist/net-address.d.ts +53 -0
  162. package/dist/net-address.js +111 -0
  163. package/dist/observability/fail-open.d.ts +14 -10
  164. package/dist/observability/fail-open.js +14 -10
  165. package/dist/orchestration/workflow-notify-journal.js +2 -3
  166. package/dist/org-delivery-gate.js +1 -1
  167. package/dist/peer-directory.d.ts +6 -12
  168. package/dist/peer-directory.js +5 -1
  169. package/dist/plugins/caching-session-store.d.ts +3 -1
  170. package/dist/plugins/caching-session-store.js +3 -0
  171. package/dist/plugins/checkpoint-store-face.d.ts +426 -0
  172. package/dist/plugins/checkpoint-store-face.js +152 -0
  173. package/dist/plugins/file-run-store.d.ts +0 -6
  174. package/dist/plugins/file-run-store.js +1 -12
  175. package/dist/plugins/fork-routing-session-store.d.ts +6 -2
  176. package/dist/plugins/fork-routing-session-store.js +2 -2
  177. package/dist/plugins/imported-layer-root-retirement.d.ts +10 -8
  178. package/dist/plugins/local-checkpoint-store.d.ts +2 -2
  179. package/dist/plugins/local-checkpoint-store.js +2 -1
  180. package/dist/plugins/local-session-store.d.ts +28 -5
  181. package/dist/plugins/local-session-store.js +32 -8
  182. package/dist/plugins/memory-engine-pg.d.ts +29 -6
  183. package/dist/plugins/memory-engine-pg.js +96 -214
  184. package/dist/plugins/memory-engine-sql-apply.d.ts +121 -0
  185. package/dist/plugins/memory-engine-sql-apply.js +358 -0
  186. package/dist/plugins/memory-engine-tidb.d.ts +35 -9
  187. package/dist/plugins/memory-engine-tidb.js +97 -227
  188. package/dist/plugins/memory-key-guards.d.ts +1 -1
  189. package/dist/plugins/memory-origin-law.d.ts +3 -3
  190. package/dist/plugins/one-time-migrations.d.ts +32 -56
  191. package/dist/plugins/one-time-migrations.js +25 -117
  192. package/dist/plugins/permission-rule-store-file.d.ts +17 -3
  193. package/dist/plugins/permission-rule-store-file.js +8 -1
  194. package/dist/plugins/pg-pool.js +1 -1
  195. package/dist/plugins/pg-session-storage.d.ts +17 -4
  196. package/dist/plugins/pg-session-storage.js +8 -4
  197. package/dist/plugins/run-store-sql.js +2 -2
  198. package/dist/plugins/schema-ensure-legs.js +2 -2
  199. package/dist/plugins/session-fork.d.ts +31 -9
  200. package/dist/plugins/session-fork.js +3 -2
  201. package/dist/plugins/session-placement.d.ts +4 -2
  202. package/dist/plugins/sql-json-column.d.ts +5 -1
  203. package/dist/plugins/store-backend.d.ts +11 -8
  204. package/dist/plugins/store-backend.js +8 -7
  205. package/dist/plugins/store-contracts.d.ts +1 -11
  206. package/dist/plugins/store-contracts.js +0 -9
  207. package/dist/plugins/store-sweep.d.ts +63 -0
  208. package/dist/plugins/store-sweep.js +48 -0
  209. package/dist/plugins/tidb-pool.js +1 -1
  210. package/dist/plugins/tidb-session-store.d.ts +16 -3
  211. package/dist/plugins/tidb-session-store.js +8 -4
  212. package/dist/prompt-screen.d.ts +6 -9
  213. package/dist/prompt-screen.js +2 -5
  214. package/dist/rules-consent.d.ts +20 -27
  215. package/dist/rules-consent.js +82 -6
  216. package/dist/run-cancel-context.d.ts +4 -1
  217. package/dist/run-cancel-context.js +2 -1
  218. package/dist/runs.js +1 -1
  219. package/dist/sandbox-egress-policy.js +6 -11
  220. package/dist/sandbox-rbac-rules.d.ts +88 -0
  221. package/dist/sandbox-rbac-rules.js +80 -0
  222. package/dist/sealed-key-layout.d.ts +17 -0
  223. package/dist/sealed-key-layout.js +4 -0
  224. package/dist/sealed-key.js +5 -5
  225. package/dist/security.d.ts +38 -6
  226. package/dist/security.js +9 -2
  227. package/dist/session-notify-door.d.ts +5 -3
  228. package/dist/session-resident-background.d.ts +57 -0
  229. package/dist/session-resident-background.js +20 -0
  230. package/dist/session-sync-kernel.d.ts +53 -14
  231. package/dist/session-sync-kernel.js +70 -1
  232. package/dist/session-sync.d.ts +5 -3
  233. package/dist/session-workspace.d.ts +1 -0
  234. package/dist/session-workspace.js +1 -0
  235. package/dist/sighup-idle.d.ts +9 -7
  236. package/dist/sighup-idle.js +4 -5
  237. package/dist/spec-fields.d.ts +16 -0
  238. package/dist/spec-fields.js +7 -0
  239. package/dist/stored-request-env-credentials-m38.d.ts +3 -0
  240. package/dist/stored-request-env-credentials-m38.js +8 -0
  241. package/dist/stored-request-prompt-profile-m30.d.ts +1 -1
  242. package/dist/{plugins → stores-sql}/checkpoint-store-sql.d.ts +13 -427
  243. package/dist/{plugins → stores-sql}/checkpoint-store-sql.js +33 -194
  244. package/dist/{plugins → stores-sql}/mailbox-store-sql.d.ts +2 -2
  245. package/dist/{plugins → stores-sql}/mailbox-store-sql.js +4 -4
  246. package/dist/{plugins → stores-sql}/roster-store-sql.d.ts +2 -2
  247. package/dist/{plugins → stores-sql}/roster-store-sql.js +1 -1
  248. package/dist/{plugins → stores-sql}/tool-result-store-sql.d.ts +2 -2
  249. package/dist/{plugins → stores-sql}/tool-result-store-sql.js +6 -6
  250. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.d.ts +7 -12
  251. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.js +3 -8
  252. package/dist/task-settings.d.ts +59 -4
  253. package/dist/task-settings.js +50 -4
  254. package/dist/tool-approval.d.ts +2 -2
  255. package/dist/trace/core-keyset-guard.d.ts +8 -7
  256. package/dist/trace/engine-notice-wire.d.ts +1 -1
  257. package/dist/trace/engine-notice-wire.js +2 -0
  258. package/dist/trace/exit-seat.d.ts +6 -0
  259. package/dist/trace/exit-seat.js +1 -0
  260. package/dist/trace/project.js +9 -0
  261. package/dist/trace/redact.d.ts +13 -2
  262. package/dist/trace/redact.js +305 -24
  263. package/dist/trace/self-minted-wire.d.ts +326 -8
  264. package/dist/trace/self-minted-wire.js +125 -7
  265. package/dist/trace/wire-projection-faces.d.ts +1 -1
  266. package/dist/trace/wire-projection-faces.js +1 -1
  267. package/dist/usage-analytics.d.ts +4 -0
  268. package/package.json +12 -4
  269. package/scripts/drain-check-local.mjs +8 -29
  270. package/scripts/drain-check.mysql.sql +3 -24
  271. package/scripts/drain-check.pg.sql +3 -24
  272. package/skills/update-config.md +1 -1
  273. package/wire-changes/server@7.146.0.json +3221 -0
  274. package/dist/approval-batch-census.d.ts +0 -27
  275. package/dist/approval-batch-census.js +0 -27
@@ -28,7 +28,8 @@
28
28
  * `ENGINE_AUTO_EXIT` / `SEMA_PARENT_PID` 的表态上。
29
29
  * 2. **自退判据 = 三合取,且必须持续满 `linger`**(`linger` = 孤儿态 `orphan.lingerMs`,否则 `lingerMs`):
30
30
  * · 附着租约集空(`attachedStreams() === 0`);
31
- * · 无在飞 run(`inflight() === 0` —— **与 /health 的 `inflight` 同一只 getter**,不另立第二判据);
31
+ * · 无必须等完的活(`inflight() === 0` —— 读 `drainState.blockingInflight` = **阻断集** `http + leader`,与排空等的、SIGHUP 空闲窗**同一只**,不另立第二判据;
32
+ * `/health.inflight` 是它再加会话驻留的后台 shell / monitor 的**总数**:同一份分项 `/health.inflightBreakdown` 派生的两个事实,不是两套计数,见下「已知收窄」);
32
33
  * · HTTP 触点静默(`now() - lastActivityAt() >= lingerMs`),**且**最后一条附着流的**离场**
33
34
  * 也已满窗(`now() - lastAttachEndedAt() >= lingerMs` —— codex R1-F1,见该 dep 的注)。
34
35
  * 任何一条不满足即把计时清零(连续性判据,不是累计)。
@@ -67,22 +68,99 @@
67
68
  * 已由 `inflight()` 结构性守住;parked 行是 durable 的,引擎退掉不丢:下次起来仍可赎回(spawn 形里
68
69
  * 「下次」就是壳的下一次 spawn)。伤害面 = 一条 parked run 的赎回被推迟到下次启动,不是丢失。
69
70
  * 这是**允许的收窄,不是静默**:有了廉价 parked 计数口(或本腿改成事件驱动)之后应当补上。
70
- * · **活过父 run 的后台子代理不入判据**(codex R1 [high] 验真后**如实登记**,不是本件引入:[ref] 之前
71
- * 父死走的是**当场** drain,而 `drainThenShutdown` 读的是同一只 `inflight()` ⇒ 同一个缺口,只是那时
72
- * 连这 2s 都没有)。`inflight()` 的口径是**本副本的 HTTP 腿 ∪ leader 腿**(`http/server.ts` 的
73
- * `inflightRuns`/`steerableRuns`/两只计数器),而 core 侧 `backgroundScope:"session"` 的子代理可以
74
- * **活过**生它的那条 HTTP 腿 —— 那之后它在本腿眼里是 0。伤害面 = 壳没了且没人附着时,一条还在跑的
75
- * 后台子代理会随引擎的受控退被中断(它的账本行照常落终态,不是静默丢)。收口需要一只**廉价**的
76
- * 「本副本活跃子代理数」读口(与 parked 那条同族:要么 core 给计数,要么本腿改事件驱动),归属
77
- * `inflight()` 的属主而不是本腿 —— 在那之前这条收窄**成文在此**,不许再被读成「bg 全被守住」。
71
+ * · **会话驻留的后台 shell / monitor 与活过父 run 的后台子代理都不入判据**(两件事实不同,同一条判据)。
72
+ * 阻断集 `http + leader` 是「还有没有**必须等完**的活」:HTTP 腿(在飞 run 集 · 同步计费腿 · admission 期请求)与 leader 腿。
73
+ * 会话驻留的后台 shell / monitor(`session-resident-background.ts` 读;running ∧ 会话驻留 ∧ 非 retain)**报**在 `/health.inflight`(总数)与
74
+ * `inflightBreakdown.sessionResident` 里 —— 外部判官据此判「这台引擎此刻托管着多少活」—— 但**不阻断**自退:它不是一条腿,等它没有意义,而一只不退出的 shell
75
+ * (`while true`)若阻断,崩溃孤儿引擎就永生(孤儿窗 / SIGHUP 空闲窗同读阻断集,排空也每次等满窗)。窗到 / 排空完成,退出边界的后台收割
76
+ * (`reapAllSessionBackground` + 硬面,`boot/exit-boundary.ts`)收掉它们;retain 行按 retain 既有语义活过受控退。伤害面 = 壳没了且没人附着时,引擎照退、
77
+ * 驻留的进程随受控退被收掉(与 7.145.x 逐字同,**本件不改这一行为**,只把它报出来)。
78
+ * 后台**子代理**(agent 行)同样不入(codex R1 [high] 验真后如实登记,不是本件引入:[ref] 之前父死走的是**当场** drain,而 `drainThenShutdown` 读的是同一只 `inflight()` ⇒ 同一个缺口):
79
+ * core 侧 `backgroundScope:"session"` 的子代理可以**活过**生它的那条 HTTP 腿,那之后它在本腿眼里是 0;伤害面 = 一条还在跑的后台子代理会随引擎的受控退被中断(它的账本行照常落终态,不是静默丢)。
80
+ * 这条收窄**成文在此**,不许再被读成「bg 全被守住」。
78
81
  * · **`/health` 探针不算触点**:`lastActivityAt` 的属主(`http/server.ts` 的 `noteActivity`)按既有契约
79
82
  * 把 `/health` 与 `/metrics*` 排除在活性时钟之外(常态采样噪音不得把空闲窗永远撑开)。⇒ 一只**只**
80
83
  * 轮询 `/health` 的壳不构成租约。这与 SIGHUP 空闲窗的判据同源,刻意保持一致而非在本腿另开一份。
84
+ *
85
+ * ## [ref]:首次附着前的宽限 —— 窗表从两态变三态(判据 / 钟 / 出口都不变;[ref] 起并进下面那张四行车道表)
86
+ *
87
+ * 病:壳在 spawn 与**首次接触**之间被饿住(一次性 `-p` 车道 / CI / 高载宿主)超过 owned 窗 ⇒ 一只**从没被碰过**
88
+ * 的引擎按 owned 窗自退,壳醒来起步失败。收法只改「生效窗 = f(态)」这一张表,不加第二只钟、不加第二条自退路径:
89
+ *
90
+ * | 态(优先级从上到下) | 进入 | 生效窗 |
91
+ * |----------------------|------------------------------------------|--------------------------------|
92
+ * | 孤儿 | 装了孤儿车道 ∧ 看门狗已判父死 | `orphan.lingerMs` |
93
+ * | 候首触 | 起服以来还没有过非探针触点 ∧ 非孤儿 | `max(lingerMs, firstAttach.windowMs)` |
94
+ * | 已触(owned) | 有过触点 ∧ 非孤儿 | `lingerMs` |
95
+ *
96
+ * · **首触的唯一属主 = 活性时钟的写者**(`http/server.ts` 的 `noteActivity`,{@link EngineLeaseFirstAttachLane.firstTouchAt}):
97
+ * 第一个非探针请求(含打开 SSE 流 —— 它本身就是一个非探针请求)写一次就定,之后不回 null ⇒ 态**单向**。探针律不变;
98
+ * **在飞不翻态**(boot 赎回的 leader 腿没有客户端到场 —— 它在跑时三合取本就不成立,跑完仍按候首触窗计)。
99
+ * · **孤儿压过候首触**:宽限保护的是「父还活着、只是被饿住」的壳;父没了就没人会回来,顶注 7 那句「壳在接触前崩了 ⇒ 孤儿窗内退」不变。
100
+ * · **宽限只拉长不缩短**:`max(lingerMs, …)` —— 旋钮 `0`(或任何不大于 owned 窗的值)= 候首触窗落成 owned 窗 = [ref] 前逐字同;
101
+ * owned 窗 = `Infinity`(`ENGINE_AUTO_EXIT` 关着)⇒ 候首触窗也是 `Infinity`。
102
+ * · **拍频与 `/health.engineLease.lingerMs` 不变**:候首触窗 ≥ owned 窗,不进「最短窗」推导(顶注 9 / [ref])。
103
+ *
104
+ * ## [ref]([ref] v3):交接车道 —— 一只车道函数、一张四行表、一只区间、一只谓词
105
+ *
106
+ * 「Move to background and exit」:壳经 `POST /v1/engine/handover` 把引擎交给后台、自己退出(不 SIGTERM 引擎);下一只壳 `POST /v1/engine/attach`
107
+ * 即收回。规则只有一条 —— 车道是闭集 {@link ENGINE_LANES},由 {@link deriveEngineLane} 一处派生(从上到下第一条成立者),每条车道 = 表里一行:
108
+ *
109
+ * | 车道 | 进入 | 窗(唯一选窗处 `windowOf`) | 后台 shell 计入工作 | SIGHUP 空闲窗 |
110
+ * |----------------------|----------------------------------------|----------------------------------------|---------------------|---------------|
111
+ * | handover | 交接口置位(attach 口清位) | `handover.lingerMs` | 是 | 让位本腿 |
112
+ * | orphan | 装了孤儿车道 ∧ 看门狗已判父死 | `orphan.lingerMs` | 否 | 照旧 |
113
+ * | awaitingFirstAttach | 还没有过非探针触点 | `max(lingerMs, firstAttach.windowMs)` | 否 | 照旧 |
114
+ * | owned | 其余 | `lingerMs` | 否 | 照旧 |
115
+ *
116
+ * · **工作谓词一只**({@link laneWork}):附着 > 0 ∨ 在飞 > 0 ∨(本车道计后台 shell ∧ 运行中、引擎一退就会被收掉的后台 shell > 0)。本腿与
117
+ * `sighup-idle.ts` 都只调它(读者词 `"lease"` / `"sighup"`);SIGHUP 读者在「让位」那一格恒读到「有工作」—— handover 车道的寿命只归本腿这一只
118
+ * 定时器,终端关闭不会按另一套谓词 / 另一个窗把引擎拆掉(两只定时器判同一件事 = 两条自退律,顶注 6 那句纪律)。
119
+ * · **后台 shell 只在 handover 那一格计入**:owned / orphan 若也计,一只 `while true` 就让崩溃孤儿永生([ref] 倒退)。不是特判,是表里一格的值。
120
+ * · **孤儿照旧只由看门狗判**(没有租约集):交接压过孤儿;父已死时 attach ⇒ 回到 orphan(与 [ref]「复用孤儿引擎」同)。壳在交接之后退出不是
121
+ * 本腿的事件 —— 车道一直是 handover,直到新的 attach。
122
+ * · **交接车道与计数读口捆在一起**({@link EngineLeaseHandoverLane},与孤儿车道同理):缺席 ⇒ 门不建、车道结构上不可达。读口 = 退出边界此刻会硬收的
123
+ * 后台 shell 行数(`boot/exit-boundary.ts` 的 `countShellsExitWouldKill`;数与收同一只 `includeRetained`);装配层只按「非多租户」给,
124
+ * 不再有「读口缺席」这一态。它**不是** `/health.inflightBreakdown.sessionResident`(那是总数里会话驻留的分项;两问之差见 exit-boundary 那只函数的注)。
125
+ * · **窗与拍频**:交接窗进「最短窗」推导(本腿会执行它);判据 / 钟 / 出口(SIGTERM 那条 drain + `exit-last.json`)全不变。
126
+ * · 自退读数 `lane` 一键说清那一拍的车道(`orphan` / `firstAttachWindowExpired` 两只布尔随 `exit-last.json` 格式 3 退役 —— 同一事实只留一种读法);
127
+ * 不变量 `reason !== "idle"` ⟺ `lane === "orphan"`,同一处铸出。
81
128
  */
82
129
  import type { ParentOrphanReason } from "../parent-watch.js";
83
130
  /** 自退检查拍频的**上限**。**不开 env 旋钮**(与 `PARENT_WATCH_INTERVAL_MS` 同纪律:有真需求再议,
84
131
  * 防旋钮增殖);实际拍频由顶注 9 的推导取更小者。 */
85
132
  export declare const ENGINE_LEASE_TICK_MS = 15000;
133
+ /** [ref] 车道闭集(书写序 = 派生优先序,见 {@link deriveEngineLane})。 */
134
+ export declare const ENGINE_LANES: readonly ["handover", "orphan", "awaitingFirstAttach", "owned"];
135
+ export type EngineLane = (typeof ENGINE_LANES)[number];
136
+ /** 车道表里与配置无关的两列(窗那一列随配置,在 {@link createEngineLeaseTracker} 的 `windowOf` 里一处选)。
137
+ * `Record<EngineLane, …>`:加一条车道而这里没跟 ⇒ 编译红。 */
138
+ export interface EngineLaneRow {
139
+ /** 运行中、引擎一退就会被收掉的后台 shell 计入工作谓词吗。 */
140
+ readonly countsBackgroundShells: boolean;
141
+ /** SIGHUP 空闲窗在这条车道上让位本腿吗(让位 = SIGHUP 读者恒读到「有工作」)。 */
142
+ readonly sighupYieldsToLease: boolean;
143
+ }
144
+ export declare const ENGINE_LANE_TABLE: Readonly<Record<EngineLane, EngineLaneRow>>;
145
+ /** 车道派生 —— **唯一**一处(从上到下第一条成立者;交接压过孤儿,孤儿压过候首触)。三个输入都是持久态。 */
146
+ export declare function deriveEngineLane(s: {
147
+ readonly handedOver: boolean;
148
+ readonly orphaned: boolean;
149
+ readonly touched: boolean;
150
+ }): EngineLane;
151
+ /** 工作谓词的采样输入。`backgroundShells` 惰性:只在本车道计它时才读(那是一次注册表遍历)。 */
152
+ export interface EngineWorkSample {
153
+ readonly attached: number;
154
+ readonly inflight: number;
155
+ readonly backgroundShells: () => number;
156
+ }
157
+ /** 谁在读工作谓词(闭集):本腿 = `"lease"`;SIGHUP 空闲窗 = `"sighup"`。 */
158
+ export type EngineWorkReader = "lease" | "sighup";
159
+ /**
160
+ * **一只**工作谓词(采样格;活性钟 / 离场钟 / 连续空闲满窗由各读者自己的窗判)。`lane` 缺席 = 租约腿未装配(纯服务器形)或已停 ——
161
+ * 那时没有任何一行可读,谓词 = 附着 ∨ 在飞(与 [ref] 之前的 SIGHUP 逐字同)。
162
+ */
163
+ export declare function laneWork(lane: EngineLane | undefined, s: EngineWorkSample, reader: EngineWorkReader): boolean;
86
164
  /**
87
165
  * 孤儿态读数(装配层把看门狗的 `onOrphaned` 落成这一笔)。
88
166
  *
@@ -109,55 +187,142 @@ export interface EngineLeaseOrphanLane {
109
187
  * `null` = 父还活着。 */
110
188
  read: () => EngineOrphanState | null;
111
189
  }
112
- /** 自退缘由闭集:`"idle"` = 没人用了(owned 态);另两个词 = 看门狗判的孤儿那两支。
113
- * `orphan === (reason !== "idle")` 是不变量 —— 两者在**同一处**铸出(见 {@link createEngineLeaseTracker}),
114
- * 不存在各算一次的可能。 */
190
+ /**
191
+ * [ref] 首次附着车道 —— 窗长与读口**捆在一起**(与 {@link EngineLeaseOrphanLane} 同理:平行的两只 dep 会让「配了首窗、却没有
192
+ * 首触读口」成为一个可表达的状态)。装配层恒给(旋钮恒有值);`windowMs` 为 `0` 即不给额外宽限。
193
+ */
194
+ export interface EngineLeaseFirstAttachLane {
195
+ /** 旋钮值(`config.engineFirstAttachMs`;`0` = 关)。生效窗 = `max(lingerMs, windowMs)` —— 由本模块一处算,不在装配层算。 */
196
+ windowMs: number;
197
+ /** 首个**非探针** HTTP 触点的单调时刻(`drainState.firstActivityAt`;与 `now` 同为 `performance.now()` 域)。`null` = 还没有过。
198
+ * 属主 = 活性时钟的写者(写一次就定):本腿只读,不锁存、不另算 —— 首触的判据(探针律)只有那一处。 */
199
+ firstTouchAt: () => number | null;
200
+ }
201
+ /**
202
+ * [ref] 交接车道 —— 窗长与后台 shell 计数读口**捆在一起**(与孤儿车道同理:「配了交接窗、却数不了后台 shell」不可表达)。
203
+ * 装配层只在「非多租户」时给;缺席 ⇒ 把手上没有交接门,handover 车道结构上不可达。
204
+ */
205
+ export interface EngineLeaseHandoverLane {
206
+ /** 交接车道的窗(`config.engineHandoverLingerMs`,缺省 30 min)。 */
207
+ lingerMs: number;
208
+ /** 此刻运行中、引擎一退就会被收掉的后台 shell 行数(装配层给退出边界的 `shellsExitWouldKill`:与硬收面同一只 `includeRetained`,读不出按 1 计、不抛)。 */
209
+ backgroundShells: () => number;
210
+ }
211
+ /** 交接的缘由闭集(交接口体的 `reason`)。 */
212
+ export declare const HANDOVER_REASONS: readonly ["background_exit"];
213
+ export type HandoverReason = (typeof HANDOVER_REASONS)[number];
214
+ /** `engine_lane_changed` 的缘由闭集(只由交接门铸:孤儿已有 `parent_watch_orphaned`,首触今天无日志)。 */
215
+ export type EngineLaneChangeReason = "handover_requested" | "attached";
216
+ /** 自退缘由闭集:`"idle"` = 不在孤儿车道;另两个词 = 看门狗判的孤儿那两支。
217
+ * 不变量 `reason !== "idle"` ⟺ `lane === "orphan"` —— 两者在**同一处**铸出(见 {@link createEngineLeaseTracker})。 */
115
218
  export type EngineLeaseExitReason = "idle" | ParentOrphanReason;
116
219
  /**
117
220
  * 自退那一刻的**判据读数**(一个对象,三个消费者:`engine_lease_auto_exit` 日志行、数据根上的
118
221
  * `exit-last.json` 留痕、以及 drain 本身)。一处铸出 ⇒ 日志与留痕永远不会各说各话。
119
222
  */
120
223
  export interface EngineLeaseExitReport {
121
- /** 父已经不在了那一路退的吗(壳的 exit watch 读这一键把 pid 归入**预期**退出)。 */
122
- orphan: boolean;
224
+ /** [ref]:自退那一拍的车道(窗就是按它选的)。 */
225
+ lane: EngineLane;
123
226
  reason: EngineLeaseExitReason;
124
227
  /** 真实空闲时长(不是配置的窗长 —— 见顶注 8)。 */
125
228
  idleForMs: number;
126
229
  attached: number;
127
230
  inflight: number;
128
- /** 本次**生效**的窗长(孤儿态 = 孤儿窗)。 */
231
+ /** 本次**生效**的窗长(= 那一拍车道的窗)。 */
129
232
  lingerMs: number;
130
- /** 成孤儿到自退之间隔了多久;非孤儿退出时键缺席(不编 0)。 */
233
+ /** 成孤儿到自退之间隔了多久;非孤儿车道退出时键缺席(不编 0)。 */
131
234
  orphanedForMs?: number;
132
235
  }
133
236
  /**
134
237
  * [ref] —— 租约窗的 wire 读数(`GET /health` 的 `engineLease`;壳持租约的触点节拍据此定,别处起的短窗引擎不再让第二句失败)。
135
238
  *
136
239
  * ## 报的是什么
137
- * · `lingerMs` = 本腿**会执行的最短静默窗**:父还活着的窗({@link EngineLeaseDeps.lingerMs})与孤儿窗({@link EngineLeaseOrphanLane.lingerMs},只在看门狗装了时存在)取小。
240
+ * · `lingerMs` = 本腿**会执行的最短静默窗**:父还活着的窗({@link EngineLeaseDeps.lingerMs})、孤儿窗({@link EngineLeaseOrphanLane.lingerMs},只在看门狗装了时存在)、
241
+ * 交接窗({@link EngineLeaseHandoverLane.lingerMs},只在交接车道装了时存在)取小。
138
242
  * 取**最短**而不是「此刻生效」:此刻生效的窗在父死那一拍从 owned 窗掉到孤儿窗,按旧读数定节拍的壳会在下一次读 `/health` 之前被拆台;
139
243
  * 最短窗是配置的性质、不随状态变,壳的触点只要比它密,在任何状态下都留得住引擎。
140
244
  * · `tickMs` = 自退检查的采样拍。自退发生在**生效窗**满之后、至多再晚一拍(顶注 8);生效窗恒 ≥ `lingerMs`(`lingerMs` 是各窗里最短的一个),所以触点比 `lingerMs` 密 ⇒ 在任何状态下都先于自退。
141
245
  * 读数**不预言**自退时刻:父还活着时生效的是 owned 窗,自退只会更晚。
246
+ * · `lane`([ref])= 此刻的车道(与自退判据同一只派生函数,每次现读)。
142
247
  *
143
248
  * ## 一源
144
249
  * 与租约腿自己的行为同一处铸出:`engine_lease_armed` 日志、自退判据用的窗、拍频推导(`tickMs = min(15s, 最短窗)`)和本读数读的是同一组数 ——
145
- * 不存在第二处去「算一遍租约窗」。一个永远不到期的窗(owned 窗 = `Infinity` 且没有孤儿车道)不是租约:读数整个缺席,不补 0、不把 `Infinity` 送进 JSON。
250
+ * 不存在第二处去「算一遍租约窗」。一个永远不到期的窗(owned 窗 = `Infinity` 且没有孤儿 / 交接车道)不是租约:读数整个缺席,不补 0、不把 `Infinity` 送进 JSON。
146
251
  */
147
252
  export interface EngineLeaseFacts {
148
253
  readonly lingerMs: number;
149
254
  readonly tickMs: number;
255
+ /** [ref]:此刻的车道。 */
256
+ readonly lane: EngineLane;
257
+ /** [ref] 首窗格({@link EngineLeaseFirstAttachFacts}):候首触窗有限(owned 窗有限)才在场 —— 永不到期的窗不上 wire(同上一段那条律)。 */
258
+ readonly firstAttach?: EngineLeaseFirstAttachFacts;
259
+ /** [ref] 交接格({@link EngineHandoverFacts}):只在 handover 车道在场。 */
260
+ readonly handover?: EngineHandoverFacts;
261
+ }
262
+ /**
263
+ * [ref] —— `GET /health` 的 `engineLease.firstAttach`:首次附着前的宽限窗。
264
+ *
265
+ * · `windowMs` = **生效**的候首触窗 `max(owned 窗, ENGINE_FIRST_ATTACH_MS)`(解析后的真值,不是旋钮回声;旋钮 `0` ⇒ 等于 owned 窗)。
266
+ * · `since` = 窗开始的时刻(装配那一拍,epoch ms)。父一直活着且一直没人碰时,`since + windowMs` 是「最早何时自退」的**下界**,不是预言 —— 父死之后孤儿窗优先、可以更早退
267
+ * (那一态的窗看同一格的 `lingerMs`:最短窗已含孤儿窗;本格在孤儿态照旧在场,codex 对抗审 F2 成文);
268
+ * 候首触期间有在飞会让它后移,另有至多一拍的采样滞后(顶注 8)。没有单列 `until` —— 它可由这两格推出(一个事实一处读)。
269
+ * · `attachedAt` = 首个非探针触点的时刻(epoch ms);`null` = 还在候首触。非 null ⇒ 首窗已永久关闭(单向)。
270
+ * · 两个时刻是**同一只单调钟**的投影:装配时只采一次挂钟作锚,`epoch = since + (单调时刻 − 装配单调时刻)`。不跑第二只钟;系统改钟之后它与真实期限会漂开(成文)。
271
+ */
272
+ export interface EngineLeaseFirstAttachFacts {
273
+ readonly windowMs: number;
274
+ readonly since: number;
275
+ readonly attachedAt: number | null;
276
+ }
277
+ /**
278
+ * [ref] —— 交接格(`GET /health` 的 `engineLease.handover`;交接口 200 体 = 其中 `since` / `lingerMs` / `backgroundShells` 三键加 `lane`)。
279
+ * · `since` = 进入交接车道的时刻(epoch ms,与首窗格同一只单调钟投影);幂等的再次交接回同一个值。
280
+ * · `lingerMs` = 交接窗;`backgroundShells` = 退出边界此刻会硬收的后台 shell 行数(现读)。窗只在它为 0、阻断集在飞为 0、且无附着与触点时起算。
281
+ * · 不带在飞数:阻断集在同一份 `/health` 里已由 `inflightBreakdown.http + leader` 读出(一个事实一处读);交接口 200 体若带它,读到的恒含这次请求自己。
282
+ */
283
+ export interface EngineHandoverFacts {
284
+ readonly since: number;
285
+ readonly reason: HandoverReason;
286
+ readonly lingerMs: number;
287
+ readonly backgroundShells: number;
288
+ }
289
+ /** 交接口的应答(200 体):`lane` 恒 `"handover"` + 交接格里的三键。 */
290
+ export interface EngineHandoverEntry {
291
+ readonly lane: "handover";
292
+ readonly since: number;
293
+ readonly lingerMs: number;
294
+ readonly backgroundShells: EngineHandoverFacts["backgroundShells"];
295
+ }
296
+ /** attach 口的应答(200 体):清位之后的车道;`handedOverForMs` 只在这一次 attach 真的清掉了交接位时在场。 */
297
+ export interface EngineAttachAnswer {
298
+ readonly lane: EngineLane;
299
+ readonly handedOverForMs?: number;
300
+ }
301
+ /**
302
+ * [ref] 交接门 —— 本腿交给装配层(经排空盒子到两只 HTTP 口)的**唯一**写口。只在交接车道装了时存在。
303
+ * `observing` = 腿还在观察(停了 ⇒ 能力事实答不可用;两口不再写)。
304
+ */
305
+ export interface EngineHandoverDoor {
306
+ readonly observing: boolean;
307
+ /** 置交接位(幂等:已在 handover ⇒ 回同一个 `since`,不再打日志)。 */
308
+ enter(reason: HandoverReason): EngineHandoverEntry;
309
+ /** 清交接位(不在 handover ⇒ 照回此刻车道)。 */
310
+ attach(): EngineAttachAnswer;
150
311
  }
151
312
  /** {@link createEngineLeaseTracker} 的把手:`stop()` 收摊;`facts` 现读 —— **腿还在观察**时才有(收摊 / 自退触发之后缺席,says-yes ⟺ 腿在转)。 */
152
313
  export interface EngineLeaseHandle {
153
314
  stop(): void;
154
315
  readonly facts: EngineLeaseFacts | undefined;
316
+ /** [ref]:此刻的车道(腿在观察才有;SIGHUP 读者经它读同一只车道函数)。 */
317
+ lane(): EngineLane | undefined;
318
+ /** [ref]:交接门(交接车道装了才有)。 */
319
+ readonly handover?: EngineHandoverDoor;
155
320
  }
156
321
  export interface EngineLeaseDeps {
157
322
  /** 活附着数 = 本副本当前**打开着的 SSE 流**条数(`drainState.attachedStreams`,由 `createHttpServer` 自赋)。 */
158
323
  attachedStreams: () => number;
159
- /** 在飞腿数 —— **`/health` 的 `inflight` 同一只 getter**(`drainState.inflight`)。不自建第二套计数:
160
- * 两套计数迟早分叉,而分叉的那一侧恰好是「以为没 run 了」就把引擎关掉。 */
324
+ /** 必须等完的活的数 —— **阻断集** `drainState.blockingInflight`(`http + leader`;与排空等的、SIGHUP 空闲窗同一只 getter。会话驻留的后台 shell / monitor 在
325
+ * `/health.inflight` 的总数里、不在这里,见顶注「已知收窄」)。不自建第二套计数:两套计数迟早分叉,而分叉的那一侧恰好是「以为没 run 了」就把引擎关掉。 */
161
326
  inflight: () => number;
162
327
  /** 最后一次**非探针** HTTP 触点的单调时刻(`drainState.lastActivityAt`)。
163
328
  * ⚠️ 时基契约:必须与本模块的 `now` 同为 `performance.now()` 单调域(`server.ts` 的打点即该域)——
@@ -180,8 +345,12 @@ export interface EngineLeaseDeps {
180
345
  /** owned 态(父还活着 / 没被监视)下三合取**持续**满这么久才触发(`config.engineLingerMs`,默认 60s)。
181
346
  * **`Infinity`** = 永不自退(`ENGINE_AUTO_EXIT` 关着的 owned 引擎;窗永远追不上 ⇒ 无需第二条分支)。 */
182
347
  lingerMs: number;
183
- /** 孤儿车道(`SEMA_PARENT_PID` 在场才给)。缺席 ⇒ 这台引擎不可能成孤儿,窗恒为 `lingerMs`。 */
348
+ /** 孤儿车道(`SEMA_PARENT_PID` 在场才给)。缺席 ⇒ 这台引擎不可能成孤儿。 */
184
349
  orphan?: EngineLeaseOrphanLane;
350
+ /** [ref] 首次附着车道(恒给;见 {@link EngineLeaseFirstAttachLane})。 */
351
+ firstAttach: EngineLeaseFirstAttachLane;
352
+ /** [ref] 交接车道(非多租户才给;见 {@link EngineLeaseHandoverLane})。缺席 ⇒ 把手上没有交接门。 */
353
+ handover?: EngineLeaseHandoverLane;
185
354
  log: (event: string, fields: Record<string, unknown>) => void;
186
355
  /** 检查拍频 seam(测试)。缺省 = 顶注 9 的推导(`min(ENGINE_LEASE_TICK_MS, 本腿可能执行的每个窗)`)。 */
187
356
  tickMs?: number;
@@ -189,6 +358,9 @@ export interface EngineLeaseDeps {
189
358
  * 拉长 linger 窗 —— 前跳把窗直接跳过去 = 在一只还有人用的引擎底下提前关停。 */
190
359
  now?: () => number;
191
360
  }
361
+ /** 后台 shell 计数在没有交接车道时**结构上读不到**(只有 handover 那一格读它,而 handover 车道只经交接门进入,门只在车道装了时建)。
362
+ * 真被读到 = 不变量破了 —— 响亮抛,不回一个看起来合理的 0([ref])。 */
363
+ export declare const backgroundShellsUnreachable: () => number;
192
364
  /** 装配并**立刻**起自退检查(装配即 `engine_lease_armed`)。返回停机链用的 `stop()`。 */
193
365
  export declare function createEngineLeaseTracker(deps: EngineLeaseDeps): EngineLeaseHandle;
194
366
  //# sourceMappingURL=engine-lease.d.ts.map
@@ -1,20 +1,86 @@
1
1
  export const ENGINE_LEASE_TICK_MS = 15_000;
2
+ export const ENGINE_LANES = ["handover", "orphan", "awaitingFirstAttach", "owned"];
3
+ export const ENGINE_LANE_TABLE = Object.freeze({
4
+ handover: { countsBackgroundShells: true, sighupYieldsToLease: true },
5
+ orphan: { countsBackgroundShells: false, sighupYieldsToLease: false },
6
+ awaitingFirstAttach: { countsBackgroundShells: false, sighupYieldsToLease: false },
7
+ owned: { countsBackgroundShells: false, sighupYieldsToLease: false },
8
+ });
9
+ export function deriveEngineLane(s) {
10
+ if (s.handedOver)
11
+ return "handover";
12
+ if (s.orphaned)
13
+ return "orphan";
14
+ return s.touched ? "owned" : "awaitingFirstAttach";
15
+ }
16
+ export function laneWork(lane, s, reader) {
17
+ const row = lane === undefined ? undefined : ENGINE_LANE_TABLE[lane];
18
+ switch (reader) {
19
+ case "sighup":
20
+ if (row?.sighupYieldsToLease === true)
21
+ return true;
22
+ break;
23
+ case "lease":
24
+ break;
25
+ default: {
26
+ const unhandled = reader;
27
+ throw new Error(`laneWork: unknown reader ${String(unhandled)}`);
28
+ }
29
+ }
30
+ return s.attached > 0 || s.inflight > 0 || (row?.countsBackgroundShells === true && s.backgroundShells() > 0);
31
+ }
32
+ export const HANDOVER_REASONS = ["background_exit"];
33
+ export const backgroundShellsUnreachable = () => {
34
+ throw new Error("engine lease: background-shell count read on a lane table row that counts it, but no handover lane is armed (invariant broken)");
35
+ };
2
36
  export function createEngineLeaseTracker(deps) {
3
37
  const orphanLane = deps.orphan;
4
- const shortestLingerMs = Math.min(deps.lingerMs, orphanLane?.lingerMs ?? Number.POSITIVE_INFINITY);
38
+ const handoverLane = deps.handover;
39
+ const shortestLingerMs = Math.min(deps.lingerMs, orphanLane?.lingerMs ?? Number.POSITIVE_INFINITY, handoverLane?.lingerMs ?? Number.POSITIVE_INFINITY);
5
40
  const tickMs = deps.tickMs ?? Math.min(ENGINE_LEASE_TICK_MS, shortestLingerMs);
6
- const leaseFacts = Number.isFinite(shortestLingerMs) ? Object.freeze({ lingerMs: shortestLingerMs, tickMs }) : undefined;
41
+ const firstAttachWindowMs = Math.max(deps.lingerMs, deps.firstAttach.windowMs);
7
42
  const now = deps.now ?? (() => performance.now());
8
43
  let stopped = false;
9
- let idleSince = now();
44
+ const armedAt = now();
45
+ const armedEpochMs = Date.now();
46
+ const epochOf = (mono) => Math.round(armedEpochMs + (mono - armedAt));
47
+ let idleSince = armedAt;
48
+ let handedOver = null;
49
+ const backgroundShells = handoverLane?.backgroundShells ?? backgroundShellsUnreachable;
50
+ function windowOf(lane) {
51
+ switch (lane) {
52
+ case "handover":
53
+ return handoverLane?.lingerMs ?? Number.POSITIVE_INFINITY;
54
+ case "orphan":
55
+ return orphanLane?.lingerMs ?? Number.POSITIVE_INFINITY;
56
+ case "awaitingFirstAttach":
57
+ return firstAttachWindowMs;
58
+ case "owned":
59
+ return deps.lingerMs;
60
+ default: {
61
+ const unhandled = lane;
62
+ throw new Error(`engine lease: unknown lane ${String(unhandled)}`);
63
+ }
64
+ }
65
+ }
66
+ function laneNow() {
67
+ const orphaned = orphanLane ? orphanLane.read() : null;
68
+ const lane = deriveEngineLane({ handedOver: handedOver !== null, orphaned: orphaned !== null, touched: deps.firstAttach.firstTouchAt() !== null });
69
+ return { lane, orphaned };
70
+ }
10
71
  deps.log("engine_lease_armed", {
11
72
  lingerMs: Number.isFinite(deps.lingerMs) ? deps.lingerMs : null,
12
73
  orphanLingerMs: orphanLane?.lingerMs ?? null,
74
+ firstAttachMs: deps.firstAttach.windowMs,
75
+ firstAttachWindowMs: Number.isFinite(firstAttachWindowMs) ? firstAttachWindowMs : null,
76
+ handoverLingerMs: handoverLane?.lingerMs ?? null,
13
77
  tickMs,
14
78
  note: "engine self-exits once NO shell session is attached AND no run is in flight AND no HTTP touch, " +
15
- "continuously for the linger window — via the same graceful drain as SIGTERM. The window is " +
16
- "ENGINE_LINGER_MS while the parent is alive (or the watch is not armed) and ENGINE_ORPHAN_LINGER_MS " +
17
- "once SEMA_PARENT_PID's process is gone. Set ENGINE_AUTO_EXIT=false and unset SEMA_PARENT_PID to disable.",
79
+ "continuously for the linger window — via the same graceful drain as SIGTERM. The window is chosen by lane: " +
80
+ "ENGINE_HANDOVER_LINGER_MS after POST /v1/engine/handover (until the next POST /v1/engine/attach; running background " +
81
+ "shells count as work only there); ENGINE_ORPHAN_LINGER_MS once SEMA_PARENT_PID's process is gone; otherwise " +
82
+ "max(ENGINE_LINGER_MS, ENGINE_FIRST_ATTACH_MS) until the first non-probe HTTP request ever arrives, and ENGINE_LINGER_MS after it. " +
83
+ "Set ENGINE_AUTO_EXIT=false and unset SEMA_PARENT_PID to disable.",
18
84
  });
19
85
  const timer = setInterval(() => {
20
86
  if (stopped)
@@ -25,7 +91,8 @@ export function createEngineLeaseTracker(deps) {
25
91
  }
26
92
  const attached = deps.attachedStreams();
27
93
  const inflight = deps.inflight();
28
- if (attached > 0 || inflight > 0) {
94
+ const { lane, orphaned } = laneNow();
95
+ if (laneWork(lane, { attached, inflight, backgroundShells }, "lease")) {
29
96
  idleSince = null;
30
97
  return;
31
98
  }
@@ -34,8 +101,7 @@ export function createEngineLeaseTracker(deps) {
34
101
  idleSince = t;
35
102
  return;
36
103
  }
37
- const orphaned = orphanLane ? orphanLane.read() : null;
38
- const linger = orphanLane && orphaned !== null ? orphanLane.lingerMs : deps.lingerMs;
104
+ const linger = windowOf(lane);
39
105
  if (t - deps.lastActivityAt() < linger)
40
106
  return;
41
107
  if (t - deps.lastAttachEndedAt() < linger)
@@ -43,22 +109,17 @@ export function createEngineLeaseTracker(deps) {
43
109
  if (t - idleSince < linger)
44
110
  return;
45
111
  stop();
112
+ const orphanedNow = lane === "orphan" ? orphaned : null;
46
113
  const report = {
47
- orphan: orphaned !== null,
48
- reason: orphaned?.reason ?? "idle",
114
+ lane,
115
+ reason: orphanedNow?.reason ?? "idle",
49
116
  idleForMs: Math.round(t - Math.max(idleSince, deps.lastActivityAt(), deps.lastAttachEndedAt())),
50
117
  attached,
51
118
  inflight,
52
119
  lingerMs: linger,
53
- ...(orphaned !== null ? { orphanedForMs: Math.round(t - orphaned.since) } : {}),
120
+ ...(orphanedNow !== null ? { orphanedForMs: Math.round(t - orphanedNow.since) } : {}),
54
121
  };
55
- deps.log("engine_lease_auto_exit", {
56
- ...report,
57
- note: orphaned !== null
58
- ? "the parent that spawned this engine is gone AND nobody is attached, no run is in flight and no HTTP touch " +
59
- "for the full orphan linger window — entering the SIGTERM graceful drain path (exit code 0; see exit-last.json)"
60
- : "no attached shell session, no in-flight run and no HTTP touch for the full linger window — entering the SIGTERM graceful drain path",
61
- });
122
+ deps.log("engine_lease_auto_exit", { ...report, note: AUTO_EXIT_NOTES[lane] });
62
123
  deps.selfExit(report);
63
124
  }, tickMs);
64
125
  timer.unref?.();
@@ -68,11 +129,76 @@ export function createEngineLeaseTracker(deps) {
68
129
  stopped = true;
69
130
  clearInterval(timer);
70
131
  }
132
+ function firstAttachFacts() {
133
+ if (!Number.isFinite(firstAttachWindowMs))
134
+ return undefined;
135
+ const touchedAt = deps.firstAttach.firstTouchAt();
136
+ return Object.freeze({ windowMs: firstAttachWindowMs, since: armedEpochMs, attachedAt: touchedAt === null ? null : epochOf(touchedAt) });
137
+ }
138
+ function handoverFacts() {
139
+ if (handedOver === null || handoverLane === undefined)
140
+ return undefined;
141
+ return Object.freeze({
142
+ since: epochOf(handedOver.since),
143
+ reason: handedOver.reason,
144
+ lingerMs: handoverLane.lingerMs,
145
+ backgroundShells: handoverLane.backgroundShells(),
146
+ });
147
+ }
148
+ const door = handoverLane === undefined
149
+ ? undefined
150
+ : {
151
+ get observing() {
152
+ return !stopped;
153
+ },
154
+ enter(reason) {
155
+ if (handedOver === null) {
156
+ const from = laneNow().lane;
157
+ handedOver = { since: now(), reason };
158
+ deps.log("engine_lane_changed", { from, to: "handover", reason: "handover_requested", since: epochOf(handedOver.since) });
159
+ }
160
+ const facts = handoverFacts();
161
+ if (facts === undefined)
162
+ throw new Error("engine lease: handover facts absent right after entering the handover lane (invariant broken)");
163
+ return { lane: "handover", since: facts.since, lingerMs: facts.lingerMs, backgroundShells: facts.backgroundShells };
164
+ },
165
+ attach() {
166
+ if (handedOver === null)
167
+ return { lane: laneNow().lane };
168
+ const t = now();
169
+ const handedOverForMs = Math.round(t - handedOver.since);
170
+ handedOver = null;
171
+ const to = laneNow().lane;
172
+ deps.log("engine_lane_changed", { from: "handover", to, reason: "attached", since: epochOf(t) });
173
+ return { lane: to, handedOverForMs };
174
+ },
175
+ };
71
176
  return {
72
177
  stop,
73
178
  get facts() {
74
- return stopped ? undefined : leaseFacts;
179
+ if (stopped || !Number.isFinite(shortestLingerMs))
180
+ return undefined;
181
+ const firstAttach = firstAttachFacts();
182
+ const handover = handoverFacts();
183
+ return Object.freeze({
184
+ lingerMs: shortestLingerMs,
185
+ tickMs,
186
+ lane: laneNow().lane,
187
+ ...(firstAttach !== undefined ? { firstAttach } : {}),
188
+ ...(handover !== undefined ? { handover } : {}),
189
+ });
75
190
  },
191
+ lane: () => (stopped ? undefined : laneNow().lane),
192
+ ...(door !== undefined ? { handover: door } : {}),
76
193
  };
77
194
  }
195
+ const AUTO_EXIT_NOTES = {
196
+ handover: "this engine was handed over to the background (POST /v1/engine/handover) and nobody attached, no run and no background shell " +
197
+ "was running and no HTTP touch for the full ENGINE_HANDOVER_LINGER_MS window — entering the SIGTERM graceful drain path",
198
+ orphan: "the parent that spawned this engine is gone AND nobody is attached, no run is in flight and no HTTP touch " +
199
+ "for the full orphan linger window — entering the SIGTERM graceful drain path (exit code 0; see exit-last.json)",
200
+ awaitingFirstAttach: "no non-probe HTTP request EVER reached this engine within the first-attach window (max(ENGINE_LINGER_MS, ENGINE_FIRST_ATTACH_MS)) " +
201
+ "and nothing is attached or in flight — entering the SIGTERM graceful drain path",
202
+ owned: "no attached shell session, no in-flight run and no HTTP touch for the full linger window — entering the SIGTERM graceful drain path",
203
+ };
78
204
  //# sourceMappingURL=engine-lease.js.map
@@ -55,6 +55,9 @@ export interface SessionScopedRegistries {
55
55
  * 请求放宽凭据面)。
56
56
  */
57
57
  setSessionInheritEnv(sid: string, policy: TaskInheritEnv | undefined): void;
58
+ /** 本进程见过的会话 → 属主(围栏表本身,只读视图;`null` = 匿名)。**会话驻留后台行的枚举源**:`session-resident-background.ts` 逐会话问注册表
59
+ * (core 的访问元组要带 scope,没有进程总数的读口),读的就是这一张 —— 每次提交写一笔、`dropSession` 清一笔、LRU 有界,不另立第二张会话名册。 */
60
+ readonly sessionOwners: ReadonlyMap<string, string | null>;
58
61
  /** 记录/对拍本会话的主人。与上次不同 ⇒ **先清两张表**再记新主(换主即清)。`null` = 匿名会话,
59
62
  * 它同样是一个**稳定**的身份值(不是「未知」),所以匿名重入不会被判成换主。 */
60
63
  fenceSessionOwner(sid: string, owner: string | null): void;
@@ -171,6 +174,7 @@ export declare function createExecutionEnv(ctx: ExecutionEnvCtx): {
171
174
  setSessionInheritEnv: (sid: string, policy: TaskInheritEnv | undefined) => void;
172
175
  fenceSessionOwner: (sid: string, owner: string | null) => void;
173
176
  dropSessionScoped: (sid: string) => void;
177
+ sessionResidentBackground: () => number;
174
178
  executionEnvFactory: import("@sema-agent/core").ExecutionEnvFactory | undefined;
175
179
  worktreeReap: (() => Promise<void>) | undefined;
176
180
  sendUserFileTaskEnvs: TaskEnvRegistry | undefined;
@@ -40,6 +40,7 @@ import { createLaneHomeAbsenceReporter } from "../lane-home-probe.js";
40
40
  import { REMOTE_EXEC_PROVIDER_WORDS, buildLaneHomeBootNote, buildLaneHomeClaim, capabilitiesFor, executionLaneOf, laneBindsReads, laneIsolated, laneSandboxReadFloorState } from "../execution-lane-caps.js";
41
41
  import { warnReadDenyFloorUnkept } from "./execution-lane.js";
42
42
  import { seatText } from "../trace/exit-seat.js";
43
+ import { countSessionResidentBackground } from "../session-resident-background.js";
43
44
  function boundedSessionSetter(map, maxEntries) {
44
45
  return (sid, value) => {
45
46
  map.delete(sid);
@@ -79,6 +80,7 @@ export function createSessionScopedRegistries(maxEntries = MAX_CWD_SESSIONS) {
79
80
  return {
80
81
  perSessionCwd,
81
82
  perSessionShellEnv,
83
+ sessionOwners: owners,
82
84
  setSessionCwd: (sid, cwd) => {
83
85
  perSessionCwd.set(sid, cwd);
84
86
  touch(sid, undefined);
@@ -506,10 +508,20 @@ export function createExecutionEnv(ctx) {
506
508
  : undefined;
507
509
  if (lspManager)
508
510
  logger.info("lsp_enabled", { provider: lspProvider });
511
+ const unreadableReported = new Set();
512
+ const sessionResidentBackground = () => countSessionResidentBackground(sessionScoped.sessionOwners, {
513
+ onUnreadable: (sessionId, err) => {
514
+ if (unreadableReported.has(sessionId))
515
+ return;
516
+ unreadableReported.add(sessionId);
517
+ logger.warn("session_resident_background_unreadable", { sessionId, counted: 1, err: seatText("operator-log-text", String(err)) });
518
+ },
519
+ });
509
520
  return {
510
521
  perTaskImage, sessionEnvSelection, perSessionCwd, setSessionCwd, setSessionShellEnv, setSessionInheritEnv,
511
522
  fenceSessionOwner: sessionScoped.fenceSessionOwner,
512
523
  dropSessionScoped: sessionScoped.dropSession,
524
+ sessionResidentBackground,
513
525
  executionEnvFactory, worktreeReap, sendUserFileTaskEnvs, lspManager, spaceReclaimer, sandboxSecretSeatGate, sandboxPodReaper, sandboxGitPlane,
514
526
  };
515
527
  }
@@ -1,4 +1,49 @@
1
+ /**
2
+ * [ref] —— **进程退出边界**的装配器:全进程**一只**,两条车道(HTTP 服务 / `run-local` 一次性 CLI)同走。
3
+ *
4
+ * 不变量 = **本进程拥有的东西不得活过本进程**(自起的前台子进程组 + 自铸的 ephemeral host 工作区)。
5
+ * 它是**进程**的性质,不属于某一条退出路径,也不属于某一个入口 —— 所以收割挂 `process.once("exit")`,
6
+ * 而这只钩的**装配**从 `boot/shutdown.ts`(只有 HTTP 车道装得起:它的 ctx 绑着 server / backend / runner…)
7
+ * 抽到这里,变成一条只要一只 {@link Logger} 的函数。
8
+ *
9
+ * ── [ref] 的病(test 7.93.3 复验实测)────────────────────────────────────────────────────────────────
10
+ * [ref] 之后 HTTP 车道零孤儿,而 `src/run-local.ts` **一只 `process.on/once` 都没有** —— 它却经
11
+ * `hostExecutionEnvFactory` 走同一条 host exec 腿、同一本前台簿(`trackHostForegroundChild`)。Ctrl-C ⇒
12
+ * 进程在 12ms 内被信号**默认终止**,簿上的组留 `ppid=1` 跑满。用户面:一次 `sema run` 被 Ctrl-C 之后,
13
+ * 机器上留着它起的 `npm run build` / `sleep`,没有任何人会再去 kill。
14
+ *
15
+ * ── 为什么不是「run-local 也补一份」──────────────────────────────────────────────────────────────────
16
+ * 同一句不变量在两个入口各写一份 = 两个写者、两份会各自腐烂的注(CLAUDE.md 以点改面:修一个类,不是
17
+ * 修一个出口)。所以是**抽出**而不是复制:钩体、两条腿、哨兵读数、`dispose` 全在本文件,两条车道各自
18
+ * 只剩一行调用。HTTP 车道的行为**逐字不变**(函数体整段搬运,缩进外无一字改动)。
19
+ *
20
+ * 🔴 **光有本装配器治不了 run-local**:`exit` 事件在进程被信号**默认终止**时**不触发**。前台 CLI 的
21
+ * Ctrl-C 是常态 ⇒ run-local 必须同时装 SIGINT / SIGTERM 处理器,让信号落到一条显式 `process.exit` 上
22
+ * (见 `run-local.ts` 的 {@link installRunLocalExitBoundary} 注:退出码取 128+signo 的理由写在那里)。
23
+ * HTTP 车道的三只信号处理器留在 `boot/shutdown.ts`(它们承载的是 drain / SIGHUP 空闲窗这些**车道语义**,
24
+ * 不是退出边界)。
25
+ */
26
+ import { type TaskRegistry } from "@sema-agent/core";
1
27
  import type { Logger } from "../observability/logger.js";
28
+ /**
29
+ * 退出边界的硬收面**此刻**会收掉的后台 shell 行数(`background_bash` + `monitor`,在跑,整张进程级注册表,retain 行按
30
+ * {@link PROCESS_EXIT_INCLUDES_RETAINED} 跳过)—— 「引擎现在退,会不会把一条在跑的后台 shell 一起收掉」这一问的**唯一**读法。
31
+ *
32
+ * 一问一源:数与收读同一只 `includeRetained` 常量,core 的 `countRunningBackgroundShells` 与 `killAllBackgroundShellsNow` 又按同一只访问函数选行
33
+ * (core 注:the count and the walk cannot drift)⇒ 这只数就是硬收面会交出的 `killed` 收据数。
34
+ * **不是** `/health.inflightBreakdown.sessionResident`(`session-resident-background.ts`):那一项回答「引擎托管着多少会话驻留的活」,是总数里的一个分项 ——
35
+ * 只数围栏表里会话的、会话驻留的行(挂在 run 腿上的行算在 http 项里,数它是第二次;围栏外的会话它看不见);本函数整表数、不分会话,
36
+ * 腿上的行、围栏外会话的行都在内。两问的关系 = 读得出时本数 ≥ 驻留项(驻留项的每一行都是本数的一行)。
37
+ *
38
+ * 读不出(core:某行 env 的 retain 声明读抛 ⇒ 计数抛)⇒ 按 **1** 计并经 `onUnreadable` 报出 —— 读不出 ⇒ 非空闲,与驻留项同一条律;
39
+ * 本函数在租约拍(定时器回调)、`/health` 与交接口里被调,**不许抛**(定时器里一抛就是 uncaught,一台交接出去的引擎会因此崩掉并收掉它本该留着的 shell)——
40
+ * 报口自己再抛(抛出物转不成文本之类)也一样:落 `console.error`(不回同一只报口),读数照返 1。
41
+ * `registry` 只供测试换桩(只用到 `countRunningBackgroundShells`)。
42
+ */
43
+ export declare function countShellsExitWouldKill(opts: {
44
+ readonly registry?: Pick<TaskRegistry, "countRunningBackgroundShells">;
45
+ readonly onUnreadable: (err: unknown) => void;
46
+ }): number;
2
47
  /** {@link installProcessExitBoundary} 的把手。 */
3
48
  export interface ProcessExitBoundary {
4
49
  /**
@@ -8,6 +53,11 @@ export interface ProcessExitBoundary {
8
53
  * 返回本次软停的后台行数(core `reapAllSessionBackground` 的返回;腿抛错 ⇒ 0,warn 已记)—— 调用方据此决定欠不欠宽限。
9
54
  */
10
55
  reapChildProcessesEarly(): number;
56
+ /**
57
+ * 硬收面此刻会收掉的后台 shell 行数({@link countShellsExitWouldKill},进程级注册表)。读不出 ⇒ 按 1 计,运维行
58
+ * `engine_handover_shell_count_unreadable` 本装配**只响一次**(这只读口在租约拍里被调,逐次响会刷屏)。附着租约交接车道那一格工作谓词读它。
59
+ */
60
+ shellsExitWouldKill(): number;
11
61
  /** 摘掉本次装配的 `exit` 钩。幂等(同进程内反复装配的调用方=测试必须用它收尾)。 */
12
62
  dispose(): void;
13
63
  }