@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
package/dist/budget.js CHANGED
@@ -1,14 +1,6 @@
1
- /**
2
- * Cost / budget / trace wiring for the Runner (core 5.x) — pure, testable helpers kept out of the
3
- * auto-running main.ts:
4
- * ③ buildPricing — Model.cost catalog → injected ModelPricing (authoritative costMicroUsd).
5
- * ④ createTracer — core's content-free TraceEvents → /metrics (spend, first-token, tool calls).
6
- * ⑤ cappedCeiling — clamp a caller's requested per-task budget DOWN to the operator ceiling.
7
- */
8
1
  import { modelCostToPricing } from "@sema-agent/core";
9
2
  import { currentPrincipal } from "./observability/principal-context.js";
10
- import { promptManifestRecordOf, configAssembledRecordOf } from "./observability/prompt-manifest.js"; // design/158 N18: see re-export block below (moved to ./observability/prompt-manifest.js)
11
- /** ③ Per-model pricing keyed by model id (core falls back to each Model.cost when a model is absent). */
3
+ import { promptManifestRecordOf, configAssembledRecordOf } from "./observability/prompt-manifest.js";
12
4
  export function buildPricing(models) {
13
5
  const pricing = {};
14
6
  for (const m of Object.values(models))
@@ -16,28 +8,9 @@ export function buildPricing(models) {
16
8
  pricing[m.id] = modelCostToPricing(m.cost);
17
9
  return pricing;
18
10
  }
19
- /**
20
- * 缝合审 M3:`capabilities.pricingConfigured` 的判据 —— 提成函数放在 pricing 自己的文件里,因为
21
- * **这个表是热变的**(`boot/config-center.ts` 的 `mutateInPlace(pricing, buildPricing(config.models))`,
22
- * 那行注释自己写着 `hot: cost/model changes`)。旧码在 `main.ts` 的 capabilities 字面量里把它算成一个
23
- * **boot 时的布尔快照**,于是运维在 config center 里加上/撤掉价目表之后,`/v1/capabilities` 还报旧值 ——
24
- * 而这个键的**全部用途**就是给消费端消歧「costMicroUsd=0 是免费跑,还是根本没配价目表」,报错了就是
25
- * 把消歧信号变成误导信号。main.ts 侧改用 getter(读时求值)。
26
- *
27
- * 同一条论证在同一个字面量里**已经执行过一次**:紧邻的 `restartState: () => configCenter.restartState()`
28
- * 带着注释「A live getter (not a snapshot) so /health always reads current」—— 兄弟键漏了。本仓第 7 例同形。
29
- */
30
11
  export function isPricingConfigured(pricing) {
31
12
  return Object.values(pricing).some((p) => Object.values(p).some((v) => typeof v === "number" && v > 0));
32
13
  }
33
- /**
34
- * Bound the cardinality of a free-form metric label value (BL-11/BL-21). A label fed by untrusted/unbounded
35
- * input — MCP tool names (a misconfigured/malicious server can emit thousands of dynamic names) or center
36
- * model ids (a malformed roster) — would otherwise mint an unbounded number of metric series: a slow memory
37
- * leak in the registry + a /metrics scrape blow-up. We track the DISTINCT values seen; once the cap is hit,
38
- * further NEW values collapse into one `__other__` bucket (already-seen values still report exactly). The
39
- * cap is made observable (no silent truncation) via `onOverflow`, fired once when the cap is first crossed.
40
- */
41
14
  export function cardinalityGuard(cap, onOverflow) {
42
15
  const seen = new Set();
43
16
  let overflowed = false;
@@ -55,27 +28,11 @@ export function cardinalityGuard(cap, onOverflow) {
55
28
  return value;
56
29
  };
57
30
  }
58
- /**
59
- * E8 (shell-host contract): per-task × per-model usage accumulator, fed (synchronously) by the `brain.call`
60
- * tracer. The `TracerHook` contract is SYNC / no-IO, so we cannot persist from the hook; instead we accumulate
61
- * here in memory and the run lifecycle DRAINS us at each turn boundary, appending the delta to the durable run
62
- * event log. The **append-only delta** model is what makes it resume-safe: a task that suspends and resumes in a
63
- * different process appends only its own post-resume deltas; the result echo SUMS all `model_usage` events across
64
- * the whole (possibly multi-process) run. Memory is bounded — a task's entry is dropped on {@link drain}/{@link clear}.
65
- */
66
31
  export class ModelUsageTracker {
67
32
  byTask = new Map();
68
33
  active = new Set();
69
- /** Mark a TOP-LEVEL run active so its `brain.call` usage is tracked. **Required** — `record` ignores any taskId
70
- * that isn't registered. This is the leak fence: delegated SUB-TASKS (team members,
71
- * adversarial verifiers) run on throwaway core-minted sessionIds that the run lifecycle never registers/drains,
72
- * so without this gate their usage would accumulate under ids nothing ever clears → an unbounded process-lifetime
73
- * leak. Sub-task spend is intentionally NOT in the per-run `modelUsage` echo (it lands in `TaskStats.nested`);
74
- * `modelUsage` is the TOP-LEVEL run's own per-model usage. Call at run start; {@link clear} unregisters. */
75
34
  register(taskId) { if (taskId)
76
35
  this.active.add(taskId); }
77
- /** Accumulate one `brain.call`'s usage — ONLY for a registered top-level run (else a no-op; see {@link register}).
78
- * Sync, never throws (TracerHook contract). */
79
36
  record(taskId, model, d) {
80
37
  if (!taskId || !model || !this.active.has(taskId))
81
38
  return;
@@ -90,22 +47,16 @@ export class ModelUsageTracker {
90
47
  cur.outputTokens += d.outputTokens;
91
48
  cur.cacheReadTokens += d.cacheReadTokens;
92
49
  cur.cacheWriteTokens += d.cacheWriteTokens;
93
- // 🔴 RB-368 未知传染:已知 + 未知 = **未知**(和里含一个不知道的项,这个和就不知道)⇒ 删键,不折 0。
94
- // 顺序无关(先未知后已知同样保持未知),因为 `cur.costMicroUsd` 已是 undefined 时 `+` 也不成立。
95
50
  if (d.costMicroUsd === undefined || cur.costMicroUsd === undefined)
96
51
  delete cur.costMicroUsd;
97
52
  else
98
53
  cur.costMicroUsd += d.costMicroUsd;
99
54
  }
100
55
  else {
101
- // 显式重建而非 `{...d}`:调用方可能传 `{costMicroUsd: undefined}`(显式 undefined 键),
102
- // 那样 `Object.hasOwn` 为真、JSON.stringify 又把它抹掉 ⇒ 「缺席」在内存与落库两处形状不一致。
103
56
  const { costMicroUsd, ...tokens } = d;
104
57
  perModel.set(model, { ...tokens, ...(costMicroUsd !== undefined ? { costMicroUsd } : {}) });
105
58
  }
106
59
  }
107
- /** Return the usage accumulated for this task SINCE the last drain (per neutral model id), and RESET it.
108
- * `undefined` when nothing accrued — callers skip the durable append then. */
109
60
  drain(taskId) {
110
61
  const perModel = this.byTask.get(taskId);
111
62
  if (!perModel || perModel.size === 0)
@@ -116,251 +67,70 @@ export class ModelUsageTracker {
116
67
  this.byTask.delete(taskId);
117
68
  return out;
118
69
  }
119
- /** Drop a task's pending accumulation AND unregister it (cleanup at run end / on an abandoned/errored leg). After
120
- * this, further `record(taskId,…)` no-ops until a fresh {@link register}. `drain` deliberately does NOT unregister
121
- * (the run continues across turns); only `clear` ends the run's tracking. */
122
70
  clear(taskId) { this.byTask.delete(taskId); this.active.delete(taskId); }
123
71
  }
124
- // design/158 N18: PromptManifestRecord / promptManifestRecordOf / configAssembledRecordOf / PromptManifestTracker
125
- // moved VERBATIM to ./observability/prompt-manifest.js (budget.ts's name says cost/budget; this face is
126
- // wire-observability projection of prompt.assembled/config.assembled, unrelated). Re-exported here so existing
127
- // importers (main.ts, runs.ts, http/server.ts, trace/project.ts, tests) keep working untouched — createTracer
128
- // below still consumes promptManifestRecordOf/configAssembledRecordOf/PromptManifestTracker.
129
72
  export { promptManifestRecordOf, configAssembledRecordOf, PromptManifestTracker, } from "./observability/prompt-manifest.js";
130
- /**
131
- * ④ Bridge core's fire-and-forget trace events to /metrics (+ the E8 per-task model-usage accumulator). The hook
132
- * is void / never throws / does no sync IO (in-memory counters only), per the TracerHook contract. `brain.call`
133
- * fires for EVERY call (top-level + async + council sub-tasks) → `model_cost_micro_usd_total` is the authoritative
134
- * spend, and (when a {@link ModelUsageTracker} is supplied) the per-task `modelUsage` echo's source.
135
- */
136
73
  export function createTracer(metrics, costQuota, modelUsage, fleetUsage, fleetLease, quotaWeightFor, promptManifests) {
137
- // Per-process cardinality guards for the two unbounded-input labels (BL-11/BL-21). 256 is generous for any
138
- // real tool/model catalog; overflow bumps a counter so an over-cap deploy is observable, not silent.
139
74
  const toolName = cardinalityGuard(256, () => metrics.inc("metric_label_overflow_total", { label: "tool_name" }));
140
75
  const modelId = cardinalityGuard(256, () => metrics.inc("metric_label_overflow_total", { label: "model" }));
141
- // provider flows from env/registry config (not a bounded enum) — guard it like modelId (1.127.0):
142
- // a misconfigured registry entry must not explode Prometheus label cardinality. 64 is generous for real providers.
143
76
  const providerName = cardinalityGuard(64, () => metrics.inc("metric_label_overflow_total", { label: "provider" }));
144
77
  return (e) => {
145
78
  if (e.kind === "brain.call") {
146
- // S18 (SILENT-FALLBACK P1): count every call by provider+model. provider is optional on the trace event —
147
- // "unknown" = the routing layer fell through without a recognized provider (previously unobservable).
148
79
  metrics.inc("brain_calls_total", { provider: providerName(e.provider ?? "unknown"), model: modelId(e.model) });
149
80
  if (e.costMicroUsd) {
150
81
  metrics.inc("model_cost_micro_usd_total", { model: modelId(e.model) }, e.costMicroUsd);
151
- // ⑤b attribute spend to the originating principal (ALS) — reaches council/team sub-tasks too.
152
82
  const principal = currentPrincipal();
153
83
  if (principal)
154
84
  costQuota?.add(principal, e.costMicroUsd);
155
85
  }
156
86
  else if (e.costMicroUsd === undefined) {
157
- // D5:cost 整键缺席 = 未知(RB-368)——计数供 /metrics/summary 的 costUnknown/unpricedCalls
158
- // 信号(costUsd 在 unpriced 部署恒 0,没有这个信号消费端会读成「零花费」)。显式 0 =
159
- // declared free,走上面 truthy 为假的静默臂,不计入 unpriced。
160
87
  metrics.inc("brain_calls_unpriced_total", { model: modelId(e.model) });
161
88
  }
162
- // ┌─ #59 裁定 / RB-368 结案([2052]① cli 报案 [2073] 立案 [2076] 工单挂 core [2083] 到货) ─┐
163
- // 病灶报案:下面三处曾是 `e.costMicroUsd ?? 0`(账本 + fleetUsage + fleetLease),把「成本未知」记成
164
- // 「免费」,与 `pricingConfigured=false` 的部署组合 ⇒ 全部账目假 0。
165
- //
166
- // **第一轮裁定(core ≤2.2.0,已被上游修复取代,留档见证判断链)**:亲读上游后纠偏——core 当时在
167
- // brain.call 帧上**恒发数字、从不缺席**(`costMicroUsd: turnCostMicroUsd` 是无条件键;无价目表时
168
- // `modelCostToPricing(undefined)` 产出全零单价 ⇒ 如实算出 0)。歧义**产生在 core**,server 收到的
169
- // 信息里根本不含这个区分 ⇒ 当时改透传治不了根(缺席永不到达),`?? 0` 定性为**类型面守卫**。
170
- // 结论是把根因工单挂回 core,而不是在本层做样子。
171
- //
172
- // 🔴 **第二轮 = 现行(core ≥2.3.0,RB-368 到货)**:core 已在源头消歧——unpriced 部署下
173
- // brain.call(`runtask.js:831`)/ task.end(`:2851`)/ `stats.costMicroUsd`+`costBreakdown`
174
- // (`assemble-result.js:39,144`)**整键缺席**;显式全零 `cost` = 声明免费 ⇒ 照发 0。于是三处
175
- // `?? 0` 全部改为**条件展开透传**:键在 ⇒ 原样传(含真 0);键不在 ⇒ 三路都不带该键。
176
- // 事实钉在 test/budget.test.ts(真 Runner 跑一趟取证),floor 已钉 ^2.3.0——装回旧 core 这三条
177
- // 透传臂会变成永不到达的死枝而全量照绿,那正是 floor 门存在的理由。
178
- //
179
- // 三问(第二轮):
180
- // · 谁需要 —— 消费端要区分真零/未知(cli 在 CC 对照里正是靠「诚实缺席不编造」赢的那一点;
181
- // cli #68 的两信号分渲 = 部署级 pricingConfigured + 行级缺席,行级这一半由本改动供给)。
182
- // · 谁受伤 —— (a) 聚合面遇缺席:usage-analytics 与 aggregateModelUsage 此前把 DB JSON 当全必填做裸
183
- // 算术,一行缺键就 NaN/字符串毒掉整窗(#59 同批已修,红先复现在
184
- // test/usage-analytics.test.ts + test/model-usage.test.ts);(b) 历史行:已按 0 记账的行无法回溯
185
- // 区分,只能靠部署位读回——**本改动不回溯**,分界线 = 升到 core 2.3.0 那一刻。
186
- // · 补偿 —— 第一级 = `capabilities.pricingConfigured`(main.ts,已在):false ⇒ 消费端把任何 0
187
- // 渲染成「未知」而非「免费」;第二级 = 行级缺席(本改动),沿账本→durable `model_usage`→
188
- // `aggregateModelUsage`→`TaskStats.modelUsage` 回声→usage-analytics 的 `estimated` 标记,一路
189
- // 保持缺席(聚合面的**未知传染**语义见 trace/project.ts)。
190
- // · fleet 批报腿(下面 fleetUsage)自 registry-core 0.11.0/[2103] 起**同样接住了**:wire 有
191
- // 缺席位(UsageEntry.costMicroUsd optional),accumulator cell 级传染透传缺席,center 聚合面
192
- // 「下界 + costUnknown」——两层语义与本处的关系见 fleet-client.ts 的旁注(3.7.0 前这里曾写
193
- // 「wire 表达不了缺席、center 只见低估」,已被该车推翻;doc-rot 修 2026-07-30)。
194
- // └──────────────────────────────────────────────────────────────────────────────────────────────┘
195
- // [2153]③ 侦察钉(core 2.6.0):`brain.call` 帧新增可选 `turn?: number`(trace.d.ts 确认;此前只有
196
- // `turn.end`/`repetition.detected`/`task.degraded` 三帧带 turn)。**刻意不接**——`modelUsage.record`
197
- // 喂的是 E8 durable `model_usage` echo,累加键=`taskId → model`(见 `ModelUsageTracker.byTask`,
198
- // 上方 class 定义),drain 边界=**per-turn**(run lifecycle 在每个 turn 边界 drain 一次、append 一条
199
- // durable 事件行)。也就是说 turn 粒度已经由「何时 drain」这个外层动作表达,一次 drain 窗口内本就
200
- // 只可能有一个 turn 的 delta——帧上逐行再带一份 `turn` 是重复信息,对已经 per-turn 聚合的行没有
201
- // 增量语义(不是「丢了信息」,是「信息已经在结构里」)。将来若变盘:若聚合粒度改粗(比如攒够 N
202
- // turn 才 drain 一次,单行 delta 跨多个 turn),或 usage-analytics 想要「同一 drain 窗口内哪个 turn
203
- // 花了多少」的细分,才值得把 `e.turn` 塞进 `ModelUsageDelta`(那时 byTask 需要再加一层 turn 键)。
204
- // E8 (shell-host): per-task × per-model usage for the `TaskStats.modelUsage` echo. `record` is a NO-OP unless
205
- // e.taskId is a REGISTERED top-level run (the leak fence — sub-task/throwaway taskIds are never registered, so
206
- // they neither leak nor enter the echo; their spend is in TaskStats.nested). Drained at turn boundaries into the
207
- // durable log (resume-safe); the registered entry is dropped at run end (clear). NOT cardinality-guarded by
208
- // model: bounded by a run's model set, and by the active-run set.
209
- // core 3.0.0([2296] S1):this lane's `inputTokens` stays `e.promptTokens` — DELIBERATELY zero-changed.
210
- // The E8 ledger's semantics are COMPONENT-wise (MISS + cacheRead/cacheWrite listed separately;
211
- // usage-analytics' read side SUMS them back into a total), unlike the weightedTokens/fleetUsage lane just
212
- // below (which wants the pre-summed normalized total and migrates to `e.totalInputTokens`). Pre-3.0.0 the
213
- // gateway family's `promptTokens` already included cache, so this same read-side sum double-counted for
214
- // that family — an existing accounting gap in historical rows that this version does not retroactively fix
215
- // (declared, not silently carried forward): see CHANGELOG's "存量口径断层" note.
216
- modelUsage?.record(e.taskId, e.model, { inputTokens: e.promptTokens, outputTokens: e.completionTokens, cacheReadTokens: e.cacheRead, cacheWriteTokens: e.cacheWrite, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}) }); // RB-368 透传缺席
217
- // weight-at-burn:加权 tokens = (prompt+completion)×模型配额倍率,在记账点固化(此后改倍率
218
- // 不回算历史,registry ModelEntry.quotaWeight 同句契约)。weight 源=config.modelQuotaWeights(registry
219
- // hot-apply;env lane 空表 ⇒ 1 兜底);非法回调值防御性归 1(tracer 契约 never-throws,不让坏表毒记账)。
220
- // core 3.0.0([2296] S1):`e.promptTokens` → `e.totalInputTokens`. This axis's contract (line ~365 below)
221
- // has always been "normalized total input INCLUDING cache" — pre-3.0.0 that's what `promptTokens` meant for
222
- // the gateway family; 3.0.0 flips `promptTokens` to mean cache-MISS-only, so staying on it here would
223
- // massively under-count the quota/lease axis for any cache-heavy load (a lease could be walked through by
224
- // cache-heavy traffic — the exact same failure class as the side-query R4 finding below).
89
+ modelUsage?.record(e.taskId, e.model, { inputTokens: e.promptTokens, outputTokens: e.completionTokens, cacheReadTokens: e.cacheRead, cacheWriteTokens: e.cacheWrite, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}) });
225
90
  const qwRaw = quotaWeightFor?.(e.model);
226
91
  const qw = typeof qwRaw === "number" && Number.isFinite(qwRaw) && qwRaw > 0 ? qwRaw : 1;
227
92
  const weightedTokens = Math.round((e.totalInputTokens + e.completionTokens) * qw);
228
- // P2 用量批报:principal×model 窗口累计( ⑤b ALS 归因点 子任务花费也归到发起 principal),
229
- // fleet-client 每窗口 drain→POST /api/fleet/usage。未配 fleet(accumulator 缺席)= 零行为。
230
- // core 3.0.0:同上迁移理由,inputTokens=归一化总输入含 cache(totalInputTokens),与 weightedTokens 同轴。
231
- fleetUsage?.record(currentPrincipal(), e.model, e.taskId, { inputTokens: e.totalInputTokens, outputTokens: e.completionTokens, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}), weightedTokens, quotaWeightAtUse: qw }); // RB-368 透传缺席
232
- // lease 消费(D4 AP):同一 ALS 归因点做 lease 本地扣减 —— 只对当前持 lease 的 principal
233
- // 生效(FleetLeaseManager 内部判),sync + never throws。双轴=weightedTokens(配额轴)+costMicroUsd($ 轴)。
234
- fleetLease?.recordSpend(currentPrincipal(), { weightedTokens, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}) }); // RB-368 透传缺席
93
+ fleetUsage?.record(currentPrincipal(), e.model, e.taskId, { inputTokens: e.totalInputTokens, outputTokens: e.completionTokens, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}), weightedTokens, quotaWeightAtUse: qw });
94
+ fleetLease?.recordSpend(currentPrincipal(), { weightedTokens, ...(e.costMicroUsd !== undefined ? { costMicroUsd: e.costMicroUsd } : {}) });
235
95
  if (e.firstTokenMs !== undefined)
236
96
  metrics.observe("brain_first_token_ms", e.firstTokenMs);
237
97
  metrics.observe("brain_call_latency_ms", e.latencyMs);
238
98
  }
239
99
  else if (e.kind === "tool.call") {
240
- // [2153]③ 侦察钉(core 2.6.0):`tool.call` 帧新增可选 `toolCallId?: string` / `turn?: number`(trace.d.ts
241
- // 确认)。**刻意不接进 `tool_calls_total` 的标签**:`toolCallId` 逐调用唯一 → 直接当标签会让这个
242
- // 计数器的基数=调用总数(等于没聚合),是本文件 `cardinalityGuard` 存在的理由要防的那类爆炸,
243
- // 连 guard 都救不了(不是「用户输入无界」那类可折叠 __other__ 的情形,是键本身设计上就唯一)。
244
- // `turn` 本身是低基数(有界于单任务的 turn 数),但作为**跨任务**的 Prometheus 标签会随长任务/
245
- // 多任务叠加成事实上无界的序列(同 model/tool_name 已用 guard 的理由,turn 目前没有等价上限)——
246
- // 且当前没有「按 turn 分桶看 tool 调用量」的仪表盘/告警消费者在等这个切面,先不加标签。这两个键
247
- // 已经在别的通道有名分:`toolCallId` 走 wire 的 `tool_start`/`tool_end` TaskEvent(trace/project.ts
248
- // toolStartEventData/toolEndEventData,逐调用可关联、不进 metrics 基数);`turn` 走 `turn.end`/
249
- // `task.end.turns`。本臂(createTracer→/metrics)只做计数,不是这两键的合适去处。将来若要接:
250
- // (a) 出现「per-turn tool 调用密度」的真实消费者(如异常 turn 侦测),`turn` 分桶价值才成立——
251
- // 用有界桶(如 `turn <= 20 ? String(turn) : "20+"`)而非原始整数,避免上面同款无界问题;
252
- // (b) `toolCallId` 除非改造成非标签的关联字段(如结构化日志行,不是 Prometheus label),否则永远
253
- // 不该进这个计数器。
254
100
  metrics.inc("tool_calls_total", { name: toolName(e.name), ok: String(e.ok) });
255
101
  }
256
102
  else if (e.kind === "task.degraded") {
257
- // 1.40: a task was switched to a cheaper model (near-budget / rate_limit / breaker-open). Quality
258
- // dropped → alert-worthy operational signal, by reason.
259
103
  metrics.inc("degraded_total", { reason: e.reason });
260
104
  }
261
105
  else if (e.kind === "prompt.assembled") {
262
- // core 1.243(提示词主权批): who owned the constitution layer, per task. The
263
- // steady state for EVERY service scenario is "core" (structural constitution — our providers all
264
- // return role bases only). Anything else is worth eyes: "provider-assembled" = a pre-1.243-shaped
265
- // provider tripped the migration guard (upgrade it); "replaced" = an explicit replaceAll opt-out
266
- // (none exist in this repo today); "legacy" = a deprecated free-form `system` provider. Alert on
267
- // `prompt_constitution_total{mode!="core"} > 0` — the "TOC lost its safety blocks and called it a
268
- // config issue" failure class becomes a one-glance dashboard check.
269
106
  metrics.inc("prompt_constitution_total", { mode: e.constitution });
270
- // [998]② durable half: record the whitelisted manifest for the run lifecycle to drain into a
271
- // `prompt_assembled` event (turns/stream). register-gated (top-level runs only), sync, never persists here.
272
107
  promptManifests?.record(e.taskId, promptManifestRecordOf(e));
273
108
  }
274
109
  else if (e.kind === "config.assembled") {
275
- // [1301]③:config catalog 观测半场——生效配置逐字段来源(default/spec/derived/host-declared+
276
- // overrideReasons)随任务进 durable 事件日志(config_assembled),「谁设的 80」读帧不考古。
277
110
  const rec = configAssembledRecordOf(e);
278
111
  if (rec)
279
112
  promptManifests?.recordConfig(e.taskId, rec);
280
113
  }
281
114
  else if (e.kind === "compaction.phase_timings") {
282
- // codex F6: core 1.289's `compaction.phase_timings` fires on SUCCESS (per-phase timing telemetry) — it is
283
- // NOT an outcome, so counting it into compaction_events_total would add a fake "phase_timings" failure
284
- // outcome for every successful compaction (poisoning the "non-success outcomes" dashboard semantics
285
- // documented below). Record the total duration on the existing observe face instead (the
286
- // brain_call_latency_ms precedent); non-finite values are dropped (tracer never throws, never poisons).
287
- // 三路复审修5 核查(workflow W5 报「histogram never」):duration 观测出口就在下一行(F6 起已在),
288
- // event 契约(core trace.d.ts)带 `durationMs: number`,budget.test.ts 咬合断言 observe 只进
289
- // compaction_duration_ms 不进失败计数器 —— W5 判 STALE,零改。per-phase 明细走 compacted 事件的
290
- // phaseDurations(trace/project.ts compactedEventData)上 wire,不在 /metrics 重复开面(有界纪律)。
291
115
  const d = e.durationMs;
292
116
  if (typeof d === "number" && Number.isFinite(d))
293
117
  metrics.observe("compaction_duration_ms", d);
294
118
  }
295
119
  else if (e.kind.startsWith("compaction.")) {
296
- // core 1.257(压缩健康面): every NON-SUCCESS compaction outcome
297
- // now emits a `compaction.*` trace frame (failed / mooted / blocked / noop / disabled / suppressed +
298
- // the input_truncated disclosure and announce_dropped). Before these frames the only evidence of a
299
- // failing compactor was `onError(phase:"compaction")` log lines — no aggregate, so "3 silent auto
300
- // failures opened the breaker and manual verbs drain mooted" (the exact failure fingerprint)
301
- // was invisible on a dashboard. Count by outcome+trigger: `compaction_events_total{outcome="failed"}`
302
- // rising = the summarizer is failing; sustained `mooted`/`blocked` explains "202 → no compacted
303
- // event" without log spelunking. Labels are bounded: 8 kinds × 3 triggers (frames without a trigger
304
- // — noop/disabled/mooted/input_truncated/announce_dropped — label "none"). SUCCESS is deliberately
305
- // not counted here: `compacted` is a TaskEvent riding the run stream, not a trace frame.
306
120
  metrics.inc("compaction_events_total", { outcome: e.kind.slice("compaction.".length), trigger: e.trigger ?? "none" });
307
121
  }
308
122
  else if (e.kind === "permission.persisted_rule_allowed" || e.kind === "permission.rule_store_unreadable") {
309
- // 🔴 [3372] core 5.23.0 提货批,codex 复审 round2 [medium](验真后修)。首版把这两帧登记成
310
- // 「丢弃,已有等价信号」——**那条理由是假的**(亲验:`plugins/permission-rule-store-sql.ts` 里既没有
311
- // `noteStoreError` 也没有 `onCorruptRead`),而它们又是**真可达**的(#154 车二把规则店接上了)。
312
- // 一条假理由比没有理由更坏:台账的全部价值就在于它写的话是真的。
313
- //
314
- // 两帧各自答一个**别处答不出来**的问题,而且都在安全轴上:
315
- // · `persisted_rule_allowed` —— design/179 明写它是 allow 侧的**归因通道**:「这次为什么没问我?」
316
- // `permission_denied_total` 的补集答不了(它连「有没有走规则」都不知道),`tool_trace` 是默认关
317
- // 的诊断旋钮、也不带规则归因。持久规则店是本装配里**唯一的放宽缝**,它每放行一次都该有账。
318
- // · `rule_store_unreadable` —— 规则店读不出 ⇒ 该次调用按**零规则**裁决(诚实地问,而不是假装空店)。
319
- // 后果是一场**审批风暴**:运维看到的是「卡突然变多」,而病因在店上。没有这个计数就只能靠猜。
320
- // 标签有界:`event` 二取一(闭集,由上面的 `===` 判别式本身封闭)。规则原文/店的错误句**不进标签**
321
- // (前者是用户数据、后者无界)——它们该去结构化日志,不是 Prometheus 基数。
322
123
  metrics.inc("permission_rule_events_total", { event: e.kind.slice("permission.".length) });
323
124
  }
324
- // ── 未命中 = 该帧不进 /metrics ────────────────────────────────────────────────────────────────
325
- // 这条 if/else 链没有 else:没命中的 `TraceEvent` 就地丢弃。对多数 kind 这是对的(计量面不该给每个
326
- // 诊断帧开计数器),但「丢」必须是**裁定**而不是漏接。[3372](core 5.23.0 一次给安全轴加了三个
327
- // `permission.*` 帧)之后,该族的逐条处置记在 `trace/core-keyset-guard.ts` 的 ⑦ 面并由 tsc 守着:
328
- // core 再加一个 `permission.*` 帧,那里编译期先红,逼人裁「接还是不接」。其余族(compaction/brain/
329
- // task/…)仍按本链的显式分支处置,无门。
330
125
  };
331
126
  }
332
- /**
333
- * [1469] side-query 记账 seam(codex R3 high:sideQuery 不走 core tracer——brain.stream 直调不发
334
- * brain.call——createTracer 的四路 sink(costQuota/model_cost 指标/fleetUsage 批报/fleetLease 本地扣减)
335
- * 全部旁路)。本 seam 与 createTracer 的 brain.call 臂**同语义**喂同一组 sink:weightedTokens 同式
336
- * (tokens×quotaWeight,weight-at-burn 固化)、micro-USD 同轴。装配点与 createTracer 同一处(main.ts),
337
- * 传同一组实例——两臂永不分家。
338
- *
339
- * 已知残差(codex R3 medium,记档):cost 源=core SideQueryResult.usage.cost.total(brain 层计价),
340
- * OpenAI cache-heavy 输入按全价入账(tracer 的 e.costMicroUsd 是 cache-family-aware 价)——方向=多收
341
- * (保守,绝不少收);根修=core 在 SideQueryResult 上回权威 costMicroUsd(已提货 core)。model 标签/
342
- * weight 键用**路由身份 r.model**(池配置有界)非 servedModel(provider 上报,无界)。
343
- */
344
- export function createSideQueryAccountant(metrics, costQuota,
345
- // 依赖面收窄到真消费的方法(§X 同 ModelKeyRef 一式):全形对象照传(结构超集),测试桩零铸型。
346
- fleetUsage, fleetLease, quotaWeightFor) {
127
+ export function createSideQueryAccountant(metrics, costQuota, fleetUsage, fleetLease, quotaWeightFor) {
347
128
  return (principal, r) => {
348
129
  const input = typeof r.usage?.input === "number" ? r.usage.input : 0;
349
130
  const output = typeof r.usage?.output === "number" ? r.usage.output : 0;
350
131
  const cacheRead = typeof r.usage?.cacheRead === "number" ? r.usage.cacheRead : 0;
351
132
  const cacheWrite = typeof r.usage?.cacheWrite === "number" ? r.usage.cacheWrite : 0;
352
- // #257(codex #256-R3 high 定谳):core 在 side-query 出口已把 `usage.input` **归一为 uncached 量**
353
- // ——对每个 provider 族(side-query.d.ts:「`input` NORMALIZED to the uncached prompt quantity …
354
- // for EVERY provider family」,与 run loop 的 usage-accounting 同一道门)。所以配额轴折算恒为
355
- // input + cacheRead + cacheWrite,**不再按 cache family 分臂**:曾经的
356
- // `family === "input-includes-cached" ? input : …` 分支建立在「OpenAI 族 input 仍含 cache」这个
357
- // 已被 core 出口归一推翻的前提上,每次 cache 命中都把 OpenAI 族配额/lease 少收 cacheRead+cacheWrite
358
- // (红先钉 = budget.test「#257 OpenAI 族 cache 命中不许少计」格)。family 判定链
359
- // (cacheFamilyOfMirror + catalog 快照)随本修整体退役——LEDGER 的 cacheFamilyOf/CacheFamily
360
- // 两行同步改 skip(消费点消失,理由=core 出口归一)。
361
133
  const promptTokens = input + cacheRead + cacheWrite;
362
- // RB-368/#59 同判据(createTracer 上方注释同款纪律):`usage.cost.total` 整键缺席 = 未知(unpriced 部署的
363
- // side query 真形),不是「花了 $0」——折 0 就是把「不知道」编造成「免费」。已知(哪怕显式 0)才带键。
364
134
  const costTotal = typeof r.usage?.cost?.total === "number" ? r.usage.cost.total : undefined;
365
135
  const micro = costTotal !== undefined ? Math.round(costTotal * 1_000_000) : undefined;
366
136
  if (micro !== undefined && micro > 0) {
@@ -372,20 +142,15 @@ fleetUsage, fleetLease, quotaWeightFor) {
372
142
  const qw = typeof qwRaw === "number" && Number.isFinite(qwRaw) && qwRaw > 0 ? qwRaw : 1;
373
143
  const weightedTokens = Math.round((promptTokens + output) * qw);
374
144
  if (weightedTokens > 0 || (micro !== undefined && micro > 0)) {
375
- fleetUsage?.record(principal, r.model, "", { inputTokens: promptTokens, outputTokens: output, ...(micro !== undefined ? { costMicroUsd: micro } : {}), weightedTokens, quotaWeightAtUse: qw }); // taskId ""(无 run 语义;accumulator 对 "" 不计 runsSeen);inputTokens=归一化总输入含 cache(fleet-bus 同口径);RB-368 透传缺席
376
- fleetLease?.recordSpend(principal, { weightedTokens, ...(micro !== undefined ? { costMicroUsd: micro } : {}) }); // RB-368 透传缺席
145
+ fleetUsage?.record(principal, r.model, "", { inputTokens: promptTokens, outputTokens: output, ...(micro !== undefined ? { costMicroUsd: micro } : {}), weightedTokens, quotaWeightAtUse: qw });
146
+ fleetLease?.recordSpend(principal, { weightedTokens, ...(micro !== undefined ? { costMicroUsd: micro } : {}) });
377
147
  }
378
148
  };
379
149
  }
380
- /**
381
- * ⑤ Resolve a per-task budget: honor the caller's requested value but never let it EXCEED the operator
382
- * ceiling. `0`/unset ceiling = no limit (returns the caller's value, or undefined). A non-positive /
383
- * non-numeric request is ignored (falls back to the ceiling). Returns undefined = "no budget gate".
384
- */
385
150
  export function cappedCeiling(requested, ceiling) {
386
151
  const req = typeof requested === "number" && requested > 0 ? requested : undefined;
387
152
  if (!ceiling || ceiling <= 0)
388
- return req; // no operator ceiling → honor caller (or none)
389
- return req === undefined ? ceiling : Math.min(req, ceiling); // cap caller down to the ceiling
153
+ return req;
154
+ return req === undefined ? ceiling : Math.min(req, ceiling);
390
155
  }
391
156
  //# sourceMappingURL=budget.js.map
@@ -1,7 +1,4 @@
1
1
  import { Type } from "typebox";
2
- /** Current time — the model has no clock; OA/most tasks need it. No side effects. CC-parity tool name `Now`
3
- * (PascalCase, core 1.176 first-party rename wrap-up — the SERVICE owns this one; `nowTool` is not a core tool).
4
- * effect:"read" so it never suspends → no durable-resume rename strand (no pending-tool checkpoint to canonicalize). */
5
2
  export function nowTool() {
6
3
  return {
7
4
  name: "Now",
@@ -1,41 +1,16 @@
1
- /**
2
- * plugins 域 worker 消费半场(契约冻结 board [1361]②/[1362] 四收紧/[1366]①;2026-07-28 开工车)。
3
- *
4
- * center 只存"插件从哪来"(PluginSpec 引用,永不承载可执行字节);worker 半场 = 把引用物化成本地
5
- * 目录并把其中的 **skills** 装进 baseline(v1 装载面;commands 由 cli 壳消费——board [1363] 即 cli
6
- * 答的命令契约稿,worker 侧本无命令面,这是分工不是降级)。挂 applyCenterSkills 同 reconcile 周期
7
- * (boot 期,buildScenarios 之前)。
8
- *
9
- * 契约条款落点(与 test/center-plugins.test.ts 逐条对应):
10
- * - https-only + `PLUGINS_ALLOW_HOSTS`(缺省 github.com):{@link validatePluginDecl}。registry-core
11
- * 0.10.23 已在 schema 层单真源拒非 https,worker 侧**仍复验**——纵深:wire 可能来自旧 center。
12
- * - sha 全 40hex + checkout 强制:{@link gitClonePlan}(有 sha = init+fetch-by-sha+checkout,不信
13
- * 分支名也不拉全史;无 sha = `--depth 1` HEAD)。
14
- * - clone 姿势隔离:GIT_CONFIG_GLOBAL/SYSTEM=/dev/null + GIT_TERMINAL_PROMPT=0(无凭据仓 fail 而非
15
- * 挂交互 prompt)。私仓凭据走各机自己的 credential helper,永不入声明。
16
- * - manifest 路径逃逸(../ /绝对路径/symlink 出根)= 整条拒装:{@link resolvePluginSkillsRoot}
17
- * (realpath 后前缀判定)。
18
- * - 撞名让位(收紧④):plugin 的 skill 与 baseline(builtin/center 直发)同名 ⇒ **plugin 让位**
19
- * (跳过+letGo 留痕)。⚠️ 方向与 applyCenterSkills 的 center-wins 相反——center 直发 skills 过了
20
- * publisher RBAC+发布审批,plugin 内容只是 git 仓字节,信任级不同,让位方向因此相反。
21
- * - fail-loud 单条不连坐 + LKG(上次成功物化留盘,拉取失败沿用):{@link applyCenterPlugins}。
22
- */
23
1
  import { execFile } from "node:child_process";
24
2
  import { promisify } from "node:util";
25
3
  import { promises as fsp, realpathSync, existsSync, readFileSync, lstatSync, statSync } from "node:fs";
26
4
  import { join, resolve, isAbsolute, sep } from "node:path";
27
5
  import { assertConfined, loadSkills } from "./skills.js";
28
6
  const execFileP = promisify(execFile);
29
- /** 缺省 clone host 白名单(契约条款 1;国内镜像走本名单加 https 镜像域,不开 http 口)。 */
30
7
  export const DEFAULT_PLUGIN_ALLOW_HOSTS = ["github.com"];
31
- /** 条款 1/2/4:https-only、host 全等白名单、sha 40hex。纯函数,worker 侧纵深复验(不信任 wire 已验过)。 */
32
8
  export function validatePluginDecl(decl, allowHosts) {
33
9
  const name = typeof decl.name === "string" && decl.name !== "" ? decl.name : "(unnamed)";
34
10
  if (decl.enabled === false)
35
11
  return { ok: false, skip: true, name, reason: "enabled:false" };
36
12
  const kind = decl.source?.kind;
37
13
  if (kind === "marketplace") {
38
- // v1 暂缓:marketplace 物化(add-by-url + install-by-name)是 v2 面;skip 不连坐,warn 留痕。
39
14
  return { ok: false, skip: true, name, reason: "marketplace source: not materialized in v1 (skills lane ships git-form first)" };
40
15
  }
41
16
  if (kind !== "git")
@@ -51,7 +26,6 @@ export function validatePluginDecl(decl, allowHosts) {
51
26
  catch {
52
27
  return { ok: false, name, reason: "https url does not parse" };
53
28
  }
54
- // host 全等匹配——evil-github.com / github.com.evil.io 都不隐含放行。
55
29
  if (!allowHosts.some((h) => h.toLowerCase() === host)) {
56
30
  return { ok: false, name, reason: `host ${host} not in PLUGINS_ALLOW_HOSTS (default github.com; extend with a comma-separated https mirror domain list)` };
57
31
  }
@@ -63,17 +37,15 @@ export function validatePluginDecl(decl, allowHosts) {
63
37
  }
64
38
  return { ok: true, name, url };
65
39
  }
66
- /** 条款 3/5:clone 计划(argv 序列 + 隔离 env)。纯数据,让"姿势"可被钉死。 */
67
40
  export function gitClonePlan(o) {
68
41
  const env = {
69
- GIT_CONFIG_GLOBAL: "/dev/null", // 收紧③:宿主 git 配置(insteadOf/credential/hooks)一概不继承
42
+ GIT_CONFIG_GLOBAL: "/dev/null",
70
43
  GIT_CONFIG_SYSTEM: "/dev/null",
71
- GIT_TERMINAL_PROMPT: "0", // 无凭据仓 ⇒ fail-loud,绝不挂在交互 prompt 上
44
+ GIT_TERMINAL_PROMPT: "0",
72
45
  };
73
46
  if (o.sha === undefined) {
74
47
  return { steps: [["clone", "--depth", "1", "--quiet", o.url, o.dest]], env };
75
48
  }
76
- // 有 sha:init + fetch-by-sha + checkout —— 不信分支名、不拉全史;fetch 不到该 sha = 该条失败。
77
49
  return {
78
50
  steps: [
79
51
  ["init", "--quiet", o.dest],
@@ -83,25 +55,17 @@ export function gitClonePlan(o) {
83
55
  env,
84
56
  };
85
57
  }
86
- /** manifest 本体尺寸上限(#97 redesign⑥ V2-⑦:**新的本地输入上限**,非既有格式事实——manifest 只被
87
- * 消费一个 `skillsPath` 字段,几行 JSON;超界视为畸形输入整条拒装)。 */
88
58
  export const PLUGIN_MANIFEST_MAX_BYTES = 64 * 1024;
89
- /** 条款 5 的 manifest 半场:`.claude-plugin/plugin.json` 的 `skillsPath`(缺省 `skills/`;cli plugin
90
- * 布局同族)。返回 ok:true+dir:undefined = 插件没有 skills 目录(可以只有 commands——那是 cli 壳的
91
- * 半场,worker 装 0 个 skill 不算失败);ok:false = 路径逃逸/manifest 本体畸形,整条拒装。
92
- * manifest 臂分立(V2-⑦,原 broad-catch 是 fail-open):**不存在**=可选面,缺省 skills/;**存在但**
93
- * 本体逃逸根外/非普通文件/超界/读失败/JSON 坏 = ok:false 走既有 warning 面。 */
94
59
  export function resolvePluginSkillsRoot(cloneRoot) {
95
60
  let skillsPath = "skills";
96
61
  const rootReal = realpathSync(cloneRoot);
97
62
  const manifestPath = join(cloneRoot, ".claude-plugin", "plugin.json");
98
63
  let manifestPresent = false;
99
64
  try {
100
- lstatSync(manifestPath); // 条目在场判定(lstat:悬空 symlink 也算在场——它该走拒绝臂,不是缺席臂)
65
+ lstatSync(manifestPath);
101
66
  manifestPresent = true;
102
67
  }
103
68
  catch {
104
- /* 无 manifest ⇒ 缺省 skills/(manifest 是可选面) */
105
69
  }
106
70
  if (manifestPresent) {
107
71
  try {
@@ -123,21 +87,17 @@ export function resolvePluginSkillsRoot(cloneRoot) {
123
87
  if (isAbsolute(skillsPath))
124
88
  return { ok: false, reason: `skillsPath must be relative (got ${skillsPath})` };
125
89
  const candidate = resolve(rootReal, skillsPath);
126
- // 先做词法前缀判定(../ 逃逸在 realpath 之前就拒——目标可能不存在)
127
90
  if (candidate !== rootReal && !candidate.startsWith(rootReal + sep)) {
128
91
  return { ok: false, reason: `skillsPath escapes the plugin root (${skillsPath})` };
129
92
  }
130
93
  if (!existsSync(candidate))
131
94
  return { ok: true };
132
- // symlink 出根:对存在的目标 realpath 再判一次
133
95
  const real = realpathSync(candidate);
134
96
  if (real !== rootReal && !real.startsWith(rootReal + sep)) {
135
97
  return { ok: false, reason: `skillsPath resolves outside the plugin root via symlink (${skillsPath})` };
136
98
  }
137
99
  return { ok: true, dir: candidate };
138
100
  }
139
- /** 总入口(条款 3/7/8):逐条物化 + 装载 + 让位合并。materialize 盘位 = `<cacheRoot>/<name>/live`
140
- * (成功后原子 rename 替换,失败保留旧盘位 = LKG)。 */
141
101
  export async function applyCenterPlugins(baseline, eff, opts) {
142
102
  const declared = Array.isArray(eff.plugins)
143
103
  ? (eff.plugins)
@@ -150,7 +110,7 @@ export async function applyCenterPlugins(baseline, eff, opts) {
150
110
  const runGit = opts.runGit ?? (async (args, env) => {
151
111
  await execFileP("git", args, { env: { ...process.env, ...env }, timeout: 120_000, maxBuffer: 4 * 1024 * 1024 });
152
112
  });
153
- const taken = new Set(baseline.map((s) => s.spec.name)); // 让位判据:baseline(builtin+center 直发)恒赢
113
+ const taken = new Set(baseline.map((s) => s.spec.name));
154
114
  for (const decl of declared) {
155
115
  const v = opts.skipValidate
156
116
  ? { ok: true, name: String(decl.name), url: String(decl.source?.url), ...(typeof decl.sha === "string" ? { sha: decl.sha } : {}) }
@@ -165,14 +125,12 @@ export async function applyCenterPlugins(baseline, eff, opts) {
165
125
  continue;
166
126
  }
167
127
  const liveDir = join(opts.cacheRoot, v.name, "live");
168
- // ── 物化:clone 到 staging,成功后原子替换 live;失败保留旧 live = LKG ──
169
128
  const staging = join(opts.cacheRoot, v.name, `staging-${process.pid}`);
170
129
  let materialized = false;
171
130
  try {
172
131
  await fsp.rm(staging, { recursive: true, force: true });
173
132
  await fsp.mkdir(staging, { recursive: true });
174
133
  const plan = gitClonePlan({ url: v.url, dest: staging, sha: v.sha });
175
- // clone 目标目录已建:单步 clone 允许空目录;init 形对空目录同样成立。
176
134
  for (const step of plan.steps)
177
135
  await runGit(step, plan.env);
178
136
  await fsp.rm(liveDir, { recursive: true, force: true });
@@ -183,7 +141,6 @@ export async function applyCenterPlugins(baseline, eff, opts) {
183
141
  catch (err) {
184
142
  await fsp.rm(staging, { recursive: true, force: true }).catch(() => undefined);
185
143
  if (existsSync(liveDir)) {
186
- // LKG:拉取失败沿用上次成功物化(applyCenterSkills 同款语义)。
187
144
  opts.logger?.warn("center_plugin_fetch_failed_lkg", { plugin: v.name, error: err instanceof Error ? err.message : String(err) });
188
145
  out.lkgUsed.push(v.name);
189
146
  }
@@ -193,7 +150,6 @@ export async function applyCenterPlugins(baseline, eff, opts) {
193
150
  continue;
194
151
  }
195
152
  }
196
- // ── 装载:skills 目录(路径逃逸=整条拒装;fail-loud 单条不连坐)──
197
153
  try {
198
154
  const rootCheck = resolvePluginSkillsRoot(liveDir);
199
155
  if (!rootCheck.ok) {
@@ -202,11 +158,10 @@ export async function applyCenterPlugins(baseline, eff, opts) {
202
158
  continue;
203
159
  }
204
160
  if (!rootCheck.dir)
205
- continue; // 无 skills 目录:合法(commands-only 插件,cli 壳的半场)
206
- const loaded = loadSkills(rootCheck.dir, { confineTo: liveDir }); // 不可信 lane:逐条目约束在 clone root 内(根守卫只守了 skills 根,根下 symlink 可逃逸)
161
+ continue;
162
+ const loaded = loadSkills(rootCheck.dir, { confineTo: liveDir });
207
163
  for (const skill of loaded) {
208
164
  if (taken.has(skill.spec.name)) {
209
- // 收紧④:plugin 让位(方向与 center-wins 相反,理由见模块顶注)。
210
165
  out.letGo.push({ plugin: v.name, skill: skill.spec.name });
211
166
  opts.logger?.warn("center_plugin_skill_shadowed", { plugin: v.name, skill: skill.spec.name, note: "name taken by builtin/center skill — plugin yields (contract tightening-4)" });
212
167
  continue;