@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,16 +1,3 @@
1
- /**
2
- * design/158 A10:composition root 分段 —— workflow 编排面(run store / notify 门 / completion inbox /
3
- * fleet 总线 / 可 steer 句柄注册表)。
4
- *
5
- * 纯搬运:函数体逐字来自 `main.ts`(原 1315-1441 行),缩进不变;新增的只有 import 与包壳。
6
- *
7
- * ⚠️ **位置即契约**:
8
- * 1. `fleetBus` 必须**先于** `workflowRunStore` 构造 —— `JournalingWorkflowRunStore` 要把 workflow 行
9
- * 发布到它上面(原文逐字注释即此)。
10
- * 2. `baseWorkflowRunStore` → `workflowNotifyJournal` → `workflowCompletionInbox` → `workflowNotifyGate`
11
- * → `workflowRunStore`(包装体)的顺序是数据依赖链,不可重排。
12
- * 3. `getRunStore()` 是晚绑取值(runStore 在本段之后构造),原文靠同作用域前向引用。
13
- */
14
1
  import { FileWorkflowRunStore, InMemoryWorkflowRunStore } from "@sema-agent/core";
15
2
  import { join } from "node:path";
16
3
  import { FleetEventBus } from "../fleet/fleet-bus.js";
@@ -20,59 +7,21 @@ import { SubagentSteerRegistry } from "../orchestration/subagent-steer.js";
20
7
  import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
21
8
  export function createWorkflowOrchestration(ctx) {
22
9
  const { config, logger, metrics, localRoot, backend, getRunStore } = ctx;
23
- // [2995] boot-orphan 判死界:此刻之前记入 journal 的 pending 条目属于**前世进程**(workflow 执行器是
24
- // in-process 的,随前世死)。取在 journal/gate 构造之前 ⇒ 本进程随后 record 的每条 createdAt 常态下都严格
25
- // 晚于它。⚠️ 它是**必要不充分**条件:时钟回拨会让本世条目落到界下,判死的最终锚是 gate 内的
26
- // incarnation 名册(见 WorkflowNotifyGate.recordedThisIncarnation),两者取交集。
27
10
  const workflowBootAtMs = Date.now();
28
- // S8 / SVC-1 workflow run store (design/97 S1b): records LLM-authored BACKGROUND workflow
29
- // runs for the /v1/workflows list+detail view (live subscribe is in-process via subscribeWorkflow,
30
- // store-independent). Only present when self-orchestration is enabled (else no runs to record).
31
- //
32
- // SVC-1: the DEFAULT is now `FileWorkflowRunStore` (durable, crash-safe ledger) — NOT InMemory — so a
33
- // background workflow's record SURVIVES a replica restart, which is the prerequisite for the at-least-once
34
- // completion notify below to re-derive a run's terminal state after a crash. InMemory stays available only for
35
- // an explicit ephemeral opt-out (WORKFLOW_RUN_STORE=memory); a durable cross-replica TiDB/PG WorkflowRunStore
36
- // is a clean follow-on port (the workflowRunStoreContract makes it a drop-in like the run-store).
37
- // P1 (fleet failover, 2026-07-05): `auto` (the default) prefers the SQL backend's cross-replica twin when
38
- // present — a failover-landed session sees the run's history + its pending completion push on ANY replica.
39
- // Explicit WORKFLOW_RUN_STORE=file/memory still wins (single-box File posture unchanged: local backend has
40
- // no workflowRun() so auto falls to File there).
41
- const sqlWorkflowRunStore = config.workflowRunStoreBackend === "auto" ? backend?.workflowRun?.((msg, meta) => logger.warn(msg, meta)) : undefined; // C5: oversize-slim 留痕接 logger(completionInbox :82 同款)
11
+ const sqlWorkflowRunStore = config.workflowRunStoreBackend === "auto" ? backend?.workflowRun?.((msg, meta) => logger.warn(msg, meta)) : undefined;
42
12
  const baseWorkflowRunStore = config.selfOrchestrationEnabled
43
13
  ? config.workflowRunStoreBackend === "memory"
44
14
  ? new InMemoryWorkflowRunStore()
45
15
  : (sqlWorkflowRunStore ?? new FileWorkflowRunStore(join(config.localDataRoot ?? localRoot, "workflows")))
46
16
  : undefined;
47
- // SVC-1 at-least-once completion-notify TRUST GATE (the half that is service's, [[core-service-boundary]]):
48
- // core's `run_workflow` fires an in-process at-MOST-once notify on terminal — a crash between terminal and the
49
- // receiver loses it. The gate makes it at-LEAST-once: a durable notify-journal records every STARTED run
50
- // (observed via the JournalingWorkflowRunStore decorator's `put`), the LIVE notify path delivers-then-acks, and
51
- // a BOOT recovery sweep re-derives every un-acked run's terminal state from the run store + re-delivers (the
52
- // receiver is idempotent on runId). The default journal is the zero-dependency crash-safe File ledger; with no
53
- // run store (self-orchestration off) there's nothing to journal, so the gate is absent.
54
- // 1.108 review fix (lens③ HIGH): the journal FOLLOWS the run-store axis like the inbox — a SQL run store +
55
- // inbox with a replica-LOCAL File journal meant a replica that died holding an un-acked notify stranded it
56
- // forever (no surviving replica could recover it: at-least-once silently degraded to at-most-once across
57
- // replica death — the exact fleet-failover gap P1 exists to close).
58
17
  const workflowNotifyJournal = config.selfOrchestrationEnabled && config.workflowRunStoreBackend !== "memory"
59
18
  ? (sqlWorkflowRunStore ? backend.notifyJournal() : new FileWorkflowNotifyJournalStore(join(config.localDataRoot ?? localRoot, "workflows")))
60
19
  : undefined;
61
- // P1 ①②: the async-workflow COMPLETION INBOX — the push half. A finished background
62
- // workflow's completion is enqueued here keyed by the ORIGINATING session, and drained + emitted as a
63
- // `workflow_complete` out-of-band SSE frame when that session next opens a stream (see server.ts). File-backed
64
- // (crash-safe) when self-orchestration is on with a durable store; the ephemeral opt-out uses in-memory.
65
- // P1: the inbox FOLLOWS the run-store choice (one axis, no split-brain: a SQL run record with a File inbox
66
- // would re-open the cross-replica double-push the fence rows exist to close).
67
20
  const workflowCompletionInbox = config.selfOrchestrationEnabled
68
21
  ? config.workflowRunStoreBackend === "memory"
69
22
  ? new InMemoryWorkflowCompletionInbox((msg, meta) => logger.warn(msg, meta))
70
23
  : (sqlWorkflowRunStore ? backend.completionInbox((msg, meta) => logger.warn(msg, meta)) : new FileWorkflowCompletionInbox(join(config.localDataRoot ?? localRoot, "workflows"), (msg, meta) => logger.warn(msg, meta)))
71
24
  : undefined;
72
- // The deployment's REAL completion delivery: log + meter, THEN push into the originating session's inbox so the
73
- // model that launched the workflow learns it finished (the `WorkflowStatus` poll is the deterministic floor;
74
- // this is the proactive push). The gate guards this so it fires at-least-once + at-most-once-per-run-steady.
75
- // `runStore` (below) is resolved at CALL time (post-boot) — a forward reference into the same boot scope.
76
25
  const deliverWorkflowCompletion = async (p) => {
77
26
  logger.info("workflow_completed", {
78
27
  runId: p.runId,
@@ -80,20 +29,12 @@ export function createWorkflowOrchestration(ctx) {
80
29
  ...(p.sourceTaskId ? { sourceTaskId: p.sourceTaskId } : {}),
81
30
  });
82
31
  metrics.inc("workflow_runs_total", { status: p.status });
83
- // Routing (core 1.208): session = payload's `originatingSessionId` (lookup-free); owner = run-row
84
- // owner → payload `principal` (= VERIFIED spec.principal, the 1.55 F-fix invariant — covers the sync/resume
85
- // legs whose sourceTaskId is a sessionId and misses getRun) → multi-tenant fail-closed /
86
- // single-user null. Full rationale on `resolveCompletionRoute` (tested there).
87
32
  if (!workflowCompletionInbox)
88
33
  return;
89
34
  try {
90
- const route = await resolveCompletionRoute(p, getRunStore() ? (id) => getRunStore().getRun(id) : undefined, config.requirePrincipal === true); // A10 搬运改写:晚绑取值
35
+ const route = await resolveCompletionRoute(p, getRunStore() ? (id) => getRunStore().getRun(id) : undefined, config.requirePrincipal === true);
91
36
  if (!route)
92
- return; // no route / unverifiable owner → the WorkflowStatus poll floor covers it
93
- // `p.summary` is HUMAN-READABLE at the source since core 1.232 (completed lane = bounded
94
- // one-liner with name/elapsed/agents + a TaskOutput pointer; failed lane = the bounded redacted error)
95
- // — pass through verbatim, single-sourced (the 1.101.0 service-side re-wrap was superseded by core's
96
- // own half and removed to avoid two drifting formats).
37
+ return;
97
38
  await workflowCompletionInbox.enqueue({
98
39
  sessionId: route.sessionId,
99
40
  owner: route.owner,
@@ -112,56 +53,19 @@ export function createWorkflowOrchestration(ctx) {
112
53
  onError: (stage, runId, err) => logger.warn("workflow_notify_failed", { stage, runId, err: String(err) }),
113
54
  })
114
55
  : undefined;
115
- // MF-Fleet (data contract): the in-process fleet aggregation bus backing GET /v1/fleet/stream. Created HERE
116
- // (before the workflow run store) so the JournalingWorkflowRunStore can publish workflow rows to it. Always
117
- // present (lightweight); the run (POST /v1/runs) + workflow lifecycle publish per-row deltas. Replica-local.
118
56
  const fleetBus = new FleetEventBus();
119
- // The store handed to core (+ the /v1/workflows reader): ALWAYS wrapped when a run store exists ([1262]
120
- // fleet decoupling) — the wrap publishes the workflow's MF-Fleet row on each put/update (the write-observation
121
- // point the shell's /workflows panel lives on), and ADDITIONALLY journals the start-time `put` when the notify
122
- // gate is active (durable backends). The old `baseWorkflowRunStore && workflowNotifyGate` guard silently
123
- // dropped the FLEET half on the memory backend (gate needs a durable journal; fleet needs neither) — clay's
124
- // workflow panel went empty on exactly that shape.
125
57
  const workflowRunStore = baseWorkflowRunStore
126
58
  ? new JournalingWorkflowRunStore(baseWorkflowRunStore, workflowNotifyGate, fleetBus)
127
59
  : undefined;
128
- // [2995]/[2999] recover 两半的**单点合成**(boot 扫描与周期扫描共用同一对象,防两处漂移——#177 同族教训):
129
- // · finalizeStartedBeforeMs = 诚实性半场,**只在 replica-local(File)journal 形**:SQL journal 是跨副本
130
- // 的(上面 1.108 注),pending running run 可能活在别的 replica,那里年龄(grace)是唯一健全的孤儿代理。
131
- // (进程内是否本世记录由 gate 自己的 incarnation 锚判,时钟回拨不参与——见 recover 的键注。)
132
- // · publishTerminalFleetRow = 判死臂的**撤行**口:sweep 翻完 durable 行后经 wrapper 的同一投影发终帧+移除,
133
- // store/面板/notify 三面一个答案。⚠️ 复审第 1 轮两条 high 的收口:sweep **绝不**发 running 行(读时快照
134
- // 铸出来的行可能永远无人可撤——跨副本形属主在别的副本、同副本形 live 终态可能已把行撤了)。fleet 活行的
135
- // 唯一写者是本副本 wrapper 的 put/update 观察点。
136
- // 授权条件直接问「这本 journal 是不是 replica-local」,**不拿 sqlWorkflowRunStore 当代理**:两处各写一遍
137
- // 必漂移,而漂移方向是把判死授权交给共享 journal = 杀别副本正在跑的 run。未知/新 journal 实现 ⇒ 恒 false
138
- // (fail-closed:最坏只是孤儿多等一个 grace 窗)。
139
60
  const replicaLocalNotifyJournal = workflowNotifyJournal instanceof FileWorkflowNotifyJournalStore;
140
61
  const workflowRecoverOpts = {
141
62
  orphanGraceMs: config.workflowOrphanGraceMs,
142
63
  ...(replicaLocalNotifyJournal ? { finalizeStartedBeforeMs: workflowBootAtMs } : {}),
143
64
  ...(workflowRunStore ? { publishTerminalFleetRow: (id, run) => workflowRunStore.republishFleet(id, run) } : {}),
144
65
  };
145
- // SVC-2 (core CORE-7/CORE-9 Part A): the durable resume journal. core 1.145.0 added `RunnerDeps.workflowJournalStore`
146
- // (twin of workflowRunStore) + auto-wires it into the run_workflow tool's startWorkflow(RunWorkflowOptions.journalStore);
147
- // the tool's `resumeFromRunId` input then replays the longest unchanged prefix. tidb/pg = durable cross-replica resume;
148
- // local = core's InMemory (single-process — in-process resume only, not restart-durable). Only when self-orchestration
149
- // is on AND a backend is present (the load-bearing journal needs a real store).
150
66
  const workflowJournalStore = config.selfOrchestrationEnabled && backend ? backend.workflowJournal() : undefined;
151
- // design/73 §1 consumption sink (see RunnerDeps.onTaskOutcome below). Present whenever a StoreBackend is —
152
- // NOT gated on self-orchestration (goal-mode/harness emissions are orthogonal to workflows).
153
67
  const outcomeSink = backend?.outcomeSink();
154
- // SVC-5 (CORE-9 Part B): the process-local registry of STEERABLE workflow-agent handles live on THIS replica,
155
- // backing POST /v1/workflows/:id/agents/:label/steer. core 1.145.0 added `RunnerDeps.onWorkflowAgentSpawn` (the
156
- // opt-in deployment handle-sink): when set, the run_workflow tool's `agent()` runs steerable + emits the handle
157
- // HERE (the handle is a core object handed only to the trusted deployment sink — it NEVER enters the script/runner,
158
- // so the script-sandbox host-context membrane stays closed). We register by runId+label; the steer route looks it up + calls
159
- // handle.steer (core fences via CORE-5) behind our steer-in redaction + owner gate.
160
68
  const workflowAgentRegistry = config.selfOrchestrationEnabled ? new WorkflowAgentRegistry() : undefined;
161
- // C2 (core 1.219): the replica-local registry of STEERABLE Task-subagent handles, backing
162
- // POST /v1/runs/:runId/subagents/:target/steer. Core emits a handle to the per-run `onSubagentSpawn` sink
163
- // (wired in each run leg) the moment a SYNC delegation spawns; the handle never reaches the model. Always
164
- // present (lightweight Map) — the sink is only threaded where a durable runId exists to address it by.
165
69
  const subagentSteerRegistry = new SubagentSteerRegistry();
166
70
  return {
167
71
  sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion,
@@ -1,24 +1,4 @@
1
- /**
2
- * TOC integration ask③ (INTEGRATION-DESIGN §11) — boot-time orphan-run reclaim, extracted from main()'s boot
3
- * sequence so the env-only/no-backend path (runStore undefined) is UNIT-TESTABLE.
4
- *
5
- * Why a helper: the original inline version did `(runStore as {...}).reclaimOrphanedAtBoot` and a bare
6
- * `typeof reclaimable.reclaimOrphanedAtBoot === "function"` — which is a MEMBER ACCESS on `undefined` when runStore is
7
- * absent (the default env-only deploy: SESSION_BACKEND=memory, no DB → backend undefined → runStore undefined).
8
- * `typeof x.y` does NOT guard against `x` being undefined (only a bare `typeof y` does), so it threw a TypeError at
9
- * boot on that lane. A workflow 2nd-pass re-review caught it (HIGH regression from 1.49.0); the 6-dim review + the full
10
- * suite missed it because they exercised FileRunStore.reclaimOrphanedAtBoot DIRECTLY, never the env-only main() boot.
11
- */
12
- /**
13
- * Reclaim runs orphaned by a previous host-lane engine process (flip orphaned `running` → failed + release their
14
- * `active/<sessionId>` claims, so a re-submit isn't EEXIST-blocked). NULL-SAFE: a no-op (returns 0) when runStore is
15
- * absent or doesn't implement the capability (env-only / cross-replica tidb-pg lane). `checkpointStore`, when present,
16
- * EXCLUDES resume-in-progress (markResuming) sessions from reclaim so a resumable session is never stranded.
17
- */
18
1
  export async function reclaimOrphansAtBoot(runStore, checkpointStore, logger) {
19
- // `unknown` param: the caller's run store is a union of concrete stores (TiDB/Pg/File) | undefined that don't share
20
- // the optional capability shape. Cast + optional-chain here — runStore is `undefined` on the env-only/no-backend
21
- // deploy and is a non-File store (no reclaimOrphanedAtBoot) on the cross-replica tidb/pg lane; both must no-op.
22
2
  const store = runStore;
23
3
  if (typeof store?.reclaimOrphanedAtBoot !== "function")
24
4
  return 0;
@@ -1,25 +1,9 @@
1
- /**
2
- * #97 R11 收编:两个既有 session-keyed registry({@link import("./per-task-image.js").PerTaskImageRegistry}
3
- * 与 {@link import("./capabilities/select-environment-tool.js").SessionEnvironmentSelection})共享的同一形
4
- * (delete-then-set + 容量闩逐最老 + get 吞 undefined)收编到一处——基准实现逐字取自
5
- * `PerTaskImageRegistry`(唯一行为变化:泛型化 value 类型)。两消费类改为持有本类的组合薄壳,自己的
6
- * 构造器/方法签名不变。
7
- */
8
- /**
9
- * KEYED BY sessionId, NON-removing `get`, bounded by oldest-first eviction on overflow.
10
- *
11
- * `set` re-inserts at the tail (Map iteration order = insertion order) via delete-then-set, so a re-set
12
- * of an EXISTING key becomes the newest entry — the eviction below always removes the map's current
13
- * first key, so a key that was just re-set is the LAST to be evicted, not the first.
14
- */
15
1
  export class BoundedSessionMap {
16
2
  maxEntries;
17
3
  map = new Map();
18
4
  constructor(maxEntries = 4096) {
19
5
  this.maxEntries = maxEntries;
20
6
  }
21
- /** A re-set (same key) overwrites and moves the entry to the newest position; on overflow the oldest
22
- * insertion is evicted. */
23
7
  set(sessionId, value) {
24
8
  this.map.delete(sessionId);
25
9
  this.map.set(sessionId, value);
@@ -29,12 +13,9 @@ export class BoundedSessionMap {
29
13
  this.map.delete(oldest);
30
14
  }
31
15
  }
32
- /** `undefined` sessionId or an unregistered session both → `undefined` (non-removing read). */
33
16
  get(sessionId) {
34
17
  return sessionId === undefined ? undefined : this.map.get(sessionId);
35
18
  }
36
- /** #318:显式逐出(会话删除级联要它 —— 会话没了,它的进程内缓存条目也必须跟着没)。
37
- * 返回是否真删掉了一条(幂等:未登记的 session 返回 false)。 */
38
19
  delete(sessionId) {
39
20
  return this.map.delete(sessionId);
40
21
  }
package/dist/brain.js CHANGED
@@ -1,18 +1,7 @@
1
1
  import { createAnthropicBrain, createCircuitBreakerBrain, createDegradingBrain, createFailoverBrain, createOpenAIBrain, createRoutingBrain, } from "@sema-agent/core";
2
2
  import { resolveModelApiKey } from "./key-resolver.js";
3
- /**
4
- * 配了快切层(failover ≥2 路 / 断路器)时,**每路**的重试上限。
5
- *
6
- * 取 2 的理由:留够吸收一次瞬时抖动(2 次递增退避 ≈ 500ms + 1000ms ≈ 1.5s),同时让外层在秒级而不是
7
- * 分钟级拿到错误。断路器阈值 5 在这个值下 ≈ 7.5s 开路 —— 与它"快速失败让 failover 立刻切备"的设计意图
8
- * 同量级。详见 createBrain 里 retryCap 那段的由来。
9
- */
10
3
  export const FAST_FAIL_MAX_RETRIES = 2;
11
4
  const DEFAULT_RESILIENCE = {
12
- // Watchdog defaults ON — mirrors config.ts env defaults so a config object without a
13
- // resilience block gets the same posture. connect/firstToken per the [880] fleet audit
14
- // (core's idle timer only arms after the first token, so firstToken is the sole guard for a
15
- // stream that opens and never emits); idle = CC 196 parity. 0 restores off.
16
5
  connectTimeoutMs: 30_000,
17
6
  firstTokenTimeoutMs: 120_000,
18
7
  idleTimeoutMs: 300_000,
@@ -20,87 +9,22 @@ const DEFAULT_RESILIENCE = {
20
9
  failureThreshold: 5,
21
10
  cooldownMs: 30_000,
22
11
  };
23
- /**
24
- * Build the external "Brain" (LLM) from config.
25
- *
26
- * Composition (A1 + the 1.38 resilience stack, ordered timeout-innermost → breaker → failover-outermost):
27
- * - **Tiered timeouts** (1.38, `connectTimeoutMs`/`firstTokenTimeoutMs`) are passed into each
28
- * openai-compatible brain at creation — they bound "gateway never accepts the connection" and
29
- * "stream opened but hangs without a token" as retryable `[network]` failures. Default ON
30
- * (30s/120s, [880] fleet audit; env =0 disables).
31
- * - **Circuit breaker** (`createCircuitBreakerBrain`) wraps the *primary* gateway route(s) when
32
- * `MODEL_CIRCUIT_BREAKER=true`: after N transient failures it opens and fast-fails, so failover
33
- * switches to the bare backup immediately instead of waiting out each timeout. The last gateway
34
- * route stays bare (last-resort) — matching core design/20. State is core's in-process Map (each
35
- * replica trips independently; cross-replica shared state via a TiDB adapter is a follow-up).
36
- * - **Failover** (`createFailoverBrain`) stacks the primary gateway with optional
37
- * `MODEL_GATEWAY_FALLBACK_URLS` — same protocol, same model id, tried in order.
38
- * - **Routing** (`createRoutingBrain`, by `model.provider`) selects the cloud Anthropic Messages
39
- * route for `provider:"anthropic"`, and the gateway/failover stack for everything else. (Anthropic
40
- * ↔ vLLM is a *routing* choice, not failover.)
41
- *
42
- * When no fallback URL, no Anthropic key, and no resilience thresholds are configured, this returns the
43
- * bare openai brain (carrying the default-on watchdog timeouts — set the three env knobs to 0 for the
44
- * pre-[880] fully-bare shape). The Model itself (id / contextWindow / maxTokens / provider) flows
45
- * per-request through `Runner({ models })`.
46
- */
47
12
  export function createBrain(config, deps = {}) {
48
13
  const { fetchImpl, breakerState } = deps;
49
14
  const r = config.resilience ?? DEFAULT_RESILIENCE;
50
- // [1.207 codex H4] pass 0 THROUGH (never map to undefined): core 1.296's wall-clock lane supplies per-call
51
- // stall suggestions merged `configValue ?? suggested` — an undefined field silently re-arms the watchdog a
52
- // task-timeout run (idle would come back at 90s). An explicit 0 is core's documented "constructor-explicit
53
- // disable, wins over suggestions"; 0 also disarms the timer on pre-1.296 arming code (`value > 0` guards).
54
15
  const timeouts = {
55
16
  connectTimeoutMs: r.connectTimeoutMs,
56
17
  firstTokenTimeoutMs: r.firstTokenTimeoutMs,
57
18
  idleTimeoutMs: r.idleTimeoutMs,
58
19
  };
59
- // 🔴 重试上限:**配了才传**。此前这里(和下面 degrade fallback 那处)硬编码 `maxRetries: 2`,而显式传参
60
- // 压过引擎默认 ⇒ core 把默认抬到 10 对 server 部署零效果。缺席不传之后引擎默认当家、抬默认自动继承。
61
- // (五报清查 R1① 的 server 半场:限流 provider 走的正是这条 openai 兼容腿,而它此前**零配置出口**——
62
- // 同文件的 anthropic 腿反倒早有 `ANTHROPIC_MAX_RETRIES`。钉在 test/brain.test.ts 的两条上。)
63
- //
64
- // 🔴 **拓扑感知的缺席值**(2026-08-01,core 2.9.0 提货验收当场实测出来的层间相互作用):
65
- // core 2.9.0 把每次调用的重试默认抬到 10 且退避改递增形(`min(500·2^(n-1),32000)`),一次失败调用
66
- // 要耗尽 **~180s** 才把错误交给外层(实测 179.7s)。而 failover / 断路器 / 反应式降级 **三层全在
67
- // 每路 brain 的重试之外** —— 内层不放弃,外层看不见错误 ⇒ 主网关死了要 ~180s 才切备(旧形 ~0.6s)。
68
- // 断路器更反讽:它要数失败次数才开路,每笔失败 180s、阈值 5 = 15 分钟,而它的存在理由正是"快速失败
69
- // 让 failover 立刻切备"。
70
- //
71
- // 两边各自都对:core 的 10 次对**单 provider 被限流**是正解;server 的韧性栈对**多网关拓扑**是正解。
72
- // **正确的默认取决于拓扑,而引擎不知道拓扑** —— 这条信息只有 server 有。
73
- //
74
- // ⚠️ 这**不是**又一次"中间层钉死引擎默认"(那正是本文件上一版刚修掉的病),三点区别:
75
- // ① 显式 `GATEWAY_MAX_RETRIES` **恒赢** —— 运维的话压过我们的推断;
76
- // ② 只在**外层真有快切层**时才收窄(≥2 路 failover 或断路器开);单路 = 缺席不传,引擎默认当家
77
- // (那正是限流 provider 的形,10 次是对的);
78
- // ③ 收窄**在启动日志里说出来**(`gatewayRetryCap` 字段),不是静默替换。
79
- // 三个快切层,缺一个谓词就漏一族(反应式降级这一格是首版漏掉、被 brain 套件当场咬出来的):
80
- // ① failover(≥2 路)② 断路器 ③ 反应式降级 —— 后者尤其要算:它的触发条件**就是** `rate_limit`,
81
- // 而运维开 `MODEL_DEGRADE_REACTIVE` 等于明说"撞限流就换便宜模型,别在原地等"。若这里不收窄,
82
- // 主模型会先把 10 次递增退避耗完(~180s)才轮到降级 —— 把运维的选择拖成一句空话。
83
20
  const fastFailTopology = config.gatewayFallbackUrls.length > 0 || r.circuitBreaker || Boolean(config.degrade?.reactive);
84
21
  const retryCap = config.gatewayMaxRetries !== undefined
85
22
  ? { maxRetries: config.gatewayMaxRetries }
86
23
  : fastFailTopology
87
24
  ? { maxRetries: FAST_FAIL_MAX_RETRIES }
88
25
  : {};
89
- // Local openai-compatible gateway stack: primary + optional same-protocol fallbacks (failover).
90
- // 🔴 key↔URL 配对不变式([4998],clay 定性:两个不同 URL 不得引用同一个 key):这组 routes 共享
91
- // `gatewayApiKey` 的**语义前提**=MODEL_GATEWAY_FALLBACK_URLS 是**同一逻辑网关的镜像/备用地址**
92
- // (同一供应商、同一账户、同一 key 有效域)——在此语义下多 URL 共享一 key 是「拉平后仍一一对应」
93
- // 的合法形,不是配对拆散。把异供应商 URL 配进 fallback=违约使用(主 key 会被送往异站),契约在此
94
- // 成文;机器强制(URL 域校验/响亮拒)不在本仓单点做,归 [4995]/[4996] core 路由完整性裁决器同窗
95
- // ——届时「配对拆」与「key 缺」并入同一个「路由不可用」判定。
96
26
  const routes = [config.gatewayBaseUrl, ...config.gatewayFallbackUrls];
97
27
  const openaiBrains = routes.map((baseUrl, i) => {
98
- // 🔴 #303 顺修:网关 key **活取**(getter),不是 boot 快照。core 每次请求在 `buildRequest` 里读
99
- // `config.apiKey`(brain/openai.js:281 `options?.apiKey ?? config.apiKey`),所以这只闭包持有的
100
- // `config` 引用让 key 值随配置对象走 —— 与本文件下面 degrade fallback 腿 2026-07-29 起就写着的
101
- // `get apiKey()` 同形同理由。修前两条腿对「网关凭据什么时候生效」给出不同答案(主腿要重启,
102
- // 降级腿即时),那种分歧只在轮换那一刻显形,且是静默的。结构半场(baseUrl/failover 拓扑)仍是
103
- // boot 冻结,不受此影响。钉:test/brain.test.ts「#303:网关 key 活取」。
104
28
  const brain = createOpenAIBrain({
105
29
  baseUrl,
106
30
  get apiKey() {
@@ -110,116 +34,62 @@ export function createBrain(config, deps = {}) {
110
34
  fetchImpl,
111
35
  ...timeouts,
112
36
  });
113
- // Breaker on every route EXCEPT the last (the bare last-resort backup), and only with ≥2 routes —
114
- // a breaker without a failover alternative would just hard-fail the only gateway during cooldown.
115
37
  const isLast = i === routes.length - 1;
116
38
  return r.circuitBreaker && !isLast
117
39
  ? createCircuitBreakerBrain(brain, {
118
40
  failureThreshold: r.failureThreshold,
119
41
  cooldownMs: r.cooldownMs,
120
- state: breakerState, // shared cross-replica state when provided; else core's in-process Map
121
- // L14(接入审计):core 默认 key=`<provider>:<id>`(与 baseUrl 无关)——≥3 路由时 routes[0..n-2]
122
- // 命中同一 breaker 条目,一条网关的故障计入另一条的阈值(失 per-route 隔离)。key 掺入本路由
123
- // baseUrl 恢复隔离;1-2 路由行为不变(只有一个带 breaker 的路由)。
42
+ state: breakerState,
124
43
  key: (m) => `${m.provider ?? "openai"}:${m.id}@${baseUrl}`,
125
44
  })
126
45
  : brain;
127
46
  });
128
47
  const gatewayBrain = openaiBrains.length > 1 ? createFailoverBrain(openaiBrains) : openaiBrains[0];
129
- // Route by model.provider: "anthropic" → cloud Messages API (prompt caching + thinking replay),
130
- // everything else → the local gateway stack (also the fallback for an unrecognized provider).
131
48
  const anthropicBrain = config.anthropic
132
49
  ? createAnthropicBrain({
133
- // Anthropic-compatible Bearer auth: `ANTHROPIC_AUTH_TOKEN` → `Authorization: Bearer`
134
- // via core's extra-`headers` seam, and apiKey is then NOT passed (x-api-key only emits when set) —
135
- // 二选一, AUTH_TOKEN priority. Bearer-first providers in this ecosystem commonly accept only
136
- // the Bearer form.
137
50
  ...(config.anthropic.authToken
138
51
  ? { headers: { Authorization: `Bearer ${config.anthropic.authToken}` } }
139
52
  : { apiKey: config.anthropic.apiKey }),
140
53
  baseUrl: config.anthropic.baseUrl,
141
54
  version: config.anthropic.version,
142
55
  cacheBreakpoints: config.anthropic.cacheBreakpoints,
143
- // 同 retryCap:配了才传(`ANTHROPIC_MAX_RETRIES` 缺席时 config 里就没这个键)
144
56
  ...(config.anthropic.maxRetries !== undefined ? { maxRetries: config.anthropic.maxRetries } : {}),
145
57
  fetchImpl,
146
58
  ...timeouts,
147
59
  })
148
60
  : undefined;
149
- // No cloud route + single gateway → bare brain (unchanged); else route by provider.
150
61
  let brain = anthropicBrain
151
62
  ? createRoutingBrain({ anthropic: anthropicBrain }, { fallback: gatewayBrain })
152
63
  : gatewayBrain;
153
- // Reactive degradation (1.39) — wrapped OUTERMOST (core council #8): on rate_limit / breaker-open from
154
- // the primary, serve a cheaper catalog model instead of failing. The fallback brain carries its own
155
- // credentials (the decorator clears the primary's per-call apiKey so it can't leak to a different
156
- // provider). fallbackModel resolved from the catalog.
157
64
  if (config.degrade?.reactive) {
158
65
  const degradeTo = config.degrade.to;
159
66
  const fallbackModel = config.models[degradeTo];
160
67
  if (fallbackModel) {
161
- // Cross-gateway degrade (audit B, brain.ts): the degrading decorator clears the per-call apiKey
162
- // (so the primary's key can't leak to another provider), which left the fallback authenticating
163
- // with the PRIMARY gateway key even when the catalog points the target at a different gateway.
164
- // Resolve the target's own key via the SAME chain the main path uses; `model.baseUrl` already wins
165
- // inside the openai brain — pass it explicitly so the route is visible here. No per-model key =
166
- // same-gateway degrade (unchanged), which stays meaningful for per-model limits (e.g. deepseek pro→flash).
167
- //
168
- // 复审 2026-07-29(同族缺口): this resolution used to read `config.modelApiKeyEnv` ONLY — half of the
169
- // main path's chain. A degrade target under sealed-box custody lands in `config.modelApiKeys` and, by the
170
- // registry-core mutual-exclusion contract, NEVER in `modelApiKeyEnv` ⇒ the hop silently authenticated the
171
- // target's own gateway with the PRIMARY gateway key (wrong key upstream, not fail-loud), and a poison pill
172
- // (unsealable key) degraded to "no per-model key configured" = the exact silent gateway-account burn the
173
- // poison pill exists to prevent. Now it calls `resolveModelApiKey` — one chain, one implementation:
174
- // sealed → env-NAME reference → gateway key, poison throws.
175
68
  const fbApiKey = () => {
176
- // Two lookups for the same reason the pre-fix code had them: `degrade.to` may name the catalog's
177
- // `default` alias, whose Model carries the REAL name that the key maps are keyed by (and vice versa on
178
- // env-lane catalogs). Poison in either lookup throws — never falls through to the gateway key.
179
69
  const own = resolveModelApiKey(fallbackModel.name, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {}) ??
180
70
  resolveModelApiKey(degradeTo, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {});
181
- // 🔴 BROKEN 实锤登记([4998] server 首扫,clay 定性=key↔URL 配对不变式):`own` 缺席时这里回落
182
- // **主网关 key**,而本 brain 的 baseUrl 是 `fallbackModel.baseUrl || gatewayBaseUrl`——降级目标指
183
- // **外路** URL 且无自 key 时=外路 URL+主 key,配对拆散(同仓 hook-llm.ts 2026-07-13 已修对:主
184
- // key 只随 sameGateway 走;本腿是漏网的姊妹)。修=候 [4995]/[4996] core 路由完整性裁决器同窗换装
185
- // (外路+缺 key ⇒「路由不可用」,降级链按不可用后退),本仓不抢单点行为修——两处两答比单病更糟。
186
71
  return own ?? config.gatewayApiKey;
187
72
  };
188
73
  const fallback = fallbackModel.provider === "anthropic" && anthropicBrain
189
- ? // Cloud Anthropic target: the shared cloud route (its own ANTHROPIC_* credential) — a per-model key
190
- // on an anthropic degrade target is NOT honored here (the hop shares the primary's brain instance).
191
- // Named residual, not an oversight: the openai-compatible lane below is where catalog-directed
192
- // per-model credentials live (every sema-registry `apiKeyEnv`/`sealedApiKey` deployment to date).
74
+ ?
193
75
  anthropicBrain
194
76
  : createOpenAIBrain({
195
77
  baseUrl: fallbackModel.baseUrl || config.gatewayBaseUrl,
196
- // HOT by getter (core reads `config.apiKey` per request inside buildRequest, openai.js:296): the key
197
- // VALUE tracks refreshes exactly like the main path's rebuilt keyResolver — `applyEffective`
198
- // REASSIGNS config.modelApiKeys/modelApiKeyEnv, and this closure holds `config`, so it always reads
199
- // the live tables. Freezing it at boot would make a rotated/newly-sealed key take effect only after a
200
- // restart, silently. The STRUCTURAL half (target present/absent, its baseUrl/id/apiKeyEnv/sealedApiKey)
201
- // stays boot-frozen by construction and is registered as the `degrade-route` restart slice.
202
78
  get apiKey() {
203
79
  return fbApiKey();
204
80
  },
205
- // 🔴 degrade fallback **不吃** fastFail 收窄:它是最外层的兜底,外面**没有**别的切换层了,
206
- // 所以它该按引擎默认那样耐心重试(限流场景正靠它)。显式 env 仍然恒赢。
207
81
  ...(config.gatewayMaxRetries !== undefined ? { maxRetries: config.gatewayMaxRetries } : {}),
208
82
  fetchImpl,
209
83
  ...timeouts,
210
84
  });
211
- // core 5.1.0([2390]②):chain-of-one 链形;downgradeOn 缺席时 core 默认含 server_error(默认门
212
- // 变宽)——MODEL_DEGRADE_ON 是运维收窄旋钮,词表验门在 config.ts parseDegradeOn。
213
85
  brain = createDegradingBrain({ primary: brain, fallbacks: [{ brain: fallback, model: fallbackModel }], downgradeOn: config.degrade.downgradeOn });
214
86
  }
215
87
  }
216
88
  return brain;
217
89
  }
218
- /** One-line summary of the active brain composition, for the startup log. */
219
90
  export function brainSummary(config) {
220
91
  const r = config.resilience ?? DEFAULT_RESILIENCE;
221
92
  const hasFailover = config.gatewayFallbackUrls.length > 0;
222
- // The breaker only does anything with ≥2 gateway routes (it pairs with failover); flag a no-op config.
223
93
  const breakerActive = r.circuitBreaker && hasFailover;
224
94
  return {
225
95
  gateway: config.gatewayBaseUrl,
@@ -232,20 +102,13 @@ export function brainSummary(config) {
232
102
  idleTimeoutMs: r.idleTimeoutMs || null,
233
103
  circuitBreaker: breakerActive,
234
104
  circuitBreakerRequested: r.circuitBreaker,
235
- // 🔴 每路重试上限的**来源**必须可见(2026-08-01):收窄是我们按拓扑推断的,不是引擎默认,
236
- // 静默替换等于让运维读不出"为什么这台机的失败切换比另一台快"。三态:
237
- // `env:<n>` = 运维显式配了 GATEWAY_MAX_RETRIES(恒赢)
238
- // `fast-fail:<n>`= 外层有快切层(failover/断路器),我们收窄到 n(见 createBrain 的由来注)
239
- // `engine-default` = 缺席不传,引擎默认当家(单路限流 provider 的正解)
240
105
  gatewayRetryCap: config.gatewayMaxRetries !== undefined
241
106
  ? `env:${config.gatewayMaxRetries}`
242
107
  : hasFailover || r.circuitBreaker || config.degrade?.reactive
243
108
  ? `fast-fail:${FAST_FAIL_MAX_RETRIES}`
244
109
  : "engine-default",
245
110
  circuitBreakerNoop: r.circuitBreaker && !hasFailover ? "needs ≥2 gateway routes" : null,
246
- // reactive degrade is active only if a valid catalog target resolves
247
111
  reactiveDegradeTo: config.degrade?.reactive && config.models?.[config.degrade.to] ? config.degrade.to : null,
248
- // cross-gateway degrade visibility: the fallback's effective route (null = degrade off)
249
112
  reactiveDegradeBaseUrl: config.degrade?.reactive && config.models?.[config.degrade.to]
250
113
  ? config.models[config.degrade.to].baseUrl || config.gatewayBaseUrl
251
114
  : null,