@sema-agent/server 7.144.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 (305) hide show
  1. package/MIGRATION.md +621 -0
  2. package/README.md +3 -0
  3. package/README.zh-CN.md +1 -0
  4. package/USAGE.md +233 -54
  5. package/deploy/facts/CHANGES.json +25 -0
  6. package/deploy/facts/README.md +57 -0
  7. package/deploy/facts/lanes.json +54 -0
  8. package/deploy/facts/lanes.schema.json +57 -0
  9. package/deploy/facts/sandbox-rbac.json +113 -0
  10. package/deploy/facts/sandbox-rbac.schema.json +74 -0
  11. package/deploy/facts/sealed-key.json +9 -0
  12. package/deploy/facts/sealed-key.schema.json +52 -0
  13. package/deploy/sema-up/chart/templates/_helpers.tpl +19 -0
  14. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +47 -7
  15. package/deploy/sema-up/chart/templates/server-v4pro.yaml +4 -0
  16. package/deploy/sema-up/chart/templates/server.yaml +4 -0
  17. package/deploy/sema-up/chart/values.yaml +7 -0
  18. package/deploy/sema-up/stack/sema-stack-cluster.yaml +30 -14
  19. package/dist/adoption/import-limits.d.ts +6 -0
  20. package/dist/adoption/import-limits.js +7 -0
  21. package/dist/adoption/import.d.ts +62 -0
  22. package/dist/adoption/import.js +107 -0
  23. package/dist/adoption/wire.d.ts +52 -0
  24. package/dist/adoption/wire.js +9 -0
  25. package/dist/approval-ask-machine.d.ts +2 -2
  26. package/dist/approval-card.d.ts +3 -12
  27. package/dist/approval-card.js +0 -4
  28. package/dist/approval-gate-currency.d.ts +1 -1
  29. package/dist/approval-gate-currency.js +1 -1
  30. package/dist/approval-reconciler.d.ts +1 -1
  31. package/dist/audit.d.ts +2 -21
  32. package/dist/audit.js +0 -9
  33. package/dist/boot/deployment-mods.d.ts +11 -1
  34. package/dist/boot/deployment-mods.js +15 -2
  35. package/dist/boot/device-lane.js +2 -0
  36. package/dist/boot/engine-lease.d.ts +193 -21
  37. package/dist/boot/engine-lease.js +146 -20
  38. package/dist/boot/execution-env.d.ts +4 -0
  39. package/dist/boot/execution-env.js +12 -0
  40. package/dist/boot/exit-boundary.d.ts +50 -0
  41. package/dist/boot/exit-boundary.js +32 -0
  42. package/dist/boot/exit-last.d.ts +20 -4
  43. package/dist/boot/exit-last.js +21 -4
  44. package/dist/boot/governance-seams.js +1 -0
  45. package/dist/boot/memory-consolidation.d.ts +29 -17
  46. package/dist/boot/memory-consolidation.js +26 -15
  47. package/dist/boot/reapers.d.ts +12 -4
  48. package/dist/boot/reapers.js +39 -15
  49. package/dist/boot/resolve-spec.d.ts +2 -0
  50. package/dist/boot/resolve-spec.js +38 -15
  51. package/dist/boot/runner-deps.js +1 -1
  52. package/dist/boot/session-faces.d.ts +1 -1
  53. package/dist/boot/session-faces.js +6 -4
  54. package/dist/boot/shutdown.js +27 -9
  55. package/dist/boot/stage-01-config.js +1 -1
  56. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  57. package/dist/boot/stage-03-store-backend.js +2 -2
  58. package/dist/boot/stage-04-stores.d.ts +1 -0
  59. package/dist/boot/stage-05-execution-env.d.ts +3 -0
  60. package/dist/boot/stage-05-execution-env.js +3 -3
  61. package/dist/boot/stage-06-runners.d.ts +3 -0
  62. package/dist/boot/stage-06-runners.js +11 -10
  63. package/dist/boot/stage-07-capability-layer.d.ts +3 -0
  64. package/dist/boot/stage-07-capability-layer.js +7 -2
  65. package/dist/boot/stage-08-reapers.d.ts +3 -0
  66. package/dist/boot/stage-08-reapers.js +3 -3
  67. package/dist/boot/stage-09-leader.d.ts +4 -1
  68. package/dist/boot/stage-10-http-server.d.ts +4 -1
  69. package/dist/boot/stage-10-http-server.js +4 -3
  70. package/dist/boot/stores.d.ts +1 -0
  71. package/dist/boot/stores.js +35 -19
  72. package/dist/boot/workflow-orchestration.d.ts +1 -1
  73. package/dist/boot/workflow-orchestration.js +2 -1
  74. package/dist/capabilities/composite-members.d.ts +3 -1
  75. package/dist/capabilities/composite-members.js +3 -1
  76. package/dist/capabilities/scenarios.d.ts +2 -2
  77. package/dist/capability-facts.d.ts +18 -1
  78. package/dist/capability-facts.js +25 -7
  79. package/dist/checkpoint-ctx-seal-m28.d.ts +1 -1
  80. package/dist/config-catalog.js +13 -4
  81. package/dist/config-d-migration.d.ts +3 -2
  82. package/dist/config-provider.d.ts +2 -2
  83. package/dist/config-types.d.ts +43 -4
  84. package/dist/config.d.ts +6 -1
  85. package/dist/config.js +67 -11
  86. package/dist/deleted-session-ids.d.ts +19 -0
  87. package/dist/deleted-session-ids.js +26 -0
  88. package/dist/deploy-facts.d.ts +155 -0
  89. package/dist/deploy-facts.js +94 -0
  90. package/dist/device-ws-hub.d.ts +48 -3
  91. package/dist/device-ws-hub.js +48 -2
  92. package/dist/error-code-family.d.ts +35 -0
  93. package/dist/error-code-family.js +26 -0
  94. package/dist/fleet/drain-entry.d.ts +39 -3
  95. package/dist/fleet/drain-entry.js +6 -0
  96. package/dist/fleet/fleet-bus.d.ts +15 -4
  97. package/dist/fleet/fleet-bus.js +17 -3
  98. package/dist/hooks/branch-transcript.d.ts +1 -1
  99. package/dist/hooks/branch-transcript.js +1 -0
  100. package/dist/hooks/classic-stations.d.ts +25 -8
  101. package/dist/hooks/classic-stations.js +29 -3
  102. package/dist/hooks/hook-runner.d.ts +33 -16
  103. package/dist/hooks/hook-runner.js +166 -73
  104. package/dist/hooks/hook-source.d.ts +14 -2
  105. package/dist/hooks/hook-source.js +8 -0
  106. package/dist/http/active-run-conflict.d.ts +1 -1
  107. package/dist/http/active-run-conflict.js +1 -1
  108. package/dist/http/admission.js +13 -2
  109. package/dist/http/checkpoint-ctx.d.ts +60 -71
  110. package/dist/http/checkpoint-ctx.js +35 -40
  111. package/dist/http/dispatch.js +18 -2
  112. package/dist/http/durable-run-leg.d.ts +5 -0
  113. package/dist/http/durable-run-leg.js +8 -6
  114. package/dist/http/execution-context.d.ts +4 -0
  115. package/dist/http/execution-context.js +9 -0
  116. package/dist/http/health-self-report.d.ts +6 -2
  117. package/dist/http/health-self-report.js +3 -2
  118. package/dist/http/memory-compliance-failure-reply.d.ts +1 -1
  119. package/dist/http/memory-compliance-failure-reply.js +2 -15
  120. package/dist/http/memory-control-plane-corrupt-reply.d.ts +25 -0
  121. package/dist/http/memory-control-plane-corrupt-reply.js +19 -0
  122. package/dist/http/principal-gate.d.ts +35 -12
  123. package/dist/http/principal-gate.js +16 -12
  124. package/dist/http/request-guards.d.ts +1 -1
  125. package/dist/http/request-guards.js +5 -5
  126. package/dist/http/resume-legs.d.ts +22 -0
  127. package/dist/http/resume-legs.js +31 -20
  128. package/dist/http/route-ctx.d.ts +26 -10
  129. package/dist/http/route-table.js +2 -0
  130. package/dist/http/routes/adoption.d.ts +11 -0
  131. package/dist/http/routes/adoption.js +80 -6
  132. package/dist/http/routes/agents-roster.js +1 -0
  133. package/dist/http/routes/approvals-assistant.d.ts +20 -1
  134. package/dist/http/routes/approvals-assistant.js +29 -7
  135. package/dist/http/routes/background.js +1 -0
  136. package/dist/http/routes/capabilities.js +4 -1
  137. package/dist/http/routes/diagnostics.d.ts +9 -14
  138. package/dist/http/routes/diagnostics.js +10 -20
  139. package/dist/http/routes/engine.d.ts +31 -0
  140. package/dist/http/routes/engine.js +81 -0
  141. package/dist/http/routes/memory-bundle.js +3 -0
  142. package/dist/http/routes/memory-consolidation.js +3 -5
  143. package/dist/http/routes/memory-origin.d.ts +1 -1
  144. package/dist/http/routes/memory-origin.js +2 -4
  145. package/dist/http/routes/memory-policy.js +5 -1
  146. package/dist/http/routes/observability.js +3 -6
  147. package/dist/http/routes/rules.d.ts +1 -1
  148. package/dist/http/routes/rules.js +5 -2
  149. package/dist/http/routes/runs.js +7 -3
  150. package/dist/http/routes/session-sync.js +24 -25
  151. package/dist/http/routes/sessions-list.d.ts +1 -1
  152. package/dist/http/routes/sessions.js +4 -0
  153. package/dist/http/routes/tasks.js +35 -14
  154. package/dist/http/routes/trace-usage.js +1 -1
  155. package/dist/http/run-concurrency-cap.d.ts +86 -0
  156. package/dist/http/run-concurrency-cap.js +54 -0
  157. package/dist/http/run-meta.d.ts +14 -7
  158. package/dist/http/run-meta.js +5 -2
  159. package/dist/http/server.d.ts +27 -16
  160. package/dist/http/server.js +21 -3
  161. package/dist/http/stored-body-replay.d.ts +9 -6
  162. package/dist/http/stored-body-replay.js +19 -5
  163. package/dist/http/submit-ctx.d.ts +12 -0
  164. package/dist/http/submit-ctx.js +23 -0
  165. package/dist/http/wire-types.d.ts +8 -3
  166. package/dist/json-body-limit.d.ts +8 -0
  167. package/dist/json-body-limit.js +2 -0
  168. package/dist/leader/diffout.d.ts +1 -1
  169. package/dist/local-data-root.d.ts +2 -2
  170. package/dist/memory-operator-faces.d.ts +9 -4
  171. package/dist/memory-scope-stores.d.ts +9 -7
  172. package/dist/memory-scope-stores.js +3 -14
  173. package/dist/memory-scope.d.ts +36 -0
  174. package/dist/memory-scope.js +32 -1
  175. package/dist/net-address.d.ts +53 -0
  176. package/dist/net-address.js +111 -0
  177. package/dist/observability/fail-open.d.ts +28 -12
  178. package/dist/observability/fail-open.js +28 -12
  179. package/dist/orchestration/workflow-notify-journal.js +2 -3
  180. package/dist/org-delivery-gate.js +1 -1
  181. package/dist/peer-directory.d.ts +6 -12
  182. package/dist/peer-directory.js +5 -1
  183. package/dist/plugins/approval-ask-store-file.d.ts +16 -2
  184. package/dist/plugins/approval-ask-store-file.js +21 -1
  185. package/dist/plugins/approval-ask-store-memory.d.ts +8 -2
  186. package/dist/plugins/approval-ask-store-memory.js +11 -8
  187. package/dist/plugins/caching-session-store.d.ts +6 -1
  188. package/dist/plugins/caching-session-store.js +6 -0
  189. package/dist/plugins/checkpoint-store-face.d.ts +426 -0
  190. package/dist/plugins/checkpoint-store-face.js +152 -0
  191. package/dist/plugins/file-run-store.d.ts +13 -7
  192. package/dist/plugins/file-run-store.js +12 -13
  193. package/dist/plugins/fork-routing-session-store.d.ts +6 -2
  194. package/dist/plugins/fork-routing-session-store.js +2 -2
  195. package/dist/plugins/imported-layer-root-retirement.d.ts +10 -8
  196. package/dist/plugins/local-checkpoint-store.d.ts +16 -5
  197. package/dist/plugins/local-checkpoint-store.js +25 -6
  198. package/dist/plugins/local-session-store.d.ts +80 -21
  199. package/dist/plugins/local-session-store.js +79 -22
  200. package/dist/plugins/memory-engine-pg.d.ts +29 -6
  201. package/dist/plugins/memory-engine-pg.js +96 -214
  202. package/dist/plugins/memory-engine-sql-apply.d.ts +121 -0
  203. package/dist/plugins/memory-engine-sql-apply.js +358 -0
  204. package/dist/plugins/memory-engine-tidb.d.ts +35 -9
  205. package/dist/plugins/memory-engine-tidb.js +97 -227
  206. package/dist/plugins/memory-key-guards.d.ts +1 -1
  207. package/dist/plugins/memory-origin-law.d.ts +3 -3
  208. package/dist/plugins/memory-run-store.d.ts +2 -0
  209. package/dist/plugins/memory-run-store.js +6 -0
  210. package/dist/plugins/one-time-migrations.d.ts +32 -56
  211. package/dist/plugins/one-time-migrations.js +27 -119
  212. package/dist/plugins/permission-rule-store-file.d.ts +17 -3
  213. package/dist/plugins/permission-rule-store-file.js +8 -1
  214. package/dist/plugins/pg-pool.js +1 -1
  215. package/dist/plugins/pg-session-storage.d.ts +17 -4
  216. package/dist/plugins/pg-session-storage.js +8 -4
  217. package/dist/plugins/run-store-sql.d.ts +4 -0
  218. package/dist/plugins/run-store-sql.js +5 -2
  219. package/dist/plugins/schema-ensure-legs.js +2 -2
  220. package/dist/plugins/session-fork.d.ts +31 -9
  221. package/dist/plugins/session-fork.js +3 -2
  222. package/dist/plugins/session-placement.d.ts +4 -2
  223. package/dist/plugins/session-store.d.ts +1 -1
  224. package/dist/plugins/sql-json-column.d.ts +5 -1
  225. package/dist/plugins/store-backend.d.ts +11 -8
  226. package/dist/plugins/store-backend.js +8 -7
  227. package/dist/plugins/store-contracts.d.ts +1 -11
  228. package/dist/plugins/store-contracts.js +0 -9
  229. package/dist/plugins/store-sweep.d.ts +63 -0
  230. package/dist/plugins/store-sweep.js +48 -0
  231. package/dist/plugins/tidb-pool.js +1 -1
  232. package/dist/plugins/tidb-session-store.d.ts +16 -3
  233. package/dist/plugins/tidb-session-store.js +8 -4
  234. package/dist/prompt-screen.d.ts +18 -0
  235. package/dist/prompt-screen.js +5 -0
  236. package/dist/rules-consent.d.ts +56 -51
  237. package/dist/rules-consent.js +86 -14
  238. package/dist/run-acceptance.d.ts +40 -10
  239. package/dist/run-acceptance.js +38 -5
  240. package/dist/run-cancel-context.d.ts +4 -1
  241. package/dist/run-cancel-context.js +2 -1
  242. package/dist/runs.js +2 -2
  243. package/dist/sandbox-egress-policy.js +6 -11
  244. package/dist/sandbox-rbac-rules.d.ts +88 -0
  245. package/dist/sandbox-rbac-rules.js +80 -0
  246. package/dist/sealed-key-layout.d.ts +17 -0
  247. package/dist/sealed-key-layout.js +4 -0
  248. package/dist/sealed-key.js +5 -5
  249. package/dist/security.d.ts +48 -9
  250. package/dist/security.js +9 -2
  251. package/dist/session-agent.d.ts +39 -0
  252. package/dist/session-agent.js +16 -0
  253. package/dist/session-error-wire.d.ts +4 -4
  254. package/dist/session-notify-door.d.ts +5 -3
  255. package/dist/session-resident-background.d.ts +57 -0
  256. package/dist/session-resident-background.js +20 -0
  257. package/dist/session-sync-kernel.d.ts +53 -14
  258. package/dist/session-sync-kernel.js +70 -1
  259. package/dist/session-sync.d.ts +5 -3
  260. package/dist/session-workspace.d.ts +1 -0
  261. package/dist/session-workspace.js +1 -0
  262. package/dist/sighup-idle.d.ts +9 -7
  263. package/dist/sighup-idle.js +4 -5
  264. package/dist/spec-fields.d.ts +16 -0
  265. package/dist/spec-fields.js +7 -0
  266. package/dist/stored-request-env-credentials-m38.d.ts +3 -0
  267. package/dist/stored-request-env-credentials-m38.js +8 -0
  268. package/dist/stored-request-prompt-profile-m30.d.ts +1 -1
  269. package/dist/{plugins → stores-sql}/checkpoint-store-sql.d.ts +33 -429
  270. package/dist/{plugins → stores-sql}/checkpoint-store-sql.js +70 -209
  271. package/dist/{plugins → stores-sql}/mailbox-store-sql.d.ts +2 -2
  272. package/dist/{plugins → stores-sql}/mailbox-store-sql.js +4 -4
  273. package/dist/{plugins → stores-sql}/roster-store-sql.d.ts +2 -2
  274. package/dist/{plugins → stores-sql}/roster-store-sql.js +1 -1
  275. package/dist/{plugins → stores-sql}/tool-result-store-sql.d.ts +2 -2
  276. package/dist/{plugins → stores-sql}/tool-result-store-sql.js +6 -6
  277. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.d.ts +7 -12
  278. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.js +3 -8
  279. package/dist/task-settings.d.ts +59 -4
  280. package/dist/task-settings.js +50 -4
  281. package/dist/tool-approval.d.ts +2 -2
  282. package/dist/trace/core-keyset-guard.d.ts +8 -7
  283. package/dist/trace/engine-notice-wire.d.ts +1 -1
  284. package/dist/trace/engine-notice-wire.js +2 -0
  285. package/dist/trace/exit-seat.d.ts +6 -0
  286. package/dist/trace/exit-seat.js +1 -0
  287. package/dist/trace/model-segment.d.ts +3 -2
  288. package/dist/trace/model-segment.js +16 -8
  289. package/dist/trace/project.js +9 -0
  290. package/dist/trace/redact.d.ts +79 -4
  291. package/dist/trace/redact.js +523 -123
  292. package/dist/trace/self-minted-wire.d.ts +391 -1
  293. package/dist/trace/self-minted-wire.js +145 -0
  294. package/dist/trace/wire-projection-faces.d.ts +1 -1
  295. package/dist/trace/wire-projection-faces.js +1 -1
  296. package/dist/usage-analytics.d.ts +4 -0
  297. package/package.json +12 -4
  298. package/scripts/drain-check-local.mjs +8 -29
  299. package/scripts/drain-check.mysql.sql +3 -24
  300. package/scripts/drain-check.pg.sql +3 -24
  301. package/skills/update-config.md +1 -1
  302. package/wire-changes/server@7.145.0.json +146 -0
  303. package/wire-changes/server@7.146.0.json +3221 -0
  304. package/dist/approval-batch-census.d.ts +0 -27
  305. package/dist/approval-batch-census.js +0 -27
@@ -0,0 +1,94 @@
1
+ import { TENANT_SEPARATION_WORDS } from "@sema-agent/settings-schema/execution-lanes";
2
+ import { CHECKPOINT_CTX_KEY_SUBDIR, SEALED_KEY_DIR_MODE, SEALED_KEY_FILE_MODE } from "./sealed-key-layout.js";
3
+ export const DEPLOY_FACTS_DIR = "deploy/facts";
4
+ export const DEPLOY_FACTS_MEMBERS = ["lanes", "sandbox-rbac", "sealed-key"];
5
+ export const DEPLOY_FACTS_CHANGES_FILE = "CHANGES.json";
6
+ export const FACTS_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
7
+ export const FACTS_SCHEMA_KEYWORDS = {
8
+ string: ["type", "enum", "const", "description"],
9
+ integer: ["type", "const", "description"],
10
+ boolean: ["type", "description"],
11
+ object: ["type", "properties", "required", "additionalProperties", "description"],
12
+ array: ["type", "items", "minItems", "uniqueItems", "x-uniqueKey", "description"],
13
+ };
14
+ export const FACTS_SCHEMA_CHANGE_KINDS = ["introduced", "additive", "breaking"];
15
+ export const FACTS_SCHEMA_CHANGES = [
16
+ { version: "7.146.0", file: "lanes", kind: "introduced", pointer: "", why: "首发:每条执行车道的租户档、隔离位与必需键,行集 = 所依 settings-schema 的全部车道词" },
17
+ { version: "7.146.0", file: "sandbox-rbac", kind: "introduced", pointer: "", why: "首发:k8s 沙箱服务账号的 RBAC 规则表(每条规则何时授、引擎真调还是兼容保留)" },
18
+ { version: "7.146.0", file: "sealed-key", kind: "introduced", pointer: "", why: "首发:钥匙保管目录的安装契约(显式绝对 SEALED_KEY_DIR、整个目录跨重启持久且各副本共享、目录与文件的权限位)" },
19
+ ];
20
+ export function factsSchemaVersion(member, changes = FACTS_SCHEMA_CHANGES) {
21
+ const own = changes.filter((c) => c.file === member);
22
+ const intro = own.filter((c) => c.kind === "introduced");
23
+ const [first] = intro;
24
+ if (intro.length !== 1 || first === undefined)
25
+ throw new Error(`deploy-facts: member "${member}" needs exactly one "introduced" entry in FACTS_SCHEMA_CHANGES (found ${intro.length}) — its schemaVersion cannot be derived`);
26
+ return 1 + new Set(own.filter((c) => c.kind === "breaking" && c.version !== first.version).map((c) => c.version)).size;
27
+ }
28
+ export const LANE_ISOLATION_WORDS = ["supported", "unsupported"];
29
+ const envNames = (description) => ({ type: "array", uniqueItems: true, items: { type: "string" }, description });
30
+ export const LANE_FACTS_SCHEMA = {
31
+ $schema: FACTS_SCHEMA_DIALECT,
32
+ description: "引擎的执行车道事实:每条车道分不分租户、手跑在不在 OS 级隔离边界里、选它时引擎起服必须读到的键。行集 = 本包所依 settings-schema 车道表的全部车道词(含没有安装器接线的车道)。",
33
+ type: "object",
34
+ additionalProperties: false,
35
+ required: ["schemaVersion", "lanes"],
36
+ properties: {
37
+ schemaVersion: { type: "integer", const: factsSchemaVersion("lanes"), description: "本份数据的形版本;读方不认识的版本整份拒读。" },
38
+ lanes: {
39
+ type: "array",
40
+ minItems: 1,
41
+ "x-uniqueKey": ["lane"],
42
+ description: "每条车道一行,行序 = 车道表次序。",
43
+ items: {
44
+ type: "object",
45
+ additionalProperties: false,
46
+ required: ["lane", "tenantSeparation", "isolation", "requiredEnv", "requiredSecretEnv"],
47
+ properties: {
48
+ lane: { type: "string", description: "车道词,即 REMOTE_EXEC 的取值。" },
49
+ tenantSeparation: { type: "string", enum: TENANT_SEPARATION_WORDS, description: "supported = 可在多租户部署(REQUIRE_PRINCIPAL=true)上用;single-user-only = 只给单用户部署,多租户部署选它起服即拒。" },
50
+ isolation: { type: "string", enum: LANE_ISOLATION_WORDS, description: "supported = 手跑在 OS 级隔离边界里(独立沙箱);unsupported = 手跑在没有 OS 级隔离的地方(服务容器本身、远端主机、设备)。" },
51
+ requiredEnv: envNames("选这条车道时引擎起服必须读到的非密键(缺任一 ⇒ 起服拒)。"),
52
+ requiredSecretEnv: envNames("选这条车道时引擎起服必须读到的密钥键(缺任一 ⇒ 起服拒;值由运维自备)。"),
53
+ },
54
+ },
55
+ },
56
+ },
57
+ };
58
+ export const SEALED_KEY_REQUIREMENTS = ["persist-across-restart", "shared-by-all-replicas", "writable", "whole-directory"];
59
+ export const SEALED_KEY_DIR_PATH_FORMS = ["absolute"];
60
+ const octal = (mode) => mode.toString(8).padStart(4, "0");
61
+ const SEALED_KEY_DIR_ENV = "SEALED_KEY_DIR";
62
+ export const SEALED_KEY_FACTS = {
63
+ schemaVersion: factsSchemaVersion("sealed-key"),
64
+ dirEnv: SEALED_KEY_DIR_ENV,
65
+ dirPath: SEALED_KEY_DIR_PATH_FORMS[0],
66
+ subdirs: [CHECKPOINT_CTX_KEY_SUBDIR],
67
+ dirMode: octal(SEALED_KEY_DIR_MODE),
68
+ fileMode: octal(SEALED_KEY_FILE_MODE),
69
+ requirements: SEALED_KEY_REQUIREMENTS,
70
+ };
71
+ export const SEALED_KEY_FACTS_SCHEMA = {
72
+ $schema: FACTS_SCHEMA_DIALECT,
73
+ description: "引擎钥匙保管目录的安装契约:sealed 形模型钥匙的 X25519 钥匙对与停驻行凭据封签的钥匙都在这个目录里,私钥只存在这里。",
74
+ type: "object",
75
+ additionalProperties: false,
76
+ required: ["schemaVersion", "dirEnv", "dirPath", "subdirs", "dirMode", "fileMode", "requirements"],
77
+ properties: {
78
+ schemaVersion: { type: "integer", const: SEALED_KEY_FACTS.schemaVersion, description: "本份数据的形版本;读方不认识的版本整份拒读。" },
79
+ dirEnv: { type: "string", const: SEALED_KEY_FACTS.dirEnv, description: "安装器必须给引擎设的环境变量:值 = 保管目录的挂载位。" },
80
+ dirPath: { type: "string", enum: SEALED_KEY_DIR_PATH_FORMS, description: "absolute = 值必须是绝对路径(相对路径起服即拒;设了而目录建不了保管店也起服拒)。" },
81
+ subdirs: { type: "array", uniqueItems: true, items: { type: "string" }, description: "引擎在保管目录之下自建的子目录(随目录一起持久、一起共享)。" },
82
+ dirMode: { type: "string", const: SEALED_KEY_FACTS.dirMode, description: "引擎每次起服把保管目录设成的权限位(四位八进制);目录必须允许引擎进程改它的权限。" },
83
+ fileMode: { type: "string", const: SEALED_KEY_FACTS.fileMode, description: "引擎写钥匙文件时设的权限位(四位八进制)。" },
84
+ requirements: {
85
+ type: "array",
86
+ minItems: 1,
87
+ uniqueItems: true,
88
+ items: { type: "string", enum: SEALED_KEY_REQUIREMENTS },
89
+ description: "persist-across-restart = 目录跨进程重启保留(不然重启后封给旧钥匙的密文与停驻行凭据开不开);shared-by-all-replicas = 同一部署的每个副本挂同一份目录(多副本要支持多处读写的存储;不然 A 副本封的东西 B 副本开不开);" +
90
+ "writable = 引擎对目录可写(空目录起服时生成一把钥匙;只读挂载 ⇒ 建不了保管店);whole-directory = 持久与共享的单位是整个目录,含 subdirs 列出的子目录。",
91
+ },
92
+ },
93
+ };
94
+ //# sourceMappingURL=deploy-facts.js.map
@@ -38,6 +38,7 @@
38
38
  import http from "node:http";
39
39
  import { type DeviceOwner, type DeviceStore } from "./device-store.js";
40
40
  import type { DeviceEnrollment, DeviceRejection } from "./device-enrollment.js";
41
+ import { type CidrSet } from "./net-address.js";
41
42
  import { type DeviceChunkKind, type DeviceInstructionArgsByKind, type DeviceInstructionKind, type DeviceInstructionOutcome } from "./device-ws-protocol.js";
42
43
  /** 交给调用方的一片上行流(`data` 已从 base64 解回字节)。 */
43
44
  export interface DeviceChunkDelivery {
@@ -159,10 +160,14 @@ export interface DeviceWsHubOptions {
159
160
  chunkBufferMaxBytes?: number;
160
161
  /** codex R1-F3:本副本**未鉴权**连接的并发帽(超额在 upgrade 阶段就 503 拒)。 */
161
162
  maxPreAuthConnections?: number;
162
- /** codex R1-F3:单来源地址的未鉴权连接并发帽。[ref]([ref]):字面 `off` = 显式停用该层——
163
- * 帽键是裸 remoteAddress,XFF 反代 / 公司 NAT 部署形把全员折叠成单键,这层从公平层退化成
164
- * 「全员共享一份预算」;停层后全局帽(`maxPreAuthConnections`)照常兜底。 */
163
+ /** codex R1-F3:单来源地址的未鉴权连接并发帽。[ref]([ref]):字面 `off` = 显式停用该层;停层后全局帽
164
+ * (`maxPreAuthConnections`)照常兜底。帽键 = 真实对端的规范形({@link readPreAuthRemoteKey}):名单
165
+ * {@link trustedProxyCidrs} 空时就是 socket 对端 —— 反代 / 公司 NAT 后全员同一 socket 对端,这层是「全员共享一份预算」;
166
+ * 把自己的反代写进名单,这层就按反代转来的真实来源计。 */
165
167
  maxPreAuthConnectionsPerRemote?: number | "off";
168
+ /** [ref]:可信代理名单(CIDR 闭集;env `DEVICE_TRUSTED_PROXY_CIDRS`,配置层逐项校验)。缺省空 = 不读任何代理头(帽键 =
169
+ * socket 对端,今日行为)。socket 对端落在名单内才读 `X-Forwarded-For`。坏项构造时抛(装配缝,与 helloAck 界同姿势)。 */
170
+ trustedProxyCidrs?: readonly string[];
166
171
  }
167
172
  export interface DeviceWsHub {
168
173
  /** 把 `GET /v1/device/ws` 的 upgrade 处理器挂到裸 `http.Server` 上。 */
@@ -204,5 +209,45 @@ export interface DeviceWsHub {
204
209
  declaredSupports(deviceId: string): ReadonlySet<DeviceInstructionKind> | undefined;
205
210
  close(): Promise<void>;
206
211
  }
212
+ /** per-remote 帽键的来源(闭集;指标 `device_preauth_key_total{source}` 与 cap-hit 行 `keySource` 同一张表)。 */
213
+ export declare const DEVICE_PREAUTH_KEY_SOURCES: readonly ["socket", "xff", "fallback"];
214
+ export type DevicePreAuthKeySource = (typeof DEVICE_PREAUTH_KEY_SOURCES)[number];
215
+ /** 对端是可信代理而转发头展不开的缘由(闭集;warn `device_preauth_forwarded_unusable{reason}`)。 */
216
+ export declare const DEVICE_FORWARDED_UNUSABLE_REASONS: readonly ["absent", "malformed", "all_trusted", "too_many_headers"];
217
+ export type DeviceForwardedUnusableReason = (typeof DEVICE_FORWARDED_UNUSABLE_REASONS)[number];
218
+ export type DevicePreAuthKey = {
219
+ readonly key: string;
220
+ readonly source: "socket" | "xff";
221
+ } | {
222
+ readonly key: string;
223
+ readonly source: "fallback";
224
+ readonly reason: DeviceForwardedUnusableReason;
225
+ };
226
+ /**
227
+ * 可信对端的升级请求最多带几行头,汇聚端还认它的 `X-Forwarded-For`。node 对头行数超过它的上限(`server.maxHeadersCount`,缺省约 1000 行)
228
+ * 的请求要么**静默**丢掉多余的头、要么直接答 431(版本行为)。丢头那一形里,反代追加在末尾的那一行 XFF 正在尾部,丢了它,剩下的就只有
229
+ * 客户端自己垫在前面的那一行 —— 所以行数到这个数(或运维给 server 设的更低上限)⇒ 不采 XFF(回落 `too_many_headers`);答 431 那一形里
230
+ * 请求到不了这里。两形下垫在前面的 XFF 都不会成为帽键。一次设备握手连同反代自己加的头远不到 100 行;这个数刻意远低于 node 的缺省上限,
231
+ * 不随 node 版本漂。
232
+ */
233
+ export declare const DEVICE_PREAUTH_FORWARDED_HEADER_LINES_MAX = 100;
234
+ /** 转发头的一次读数(只在对端可信时取):`header` = node 合并后的 `X-Forwarded-For`;`complete` = 请求头行数低于可信读数上限。 */
235
+ export interface ForwardedForReading {
236
+ readonly header: string | readonly string[] | undefined;
237
+ readonly complete: boolean;
238
+ }
239
+ /**
240
+ * [ref]:per-remote 帽键 = **本连接真实对端的规范形**(`canonicalAddress`;IPv4-mapped 折成点分 IPv4 —— 同一台主机直连与经代理
241
+ * 是同一个键,不是两份名额)。
242
+ *
243
+ * 真实对端只有一个来源 = socket 对端;唯一的展开:socket 对端落在可信代理名单内 ⇒ 读 `X-Forwarded-For`(node 把重复头按到达序
244
+ * 以 `, ` 合并 = 同一张列表),**自右向左**逐项:名单内 = 又一层自己的代理,继续左移;第一个名单外的地址 = 键。选中项**以左**是
245
+ * 客户端自己写的段,不解析 —— 往那里塞什么都改不了键,也逼不出回落。
246
+ *
247
+ * 展不开(头行数到顶 / 头缺席 / 某项不是地址 / 全在名单内)⇒ 键 = socket 对端,`source=fallback`。🔴 这不是 fail-open,**不走 `recordFailOpen`**:
248
+ * 回落键把整只代理折成一个键,比展开更严(收紧方向),与名单空的今日行为同键;调用方另打一行带 reason 的 warn 指路。
249
+ * 名单外的对端:转发头读口一次都不调用(惰性 `readForwardedFor`;伪造 XFF 零效果)。不读 `Forwarded` / `X-Real-IP` / PROXY protocol —— 对端只有这一种展开。
250
+ */
251
+ export declare function readPreAuthRemoteKey(peer: string | undefined, readForwardedFor: () => ForwardedForReading, trusted: CidrSet): DevicePreAuthKey;
207
252
  export declare function createDeviceWsHub(options: DeviceWsHubOptions): DeviceWsHub;
208
253
  //# sourceMappingURL=device-ws-hub.d.ts.map
@@ -1,8 +1,10 @@
1
1
  import http from "node:http";
2
2
  import { createHash } from "node:crypto";
3
+ import { isIP } from "node:net";
3
4
  import { WebSocketServer } from "ws";
4
5
  import { DEVICE_DRAINING_CODE, DEVICE_ERROR_CODES, DEVICE_ERROR_STATUS, assertDeviceIdShape, } from "./device-store.js";
5
6
  import { MAX_TIMER_DELAY_MS } from "./config-types.js";
7
+ import { canonicalAddress, createTrustedProxySet } from "./net-address.js";
6
8
  import { DEVICE_HELLO_ACK_BOUNDS, DEVICE_MIN_PROTOCOL_VERSION, DEVICE_WS_LIMITS, DEVICE_WS_PATH, DEVICE_PROTOCOL_VERSION, asInstructionKind, buildHelloSignaturePayload, deviceHelloAckNumberFlaw, formatGeneration, mintChallengeNonce, mintInstructionId, parseDeviceFrameText, verifyHelloSignature, } from "./device-ws-protocol.js";
7
9
  const OUTCOME_UNKNOWN_SENTENCE = "outcome unknown — the command may have already executed on the device; verify its effect before re-running";
8
10
  const SETTLED_MEMO_MAX = 512;
@@ -63,6 +65,41 @@ function assertHelloAckNumbersInBounds(values) {
63
65
  }
64
66
  }
65
67
  }
68
+ export const DEVICE_PREAUTH_KEY_SOURCES = ["socket", "xff", "fallback"];
69
+ export const DEVICE_FORWARDED_UNUSABLE_REASONS = ["absent", "malformed", "all_trusted", "too_many_headers"];
70
+ export const DEVICE_PREAUTH_FORWARDED_HEADER_LINES_MAX = 100;
71
+ export function readPreAuthRemoteKey(peer, readForwardedFor, trusted) {
72
+ const raw = peer ?? "unknown";
73
+ const socketKey = canonicalAddress(raw) ?? raw;
74
+ if (!trusted.has(socketKey))
75
+ return { key: socketKey, source: "socket" };
76
+ const reading = readForwardedFor();
77
+ if (!reading.complete)
78
+ return { key: socketKey, source: "fallback", reason: "too_many_headers" };
79
+ const header = typeof reading.header === "string" ? reading.header : reading.header?.join(",");
80
+ if (header === undefined || header.replace(/[ \t]/g, "") === "")
81
+ return { key: socketKey, source: "fallback", reason: "absent" };
82
+ const hops = header.split(",");
83
+ for (let i = hops.length - 1; i >= 0; i--) {
84
+ const hop = forwardedHopAddress(hops[i]);
85
+ if (hop === undefined)
86
+ return { key: socketKey, source: "fallback", reason: "malformed" };
87
+ if (!trusted.has(hop))
88
+ return { key: hop, source: "xff" };
89
+ }
90
+ return { key: socketKey, source: "fallback", reason: "all_trusted" };
91
+ }
92
+ function forwardedHopAddress(hop) {
93
+ const t = hop.replace(/^[ \t]+|[ \t]+$/g, "");
94
+ if (t.startsWith("[")) {
95
+ const m = /^\[([^\]]*)\](?::\d{1,5})?$/.exec(t);
96
+ return m === null || isIP(m[1]) !== 6 ? undefined : canonicalAddress(m[1]);
97
+ }
98
+ const withPort = /^([^:]+):\d{1,5}$/.exec(t);
99
+ if (withPort !== null)
100
+ return isIP(withPort[1]) === 4 ? withPort[1] : undefined;
101
+ return canonicalAddress(t);
102
+ }
66
103
  export function createDeviceWsHub(options) {
67
104
  const { store, enrollment, replicaId, logger, metrics } = options;
68
105
  const heartbeatIntervalMs = options.heartbeatIntervalMs ?? 15_000;
@@ -81,6 +118,7 @@ export function createDeviceWsHub(options) {
81
118
  const chunkBufferMaxBytes = options.chunkBufferMaxBytes ?? 2 * 1024 * 1024;
82
119
  const maxPreAuthConnections = options.maxPreAuthConnections ?? 64;
83
120
  const maxPreAuthConnectionsPerRemote = options.maxPreAuthConnectionsPerRemote ?? 8;
121
+ const trustedProxies = createTrustedProxySet(options.trustedProxyCidrs ?? []);
84
122
  const wss = new WebSocketServer({ noServer: true, maxPayload: DEVICE_WS_LIMITS.maxFrameBytes });
85
123
  const conns = new Map();
86
124
  const allConns = new Set();
@@ -757,6 +795,10 @@ export function createDeviceWsHub(options) {
757
795
  }, DEVICE_WS_LIMITS.handshakeDeadlineMs);
758
796
  sendServerFrame(conn, { t: "challenge", nonce: conn.nonce, protocolVersion: DEVICE_PROTOCOL_VERSION });
759
797
  }
798
+ function forwardedHeaderLineCeiling(server) {
799
+ const cap = server.maxHeadersCount;
800
+ return typeof cap === "number" && cap > 0 ? Math.min(DEVICE_PREAUTH_FORWARDED_HEADER_LINES_MAX, cap) : DEVICE_PREAUTH_FORWARDED_HEADER_LINES_MAX;
801
+ }
760
802
  function attach(server) {
761
803
  server.on("upgrade", (req, socket, head) => {
762
804
  const path = (req.url ?? "/").split("?")[0];
@@ -764,7 +806,11 @@ export function createDeviceWsHub(options) {
764
806
  return refuseUpgrade(socket, 404);
765
807
  if (draining || closed)
766
808
  return refuseUpgrade(socket, 503);
767
- const remoteKey = req.socket.remoteAddress ?? "unknown";
809
+ const keyed = readPreAuthRemoteKey(req.socket.remoteAddress, () => ({ header: req.headers["x-forwarded-for"], complete: req.rawHeaders.length / 2 < forwardedHeaderLineCeiling(server) }), trustedProxies);
810
+ const remoteKey = keyed.key;
811
+ metrics?.inc("device_preauth_key_total", { source: keyed.source });
812
+ if (keyed.source === "fallback")
813
+ logger?.warn("device_preauth_forwarded_unusable", { reason: keyed.reason });
768
814
  const perRemoteCount = preAuthByRemote.get(remoteKey) ?? 0;
769
815
  const globalHit = preAuthCount >= maxPreAuthConnections;
770
816
  const perRemoteHit = maxPreAuthConnectionsPerRemote !== "off" && perRemoteCount >= maxPreAuthConnectionsPerRemote;
@@ -772,7 +818,7 @@ export function createDeviceWsHub(options) {
772
818
  const arm = globalHit ? (perRemoteHit ? "both" : "global") : "per_remote";
773
819
  metrics?.inc("device_preauth_refused_total", { arm });
774
820
  logger?.warn("device_preauth_cap_hit", {
775
- remoteKey, arm, preAuthCount, cap: maxPreAuthConnections, perRemoteCount, perRemoteCap: maxPreAuthConnectionsPerRemote,
821
+ remoteKey, keySource: keyed.source, arm, preAuthCount, cap: maxPreAuthConnections, perRemoteCount, perRemoteCap: maxPreAuthConnectionsPerRemote,
776
822
  });
777
823
  return refuseUpgrade(socket, 503);
778
824
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 错误码目录的**码族**列(契约附录 A.1g;目录每一行的第二格)的闭集与读法 —— 纯数据 + 穷举 switch,无 import。
3
+ *
4
+ * ## 它是什么
5
+ * 目录(附录 A.1 / A.1b / A.1h)的每个码恰属一族 —— 族写在目录行的**第二格**(紧跟在码后面)。族回答的是消费端要做的那一个决定:**同一个请求原样再发一次,有没有意义**,以及出路在哪一侧。
6
+ * 它与码名前缀的「前缀族」(`auth.` / `request.` / … ,粗分支面,按**码的出身**分)是两根轴:前缀说这个码从哪来,码族说拿到它之后重发有没有用。
7
+ *
8
+ * · `transient` —— 原样重发(不改请求、不改部署、不改数据,谁也不必动一手)等一等**可能成功**:限流 / 配额窗 / 排空 / 店暂时不可用 / 竞态窗 / 在飞的活干完了自会放行(会话上的活跃 run、在跑的 bake)/ 未判为确定性的服务端故障。
9
+ * · `deterministic` —— 原样重发**改不了判**:要变的是请求本身、凭据与权限、部署声明与旋钮、存储里坏掉的字节,或一个已被别人结清的状态。
10
+ * · `store_judged` —— 重试类**逐次由店判**:体键 `permanence`(`transient` / `terminal`)在场 ⇔ 店判了;缺席 = 店没判(按 `transient` 读)。
11
+ * · `pending_decision` —— 恢复面 / 决断面的拒,拒在认领之前:体 `retriable: true` 在场 ⇒ 一张待批卡仍 pending(行重新 park);键缺席不推出「卡已不在」(服务端自铸的拒如 `wake.gate_pending` 不带该键)。
12
+ * 它说「卡还在」,**不说**「原样重发会成功」。
13
+ * · `retention_expired` —— 被寻址的留存物曾在、已被保留策略收走(TTL / 容量 / 淘汰),不会回来;出路是新开,不是重试。
14
+ *
15
+ * ## 单一真源
16
+ * 码 → 族的归属**只住在契约文档的目录行上**(消费端读的就是那份文档与它的 wire 变更清单);本文件只定**词表**与每个词的**状态律**,
17
+ * 词表与文档 A.1g 的词表行由 `test/error-code-family.test.ts` 双向钉(多一个词 / 少一个词 / 写错一个词都红),目录每个码有族、族与状态对得上、`pending_decision` 与
18
+ * 服务端真能盖 `retriable:true` 的码集对得上也在那里钉。没有第二张「码 → 族」的表。
19
+ */
20
+ /** 码族闭集(词表顺序 = 契约 A.1g 的行序)。 */
21
+ export declare const ERROR_CODE_FAMILIES: readonly ["transient", "deterministic", "store_judged", "pending_decision", "retention_expired"];
22
+ export type ErrorCodeFamily = (typeof ERROR_CODE_FAMILIES)[number];
23
+ /** 一个串是不是码族词(`includes` 判,不吃原型链上的名字)。 */
24
+ export declare function isErrorCodeFamily(word: string): word is ErrorCodeFamily;
25
+ /**
26
+ * 一个状态码落在这一族是否说得通 —— 每个词一条状态律(穷举 switch:加词而没来这里定它的状态律 ⇒ tsc 红)。
27
+ *
28
+ * · `transient` —— 等待 / 可用性类状态:408 · 425 · 429,服务端侧 500 · 502 · 503 · 504,以及「等一个明说的窗再发」的 409(忙 / 竞态);501 不是(换部署形态不是等);
29
+ * · `deterministic` —— 除了等待类三态(408 · 425 · 429:这三个状态本身就在说「等一等再来」)之外的任何状态;
30
+ * · `store_judged` —— 店答的状态:422(店的重放失败)与服务端侧 500 · 502 · 503 · 504;
31
+ * · `pending_decision` —— 只在 4xx(拒在认领之前,调用方可以带着更新后的坐标再决),且不是等待类三态;
32
+ * · `retention_expired` —— 只在 4xx(被寻址的东西不在了),且不是等待类三态。
33
+ */
34
+ export declare function familyAdmitsStatus(family: ErrorCodeFamily, status: number): boolean;
35
+ //# sourceMappingURL=error-code-family.d.ts.map
@@ -0,0 +1,26 @@
1
+ export const ERROR_CODE_FAMILIES = ["transient", "deterministic", "store_judged", "pending_decision", "retention_expired"];
2
+ export function isErrorCodeFamily(word) {
3
+ return ERROR_CODE_FAMILIES.includes(word);
4
+ }
5
+ export function familyAdmitsStatus(family, status) {
6
+ const waiting = status === 408 || status === 425 || status === 429;
7
+ const clientSide = status >= 400 && status < 500 && !waiting;
8
+ const serverSide = status === 500 || status === 502 || status === 503 || status === 504;
9
+ switch (family) {
10
+ case "transient":
11
+ return waiting || serverSide || status === 409;
12
+ case "deterministic":
13
+ return status >= 400 && status < 600 && !waiting;
14
+ case "store_judged":
15
+ return serverSide || status === 422;
16
+ case "pending_decision":
17
+ return clientSide;
18
+ case "retention_expired":
19
+ return clientSide;
20
+ default: {
21
+ const unhandled = family;
22
+ throw new Error(`unhandled error-code family: ${String(unhandled)}`);
23
+ }
24
+ }
25
+ }
26
+ //# sourceMappingURL=error-code-family.js.map
@@ -30,7 +30,7 @@
30
30
  * 纯类型 + 纯函数叶(值 import 只有 scope 读法叶与 run 行判词叶)。
31
31
  */
32
32
  import type { DrainReceipt, DrainRequest, EngineDraining } from "@sema-agent/settings-schema/fleet";
33
- import type { EngineLeaseFacts } from "../boot/engine-lease.js";
33
+ import type { EngineHandoverDoor, EngineLeaseFacts } from "../boot/engine-lease.js";
34
34
  /**
35
35
  * 引擎自铸的动作号 —— **一张表**(排空事实与回执里不来自请求体的全部拼法;请求体的 `actionId` 由调用方自取):
36
36
  * · {@link ENGINE_DRAIN_CAUSES}:没有请求承载的排空成因(停止信号 / SIGHUP 升级 / 附着租约自退);
@@ -68,6 +68,29 @@ export interface DrainDeadlineRefusal {
68
68
  }
69
69
  /** 入口的答:契约回执(进入 / 已在排空),或期限越界的拒(只在调用方声明了上界时出现)。按 `state` 穷举。 */
70
70
  export type DrainEntryAnswer = DrainReceipt | DrainDeadlineRefusal;
71
+ /**
72
+ * 在飞数的**分项**(`GET /health` 的 `inflightBreakdown`)—— 同一份分项派生**两个事实**,不是两份计数:
73
+ * · **总数**(可观测事实,{@link inflightTotal} = `/health.inflight`):这台引擎此刻托管着多少活 = `http + leader + sessionResident`;
74
+ * · **阻断集**(判据,{@link blockingInflightOf} = `drainState.blockingInflight`):还有没有**必须等完**的活 = `http + leader`。
75
+ * 排空等的、孤儿窗 / 附着租约自退 / SIGHUP 空闲窗判「可以停了吗」读它。`sessionResident` 不在其内。
76
+ * 三项:
77
+ * · `http` —— 本副本的 HTTP 腿:在飞 run 集(`inflightRuns ∪ steerableRuns`)+ 没有登记身份的同步计费腿(`/v1/tasks` 的同步腿)+ admission 期的非探针请求;
78
+ * · `leader` —— leader 口的后台 run(`leaderEndpoint.inflight()`);
79
+ * · `sessionResident` —— 会话驻留的后台 shell / monitor(活过起它的那一条腿;`session-resident-background.ts` 的读法:不含 retain 行)。
80
+ * 它只进总数、不进阻断集:一只不退出的驻留 shell(`while true`)若阻断,崩溃孤儿引擎就永生、排空每次等满窗;窗到 / 排空完成即由退出边界的后台收割收掉
81
+ * (retain 行按 retain 既有语义,活过受控退)。
82
+ * 三项在同一次读里求出,`/health` 的 `inflight` 与 `inflightBreakdown` 因此永远自洽。只有这两个函数各写一次这两个和,没有第三张计法表。
83
+ * 阻断集只需要前两项:判停路径(排空 1 s 拍 / 租约拍 / SIGHUP 拍)**不求值** `sessionResident`(那是对进程级注册表的一次逐会话走查,只在 `/health` 与崩溃遗言里付)。
84
+ */
85
+ export interface InflightBreakdown {
86
+ readonly http: number;
87
+ readonly leader: number;
88
+ readonly sessionResident: number;
89
+ }
90
+ /** 阻断集 = 必须等完的活 = `drainState.blockingInflight()`(`sessionResident` 不在内)。 */
91
+ export declare function blockingInflightOf(b: Pick<InflightBreakdown, "http" | "leader">): number;
92
+ /** 总数 = 引擎此刻托管的全部活 = `/health.inflight`(= 阻断集 + 会话驻留项)。 */
93
+ export declare function inflightTotal(b: InflightBreakdown): number;
71
94
  /**
72
95
  * 引擎与 HTTP 面共享的排空盒子(装配层建一只,`createHttpServer` 与收尾链各自往上装读口 / 入口)。
73
96
  * · `draining` / `since` / `actionId` / `deadlineAt` —— **只由** {@link enter} 整组写入(排空事实 = 契约 `EngineDraining`);
@@ -75,8 +98,8 @@ export type DrainEntryAnswer = DrainReceipt | DrainDeadlineRefusal;
75
98
  * · `yieldInflight` —— 让出本副本在飞的可抢占腿(`createHttpServer` 装);
76
99
  * · `endSubscriptions` —— 关掉本副本打开着的只读订阅流(发一帧 `draining` 终帧再关;`createHttpServer` 装,[ref])。订阅流不是在飞的工作
77
100
  * (不进 `inflight`),排空不等它;收尾链在排空进入那一拍与硬停起点各调一次(排空期间新开的订阅在硬停那一拍收);
78
- * · 其余四只是既有的读口(在飞数 / 活性时钟 / 附着流),语义见 `http/server.ts`。`inflight` 只数**工作**(run 腿 / 计费提交 / HITL 应答 /
79
- * admission 期的普通请求),只读订阅只进 `attachedStreams`([ref])。
101
+ * · 其余五只是既有的读口(在飞数 / 活性时钟两只〔最近一次 / 首次〕/ 附着流两只),语义见 `http/server.ts`。在飞数只数**工作**(run 腿 / 计费提交 / HITL 应答 /
102
+ * admission 期的普通请求),只读订阅只进 `attachedStreams`([ref]);在飞数分两个读口 —— 总数 `inflight`(事实)与阻断集 `blockingInflight`(判据),见 {@link InflightBreakdown}。
80
103
  */
81
104
  export interface DrainState {
82
105
  draining: boolean;
@@ -86,12 +109,25 @@ export interface DrainState {
86
109
  enter?: (entry: DrainEntry) => DrainEntryAnswer;
87
110
  yieldInflight?: () => void;
88
111
  endSubscriptions?: () => void;
112
+ /** **总数**读口(`createHttpServer` 装):引擎此刻托管的全部活 = `inflightTotal(inflightBreakdown())`。可观测事实 —— 只有 `/health` 与崩溃遗言读它;
113
+ * **没有任何「停不停」的判据读它**(判据读 {@link DrainState.blockingInflight})。 */
89
114
  inflight?: () => number;
115
+ /** 分项读口(`createHttpServer` 装,与 `inflight` / `blockingInflight` 同一次装、同一份求值)。`/health` 读它一次,同时出 `inflight` 与 `inflightBreakdown` 两键。 */
116
+ inflightBreakdown?: () => InflightBreakdown;
117
+ /** **阻断集**读口(`createHttpServer` 装):还有没有必须等完的活 = `blockingInflightOf(inflightBreakdown())` = `http + leader`。排空等的(`boot/shutdown.ts` 的排空驱动表)、
118
+ * 孤儿窗 / 附着租约自退(`boot/engine-lease.ts`)、SIGHUP 空闲窗(`sighup-idle.ts`)的「可以停了吗」都读它;会话驻留的后台 shell / monitor 不在内。 */
119
+ blockingInflight?: () => number;
90
120
  lastActivityAt?: () => number;
121
+ /** [ref]:首个**非探针**请求的单调时刻(`null` = 还没有过)。与 `lastActivityAt` 同一个写者(`http/server.ts` 的活性打点)、写一次就定;
122
+ * 读者 = 租约腿的候首触态(`boot/engine-lease.ts`)与 `/health.engineLease.firstAttach.attachedAt`。 */
123
+ firstActivityAt?: () => number | null;
91
124
  attachedStreams?: () => number;
92
125
  lastAttachEndedAt?: () => number;
93
126
  /** [ref]:附着租约窗的读口(`/health.engineLease`)。收尾链在**装了租约腿**时才装它,读口现读租约腿的把手 —— 腿还在观察才有读数,未装配的盒子里根本没有这只口。 */
94
127
  engineLease?: () => EngineLeaseFacts | undefined;
128
+ /** [ref]:附着租约的交接门(`POST /v1/engine/handover` · `POST /v1/engine/attach` 与能力位 `engine.handover` 读它)。收尾链在**装了交接车道**时才装
129
+ * (租约腿已装配 ∧ 非多租户);`observing` = 腿还在观察。 */
130
+ engineHandover?: EngineHandoverDoor;
95
131
  }
96
132
  /** 排空盒子里的排空事实(三键齐才是;入口整组写,所以「缺一」只出现在手搭的夹具上)。 */
97
133
  export declare function drainingFactOf(s: Pick<DrainState, "draining" | "since" | "actionId" | "deadlineAt">): EngineDraining | undefined;
@@ -10,6 +10,12 @@ export const DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL = {
10
10
  "signal:SIGHUP": true,
11
11
  "self-exit:attachment": false,
12
12
  };
13
+ export function blockingInflightOf(b) {
14
+ return b.http + b.leader;
15
+ }
16
+ export function inflightTotal(b) {
17
+ return blockingInflightOf(b) + b.sessionResident;
18
+ }
13
19
  export function drainingFactOf(s) {
14
20
  return s.draining && s.actionId !== undefined && s.since !== undefined && s.deadlineAt !== undefined ? { actionId: s.actionId, since: s.since, deadlineAt: s.deadlineAt } : undefined;
15
21
  }
@@ -1,5 +1,6 @@
1
1
  import type { TerminalCause, WorkflowRun } from "@sema-agent/core";
2
2
  import type { RunRecord } from "../plugins/store-contracts.js";
3
+ import type { RunAcceptance } from "../run-acceptance.js";
3
4
  /** The fleet vocabulary the shell renders (MF-Fleet `FleetTask.status`). The service maps its run/workflow status
4
5
  * onto this neutral set; `awaiting approval` = a needs-review/plan-approval park, `waiting` = a durable suspend.
5
6
  *
@@ -395,7 +396,8 @@ export interface HookManagedUnrun {
395
396
  * · `llm_carrier_unavailable` / `llm_cap` —— prompt / agent 条目没有模型载体 / 超出每事件的 LLM 条目数;
396
397
  * · `powershell_unsupported` —— `shell:"powershell"` 条目(本服务不跑);
397
398
  * · `if_unevaluable` —— 条目的 `if` 条件评不出;
398
- * · `budget_exhausted` —— 托管车道的事件墙钟耗尽:撞上的那一条记一帧,同一次执行里其后的条目不再起、不逐条记(PreToolUse 逐组执行 ⇒ 至多每组一帧);
399
+ * · `budget_exhausted` —— 托管车道的预算耗尽:本服务的事件墙钟到点(不再起下一条),或托管车道那张注册的**引擎 handler 预算**(`HOOKS_TIMEOUT_MS`)用尽、被引擎叫停(`detail` 点名这一种);
400
+ * 撞上的那一条记一帧,同一次执行里其后的条目不再起、不逐条记(墙钟:每一相、PreToolUse 每一组至多一帧;引擎预算:每个事件恰一帧);任务取消叫停不记;
399
401
  * · `spawn_failed` / `timeout` —— 起不来 / 超时被收;
400
402
  * · `exit_nonzero` —— 退出码非 0 非 2;`bad_json` —— 看着是 JSON 却读不成(含 http 条目 2xx 回了非 JSON 体、Stop × prompt 的判词读不出);`decision_out_of_set` —— `permissionDecision` 不在 allow / deny / ask 里;
401
403
  * · `evidence_unavailable` —— Stop × prompt 条目因会话证据面不足(或运维关了 transcript)被滤掉没跑;
@@ -642,7 +644,8 @@ export declare class FleetEventBus {
642
644
  export declare function fleetRunPublisher(bus: FleetEventBus | undefined, run: {
643
645
  runId: string;
644
646
  scope: string;
645
- name: string;
647
+ /** 行名 = 本服务持有的交付文本的预览({@link fleetRunLabels});缺席 = 腿上装着 prompt 屏、提交时还没有交付的文本 ⇒ 建行不带名,交付之后经 `onNamed` 补。 */
648
+ name?: string;
646
649
  description?: string;
647
650
  /** 嵌子代树的根:这条腿的 canonical core taskId(`spec.taskId ?? sessionId`)。转发子代的 `parentTaskId` 与它相等
648
651
  * ⇒ 挂在 run 行下,否则挂在那个子代的行下。**只管嵌套**,不是行的会话键。 */
@@ -679,6 +682,9 @@ export declare function fleetRunPublisher(bus: FleetEventBus | undefined, run: {
679
682
  * 只有**真终态**(completed/failed/killed)帧带残局——parked(suspended/needs_review)的行还活着,
680
683
  * 没有「终局」可言,误传也不上帧。 */
681
684
  onTerminal: (status: RunStatusForFleet, residuals?: FleetRunResiduals) => void;
685
+ /** 建行时没有名(腿上有屏)的行,引擎交付之后补上名(受理座的补写口调)。只改**还在 bus 上**的那一行:收场之后、或行已被别的路径收走(对账腿退场)⇒ 不发 ——
686
+ * 名不是活着的信号,一帧只带名的 delta 不许铸行(铸出来的行没有 scope / 会话键)。 */
687
+ onNamed: (name: string) => void;
682
688
  };
683
689
  /** The run-scoped publisher handle (returned by `fleetRunPublisher`). Threaded into `runInBackground` so a
684
690
  * background run TRANSITIONS its fleet row exactly like the sync POST /v1/runs leg does. */
@@ -738,10 +744,15 @@ export declare function buildRunName(preview: string): string;
738
744
  * shows `name`=objective + `description`=current activity (two DISTINCT things, killing the earlier "objective shown
739
745
  * twice" dup). (A subagent CHILD row's name = its agent-type — BC-2.)
740
746
  * 一只折法({@link buildRunName}),输入 = 与 run 行同一只预览(用户写的那段 → 脱敏 → 截):`GET /v1/assistant/tasks`
741
- * 读 run 行上存的预览、过同一只折法 ⇒ 同一个 run 两面同名。剥完没有用户正文 ⇒ `(notification turn)`;闭集外的 XML 头当普通正文标。 */
747
+ * 读 run 行上存的预览、过同一只折法 ⇒ 同一个 run 两面同名。剥完没有用户正文 ⇒ `(notification turn)`;闭集外的 XML 头当普通正文标。
748
+ * 入参 = 本服务持有的交付文本(受理座的 `objectiveAtSubmit` / 交付之后的那一份;律的属主 = `run-acceptance.ts`),缺席(腿上有屏、还没有可写的那一份)⇒ 不带名 ——
749
+ * 与运维任务视图对 NULL 预览的处置同(展示位缺席即缺席,客户端渲占位)。 */
742
750
  export declare function fleetRunLabels(objective: string | undefined): {
743
- name: string;
751
+ name?: string;
744
752
  };
753
+ /** 腿上有屏时 fleet 行名的补写口(开 fleet 行之后、起跑之前登记;律的属主 = `run-acceptance.ts`):交付的那一份 → {@link fleetRunLabels} 同一只折法 → `onNamed`。
754
+ * 腿上没有屏 ⇒ 受理座永不调它(行名在开行时已按提交时的交付文本写好)。 */
755
+ export declare function nameFleetRowOnDelivery(acceptance: Pick<RunAcceptance, "onDelivered">, publisher: Pick<FleetRunPublisher, "onNamed">): void;
745
756
  /**
746
757
  * `runStatusToFleet` 的入参**闭集**([ref] P1-④)。两段:
747
758
  * · `RunRecord["status"]` = `"running" | core.TaskStatus` —— run **行**状态词表,属主
@@ -230,7 +230,7 @@ export class FleetEventBus {
230
230
  }
231
231
  export function fleetRunPublisher(bus, run) {
232
232
  if (!bus)
233
- return { onStart: () => undefined, onEvent: () => undefined, onForwardEvent: () => undefined, onTerminal: () => undefined };
233
+ return { onStart: () => undefined, onEvent: () => undefined, onForwardEvent: () => undefined, onTerminal: () => undefined, onNamed: () => undefined };
234
234
  let tokens = 0;
235
235
  const startedAt = Date.now();
236
236
  const sessionField = run.sessionId ? { sessionId: run.sessionId } : {};
@@ -282,7 +282,14 @@ export function fleetRunPublisher(bus, run) {
282
282
  };
283
283
  return {
284
284
  onStart() {
285
- bus.publishTask({ id: run.runId, name: run.name, ...(run.description ? { description: run.description } : {}), scope: run.scope, ...sessionField, status: "running", tokens: 0, elapsedMs: 0 });
285
+ bus.publishTask({ id: run.runId, ...(run.name !== undefined ? { name: run.name } : {}), ...(run.description ? { description: run.description } : {}), scope: run.scope, ...sessionField, status: "running", tokens: 0, elapsedMs: 0 });
286
+ },
287
+ onNamed(name) {
288
+ if (settledLeg)
289
+ return;
290
+ if (bus.taskRow(run.runId) === undefined)
291
+ return;
292
+ bus.publishTask({ id: run.runId, name });
286
293
  },
287
294
  onEvent(ev) {
288
295
  if (settledLeg)
@@ -595,7 +602,14 @@ export function buildRunName(preview) {
595
602
  return oneLine.length > RUN_NAME_CHARS ? `${oneLine.slice(0, RUN_NAME_CHARS - 1)}…` : oneLine;
596
603
  }
597
604
  export function fleetRunLabels(objective) {
598
- return { name: buildRunName(buildRunObjectivePreview(objective ?? "")) };
605
+ return objective === undefined ? {} : { name: buildRunName(buildRunObjectivePreview(objective)) };
606
+ }
607
+ export function nameFleetRowOnDelivery(acceptance, publisher) {
608
+ acceptance.onDelivered("fleet-name", async (objective) => {
609
+ const { name } = fleetRunLabels(objective);
610
+ if (name !== undefined)
611
+ publisher.onNamed(name);
612
+ });
599
613
  }
600
614
  export function isTerminalFleetStatus(status) {
601
615
  switch (status) {
@@ -19,7 +19,7 @@ export declare const RENDERED_ENTRY_TYPES: readonly ["message", "compaction", "c
19
19
  * custom —— data-only(session.js 折 messages 只认 message/custom_message 两型),模型看不见它的
20
20
  * data;把它渲出来反而是把模型没见过的东西冒充成证据,与 [ref] 是同一判据的反方向。
21
21
  */
22
- export declare const SKIPPED_ENTRY_TYPES: readonly ["thinking_level_change", "model_change", "label", "session_info", "leaf", "prompt_epoch", "announced_listing", "workspace_state", "git_announcement", "reminder_mark", "custom"];
22
+ export declare const SKIPPED_ENTRY_TYPES: readonly ["thinking_level_change", "model_change", "label", "session_info", "leaf", "prompt_epoch", "announced_listing", "workspace_state", "git_announcement", "reminder_mark", "session_checkout", "custom"];
23
23
  /** 渲染结果。**判别式**:证据不足时调用方拿不到 `text`,想用也用不了(见顶注)。 */
24
24
  export type BranchTranscript = {
25
25
  sufficient: true;
@@ -11,6 +11,7 @@ export const SKIPPED_ENTRY_TYPES = [
11
11
  "workspace_state",
12
12
  "git_announcement",
13
13
  "reminder_mark",
14
+ "session_checkout",
14
15
  "custom",
15
16
  ];
16
17
  export const BRANCH_TRANSCRIPT_MAX_CHARS = 60_000;
@@ -7,8 +7,11 @@
7
7
  * ② **handler 怎么挂**:链成员「无意见 ⇒ `next(e)`」(链 handler 返回 `undefined` 是站点违约,refuse 站上即 fail-closed 拒),
8
8
  * 观察者「恒返回 `undefined`」(观察者的判词不采纳、被引擎报告)。两条都在 {@link hangClassic} / {@link observeClassic} 里成形,
9
9
  * 调用方只写「这一站我答什么」或「这一站我看什么」,不再各自记得这两条。
10
- * ③ **信号**:引擎交给每只 handler 的 `next.signal` 是「这只 handler 的预算 ∪ 这次派发被取消」的合成(core 的派发器自合成,本仓不另合成第二份)。
11
- * 它是 handler 体的第二个入参 —— 触发之后 handler 不再起新事、在飞的子进程要叫停(`hooks/hook-runner.ts` 的命令钩子腿)。
10
+ * ③ **停表**:引擎交给每只 handler 的 `next.signal` 是「这只 handler 的预算 ∪ 这次派发结束 / 被取消」的合成(core 的派发器自合成,本仓不另合成第二份);
11
+ * 成因只读引擎的读数 `next.budget.remainingMs`(用尽 = 0),不解析信号的文案。两样一起是 handler 体的第二个入参 {@link HandlerHalt} ——
12
+ * 触发之后 handler 不再起新事、在飞的子进程要叫停(`hooks/hook-runner.ts` 的命令钩子腿);是自己的预算用尽,托管车道把撞上的条目记成未跑成。
13
+ * ④ **两种链上挂法**:内层「先答,无意见再 `next(e)`」({@link hangClassic});外层「先 `next(e)`,在答回来的答案上再答」({@link wrapClassic})——
14
+ * 外层 handler 等 `next` 的那段引擎停它的表(core `SelfBudget` 的暂停),所以外层的预算只算它自己的时间。
12
15
  *
13
16
  * 站的集合 = 引擎事件表里有站的 `classic.*` 名(一条钉从 `MOD_EVENT_TABLE` 派生对拍,不手抄第二份)。
14
17
  * 观察站(`StopFailure` / `PostCompact` / `PermissionDenied` / `SessionStart` / `SessionEnd`)只许观察者:答案形是「无」,core 的迁移配方要求挂 `{ observe: true }`。
@@ -36,16 +39,30 @@ export declare const CLASSIC_STATION_ROLE: {
36
39
  /** 本仓 handler 体对一站作的答:观察站不作答(`void`);链站 = core 的答案形,去掉本仓从不作的 `null` / `false`(PreToolUse 的两个「不表态」词)。 */
37
40
  type StationAnswer<K extends ClassicStation> = (typeof CLASSIC_STATION_ROLE)[K] extends "observe" ? void : Exclude<ClassicEventMap[K]["answer"], null | false>;
38
41
  /**
39
- * 一站的 handler 体:读 `e`,答这一站的答案;`undefined` = 无意见(链成员由 {@link hangClassic} 交给 `next(e)`)。
40
- * `signal` = 引擎交给这只 handler 的 `next.signal`(预算到 ∪ 派发被取消;见头注 ③)。
42
+ * 引擎交给一只 handler 的停表(头注 ③):`signal` 触发 = 引擎不再等它;`budgetSpent()` = 触发的是不是它**自己的预算**用尽(派发结束 / 任务取消 ⇒ `false`;
43
+ * 信号还没触发 ⇒ `false`)。
41
44
  */
42
- export type ClassicAnswer<K extends ClassicStation> = (e: ClassicEventMap[K]["e"], signal: AbortSignal) => Promise<StationAnswer<K> | undefined>;
45
+ export interface HandlerHalt {
46
+ readonly signal: AbortSignal;
47
+ budgetSpent(): boolean;
48
+ }
43
49
  /**
44
- * 按站的角色挂一只 handler:链站 ⇒ 链成员 `(await answer(e)) ?? next(e)`;观察站 ⇒ 观察者(体照跑、恒返回 `undefined`)。
50
+ * 一站的 handler 体:读 `e`,答这一站的答案;`undefined` = 无意见。`halt` 见 {@link HandlerHalt}。
51
+ * `below` = 外层挂法下(见 {@link wrapClassic})链下方答回来的那份答案(接收后冻结的数据;下方都无意见时是引擎核心的空答案)—— 内层挂法与观察站恒 `undefined`。
52
+ * 外层体答 `undefined` = 下方的答案原样作数;要换掉它就交一份新答案(把下方的成员并进去是体自己的事)。
53
+ */
54
+ export type ClassicAnswer<K extends ClassicStation> = (e: ClassicEventMap[K]["e"], halt: HandlerHalt, below: unknown) => Promise<StationAnswer<K> | undefined>;
55
+ /**
56
+ * 内层挂法:链站 ⇒ 链成员 `(await answer(e)) ?? next(e)`;观察站 ⇒ 观察者(体照跑、恒返回 `undefined`)。
45
57
  * `e` 的型由 core 的 `ClassicEventMap` 给(引擎交来的是本站的 `e`;见头注 ①)。
46
58
  */
47
59
  export declare function hangClassic<K extends ClassicStation>(on: On<EngineDollar>, station: K, answer: ClassicAnswer<K>): void;
48
- /** 挂一只观察者(任何站都可):拿分离副本、不进链、恒返回 `undefined`;`signal` 同 {@link ClassicAnswer}。 */
49
- export declare function observeClassic<K extends ClassicStation>(on: On<EngineDollar>, station: K, watch: (e: ClassicEventMap[K]["e"], signal: AbortSignal) => void | Promise<void>): void;
60
+ /**
61
+ * 外层挂法:链站 ⇒ 先 `next(e)`(下方的链成员与引擎核心先跑完,这一只的表在等的那段停着),再拿答回来的答案作答 —— 体答 `undefined` ⇒ 下方的答案原样作数;
62
+ * 下方在拒站上失败 ⇒ 派发已 fail-closed 终局,`next` 抛,体不跑。观察站 ⇒ 与 {@link hangClassic} 同(观察者不进链,没有「下方」)。
63
+ */
64
+ export declare function wrapClassic<K extends ClassicStation>(on: On<EngineDollar>, station: K, answer: ClassicAnswer<K>): void;
65
+ /** 挂一只观察者(任何站都可):拿分离副本、不进链、恒返回 `undefined`;`halt` 同 {@link ClassicAnswer}。 */
66
+ export declare function observeClassic<K extends ClassicStation>(on: On<EngineDollar>, station: K, watch: (e: ClassicEventMap[K]["e"], halt: HandlerHalt) => void | Promise<void>): void;
50
67
  export {};
51
68
  //# sourceMappingURL=classic-stations.d.ts.map