@sema-agent/server 7.43.0 → 7.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,34 +1,3 @@
1
- /**
2
- * design/158 A10 尾刀:composition root 分段 —— **config-center 运行时**(boot 半场 + 60s refresh 循环 +
3
- * 两者之间的 center 能力消费点)。
4
- *
5
- * 上一刀把 12 个 boot 段搬进 `src/boot/*.ts`(纯搬运),独独留下这两段并写明理由:它们共享一组
6
- * **跨段可变状态**(`ccEtag / effective / latestEffective / lastRejectedCandidate / appliedPlaneEff /
7
- * planeDeferredNoHandoff / bootSkillStale / pendingRestart / bootLkgCandidate / lastPrewarmBodiesVerified /
8
- * lkgBooted / bootConfigPending / centerPrompts / pendingCatalogFaces / keyResolver / modelReadyState`),
9
- * 而 codex R10–R38 的重启风暴判据密集地建立在这些状态的**读写次序**上(哪一行先落盘、哪一行先发信号、
10
- * 哪个分支 splice 掉整组 reasons)。
11
- *
12
- * 本刀的做法:把这些 `let` 从 `main()` 的函数作用域搬进 **本工厂的闭包**——它们仍然是 `let`,**每一处
13
- * 读写的字面文本、次序、分支结构一行未改**(把 `x` 改写成 `state.x` 会把 800 行里的每一处读写都变成一次
14
- * 手工重写,那正是上一刀判定「不是搬运」的原因)。闭包 = 那个「持有状态的对象」;`main()` 只通过下面这
15
- * 组窄口读它们,与本目录既有的晚绑 getter 惯例(`getRunStore()` / `getKeyResolver()` / `getRunDenySweep()`)
16
- * 同款。
17
- *
18
- * ⚠️ **位置即契约**(调用点在 `main()` 里的相对次序不可动):
19
- * 1. `createConfigCenterRuntime()` 必须在 logger/metrics 之后、`openStores()` 之前 —— boot 拉取要在
20
- * Runner/持久层建起来之前把 models/roles/teams 应用进 `config`(env 是兜底,center 覆盖其上)。
21
- * 2. `applyLocalRemoteExec()` 紧随其后、在 `createExecutionEnv()` 之前 —— 它就地改写 `config.remoteExec`,
22
- * 消费点(执行环境工厂)在下游。
23
- * 3. `applyCenterCapabilities()` 必须在 `loadSkills()` 之后、`buildScenarios()` 之前,且 **boot LKG 落盘点
24
- * 就在它里面**(F7/codex R26:先装载 skill 正文再落盘,否则「manifest 可见、正文没进缓存」的窗口会被
25
- * 发布成 boot 可见 LKG);plugins 让位判据要求 plugins 消费在 center 直发 skills **之后**。
26
- * 4. `applyCenterScenarios()` 必须在 `builtinScenarioDetails()` 捕获六内建名之后(探针要打纯内建工厂)。
27
- * 5. `initKeyResolver()` 留在原位(Runner 构造之后、ServiceDeps 装配之前)—— 首建要读的是 boot apply
28
- * **之后**的 `config.modelApiKeyEnv/modelApiKeys`。
29
- * 6. `startRefreshLoop()` 必须在 `new Runner()` 之后 —— 它要 `runnerTierFrozen`(与构造函数读的是同一
30
- * config 代际),且 boot-deferred 到货的续接就挂在这里。
31
- */
32
1
  import { createHash } from "node:crypto";
33
2
  import { readFile as fsReadFile } from "node:fs/promises";
34
3
  import { homedir } from "node:os";
@@ -49,34 +18,14 @@ import { ensureSealedKeyStore, reportExecutionPublicKey } from "../sealed-key.js
49
18
  import { applyEffective, applyCenterReadFace, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, planeHasActiveTiers, fetchPromptArtifact } from "../config-center/facade.js";
50
19
  import { createMcpRevocationLedger, enabledMcpEntries } from "../config-center/mcp-revocation.js";
51
20
  import { SHA256_HEX_RE } from "../digest-form.js";
52
- import { createConfigApplyLedger } from "../config-center/apply-ledger.js"; // #322 世代账
21
+ import { createConfigApplyLedger } from "../config-center/apply-ledger.js";
53
22
  export async function createConfigCenterRuntime(ctx) {
54
23
  const { config, logger, metrics, localRoot } = ctx;
55
- // Config-center: pull the effective config on boot and apply it OVER the env
56
- // defaults — env is the fallback. Unset or unreachable → keep the env-derived config. Applied BEFORE
57
- // the Runner is built so models/roles/teams take effect. (Logical config only; secrets stay in env.)
58
24
  let ccEtag;
59
- // Kept past the fetch so the deferred skills/mcp apply (which needs the live `skills` array + async
60
- // content lazy-pull) can run right before buildScenarios, after applyEffective handled models/roles/teams.
61
- // `effective` is the BOOT snapshot — the refresh loop never reassigns it, so restartReasons() compares each
62
- // pull against what THIS process baked in (change-detection, not presence → no auto-restart loop).
63
25
  let effective;
64
- // 提示词装配协议 §9.5-2 three-store split: `effective` above = the boot snapshot (diff baseline);
65
- // `latestEffective` = the last candidate that passed validation AND was applied — the explicit
66
- // last-known-good (LKG). A refresh candidate failing validation is REJECTED whole (§9.5-5): never
67
- // applied, so the live config keeps serving this LKG instead of a domain falling to schema-default/
68
- // empty. The third store (promptCandidate — a staged prompts-domain artifact awaiting activation)
69
- // lands with the prompts domain itself; `lastRejectedCandidate` is its observability forerunner and
70
- // the prompts-domain validation (core S1 schema) hooks into the same gate.
71
26
  let latestEffective;
72
27
  let lastRejectedCandidate;
73
- // #322 世代账(§2):主键=上游 version(不造本地 epoch)。boot/refresh/迟到 boot 三条腿共用这一本;
74
- // 候选被整拒的拍只推进 target ⇒ /health 的 stale 面从结构上可见。
75
28
  const applyLedger = createConfigApplyLedger();
76
- // #325(test [4809],[4810] 阻断裁定):**收货序数**——CAS 与世代账的序轴。source version 在
77
- // local 腿是内容哈希(registry-core file-store getVersion(),无序承诺),不能当序数比;这里按
78
- // 收货顺序铸进程内单调 ordinal:version 变即 ++,等版本重放(etag 未推进的下一拍)沿用同
79
- // ordinal=幂等语义保留。version 仍是跨副本对账的身份标(账里两者双报)。
80
29
  let generationOrdinal = 0;
81
30
  let lastGenerationVersion;
82
31
  const generationOf = (version) => {
@@ -86,47 +35,18 @@ export async function createConfigCenterRuntime(ctx) {
86
35
  }
87
36
  return generationOrdinal;
88
37
  };
89
- // #322 追加件:手动触发口的实现指针 —— `startRefreshLoop` 起环时装上(cadence 与手动口共用的那只
90
- // join 形拍)。未起环(纯 env 部署 / 还没到装配那一步)⇒ 恒 undefined ⇒ 端点诚实 4xx。
91
38
  let manualRefresh;
92
- // codex R10 (models-tiers 窗收口): the last EffectiveConfig whose MODEL PLANE (models/roles/tiers/default) was
93
- // actually applied to the live config — undefined = env plane (no center plane applied yet). When the Runner is
94
- // tier-frozen, a refresh whose plane differs from THIS is deferred to restart (applyEffective deferModelPlane),
95
- // keeping admission and the Runner on one catalog generation through the restart window.
96
39
  let appliedPlaneEff;
97
- // codex R18: the persistent "deferred WITHOUT next-boot handoff" state — the one-shot warn is not enough
98
- // (the etag advances → later ticks 304 and never re-enter the defer branch). Set when a plane defers with no
99
- // restart-surviving LKG; cleared when a plane finally applies or a forced restart reason ships. Surfaced on
100
- // /health (modelPlaneDeferred) + re-warned every refresh tick (incl. 304s).
101
40
  let planeDeferredNoHandoff;
102
- // codex R35: STICKY live-skill application debt — the boot-baked skill table never rebuilds inside this
103
- // process, so once the live application is known incomplete the debt survives every refresh (a
104
- // same-generation 200 recomputes reasons vs boot as [] and would otherwise silently clear the signal).
105
- // Set at the boot writer (R34 arms); folded into every tick's reasons; only a restart resolves it.
106
41
  let bootSkillStale = false;
107
- // Config-center restart signal: set by the refresh loop when a pull's restart-to-apply slices
108
- // (skills/mcp/scenarios/runtime-gates) differ from boot; read by GET /health for orchestrator auto-restart.
109
42
  let pendingRestart;
110
- // DUAL-MODE-DESIGN §4: the config SOURCE is a backend-selectable seam — remote (config-center HTTP, the fleet
111
- // default) | local (.env + config.d via @sema-agent/registry-core FileConfigStore, the single-machine/TOC source).
112
- // The provider exists when SEMA_REGISTRY_URL (legacy CONFIG_CENTER_URL now boot-rejects, no dual-read) is set OR CONFIG_PROVIDER=local is explicit; neither = pure-env
113
- // (byte-identical to before). The remote provider DELEGATES verbatim to fetchEffective, so the fleet path is
114
- // unchanged; applyEffective consumes either source's EffectiveConfig identically (one resolver → no drift).
115
43
  const cc = config.configCenter;
116
- // Sealed-box key custody: ensure this host's X25519 keypair(s) under
117
- // <localDataRoot>/sealed-key/ (dir 0700, files 0600; SEVERAL may coexist — rotation keeps old private
118
- // keys decrypting old ciphertexts) BEFORE the first effective pull, so a roster carrying
119
- // `sealedApiKey` unseals on the boot apply. The public-key report is best-effort fire-and-forget:
120
- // the center's execution-keys face is admin-gated and a worker only holds the pull token, so without
121
- // an operator-provided SEMA_REGISTRY_ADMIN_TOKEN it prints the full public key for manual
122
- // registration instead (honest posture — see reportExecutionPublicKey). Store failure is non-fatal:
123
- // sealed models then warn per-model in applyEffective (fail-loud, never a silent apiKeyEnv fallback).
124
44
  let sealedKeys;
125
45
  try {
126
46
  sealedKeys = await ensureSealedKeyStore(join(config.localDataRoot ?? localRoot, "sealed-key"), logger);
127
47
  void reportExecutionPublicKey({
128
48
  baseUrl: cc?.baseUrl,
129
- adminToken: process.env.SEMA_REGISTRY_ADMIN_TOKEN, // [2373]B-1:REGISTRY_ADMIN_TOKEN 双读已墓碑(在场即拒启)
49
+ adminToken: process.env.SEMA_REGISTRY_ADMIN_TOKEN,
130
50
  key: sealedKeys.latest,
131
51
  ...(cc?.worker ? { notes: `worker ${cc.worker}` } : {}),
132
52
  logger,
@@ -142,22 +62,6 @@ export async function createConfigCenterRuntime(ctx) {
142
62
  ...(config.configProvider === "local" ? { localDir: localRoot } : {}),
143
63
  })
144
64
  : undefined;
145
- // boot ready 门 latch:布防时等中心 roster 落地——首次 effective 落 ≥1 enabled 模型即翻 true(one-way);
146
- // 未布防则恒 ready。守的危险=**registry 部署的 fleet worker 在 roster 落地前拿占位模型跑计费任务**
147
- // (E3 废除 workers.model 之后,占位模型正是 fleet worker 的常态起服姿势)。
148
- //
149
- // 🔧 复审 2026-07-29 P1-6 —— 布防判据从**推断**改成**运营者声明**。原式是
150
- // `!(cc && !cc.dryRun) || Boolean(process.env.MODEL_ID)`,其中 `Boolean(process.env.MODEL_ID)` 是
151
- // 3.0.0 前「运营者手上除了中心 roster 什么都没有」的代理判据。**server 3.0.0 起 `MODEL_ID` 必填**
152
- // (未设 = loadConfig throw,见 config.ts parseModelDomain)⇒ 走到这一行时它必非空 ⇒ 初值恒 true ⇒
153
- // 「等 roster」那条臂不可达、markRosterLanded 的门体是死码、/health 的 ready:false 与计费提交的 503
154
- // 全不可能发生。注意这不是「危险消失了」:必填 MODEL_ID 恰恰让**每个** fleet worker 都带着占位模型
155
- // 起服,危险比以前更常见——只是那个代理判据从此写不出来了。
156
- //
157
- // 于是改成显式声明:`CONFIG_REQUIRE_ROSTER=true`(与 CONFIG_LKG_* / CONFIG_BOOT_FETCH_BUDGET_MS 同族,
158
- // 直读 env,不进 ServiceConfig——纯 boot 期姿势旋钮)。**缺省关 = 现有部署逐字节零变化**;开了才真的
159
- // fail-closed 等 roster。仅远程 registry 且非 dryRun 车道可布防:dryRun 是 compare-only(中心 roster
160
- // 从不被应用,拿它当就绪判据无意义),local provider / 纯 env 部署则没有「中心」可等。
161
65
  const rosterGateArmed = cc !== undefined && !cc.dryRun && process.env.CONFIG_REQUIRE_ROSTER === "true";
162
66
  const modelReadyState = { ready: !rosterGateArmed };
163
67
  const markRosterLanded = (eff) => {
@@ -166,14 +70,7 @@ export async function createConfigCenterRuntime(ctx) {
166
70
  logger.info("model_roster_landed", { note: "ready gate open — billable submissions accepted" });
167
71
  }
168
72
  };
169
- // center prompts ref([1057]/[1062] 投影腿):**热采用,新任务边界生效**——core 1.316([1068] 裁 a/
170
- // [1070]d)把 provider 声明纳入 epoch artifactDigest:声明变=digest 变=下任务边界自动重 pin,进行中
171
- // 会话由 session epoch pin 保稳,resume 记账诚实(1.222 的 restart-to-apply 是 core 件落地前的正确
172
- // 过渡姿势,[1068]① 官方确认后随 1.316 采用降级为热生效——与 center「发布=新任务生效」产品语义对齐)。
173
- // 判形通过才采用(形校验=防 core-assemble-THROW 底线,非语义复检 [1057]①f);坏形=warn+沿用旧包。
174
73
  let centerPrompts;
175
- /** codex-3 F1:一次失败的 catalog 过渡的目标面(undefined 目标=欠 disable)。etag 已推进时 provider
176
- * 304 短路 adopt——本 marker 让每个 refresh 拍(含 304 拍)独立重试,直到过渡落地。 */
177
74
  let pendingCatalogFaces;
178
75
  const retryPendingCatalog = async (phase) => {
179
76
  if (!pendingCatalogFaces)
@@ -184,25 +81,8 @@ export async function createConfigCenterRuntime(ctx) {
184
81
  pendingCatalogFaces = undefined;
185
82
  }
186
83
  };
187
- // e/b(catalog 轴消费面,[1236]① seam):core 的 CenterPromptSource——server 只给根路径,core 文件形
188
- // 自管 `<root>/prompt-epoch/{artifacts,source-state.json}`;boot restore=磁盘 LKG(离线重启 candidate
189
- // 存续/disabled 不复活,状态机全在 core)。RunnerDeps.promptSource 挂它;adopt 时经 applyCatalogToSource
190
- // 推进两态过渡。restore 失败=warn(candidate 缺席,catalog 轴等下一次 adopt;declaration 轴/内置照跑)。
191
84
  const promptEpochDir = join(config.localDataRoot ?? localRoot, "prompt-epoch");
192
- // codex-3 F2:effective 拉取是 worker 轴的(config LKG 同理 worker-keyed)——同根多 worker 进程共享
193
- // content-addressed artifacts/(by-digest,天然安全),但 **candidate 状态必须按 worker 分箱**:共写一份
194
- // source-state.json 会让 A 的撤下压掉 B 的 candidate、restart 恢复到别人 worker 的 catalog(错 worker
195
- // 的系统提示词跑任务)。encodeURIComponent 消毒(worker 名进路径)。
196
- // codex-5:无撞形分箱——缺席=字面 "global",配置了 worker= "worker-<sha256 前 16>"(哈希后天然单
197
- // 段/无 dot-segment/与 "global" 不可能同名;worker 名任意字节也安全)。
198
85
  const promptStateDir = join(promptEpochDir, "state", cc?.worker === undefined ? "global" : `worker-${createHash("sha256").update(cc.worker).digest("hex").slice(0, 16)}`);
199
- // codex-6 F1:File 店构造同步 mkdir——只读/不可写数据根会把「可选的 catalog 特性」变成整服务 boot
200
- // 失败。守护工厂:文件形失败 ⇒ warn + Memory 店兜底(candidate 不跨重启持久,catalog 采用仍工作;
201
- // 内置/declaration 轴完全不受影响)。
202
- // [985]b / [1285] — §9.4 by-digest 补拉组合店:local miss(本副本从未采用过该 candidate / 盘失)⇒
203
- // 从 center 按 digest 补拉 envelope ⇒ core verifyPromptArtifact 全管线(canonical 复算;**且回验
204
- // 「拿到的=要的」digest 逐位**,center 答非所问=丢弃)⇒ best-effort 回写盘(下次直命中)。pinned
205
- // resume 的 fail-loud miss 自此只剩「center 也没有」的真缺席。仅 remote 治理形(cc.baseUrl)挂补拉。
206
86
  const withByDigestBackfill = (inner) => cc?.baseUrl ? withPromptArtifactBackfill(inner, (d) => fetchPromptArtifact(cc.baseUrl, cc.token, d), logger) : inner;
207
87
  const promptSource = (() => {
208
88
  try {
@@ -217,10 +97,6 @@ export async function createConfigCenterRuntime(ctx) {
217
97
  return new CenterPromptSource({ store: withByDigestBackfill(new MemoryPromptArtifactStore()), stateStore: new MemoryPromptSourceStateStore(), engineVersion: CORE_ENGINE_VERSION });
218
98
  }
219
99
  })();
220
- // codex-2 F4:只有「有配置源在治理且非 dry-run」才 restore 磁盘 candidate——provider-off/dry-run 姿势下
221
- // 旧部署遗留的 active candidate 不得继续影响新任务(publish-gate-off/compare-only 语义)。不 restore 只
222
- // 影响 candidate 面;pinned resume 的 by-digest 解析走 FilePromptArtifactStore.get(不依赖 restore),
223
- // 旧会话恢复不受损。
224
100
  if (configProvider && !cc?.dryRun) {
225
101
  try {
226
102
  await promptSource.restore();
@@ -232,8 +108,6 @@ export async function createConfigCenterRuntime(ctx) {
232
108
  const adoptCenterPrompts = async (eff, phase) => {
233
109
  const raw = eff?.prompts;
234
110
  if (raw === undefined) {
235
- // fail-closed 语义([1057]①d):publish 闸关=key 整体不出现 ⇒ 收敛回内置提示词(不是「保持旧包」——
236
- // 闸关是显式撤下,沿用旧包会让「关闸」失效)。
237
111
  if (centerPrompts)
238
112
  logger.info("center_prompts_cleared", { phase, note: "effective carries no prompts key (publish gate off) — built-in providers resume" });
239
113
  const cleared = await applyCatalogToSource(promptSource, undefined, centerPrompts, logger, phase);
@@ -242,7 +116,7 @@ export async function createConfigCenterRuntime(ctx) {
242
116
  pendingCatalogFaces = undefined;
243
117
  }
244
118
  else {
245
- pendingCatalogFaces = { target: undefined }; // failed disable — retried by the refresh hook (codex-3 F1)
119
+ pendingCatalogFaces = { target: undefined };
246
120
  }
247
121
  return;
248
122
  }
@@ -252,7 +126,6 @@ export async function createConfigCenterRuntime(ctx) {
252
126
  return;
253
127
  }
254
128
  if (centerPrompts?.identity !== v.value.identity) {
255
- // [985]a §3:axes 观测两轴共存窗;declaration 轴字段照旧,catalog 轴报 artifactDigest(内容身份)。
256
129
  logger.info("center_prompts_adopted", {
257
130
  phase,
258
131
  axes: v.value.axes,
@@ -264,9 +137,6 @@ export async function createConfigCenterRuntime(ctx) {
264
137
  });
265
138
  }
266
139
  const transitioned = await applyCatalogToSource(promptSource, v.value, centerPrompts, logger, phase);
267
- // codex-2 F2 + codex-3 F1:catalog 过渡失败 ⇒ 身份不推进(centerPrompts 保持 prev),且目标面记入
268
- // pendingCatalogFaces——etag 会照常推进(其他域的采用不为 prompts 的盘错扣住),此后 provider 304
269
- // 不再进本函数,重试由 refresh 拍上的 retryPendingCatalog 钩独立驱动(见 refresh 循环)。
270
140
  if (transitioned) {
271
141
  centerPrompts = v.value;
272
142
  pendingCatalogFaces = undefined;
@@ -275,57 +145,15 @@ export async function createConfigCenterRuntime(ctx) {
275
145
  pendingCatalogFaces = { target: v.value };
276
146
  }
277
147
  };
278
- // Boot-deferred pull (clay 2026-07-17): when the boot fetch outlives its budget, the SAME promise is
279
- // stashed here and, once the refresh tick exists (defined further down), chained to trigger an immediate
280
- // hot-apply on arrival — the server meanwhile starts on the env fallback in <1s instead of hanging up to
281
- // ~8s on a black-holed center (真机实测 5.7s = the "local server takes 5s" report).
282
148
  let bootConfigPending;
283
- // LKG 落盘(clay 拍 2026-07-18;config-lkg.ts 顶注=全案由):remote lane 的干净候选持久化——boot 的
284
- // 第三级来源(中心 > 盘上 LKG > env)。`lkgBooted` 标志决定 deferred continuation 的形:LKG 起服=有
285
- // 完整 boot 基线 → 到货走普通 refreshTick(候选门+hot+restartReasons 对 LKG 比较,通常零差异=无环);
286
- // 无 LKG=env 起服 → 到货走「迟到 boot」路径(deferredBootApply)。
287
149
  const lkgEnabled = configProvider?.kind === "remote" && configLkgEnabled();
288
150
  const lkgPath = process.env.CONFIG_LKG_PATH ?? defaultLkgPath(cc?.worker);
289
- // codex R17/R20: a successful LOCAL write proves nothing about the NEXT instance seeing it — a stateless pod
290
- // replaces its filesystem on restart, so treating a pod-local rename as "handoff confirmed" re-opens the R16
291
- // rolling-restart storm. Next-instance visibility is a DEPLOYMENT contract only the operator can assert, and
292
- // ONLY via the explicit knob: CONFIG_LKG_DURABLE=true declares the LKG path rides restart-surviving storage
293
- // (host disk / PVC / shared volume). CONFIG_LKG_PATH is strictly a LOCATION override (R20: it can point at an
294
- // ephemeral container path — a location says nothing about durability). Unset ⇒ the models-tiers restart
295
- // reason stays suppressed on a deferred plane (old generation keeps serving; /health modelPlaneDeferred +
296
- // per-tick warn carry the state) while normal LKG writes continue (they still help same-instance restarts).
297
151
  const lkgSurvivesRestart = lkgEnabled && process.env.CONFIG_LKG_DURABLE === "true";
298
152
  let lkgBooted = false;
299
- let bootLkgCandidate; // F7:skills 装载后才落盘
300
- // F1+F2(LKG 复审):awaited 落盘 + skill 正文预热——refresh 只落 manifest 的话,重启后 LKG-boot 在
301
- // 中心不可达时新 skill 正文缓存缺失=软跳过(静默缺 skill)。预热=枚举 enabled manifest,把缺盘的正文
302
- // 拉齐进 content-addressed 缓存(失败=warn 软降级,与 boot 的 skill_failed 同语义等级——skill 拉取
303
- // 失败从不升级为配置拒绝);await 保证 restart 信号可见前 LKG+正文已落盘。
304
- /** Returns TRUE iff the candidate is durably persisted for the NEXT boot (codex R16: the FORCED models-tiers
305
- * restart reason must never fire without a confirmed handoff — LKG disabled/unwritable + over-budget boot
306
- * pulls would otherwise turn the deferral into a rolling-restart storm: every restarted process boots the env
307
- * plane again, defers again, re-signals). Change-DETECTED reasons still ride regardless (their loop is the
308
- * pre-existing, documented no-LKG residual). */
309
- // codex R37 loop-breaker proof: TRUE iff the LAST persistLkgDurable call hash-verified EVERY enabled skill
310
- // body on disk. This is the "a restart can actually repair the live skill table" proof the NON-durable lane
311
- // requires before publishing a skills restart — a successful manifest pull only proves the CONFIG endpoint
312
- // works, not the SEPARATE body fetches; signaling on manifest-success alone lets a persistent body failure
313
- // (404 / hash mismatch / timeout) restart-storm a stateless deployment (every replacement process fails the
314
- // same body, signals again, is restarted again). With the proof, a storm cannot form: a boot that fails the
315
- // body lacks the proof and only retains visible debt (no signal) until a later verify succeeds.
153
+ let bootLkgCandidate;
316
154
  let lastPrewarmBodiesVerified = false;
317
155
  const persistLkgDurable = async (effectiveObj, etag) => {
318
156
  lastPrewarmBodiesVerified = false;
319
- // codex R21: the LOCAL provider's candidate IS the next-boot source (config.d on the same disk boot reads
320
- // BEFORE Runner construction) — the handoff is inherent, no LKG needed. Without this arm a polled config.d
321
- // edit that defers the plane (tier activation / tier-frozen change) could never authorize its own restart
322
- // (lkgEnabled is remote-only, so even CONFIG_LKG_DURABLE couldn't help) — stuck in modelPlaneDeferred forever.
323
- // codex R28 requalified: this TRUE covers the MODEL PLANE (what the deferral gate adjudicates). Skills are
324
- // NOT part of the local handoff because the local lane does not consume a center-style skills domain AT ALL
325
- // yet — the only boot consumer (applyCenterSkills) requires config.configCenter (remote HTTP), absent on
326
- // CONFIG_PROVIDER=local; bodies live inline in config.d (LocalConfigProvider.fetchSkillContent) but no boot
327
- // path reads them. Pre-existing local-lane gap (predates this train, nothing lost across restart that was
328
- // ever applied); wiring boot skills through the ConfigProvider seam is the tracked follow-up (board).
329
157
  if (configProvider?.kind === "local")
330
158
  return true;
331
159
  if (!lkgEnabled)
@@ -334,15 +162,6 @@ export async function createConfigCenterRuntime(ctx) {
334
162
  if (effectiveObj.skills && config.configCenter) {
335
163
  await applyCenterSkills([], effectiveObj.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, defaultSkillCacheDir());
336
164
  }
337
- // codex R25/R26: skill BODIES are part of the handoff contract, and completeness gates PUBLICATION —
338
- // applyCenterSkills swallows fetch/cache-write failures, so awaiting it proves nothing; verify every
339
- // ENABLED manifest hash on disk (content-true) BEFORE the atomic LKG replace (R26: verifying after left
340
- // an incomplete candidate as the boot-visible LKG — any crash/manual restart with the center down then
341
- // boots that manifest, silently skips the uncached skills, and never re-signals because the manifest is
342
- // the new boot baseline). On any miss/corruption the PREVIOUS good LKG stays in place; the per-tick
343
- // retained-candidate retry re-attempts prewarm + verify + publish.
344
- // Residual seam (honest, partial): the skill cache roots at SEMA_CONFIG_DIR while lkgPath can point
345
- // elsewhere — CONFIG_LKG_DURABLE's contract requires BOTH on restart-surviving storage.
346
165
  const enabledSkills = (effectiveObj.skills?.skills ?? []).filter((m) => m.enabled !== false);
347
166
  const cacheDir = defaultSkillCacheDir();
348
167
  for (const m of enabledSkills) {
@@ -361,11 +180,8 @@ export async function createConfigCenterRuntime(ctx) {
361
180
  return false;
362
181
  }
363
182
  }
364
- lastPrewarmBodiesVerified = true; // every enabled body verified on disk — the repair proof stands even if the LKG write below fails
183
+ lastPrewarmBodiesVerified = true;
365
184
  await saveLkg(lkgPath, cc?.worker, effectiveObj, etag);
366
- // codex R17: TRUE = next-BOOT visibility, not merely a successful local rename — gated on the operator's
367
- // restart-surviving-storage declaration (lkgSurvivesRestart above). A stateless pod's local write still
368
- // helps same-instance restarts but must not authorize a forced restart.
369
185
  return lkgSurvivesRestart;
370
186
  }
371
187
  catch (err) {
@@ -374,9 +190,7 @@ export async function createConfigCenterRuntime(ctx) {
374
190
  }
375
191
  };
376
192
  if (configProvider) {
377
- const dryRun = cc?.dryRun ?? false; // dry-run posture is remote-only (a local source has nothing to compare against)
378
- // LKG 读腿(boot 超窗与硬失败两个入口共用):盘上有验证过的干净候选 → 当 boot 配置整套应用
379
- // (skills/mcp 进 buildScenarios——这正是 env 兜底给不了的),etag 落位 → refresh 首拍 304 或正常增量。
193
+ const dryRun = cc?.dryRun ?? false;
380
194
  const bootFromLkg = async (why) => {
381
195
  if (!lkgEnabled || dryRun)
382
196
  return null;
@@ -397,7 +211,7 @@ export async function createConfigCenterRuntime(ctx) {
397
211
  await adoptCenterPrompts(lkgR.effective, "boot-lkg");
398
212
  markRosterLanded(lkgR.effective);
399
213
  effective = lkgR.effective;
400
- appliedPlaneEff = lkgR.effective; // model plane applied pre-Runner — this IS the Runner's generation
214
+ appliedPlaneEff = lkgR.effective;
401
215
  latestEffective = lkgR.effective;
402
216
  if (lkgR.etag !== undefined)
403
217
  ccEtag = lkgR.etag;
@@ -410,7 +224,7 @@ export async function createConfigCenterRuntime(ctx) {
410
224
  let lkgFallback = null;
411
225
  if (raced === BOOT_FETCH_DEFERRED) {
412
226
  bootConfigPending = bootFetch;
413
- bootFetch.catch(() => { }); // settled in the background — failure handling lives on the refresh cadence
227
+ bootFetch.catch(() => { });
414
228
  lkgFallback = await bootFromLkg("boot fetch deferred (budget exceeded)");
415
229
  lkgBooted = lkgFallback !== null;
416
230
  logger.warn("config_boot_fetch_deferred", {
@@ -422,9 +236,6 @@ export async function createConfigCenterRuntime(ctx) {
422
236
  : "center slow/unreachable and no usable LKG — serving on the env fallback; the pull continues in the background (late-boot adoption on arrival). Raise CONFIG_BOOT_FETCH_BUDGET_MS to block boot instead.",
423
237
  });
424
238
  }
425
- // F4(LKG 复审):LKG 载荷的 apply 用专属 catch——走主分支会让 applyEffective 半程炸落到外层
426
- // catch 的「config_source_unreachable_using_env」误导日志(中心明明只是慢)。坏 LKG=点名+env 兜底
427
- // (in-place mutate 的混态风险与 live 坏载荷同级,浅域判形在 loadLkg 已挡结构性坏形)。
428
239
  if (raced === BOOT_FETCH_DEFERRED && lkgFallback) {
429
240
  try {
430
241
  await bootApplyLkgInline(lkgFallback);
@@ -436,23 +247,13 @@ export async function createConfigCenterRuntime(ctx) {
436
247
  }
437
248
  const r = raced === BOOT_FETCH_DEFERRED ? null : raced;
438
249
  if (r) {
439
- // [898] tolerant read(registry-core 0.10.12,local lane):catalog 域坏文件不再连坐整包回落 env——
440
- // 坏域按 default 落+per-domain warn 点名(文件路径在 error 文案里),好域照常生效。gate 域坏文件
441
- // tolerant 仍 throw(READ 容错不放宽 gate fail-loud)⇒ 走下面既有整包 catch。
442
- // §9.5-5 boot 与 refresh 的分工(codex F1:重启不得洗白坏候选):boot 照旧 tolerant APPLY(无 LKG
443
- // 可保,带着好域起服务),但一个带错误的候选 ①不推进 ccEtag——refresh 首 poll 拿到同一棵树,候选门
444
- // 当场整拒并持续 warn 到修复;②不写入 latestEffective——LKG 永远只指向完全干净的候选,拒绝日志的
445
- // lkgVersion 也诚实(boot 带错=「(env)」)。markRosterLanded 保留:models 域自身干净才可能开门
446
- // (坏 models 域 tolerant 落空=enabled 0=门不动),其他域的错误不构成扣住就绪门的理由。
447
250
  const bootPromptsRaw = r.effective.prompts;
448
251
  const bootPromptsOk = bootPromptsRaw === undefined || validatePromptsDomain(bootPromptsRaw).ok;
449
- // 坏 prompts 面同 domainErrors 计入 not-clean(F6 boot 半场):etag 不进 → refresh 首拍同树整拒持续
450
- // warn;LKG 不指向带坏 prompts 的候选。adoptCenterPrompts 自身会拒采用+点名。
451
252
  let bootClean = (r.domainErrors ?? []).length === 0 && bootPromptsOk;
452
253
  for (const de of r.domainErrors ?? [])
453
254
  logger.warn("config_domain_invalid", { domain: de.domain, error: de.error, note: "tolerated at BOOT (corrupt file → schema defaults; dangling ref → reported as-is); the refresh path rejects such a candidate whole and keeps the LKG (§9.5-5)" });
454
255
  if (dryRun) {
455
- logEffectiveDiff(config, r.effective, logger); // compare-only: log the would-be changes, keep env
256
+ logEffectiveDiff(config, r.effective, logger);
456
257
  if (bootClean)
457
258
  ccEtag = r.etag;
458
259
  }
@@ -467,33 +268,24 @@ export async function createConfigCenterRuntime(ctx) {
467
268
  bootReport = rep;
468
269
  applyLedger.recordApply(rep);
469
270
  },
470
- }); // sealedKeys: unseal sealedApiKey → in-memory key pipeline
271
+ });
471
272
  applyLedger.applied(bootGen, r.effective.version);
472
- // codex R2-F1(boot 半场):某组被拒 ⇒ 这棵树**不干净** —— 与 domainErrors/坏 prompts 同处置
473
- // (etag 不进、不进 LKG 候选),否则一次带坏 limits 的 boot 会把它写成盘上的 LKG。
474
273
  if (bootReport?.fullyApplied === false)
475
274
  bootClean = false;
476
275
  if (bootClean)
477
- ccEtag = r.etag; // 干净才推进 etag(判定要在 apply 之后——某组被拒也算不干净)
478
- await adoptCenterPrompts(r.effective, "boot"); // [1057] additive prompts key(判形采用;缺席=内置)
479
- markRosterLanded(r.effective); // boot pull 即落 roster 的常态:门在启动窗内就打开
480
- effective = r.effective; // skills/mcp applied below, before buildScenarios (restart-to-apply)
481
- appliedPlaneEff = r.effective; // model plane applied pre-Runner — this IS the Runner's generation
276
+ ccEtag = r.etag;
277
+ await adoptCenterPrompts(r.effective, "boot");
278
+ markRosterLanded(r.effective);
279
+ effective = r.effective;
280
+ appliedPlaneEff = r.effective;
482
281
  if (bootClean)
483
- latestEffective = r.effective; // LKG seed (§9.5-2): only a fully clean candidate may become the LKG
484
- // F7(LKG 复审三轮):boot 写点**延后**到 center skills 装载(applyCenterSkills+盘缓存)之后——
485
- // 先落 LKG 会让「新 skill 的 manifest 可见但正文没进缓存」窗口存在(此窗内崩+中心断=离线 boot
486
- // 静默缺 skill)。这里只记 candidate,真正落盘在 skills 装载后(main 后段)。
282
+ latestEffective = r.effective;
487
283
  if (bootClean && !lkgBooted)
488
284
  bootLkgCandidate = { effective: r.effective, etag: r.etag };
489
- // [875]b:带上模型数与(local lane)真实目录来源——复验实录:CONFIG_LOCAL_DIR 误配(只设
490
- // LOCAL_DATA_ROOT)时 config.d 静默读不到、0 模型且无告警,排障只能靠猜。
491
285
  const cfgModels = (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
492
286
  const cfgDir = configProvider.kind === "local" ? { dir: configProvider.root } : {};
493
287
  logger.info("config_loaded", { source: configProvider.kind, version: r.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels, ...cfgDir });
494
288
  if (configProvider.kind === "local" && cfgModels === 0) {
495
- // [3073]#204④ hint 真因三臂:域判形失败(文件在场却喊「空/指错」=对排障者说谎,真因在
496
- // config_domain_invalid 里但 hint 不指路)/ 全 enabled:false / 真空缺,三态分开点名。
497
289
  const modelsDomainErr = (r.domainErrors ?? []).find((de) => de.domain === "models");
498
290
  const totalModels = (r.effective.models?.models ?? []).length;
499
291
  logger.warn("config_local_no_models", {
@@ -511,7 +303,6 @@ export async function createConfigCenterRuntime(ctx) {
511
303
  }
512
304
  catch (err) {
513
305
  logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
514
- // 硬失败(fast-fail RST 等,非超窗)同样值得 LKG:上一进程的干净候选好过纯 env。
515
306
  const lkgFallback = await bootFromLkg(`boot fetch failed: ${String(err).slice(0, 120)}`);
516
307
  if (lkgFallback) {
517
308
  lkgBooted = true;
@@ -525,84 +316,44 @@ export async function createConfigCenterRuntime(ctx) {
525
316
  }
526
317
  }
527
318
  }
528
- // #A4([4147] 件1):READ 容纳面四键的 center 腿 —— **boot 期唯一装点**,两条 boot 路径(live 拉取与
529
- // LKG 回落)在此汇合(`effective` 只在真应用的那两支被置;dry-run 只 diff 不 apply ⇒ 恒缺席 ⇒ 不落配,
530
- // 与 observe-only 语义一致)。
531
- // ⚠️ **位置即契约**:必须留在 `createConfigCenterRuntime` 的 boot 段里 —— 这四键在 main.ts 的
532
- // `createRunnerDeps` 里被**取值捕获**(boot/runner-deps.ts 的四座),而那一步排在本函数 await 之后;
533
- // 挪到 `applyCenterCapabilities`(skills/mcp 那条线,main.ts 后段)就晚了:配置改了、runner 已经拿着旧值,
534
- // 是「半接=静默死特性」的教科书形。refresh 路径刻意不热应用(restart-to-apply,`read-face` 片通告)。
535
- // 返回值 = 本机 env 显式占住的 readFace 键(部署事实,与本次发布内容无关)。喂给 `restartCtx()`,
536
- // `read-face` 片于是只指纹**真会 boot-bake 的那部分** —— 否则 center 改一个本机永远用不上的键,
537
- // 整片 fleet 会为一次对自己毫无影响的发布滚动重启一遍(codex 对抗复审 finding 2)。
538
- // ⚠️ 必须在 apply **之前/之中**取:apply 之后 config 上的键分不清是 env 写的还是 center 刚落的。
539
- // 域缺席(今天绝大多数部署)时函数只算这一件事就返回,零副作用。
540
319
  const readFaceEnvHeld = applyCenterReadFace(config, effective ?? { version: 0, updatedAt: "", models: { models: [], roles: {} } }, effective ? logger : undefined);
541
320
  if (!modelReadyState.ready)
542
321
  logger.warn("model_roster_pending", {
543
322
  note: "CONFIG_REQUIRE_ROSTER=true and no center roster has landed yet — the env MODEL_ID is treated as a PLACEHOLDER, so billable submissions 503 (and /health reports ready:false) until the first effective pull lands ≥1 enabled model. Unset the knob if this worker's env model is authoritative.",
544
323
  });
545
- // Per-model API key (config-center `apiKeyEnv`): resolve each model's own upstream key per brain call /
546
- // cascade rung. undefined when no per-model keys are configured → spec field stays unset (core default).
547
- // `let` (not const): rebuilt on a config-center refresh so per-model key ADDITIONS hot-apply too (the
548
- // resolver is undefined when no per-model keys exist, so in-place mutation alone wouldn't cover empty→non-empty).
549
324
  let keyResolver;
550
- // 复审 2026-07-29 P1-11:restart 片里唯一一块 EffectiveConfig **看不见**的输入 —— 反应式降级车道由 env
551
- // (MODEL_DEGRADE_TO / MODEL_DEGRADE_REACTIVE)开关,而它焊死的是 CENTER 目录里那条模型的路由面。读成
552
- // 函数而非常量:`config.degrade` 今天由 env 独占(applyEffective 不写它),但若哪天中心接管这面,这里
553
- // 自动跟着走,不会退化成 boot 期快照。车道关(默认)⇒ ctx 空 ⇒ 该片恒 null ⇒ 零行为变化。
554
- // #324(core 5.50 design/338):mid-turn MCP 撤销台账。铸在这里(写路径归本 runtime:boot bake 铸
555
- // baseline、refresh commit 喂 diff),读口经 `takeMcpRevocations()` 外借给 RunnerDeps 席。
556
- // ⚠️ **取走即声明接线**:restart 豁免(纯删除向不签 mcp)只在 `mcpRevocationWired` 后生效——把豁免与
557
- // 承接绑在同一动作上,main 若哪天删了接线行,豁免自动关回旧 restart 语义(fail-closed,#157;
558
- // 反例=独立布尔,两处各写一遍必漂移,漂移方向正是「没人承接删除却不再签 restart」的静默 fail-open)。
559
325
  const mcpRevocations = createMcpRevocationLedger((ev) => logger.info("mcp_revocation_ledger", { ...(ev.revoked.length > 0 ? { revoked: ev.revoked } : {}), ...(ev.restored.length > 0 ? { restored: ev.restored } : {}) }));
560
326
  let mcpRevocationWired = false;
561
327
  const restartCtx = () => ({
562
328
  ...(config.degrade?.reactive ? { reactiveDegradeTo: config.degrade.to } : {}),
563
- // #A4:env 占住的 readFace 键(boot 期一次性事实,env 不会在进程内变)。
564
329
  ...(readFaceEnvHeld.length > 0 ? { readFaceEnvHeld } : {}),
565
- // codex F2 双门:席被取走(wired)**且** boot bake 真铸过 baseline(mount 真身在案)才豁免——
566
- // boot 超时/纯 env 形的 mount 不是中心集,撤销腿承接不了删除,豁免必须关死走旧 restart 语义。
567
330
  ...(mcpRevocationWired && mcpRevocations.hasBaseline() ? { mcpRevocationLive: true } : {}),
568
331
  });
569
- // #131-2:60s 刷新环的句柄提到 runtime 闭包层——stopRefreshLoop(hardShutdown 收尾链)要能清它。
570
332
  let ccTimer;
571
333
  return {
572
334
  providerKind: configProvider?.kind,
573
335
  promptSource,
574
336
  takeMcpRevocations: () => {
575
- mcpRevocationWired = true; // 取走即声明接线(见铸造点注)——restart 豁免从此拍起生效
337
+ mcpRevocationWired = true;
576
338
  return mcpRevocations;
577
339
  },
578
- mcpRevocationState: () => (configProvider ? mcpRevocations.view() : undefined), // 纯 env 形没有中心撤销语义 ⇒ 诚实缺席
340
+ mcpRevocationState: () => (configProvider ? mcpRevocations.view() : undefined),
579
341
  getKeyResolver: () => keyResolver,
580
342
  getCenterPrompts: () => centerPrompts,
581
343
  modelReady: () => modelReadyState.ready,
582
344
  restartState: () => pendingRestart,
583
345
  planeDeferredState: () => planeDeferredNoHandoff,
584
- configApplyState: () => (configProvider ? applyLedger.view() : undefined), // 纯 env 形没有可对账的世代 ⇒ 诚实缺席,不报一对恒 0
346
+ configApplyState: () => (configProvider ? applyLedger.view() : undefined),
585
347
  async refreshNow() {
586
348
  if (!manualRefresh)
587
349
  return "not-wired";
588
350
  return (await manualRefresh()) === "failed" ? "failed" : "triggered";
589
351
  },
590
352
  async applyLocalRemoteExec() {
591
- // DUAL-MODE §4/§5 (center #1): in LOCAL config mode, source remoteExec from the SHARED
592
- // `config.d/remote-exec.json` via the registry `loadRemoteExec` — the SAME file + zod validation that the
593
- // client's `agent-cli config` writes, so client doctor and the engine read one source of truth (zero drift).
594
- // Semantics (center, deliberate): file MISSING → undefined → keep the env/host default (config.ts already
595
- // defaults host in local mode); file PRESENT → it wins; file empty/corrupt → the loader throws → fail-closed
596
- // (an empty file silently falling back to host would be an isolation downgrade if the user meant local-docker).
597
353
  if (configProvider?.kind === "local") {
598
354
  try {
599
- const fileRx = await loadRemoteExec(localRoot); // SAME root as the config provider above (no split-brain)
355
+ const fileRx = await loadRemoteExec(localRoot);
600
356
  if (fileRx) {
601
- // 🔴 center contract: the package RemoteExecSpec is the CONTRACT form (env-NAME refs: apiKeyEnv/tokenEnv/…);
602
- // our config.remoteExec is the RESOLVED form (values). `host` carries NO secrets so it maps 1:1 (contract
603
- // `workdir` ≡ our `workspaceBase`). The ISOLATED lanes (e2b/k8s/ssh/adb) need a NAME→value resolution
604
- // before use — a follow-on; until then they are sourced from REMOTE_EXEC env, so a file-source isolated
605
- // arm is ignored (NOT cast verbatim — that would feed an env-NAME where a value is expected).
606
357
  if (fileRx.provider === "host") {
607
358
  config.remoteExec = {
608
359
  provider: "host",
@@ -612,10 +363,6 @@ export async function createConfigCenterRuntime(ctx) {
612
363
  logger.info("remote_exec_from_file", { provider: "host", root: localRoot });
613
364
  }
614
365
  else if (fileRx.provider === "local-docker") {
615
- // local-docker carries NO secret env-NAMEs (image/memory/cpus/network/workdir/mountPath only), so it
616
- // maps 1:1 from the contract form — no NAME→value resolution needed (unlike e2b/k8s/ssh). The contract
617
- // `workdir` is the in-container workspace ROOT ≡ our `mountPath`. DOCKER_HOST/secret env stay in the
618
- // worker's .env (the adapter reads its own process.env via the env-forward, not the file).
619
366
  config.remoteExec = {
620
367
  provider: "local-docker",
621
368
  image: fileRx.image,
@@ -633,20 +380,14 @@ export async function createConfigCenterRuntime(ctx) {
633
380
  }
634
381
  catch (err) {
635
382
  logger.error("remote_exec_file_invalid", { root: localRoot, err: String(err) });
636
- throw err; // fail-closed (center semantics): a corrupt remote-exec.json must not boot a wrong/weaker lane
383
+ throw err;
637
384
  }
638
385
  }
639
386
  },
640
387
  async applyCenterCapabilities(skills) {
641
- // Sema-registry B1 (restart-to-apply, baked into scenarios below): overlay center skills (lazy-fetched by
642
- // hash + sha256-verified) over the image baseline, center-wins-by-name; resolve center MCP servers' env-NAME
643
- // refs to real values from this service's env. Both empty/unset → unchanged (image SKILLS_DIR, no MCP).
644
388
  if (effective?.skills && config.configCenter) {
645
389
  skills = await applyCenterSkills(skills, effective.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, configLkgEnabled() ? defaultSkillCacheDir() : undefined);
646
390
  }
647
- // plugins 域消费半场(契约 [1361]②/[1362]/[1366]①;2026-07-28 开工车):把 center 声明的插件引用
648
- // 物化(https-only+ALLOW_HOSTS 纵深复验/sha 钉扎/隔离 clone/LKG)并装载其 skills —— **在 center 直发
649
- // skills 之后**:撞名让位判据(收紧④,plugin 让位)以此刻的 skills 集为准。失败单条不连坐,永不拒启。
650
391
  if (effective?.plugins && config.configCenter) {
651
392
  const pluginOut = await applyCenterPlugins(skills, effective, {
652
393
  cacheRoot: join(config.localDataRoot ?? join(homedir(), ".ai-agent"), "plugin-cache"),
@@ -663,44 +404,21 @@ export async function createConfigCenterRuntime(ctx) {
663
404
  }
664
405
  }
665
406
  }
666
- // F7:boot 的 LKG 落盘点。codex R26:走同一只 persistLkgDurable(完整性门在前)——此前裸 saveLkg 会把
667
- // applyCenterSkills 静默吞掉失败的「缺正文 manifest」发布成 boot 可见 LKG(离线重启=静默缺 skill 且不再
668
- // 有信号收敛)。校验失败=不落盘(warn 已在函数内),下一个干净候选/refresh 重试重写。
669
407
  if (bootLkgCandidate && lkgEnabled) {
670
408
  const bootPublished = await persistLkgDurable(bootLkgCandidate.effective, bootLkgCandidate.etag).catch((err) => {
671
409
  logger.warn("config_lkg_save_failed", { path: lkgPath, err: String(err), note: "boot LKG persist failed — retained for the per-tick retry" });
672
410
  return false;
673
411
  });
674
- // codex R30: a failed/incomplete boot publication must enter the SAME retained-candidate retry lane as the
675
- // refresh path — ccEtag has already advanced, so the next ticks 304 and nothing else would ever retry; a
676
- // restart during a center outage would then boot stale/env config instead of the generation this process
677
- // already applied. blocked=[] — everything WAS applied at boot, this is a publication-only retry (promotion
678
- // with no withheld reasons publishes NO restart signal, it just completes the LKG and clears the state).
679
412
  {
680
- // codex R32/R34: verify the boot's LIVE application UNCONDITIONALLY — applyCenterSkills skips an
681
- // unfetchable body (this process may run skill-stale / on an older same-name built-in), and the LKG
682
- // prewarm inside persistLkgDurable can succeed where the live fetch failed (bootPublished true, LKG
683
- // complete, process still stale). Any live miss ⇒ a skills restart: published immediately when the
684
- // handoff already stands, or carried in blocked for the 304-gated promotion when it doesn't.
685
413
  const enabledManifest = (bootLkgCandidate.effective.skills?.skills ?? []).filter((m) => m.enabled !== false);
686
- // codex R33: the FULL runtime projection must match, not just name+body — a same-body built-in retained
687
- // after a failed center fetch can carry different scenarios/description (e.g. global exposure where the
688
- // manifest restricts to one scenario), and the scenario bundles baked right after are then stale.
689
414
  const canonScenarios = (a) => [...(a ?? [])].sort().join("\u0000");
690
415
  const liveSkillMiss = enabledManifest.some((m) => !skills.some((sk) => sk.spec.name === m.name &&
691
416
  skillContentHash(sk.spec.content) === m.contentHash &&
692
417
  sk.spec.description === m.description &&
693
418
  canonScenarios(sk.scenarios) === canonScenarios(m.scenarios)));
694
419
  if (liveSkillMiss)
695
- bootSkillStale = true; // codex R35: sticky — survives same-generation 200 recomputes
420
+ bootSkillStale = true;
696
421
  if (liveSkillMiss && (bootPublished || (!lkgSurvivesRestart && lastPrewarmBodiesVerified))) {
697
- // Signal NOW only on the arms where a restart PROVABLY repairs (codex R34/R36/R37):
698
- // · durable + published — the complete LKG (manifest + hash-verified bodies) is the handoff;
699
- // · NON-durable + body proof — the prewarm inside persistLkgDurable just hash-verified every enabled
700
- // body on disk, so the miss was a TRANSIENT live-application failure and a restarted process
701
- // re-pulls successfully. WITHOUT the proof (persistent 404/hash mismatch) publishing here would
702
- // restart-storm a stateless deployment (codex R37) — that case falls to the retained-debt arm
703
- // below: visible on /health, retried per confirming 304, promoted only once the proof holds.
704
422
  pendingRestart = { restartRequired: true, reasons: ["skills"], version: bootLkgCandidate.effective.version, since: Date.now() };
705
423
  logger.warn("config_boot_skill_stale_restart", { version: bootLkgCandidate.effective.version, durableHandoff: bootPublished, note: bootPublished ? "live skill application incomplete but the durable LKG carries the full candidate — skills restart published (the next boot reads the complete LKG)" : "live skill application incomplete on a non-durable deployment but every enabled body is verified in the cache — skills restart published (transient failure proven repaired; the restarted process re-pulls live)" });
706
424
  }
@@ -712,26 +430,16 @@ export async function createConfigCenterRuntime(ctx) {
712
430
  }
713
431
  if (effective?.mcp)
714
432
  config.mcpServers = resolveMcpServers(effective.mcp, logger);
715
- // #324:撤销台账的 baseline 拍 = boot bake 的 enabled 条目表(mount 真身与台账「此前」同源单点;
716
- // codex F1 修:带指纹,重加同名不同形不解除撤销)。域缺席/boot 超时 ⇒ null ⇒ 台账不铸 baseline
717
- // (env 型 mcpServers 部署没有中心撤销语义)——此时 hasBaseline()=false,restart 豁免关死
718
- // (codex F2 修:迟到 boot 采用的候选集≠mount 真身,**刻意不喂**,该形走旧 restart 语义收敛)。
719
433
  mcpRevocations.applyEnabledEntries(enabledMcpEntries(effective?.mcp), { isBaseline: true });
720
- // DESIGN-269 车1 件1:A2A peers ride the SAME restart-to-apply line as MCP (one center pull, one
721
- // resolution, no hot swap). Absent `a2a` domain ⇒ the key is never written ⇒ the leg does not exist.
722
434
  if (effective?.a2a)
723
435
  config.a2aPeers = resolveA2aPeers(effective.a2a, logger);
724
436
  return skills;
725
437
  },
726
438
  applyCenterScenarios(t) {
727
439
  const { scenarioDeps, scenarios, scenarioDetails, builtinScenarioNames } = t;
728
- // Center-declared scenarios (composition-as-config): overlay over the built-ins, center wins — it MAY
729
- // shadow a built-in name (usability first), so shadows are logged loudly here for audit.
730
- // Restart-to-apply like skills/mcp (this runs once at boot; a refresh carrying scenarios only signals).
731
440
  if (effective?.scenarios) {
732
441
  const { overlay, shadows } = centerScenarios(scenarioDeps, effective.scenarios.scenarios, Object.keys(scenarios), logger);
733
442
  Object.assign(scenarios, overlay);
734
- // [C132]:可用性判据同源 —— 详情表与运行工厂读的是**同一只** scenarioDeps(repoClient 在场性)。
735
443
  Object.assign(scenarioDetails, centerScenarioDetails(effective.scenarios.scenarios, builtinScenarioNames, scenarioDeps));
736
444
  if (Object.keys(overlay).length > 0)
737
445
  logger.info("sema_registry_scenarios", { scenarios: Object.keys(overlay), ...(shadows.length > 0 ? { shadowsBuiltin: shadows } : {}) });
@@ -739,11 +447,6 @@ export async function createConfigCenterRuntime(ctx) {
739
447
  },
740
448
  initKeyResolver() {
741
449
  keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
742
- // PAIR-REVIEW F-6:Bearer-only 网关 × per-model key 组合死角的 boot 响亮告警——core 的 per-call auth 是
743
- // **整体替换**语义(1.345:per-call apiKey 在场即剥构造期 Authorization,x-api-key 成唯一凭据),而
744
- // key-resolver 只会产 { apiKey } 形;部署若以 ANTHROPIC_AUTH_TOKEN(Bearer)直连此类网关,则每个配了
745
- // per-model key 的模型必 401 且无从自诊。headers 形支持候 core/registry additive 席位(跨仓件);此前
746
- // 先把组合不可用讲清楚。只 warn 不拒:混合网关(两种头都收)是存在的,fail-open+可诊断。
747
450
  if (process.env.ANTHROPIC_AUTH_TOKEN && !process.env.ANTHROPIC_API_KEY && (Object.keys(config.modelApiKeys).length > 0 || Object.keys(config.modelApiKeyEnv).length > 0)) {
748
451
  logger.warn("per_model_key_bearer_gateway_combo", {
749
452
  perModelKeys: Object.keys(config.modelApiKeys).length + Object.keys(config.modelApiKeyEnv).length,
@@ -753,64 +456,23 @@ export async function createConfigCenterRuntime(ctx) {
753
456
  },
754
457
  startRefreshLoop(a) {
755
458
  const { runnerTierFrozen, pricing, limitSync, swapRunnerModels } = a;
756
- // Config refresh: re-pull with the ETag; teams hot-reload via the registry. A models/roles
757
- // change updates config but the live Runner keeps its startup set — logged so ops restart to apply.
758
- // 提示词装配协议 §9.5-1/-3: the loop goes through the ConfigProvider seam (never the raw HTTP fetch —
759
- // one resolver, no remote/local drift) and runs for the LOCAL provider too: config.d edits are picked
760
- // up by the same 60s poll (the local provider's etag = the store's raws hash, so an unchanged tree is
761
- // a cheap null and a corrupt↔fixed flip bumps the version). Local mode previously read config.d
762
- // exactly once at boot.
763
459
  if (configProvider) {
764
460
  const ccRef = config.configCenter;
765
- // Single-flight (codex F2): an async tick body under setInterval has no serialization — a slow read
766
- // (degraded disk/远端) outliving the 60s interval would let a LATER tick apply a newer candidate and
767
- // the stalled OLDER read then complete and roll the config (and ccEtag) back. Skip the tick while one
768
- // is in flight; the flag flips in `finally`, so a throw never wedges the loop shut.
769
461
  let refreshInFlight = false;
770
- // #322 追加件(codex R2-F3):拍的**结果**要能被手动触发口看见 —— `"failed"` = 这一拍的拉取/应用
771
- // 抛了(下面的 catch 吞掉异常保住循环,但吞掉的事实必须传出去:operator 按下刷新时,「中心超时」
772
- // 与「已刷新」必须可分辨,否则这个口的「诚实」二字就是假的)。
773
462
  const refreshTick = async (prefetched) => {
774
463
  if (refreshInFlight)
775
464
  return "ok";
776
465
  refreshInFlight = true;
777
466
  try {
778
- // codex-3/4 F1:欠账的 catalog 过渡在**每个真拍**开头无条件重试——与 fetch 成败/候选好坏解耦
779
- // (304、fetch 抛错、候选被整拒的拍此前全都跳过重试=盘错恢复后过渡仍卡死)。次序安全:本拍
780
- // 若随后采用了更新候选,adopt 自会覆盖/清空 pending(先重试旧账、后采新目标,永不倒退)。
781
467
  await retryPendingCatalog("refresh-retry");
782
- // codex R18: the stuck-deferred state must not vanish behind the advanced etag — re-warn + count on
783
- // EVERY tick (incl. 304s and fetch-error ticks) while it persists; /health carries the same state.
784
- // The RECOVERY (persist retry + promotion) moved BELOW the fetch (codex R29): promoting before
785
- // adjudicating the current source generation could publish a SUPERSEDED candidate N as the
786
- // boot-visible LKG while N+1 exists — a restart in that window resurrects withdrawn config.
787
468
  if (planeDeferredNoHandoff) {
788
469
  metrics.inc("models_tiers_plane_deferred_stuck_total");
789
470
  logger.warn("models_tiers_deferred_no_handoff", { version: planeDeferredNoHandoff.version, since: planeDeferredNoHandoff.since, note: "candidate still retained without a next-boot handoff — mount restart-surviving LKG storage + CONFIG_LKG_DURABLE=true, or restart manually after ensuring the candidate is re-pulled at boot" });
790
471
  }
791
- // F3(LKG 复审):LKG-boot 的 deferred 到货把 unconditional 结果直接喂进来——丢弃它再按 LKG 的
792
- // etag 条件拉,会在「center 重建且 etag 值撞旧」的场景永远 304=陈旧配置钉死;消费原始结果则
793
- // 应用后 ccEtag=真实新值,陈旧面消除(顺带省一次拉取)。
794
472
  const r = prefetched !== undefined ? prefetched : await configProvider.fetchEffective(ccEtag);
795
- // codex R23/R29 recovery lane: a 304 (r === null) CONFIRMS the retained candidate's generation is
796
- // still the source's current one (its etag advanced when it was retained) — only then is it safe to
797
- // retry persistence and promote the withheld restart signal. A fetch that returned a NEWER candidate
798
- // takes the normal branches below (which replace/supersede the retained state); a fetch that THREW
799
- // never reaches here (freshness unknown ⇒ no promotion this tick). Retry only where promotion is
800
- // possible (durable-declared lane; R37 adds the non-durable skills-debt lane, whose retry doubles as
801
- // the repair proof) — elsewhere a per-tick write could never promote and just burns IO.
802
473
  if (r === null && planeDeferredNoHandoff) {
803
- // Retry where promotion is possible: the durable-declared lane (full handoff), and — codex R37 —
804
- // the NON-durable skills-debt lane, where the retry's body prewarm+verify is the repair PROOF that
805
- // gates the withheld skills restart (see lastPrewarmBodiesVerified).
806
474
  const skillsDebt = (planeDeferredNoHandoff.blocked ?? []).includes("skills");
807
475
  const promoted = lkgSurvivesRestart || skillsDebt ? await persistLkgDurable(planeDeferredNoHandoff.candidate, planeDeferredNoHandoff.candidateEtag) : false;
808
- // codex R37 proof-gated promotion (non-durable lane): every enabled body now hash-verifies on disk
809
- // ⇒ the center + body endpoint are BOTH live, a restarted process re-pulls successfully — publish
810
- // the skills restart exactly now, never on manifest-success alone (restart-storm breaker: a boot
811
- // that fails the body again lacks the proof and returns to visible-debt, not to signaling).
812
- // models-tiers never rides this arm (R16: a FORCED plane restart requires a durable handoff), so a
813
- // retained candidate that ALSO defers the plane keeps everything withheld until durability exists.
814
476
  const skillsProofPromotion = !promoted && !lkgSurvivesRestart && skillsDebt && lastPrewarmBodiesVerified && planeDeferredNoHandoff.planeDeferred !== true;
815
477
  if (skillsProofPromotion) {
816
478
  pendingRestart = { restartRequired: true, reasons: ["skills"], version: planeDeferredNoHandoff.version, since: Date.now() };
@@ -818,10 +480,6 @@ export async function createConfigCenterRuntime(ctx) {
818
480
  planeDeferredNoHandoff = undefined;
819
481
  }
820
482
  else if (promoted) {
821
- // models-tiers joins only when the PLANE was deferred (codex R27: a skill-only retained candidate
822
- // must not manufacture a plane reason on promotion). codex R30: an EMPTY promotion (boot
823
- // publication retry — nothing was withheld, everything already applied) publishes NO restart
824
- // signal; it just completes the LKG and clears the state.
825
483
  const promotedReasons = [...new Set([...(planeDeferredNoHandoff.blocked ?? []), ...(planeDeferredNoHandoff.planeDeferred ? ["models-tiers"] : [])])];
826
484
  if (promotedReasons.length > 0) {
827
485
  pendingRestart = { restartRequired: true, reasons: promotedReasons, version: planeDeferredNoHandoff.version, since: Date.now() };
@@ -831,21 +489,8 @@ export async function createConfigCenterRuntime(ctx) {
831
489
  }
832
490
  }
833
491
  if (r) {
834
- // #322 世代账:**拉到即算 target**(候选是否被采纳是下面的事)。整拒的拍只推进 target ⇒
835
- // target≠applied 的持续时长在 /health 上可见 —— 「持续卡在旧代」的副本从结构上可被摘除。
836
- const tickGen = generationOf(r.effective.version); // #325:收货序数(本拍全链同一个)
492
+ const tickGen = generationOf(r.effective.version);
837
493
  applyLedger.seenTarget(tickGen, r.effective.version);
838
- // §9.5-5 candidate gate: ANY invalid domain rejects the WHOLE candidate — the live config
839
- // keeps serving the LKG (`latestEffective`) instead of that domain falling to schema-default/
840
- // empty. ccEtag is deliberately NOT advanced: the bad tree stays visible (re-judged each poll),
841
- // and fixing the file bumps the version so the next poll lands. The dedupe key is the SORTED
842
- // de-duplicated domain set (codex F3: the collector's ordering isn't guaranteed, a raw join
843
- // would re-warn every poll), and the first warn carries the full per-domain error detail —
844
- // that's the actionable diagnosis, the key only mutes repeats. The prompts-domain artifact
845
- // validation (core S1) hooks into this same gate when it ships.
846
- // F6(二轮复审):prompts 面判形进候选门——坏 pack(重复 id/空数组/坏信封)若只在 boot 验,
847
- // refresh 会带着它推进 etag+发 restart 信号,重启后 boot 拒之回内置=好包 A 被坏包 B 顶掉。
848
- // 这里与 domainErrors 同门:坏形=整拒候选(etag 不进,LKG/现用包 A 继续服务,持续 warn 到修复)。
849
494
  const promptsRaw = r.effective.prompts;
850
495
  const promptsGate = promptsRaw !== undefined ? validatePromptsDomain(promptsRaw) : { ok: true };
851
496
  const badDomains = [
@@ -867,35 +512,14 @@ export async function createConfigCenterRuntime(ctx) {
867
512
  note: "candidate rejected whole (§9.5-5) — live config keeps the last-known-good; fix the named files to unblock",
868
513
  });
869
514
  }
870
- return "ok"; // 拍本身跑完了(候选被判拒是**结论**,不是失败)
515
+ return "ok";
871
516
  }
872
517
  lastRejectedCandidate = undefined;
873
- // ccEtag advances at the END of each branch (codex lens-1 HIGH): advancing before apply meant
874
- // an applyEffective throw mid-way left a MIXED-generation live config AND a cached etag — the
875
- // next poll 304'd, so the failed candidate was never retried. Now a throw keeps the old etag,
876
- // the next poll re-fetches the same candidate, and the (idempotent) re-apply self-heals.
877
518
  if (ccRef?.dryRun) {
878
- logEffectiveDiff(config, r.effective, logger); // compare-only: surface ongoing center changes, apply nothing
519
+ logEffectiveDiff(config, r.effective, logger);
879
520
  ccEtag = r.etag;
880
521
  }
881
522
  else {
882
- // refresh: teams + runtime governance + MODELS/ROLES/ROSTER now hot. teamsOnly skips
883
- // only the boot-only runtime GATES (rate/cost/approval). applyEffective mutates config.models/roles
884
- // IN PLACE (the Runner's live ref) — but `pricing` (separate object the Runner also reads live) and
885
- // `keyResolver` (closes over modelApiKeyEnv) are ours to refresh.
886
- // codex R10: hot-apply of the MODEL PLANE holds ONLY when the Runner shares our map (tier-less at
887
- // construction). A tier-frozen Runner keeps a private expanded copy — hot-applying a changed plane
888
- // would split admission from execution (same-key retarget silently routes/bills the stale Model
889
- // object through the restart window). Defer the plane; the models-tiers slice below signals the
890
- // orchestrator; the restarted process applies it at boot BEFORE Runner construction.
891
- // codex R12: defer also when the CANDIDATE activates tiers on a tier-less-booted Runner — it never
892
- // re-runs expandTiers, so hot-applying the arriving tier table would let tier words pass the
893
- // expanded admission gate while core throws "Unknown model ref" until restart. Hot-apply is safe
894
- // only when BOTH generations are tier-less.
895
- // core 5.49 swapModels 接缝在场 ⇒ defer 判据整条退役:plane 变更(含 tier-frozen /
896
- // 候选激活 tiers 两旧 defer 子臂,也含从前的 tier-less 就地热臂)一律经 swapPlane 在
897
- // commit 前换进全部 boot Runner——统一走 swap 让「Runner 私有目录 vs 共享表」两种姿势
898
- // 收敛成一种(swap 后 Runner 恒在私有目录上,后续变更继续 swap,无姿势追踪状态)。
899
523
  const planeChanged = modelPlaneChanged(appliedPlaneEff, r.effective);
900
524
  const planeDeferred = swapRunnerModels === undefined && (runnerTierFrozen || planeHasActiveTiers(r.effective)) && planeChanged;
901
525
  let applyReport;
@@ -910,25 +534,11 @@ export async function createConfigCenterRuntime(ctx) {
910
534
  ...(planeDeferred ? { deferModelPlane: true } : {}),
911
535
  ...(swapRunnerModels !== undefined && planeChanged ? { swapPlane: swapRunnerModels } : {}),
912
536
  });
913
- // [2283]③:CAS 拒绝(更旧世代)⇒ 本拍整体跳过——prompts 采用/pricing/keyResolver/restart
914
- // 对账/etag 推进/LKG 落盘都不得从旁路半应用同一个被拒世代(拒绝 warn 已在 applyEffective 留痕)。
915
537
  if (!committed)
916
- return "ok"; // CAS 拒(更旧世代)——同上,拍成功地得出了「不采纳」
917
- // 🔴 #303 codex R1-F2(真 finding,红先钉在 test/apply-effective-stage-commit.test.ts):
918
- // commit 的**派生两件**必须紧贴 commit,零 await —— 修前它们在下面 `await adoptCenterPrompts`
919
- // 之后,而 commit 已经把 `config.models` 就地换代(三个消费面实时读)、把
920
- // `modelApiKeyEnv/modelApiKeys` 整表重赋值,老 `keyResolver` 却还闭包着**上一世代**的表。
921
- // 那段 await 里同名模型换端点+换 key 的下发 = 旧密钥发到新端点(401,或把上一家的凭据
922
- // 递给下一家);单纯换 key = 一窗瞬时 401。现在目录与密钥表成对换代。
923
- // (plane 被 defer 时 commit 压根没动模型面 ⇒ 这里按同一张旧表重建,内容不变,与修前同。)
924
- mutateInPlace(pricing, buildPricing(config.models)); // hot: cost/model changes; Runner reads this.deps.pricing live
925
- keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys); // hot: per-model key add/remove/change (env-ref + sealed)
926
- limitSync?.sync(); // #322 批1:限额座与 config 成对换代(同一条「派生件紧贴 commit」纪律)
927
- // #324:撤销台账拍(commit 接受即喂,与上面三件派生同族)。删除向由此**即时**生效
928
- // (core 每次 MCP dispatch 前探 isRevoked)——design/338 要消的「撤销等 restart」窗。
929
- // 放在 fullyApplied 判之前:mcp 域不在 limits 组,某组被整拒不构成扣住撤销的理由
930
- // (撤销是收紧向,宁早不晚)。baseline 未铸(boot 超时形)时这一喂只推 seen 集,
931
- // 恢复判据与 restart 豁免都被 hasBaseline 门关死(codex F2)。
538
+ return "ok";
539
+ mutateInPlace(pricing, buildPricing(config.models));
540
+ keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
541
+ limitSync?.sync();
932
542
  mcpRevocations.applyEnabledEntries(enabledMcpEntries(r.effective.mcp));
933
543
  applyLedger.applied(tickGen, r.effective.version);
934
544
  if (planeDeferred) {
@@ -936,31 +546,11 @@ export async function createConfigCenterRuntime(ctx) {
936
546
  }
937
547
  else {
938
548
  appliedPlaneEff = r.effective;
939
- planeDeferredNoHandoff = undefined; // plane landed — the stuck state (if any) is resolved
549
+ planeDeferredNoHandoff = undefined;
940
550
  }
941
- await adoptCenterPrompts(r.effective, "refresh"); // 热采用(core 1.316 声明轴:新任务边界自动重 pin,见 ref 注释)
942
- // codex R13: readiness may open ONLY off an APPLIED plane — a deferred candidate's roster is not
943
- // installed (config.models still the old/placeholder generation), so opening the billable gate on
944
- // it would route/bill the wrong model until restart. Deferred ⇒ gate stays as-is; the restarted
945
- // process applies the candidate pre-Runner and opens it at boot.
551
+ await adoptCenterPrompts(r.effective, "refresh");
946
552
  if (!planeDeferred)
947
- markRosterLanded(r.effective); // boot pull 失败/未发布时,refresh 落 roster 同样开门
948
- // (pricing/keyResolver 的重建已上提到 commit 紧邻处 —— 见上方 #303 R1-F2 注)
949
- // skills/mcp/scenarios/runtime-gates are baked into the live process at boot (buildScenarios / boot
950
- // middleware) — a refresh carrying a DIFFERENT value can't hot-apply, only a restart re-reads them.
951
- // We compare against the BOOT snapshot (`effective`), NOT presence: an orchestrator auto-restarts on
952
- // a non-empty `reasons` WITHOUT a restart loop (same diff every 60s is idempotent, not a fresh trigger).
953
- // Sticky `since`: keep the first-detected timestamp while `reasons` is unchanged; reset it when the set
954
- // changes; CLEAR the signal when a later pull reverts to boot (reasons empty) — restart no longer needed.
955
- // F2(LKG 复审):落盘 **await 且先于** restart 信号可见——orchestrator 见信号即杀进程,
956
- // fire-and-forget 的 rename 可能没完成 → 下一进程读旧 LKG → 同差异重现 = 本功能要消的环。
957
- // 顺序=先持久化(含 skill 正文预热,F1)再发信号;持久化失败=warn+照发(不发=配置永不生效;
958
- // 「盘坏+中心挂」双故障下环重现,接受并点名)。
959
- // 🔴 codex R2-F1(真 finding,红先):**没有完整应用的候选不许成为 LKG,也不许推进 etag**。
960
- // applyEffective 对「其余面提交了、某组被整拒」的候选返回 true;修前这里照旧落盘 + 推进
961
- // latestEffective/ccEtag ⇒ N-1 的好 LKG 被 N 盖掉,重启且中心不可达时 N 的那一组再次被拒 ⇒
962
- // 退到 env 基线 = **N-1 的限额保护丢了**。世代账已记下拒因(applied 不前进),这里只需早退:
963
- // 盘上的 LKG 与 etag 都停在上一个**完整**世代,下一拍重新判同一棵树(修好即自愈)。
553
+ markRosterLanded(r.effective);
964
554
  const fullyApplied = applyReport?.fullyApplied !== false;
965
555
  if (!fullyApplied) {
966
556
  logger.warn("config_candidate_partially_applied", {
@@ -968,79 +558,36 @@ export async function createConfigCenterRuntime(ctx) {
968
558
  rejected: applyReport?.limits.rejected?.keys ?? [],
969
559
  note: "a group of this candidate was refused — the live config keeps serving the prior values for it; the LKG and the ETag stay on the last FULLY applied generation (this tree is re-judged next tick)",
970
560
  });
971
- return "ok"; // 同上:拍跑完了,结论是「这一代没能完整应用」(世代账 + 本行 warn 是它的读面)
561
+ return "ok";
972
562
  }
973
563
  const lkgPersisted = await persistLkgDurable(r.effective, r.etag);
974
564
  const reasons = restartReasons(effective, r.effective, restartCtx());
975
- // swapModels 接缝在场 ⇒ models-tiers 指纹的 boot 基线差异不再是重启理由:能走到这一行的
976
- // 候选,其模型面要么本拍已 swap 进全部 Runner、要么与已应用面相同(swap 拒绝在 committed
977
- // === false 早退了)——留着这个 reason 就是指纹注释里点名的「applied yet signalled」不一致,
978
- // orchestrator 会为一个已生效的变更滚动重启整片 fleet。degrade-route 面**不**跟着退役:
979
- // 它指纹的是 createBrain 在 boot 构造期捕获目标 Model 的降级壳(restart-signal.ts:180),
980
- // swapModels 只换 Runner 的目录代际、不重组 server 侧 brain 壳,该臂重启理由仍然诚实。
981
565
  if (swapRunnerModels !== undefined) {
982
566
  const mi = reasons.indexOf("models-tiers");
983
567
  if (mi >= 0)
984
568
  reasons.splice(mi, 1);
985
569
  }
986
- // codex R35: the sticky boot skill debt rides EVERY tick's reasons — change-detection vs boot is
987
- // blind to it (the stale table IS the boot baseline). Under a no-handoff deferral it folds into
988
- // blocked (promoted later); otherwise it keeps pendingRestart alive until the restart happens.
989
- // codex R38: change-DETECTED skills (manifest actually changed) is distinguished from the sticky
990
- // rider — the former keeps the pre-existing documented posture (rides regardless; the restart
991
- // pulls the NEW generation live), the latter is proof-gated below on the non-durable lane.
992
570
  const skillsChangeDetected = reasons.includes("skills");
993
571
  if (bootSkillStale && !skillsChangeDetected)
994
572
  reasons.push("skills");
995
- // codex R15/R16/R19: a DEFERRED plane is adjudicated by HANDOFF STATUS alone — independent of
996
- // whether change-detection already produced the reason (R19: an already-present models-tiers with
997
- // no handoff sailed through = the exact storm R16 closed). With a confirmed next-boot handoff the
998
- // reason is ensured present (R15: env-baseline blind spot) and the stuck state clears; without one
999
- // the reason is SUPPRESSED (a restart the next process cannot satisfy — over-budget boot pull + no
1000
- // LKG — is a rolling-restart storm) and the persistent stuck state + per-tick warn carry the
1001
- // observability instead. Non-plane reasons (skills/mcp/…) keep riding either way.
1002
573
  if (planeDeferred) {
1003
574
  if (lkgPersisted) {
1004
575
  if (!reasons.includes("models-tiers"))
1005
576
  reasons.push("models-tiers");
1006
- planeDeferredNoHandoff = undefined; // handoff exists — the restart signal carries observability
577
+ planeDeferredNoHandoff = undefined;
1007
578
  }
1008
579
  else {
1009
- // codex R22: suppress the candidate's ENTIRE restart signal, not just models-tiers — a
1010
- // co-occurring skills/mcp reason would still restart the worker, and with no handoff the
1011
- // replacement can't land ANY of this candidate (same over-budget pull) = the same storm.
1012
- // The blocked reasons stay observable (state + warn) and promote once a handoff exists.
1013
580
  const blocked = reasons.splice(0, reasons.length);
1014
581
  planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), ...(blocked.length > 0 ? { blocked } : {}), planeDeferred: true, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
1015
582
  logger.warn("models_tiers_deferred_no_handoff", { version: r.effective.version, blockedReasons: blocked, note: "plane deferred but no NEXT-BOOT handoff (LKG disabled/unwritable, or storage not declared restart-surviving — set CONFIG_LKG_DURABLE=true) — the WHOLE restart signal is withheld (it would loop); the old generation keeps serving" });
1016
583
  }
1017
584
  }
1018
585
  else if (lkgSurvivesRestart && !lkgPersisted) {
1019
- // codex R27/R31: retention is NOT conditional on restart reasons — a hot-only candidate
1020
- // (autonomy/prompts/teams/tier-less models) with a failed publication would otherwise advance
1021
- // the etag and never retry (304s), and a restart during a center outage would roll back to the
1022
- // older LKG (weaker governance). blocked may be [] = publication-only recovery (R30 promotion
1023
- // publishes no signal for an empty set).
1024
- // codex R27: a DURABLE-DECLARED deployment whose candidate publication is incomplete (skill-body
1025
- // verify failed → LKG not replaced) must not signal restart off that candidate either — the next
1026
- // boot would land the OLD LKG and re-signal while the center stays unavailable. Retain + withhold
1027
- // + per-tick retry-promote, same machinery as the deferred plane (no models-tiers added on
1028
- // promotion — the plane was NOT deferred here). Non-declared deployments keep the pre-existing
1029
- // documented posture (restart converges off a live boot pull when the center is healthy).
1030
586
  const blocked = reasons.splice(0, reasons.length);
1031
587
  planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
1032
588
  logger.warn("restart_candidate_publication_incomplete", { version: r.effective.version, blockedReasons: blocked, note: "durable LKG declared but the candidate's publication is incomplete (skill bodies unverified) — restart signal withheld; retried every tick until the cache completes" });
1033
589
  }
1034
590
  else if (!planeDeferred && !lkgSurvivesRestart && bootSkillStale && !skillsChangeDetected && !lastPrewarmBodiesVerified) {
1035
- // codex R38: the NON-durable lane's sticky rider must clear the SAME proof gate as the boot/304
1036
- // arms (R37) — a same-generation 200 (center omits/rotates ETags or ignores conditionals) lands
1037
- // here with reasons=[skills] purely from the sticky debt, and publishing it unproven re-opens
1038
- // the restart storm R37 closed (every replacement fails the same body, 200s again, signals
1039
- // again). persistLkgDurable just ran for THIS candidate, so the proof flag is current: unproven
1040
- // ⇒ strip the rider from the publishable set and retain it as visible debt (the same
1041
- // planeDeferredNoHandoff machinery the 304 recovery lane re-verifies and proof-promotes).
1042
- // Proven or durable-handoff ⇒ the rider rides as before. Change-DETECTED skills is exempt
1043
- // (pre-existing posture — the restart pulls the NEW generation, a different repair claim).
1044
591
  const i = reasons.indexOf("skills");
1045
592
  if (i >= 0) {
1046
593
  reasons.splice(i, 1);
@@ -1059,17 +606,10 @@ export async function createConfigCenterRuntime(ctx) {
1059
606
  }
1060
607
  logger.info("sema_registry_refreshed", {
1061
608
  version: r.effective.version,
1062
- // hot-applied: teams + runtime governance (autonomy/commandPolicy) + MODELS/ROLES/ROSTER (+pricing
1063
- // +per-model keys). Still restart-to-apply: skills/mcp/scenarios + runtime GATES (rate/cost/approval).
1064
609
  note: "teams + runtime governance + models/roles/roster hot-applied; skills/mcp/runtime-gates/scenarios are restart-to-apply",
1065
- // Structured signal: orchestrator consumes the SAME object via GET /health.restart.
1066
610
  ...(pendingRestart ? { restartRequired: true, restartReasons: pendingRestart.reasons } : {}),
1067
611
  });
1068
- // BOTH the LKG and the etag advance ONLY after the whole apply/pricing/keyResolver/restart
1069
- // chain succeeded (codex lens-3): if any step above throws, latestEffective keeps pointing at
1070
- // the last FULLY-applied candidate and the etag stays put, so the next poll re-fetches and
1071
- // re-applies the same candidate (idempotent self-heal) — the LKG never names a half-applied one.
1072
- latestEffective = r.effective; // LKG advance (§9.5-2): candidate validated + fully applied
612
+ latestEffective = r.effective;
1073
613
  ccEtag = r.etag;
1074
614
  }
1075
615
  }
@@ -1083,15 +623,10 @@ export async function createConfigCenterRuntime(ctx) {
1083
623
  }
1084
624
  return "ok";
1085
625
  };
1086
- // #322 追加件(clay 当场裁定):**手动触发口**(POST /v1/admin/config/refresh)与 cadence 走**同一只**
1087
- // 拍。这里加的是 join 语义而不是第二条拉取路径:`refreshTick` 自己的 `refreshInFlight` 是「在飞就跳过」
1088
- // (对定时器是对的:下一拍 60s 后就来),而对一次**人发起的**触发,跳过等于骗人(回执说 triggered、
1089
- // 实际什么都没干)。所以 cadence 与手动口都经 `runTick`:在飞 ⇒ 返回**同一个 promise**(join),
1090
- // 空闲 ⇒ 起一拍。内层守卫保留(纵深:任何忘了走 runTick 的新调用点仍不会并发双拍)。
1091
626
  let inFlight;
1092
627
  const runTick = (prefetched) => {
1093
628
  if (inFlight)
1094
- return inFlight; // join:全部 joiner 拿到**同一拍的同一个结果**(成功与失败都一样)
629
+ return inFlight;
1095
630
  const p = refreshTick(prefetched).finally(() => {
1096
631
  inFlight = undefined;
1097
632
  });
@@ -1101,21 +636,9 @@ export async function createConfigCenterRuntime(ctx) {
1101
636
  manualRefresh = runTick;
1102
637
  ccTimer = setInterval(() => void runTick(), 60_000);
1103
638
  ccTimer.unref?.();
1104
- // Boot-deferred continuation(二轮复审 F5 改形):到货结果按「迟到的 boot」处理,而不是转普通 tick——
1105
- // 普通 tick 的 restartReasons(undefined, r) 会把 prompts/skills 面全判为差异 → restart → 中心持续慢时
1106
- // 下一进程又 deferred → 无限重启环。迟到 boot:①候选门同 F6(坏形=拒,cadence 重判);②热面
1107
- // teamsOnly apply(boot-only gates/skills/mcp 已按 env 建,迟到不可应用——诚实);③prompts adopt
1108
- // (进程刚起、会话面≈空,失真面⊆重启路径,且 adopt 后 prompts 不再贡献 restart 理由=prompts 无环);
1109
- // ④一次性诚实 restart 信号=剩余 restart-to-apply 面(skills/mcp/scenarios/gates)非空才发;⑤基线
1110
- // `effective`/LKG/etag 落位 → 后续 cadence 与到货值比较,不重复触发。⚠️ 残余环面(诚实记账):center
1111
- // 发布了 skills/mcp 且中心**持续**慢于 boot 预算时,每个新进程仍会为真缺失的 skills 面发一次 restart
1112
- // ——完整解=LKG 落盘(boot 直读上个进程的干净候选),已报 clay 裁量。
1113
639
  const deferredBootApply = async (r) => {
1114
640
  if (!r)
1115
641
  return;
1116
- // F6(LKG 复审三轮):与 cadence tick 同一 single-flight 车道——deferred 的 skill 预热可能拖过 60s,
1117
- // 期间 cadence 落了更新的候选 B,A 迟到完成再覆盖盘/信号=回滚。inFlight 时直接让行(丢弃 deferred
1118
- // 结果无损:cadence 已经/即将拉到更新的),持有 flag 至信号发布后。
1119
642
  if (refreshInFlight)
1120
643
  return;
1121
644
  refreshInFlight = true;
@@ -1127,8 +650,8 @@ export async function createConfigCenterRuntime(ctx) {
1127
650
  }
1128
651
  };
1129
652
  const deferredBootApplyInner = async (r) => {
1130
- const lateGen = generationOf(r.effective.version); // #325:收货序数(迟到 boot 拍全链同一个)
1131
- applyLedger.seenTarget(lateGen, r.effective.version); // #322:迟到 boot 也是「见过这一代」
653
+ const lateGen = generationOf(r.effective.version);
654
+ applyLedger.seenTarget(lateGen, r.effective.version);
1132
655
  const promptsRaw = r.effective.prompts;
1133
656
  const promptsGate = promptsRaw !== undefined ? validatePromptsDomain(promptsRaw) : { ok: true };
1134
657
  if ((r.domainErrors ?? []).length > 0 || !promptsGate.ok) {
@@ -1144,13 +667,6 @@ export async function createConfigCenterRuntime(ctx) {
1144
667
  ccEtag = r.etag;
1145
668
  return;
1146
669
  }
1147
- // codex R10 (deferred-boot twin of the refresh-lane guard): the Runner was built from the ENV plane before
1148
- // this late arrival — if it froze a tier-expanded copy, the arriving center plane must not hot-apply
1149
- // (admission/Runner split). Tier-less env boot (the common deferred-boot shape) keeps true hot-apply.
1150
- // core 5.49 swapModels 在场 ⇒ 该守卫与正常 tick 腿(:936)同判据退役:候选面经 swapPlane 在
1151
- // commit 前原子换进全部 boot Runner,admission/Runner split 不复存在。合并窗重扫 high(V2
1152
- // 同能力多腿修一漏一现行犯):换装批只接了 tick 腿,这条迟到 boot 腿漏接 ⇒ 生产恒接
1153
- // swapRunnerModels 的部署在 env 起服+首拉过窗时,plane 仍被整面 defer + 误签 restart。
1154
670
  const planeChangedLate = modelPlaneChanged(appliedPlaneEff, r.effective);
1155
671
  const planeDeferredLate = swapRunnerModels === undefined && (runnerTierFrozen || planeHasActiveTiers(r.effective)) && planeChangedLate;
1156
672
  let applyReportLate;
@@ -1165,13 +681,11 @@ export async function createConfigCenterRuntime(ctx) {
1165
681
  ...(planeDeferredLate ? { deferModelPlane: true } : {}),
1166
682
  ...(swapRunnerModels !== undefined && planeChangedLate ? { swapPlane: swapRunnerModels } : {}),
1167
683
  });
1168
- // [2283]③(refresh 腿同款):CAS 拒绝 ⇒ 迟到 boot 拍整体跳过,旁路消费与 etag/LKG 都不动。
1169
684
  if (!committedLate)
1170
685
  return;
1171
- // #303 R1-F2 的 late-boot 双胞胎(同一形,只修一条=另一条继续漏):派生两件紧贴 commit,零 await。
1172
686
  mutateInPlace(pricing, buildPricing(config.models));
1173
687
  keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1174
- limitSync?.sync(); // #322 批1(late-boot 双胞胎:只修一条腿 = 另一条继续漏)
688
+ limitSync?.sync();
1175
689
  applyLedger.applied(lateGen, r.effective.version);
1176
690
  if (planeDeferredLate)
1177
691
  logger.warn("models_tiers_plane_deferred", { version: r.effective.version, note: "tier-frozen Runner (env tiers): the late-boot center model plane is NOT hot-applied — restart applies it" });
@@ -1181,9 +695,7 @@ export async function createConfigCenterRuntime(ctx) {
1181
695
  }
1182
696
  await adoptCenterPrompts(r.effective, "boot-deferred");
1183
697
  if (!planeDeferredLate)
1184
- markRosterLanded(r.effective); // codex R13: same guard as the refresh lane — never open readiness off an unapplied plane
1185
- // (pricing/keyResolver 的重建已上提到 commit 紧邻处 —— 见上方 #303 R1-F2 注)
1186
- // codex R2-F1 的 late-boot 双胞胎(只修一条腿 = 另一条继续漏)。
698
+ markRosterLanded(r.effective);
1187
699
  const fullyAppliedLate = applyReportLate?.fullyApplied !== false;
1188
700
  if (!fullyAppliedLate) {
1189
701
  logger.warn("config_candidate_partially_applied", {
@@ -1193,28 +705,16 @@ export async function createConfigCenterRuntime(ctx) {
1193
705
  });
1194
706
  return;
1195
707
  }
1196
- effective = r.effective; // restart 比较基线=到货值(cadence 不再重复触发)
708
+ effective = r.effective;
1197
709
  latestEffective = r.effective;
1198
710
  ccEtag = r.etag;
1199
- const lkgPersistedLate = await persistLkgDurable(r.effective, r.etag); // F2 parity:信号可见前落盘+skill 正文预热
1200
- const reasons = restartReasons(undefined, r.effective, restartCtx()); // (prompts 不在 restart slices——热采用已在上方 adopt)
1201
- // swapModels 接缝在场 ⇒ models-tiers 摘除(:1007 正常 tick 腿同形同理由):能走到这一行的候选,
1202
- // 模型面已本拍 swap 进全部 Runner(swap 拒绝在 committedLate === false 早退)——env 基线对候选的
1203
- // 指纹差异不再是重启理由;degrade-route 不跟(brain 壳 boot 期捕获,swap 不重组)。
711
+ const lkgPersistedLate = await persistLkgDurable(r.effective, r.etag);
712
+ const reasons = restartReasons(undefined, r.effective, restartCtx());
1204
713
  if (swapRunnerModels !== undefined) {
1205
714
  const miLate = reasons.indexOf("models-tiers");
1206
715
  if (miLate >= 0)
1207
716
  reasons.splice(miLate, 1);
1208
717
  }
1209
- // codex R15 (late-boot twin): tiered env boot + tier-less late candidate ⇒ deferral fires but BOTH slice
1210
- // fingerprints reduce to null (undefined baseline, tier-less candidate) — no models-tiers reason, plane
1211
- // deferred forever. Deferral is the ground truth; force the reason so the orchestrator restarts and the
1212
- // next process lands the candidate at boot. codex R16: forced ONLY with a confirmed LKG handoff — without
1213
- // it the restarted process re-enters this exact lane (env plane, late candidate) = restart storm; keep
1214
- // serving the old generation and warn instead.
1215
- // codex R19 (late-boot twin): handoff status decides, regardless of whether change-detection already
1216
- // produced the reason — an active-tier candidate over an env boot deterministically carries models-tiers
1217
- // from restartReasons(undefined, candidate), and letting it through with no handoff is the R16 storm.
1218
718
  if (planeDeferredLate) {
1219
719
  if (lkgPersistedLate) {
1220
720
  if (!reasons.includes("models-tiers"))
@@ -1222,16 +722,12 @@ export async function createConfigCenterRuntime(ctx) {
1222
722
  planeDeferredNoHandoff = undefined;
1223
723
  }
1224
724
  else {
1225
- // codex R22 (late-boot twin): withhold the WHOLE signal — co-occurring skills/mcp reasons would
1226
- // restart into the same over-budget/no-LKG lane and loop.
1227
725
  const blocked = reasons.splice(0, reasons.length);
1228
726
  planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), ...(blocked.length > 0 ? { blocked } : {}), planeDeferred: true, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
1229
727
  logger.warn("models_tiers_deferred_no_handoff", { version: r.effective.version, blockedReasons: blocked, note: "late-boot plane deferred but no NEXT-BOOT handoff (see CONFIG_LKG_DURABLE) — the WHOLE restart signal is withheld (it would loop); old generation keeps serving" });
1230
728
  }
1231
729
  }
1232
730
  else if (planeDeferredLate === false && lkgSurvivesRestart && !lkgPersistedLate) {
1233
- // codex R31 (late-boot twin): retain failed publications even with zero restart reasons.
1234
- // codex R27 (late-boot twin): durable-declared + incomplete publication ⇒ retain + withhold + retry.
1235
731
  const blocked = reasons.splice(0, reasons.length);
1236
732
  planeDeferredNoHandoff = { version: r.effective.version, since: planeDeferredNoHandoff?.since ?? Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
1237
733
  logger.warn("restart_candidate_publication_incomplete", { version: r.effective.version, blockedReasons: blocked, note: "late-boot candidate publication incomplete on a durable-declared deployment — restart signal withheld; retried every tick" });
@@ -1240,15 +736,11 @@ export async function createConfigCenterRuntime(ctx) {
1240
736
  pendingRestart = { restartRequired: true, reasons, version: r.effective.version, since: Date.now() };
1241
737
  logger.info("config_loaded_deferred", { source: configProvider.kind, version: r.effective.version, models: (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length, ...(reasons.length > 0 ? { restartRequired: true, restartReasons: reasons } : {}) });
1242
738
  };
1243
- // LKG 起服的进程有完整 boot 基线 → 到货走普通 tick(候选门+hot+对 LKG 的 restartReasons,通常
1244
- // 零差异=无信号无环);env 起服(无 LKG)才走「迟到 boot」路径。
1245
- // 迟到 boot 的续接也走 runTick(同一只拍;手动口若正好在飞,这条会 join 它而不是并发跑第二拍)。
1246
739
  if (bootConfigPending)
1247
740
  void bootConfigPending.then((r) => void (lkgBooted ? runTick(r) : deferredBootApply(r)), () => { });
1248
741
  }
1249
742
  },
1250
743
  stopRefreshLoop() {
1251
- // #131-2:hardShutdown 收尾链——停机中途不再热应用配置(幂等;env 部署没起环时是 no-op)。
1252
744
  if (ccTimer !== undefined) {
1253
745
  clearInterval(ccTimer);
1254
746
  ccTimer = undefined;