okengine 0.21.1 → 0.23.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 (342) hide show
  1. package/AGENTS.md +11 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +54 -0
  6. package/package.json +53 -38
  7. package/site/content/docs/ai/mcp.mdx +13 -9
  8. package/site/content/docs/ai/skills.mdx +2 -2
  9. package/site/content/docs/client/calling.mdx +27 -19
  10. package/site/content/docs/client/index.mdx +1 -1
  11. package/site/content/docs/elements/ai/agents.mdx +61 -13
  12. package/site/content/docs/elements/ai/decide.mdx +536 -0
  13. package/site/content/docs/elements/ai/events.mdx +373 -0
  14. package/site/content/docs/elements/ai/index.mdx +9 -3
  15. package/site/content/docs/elements/ai/meta.json +1 -1
  16. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  17. package/site/content/docs/elements/channel/email.mdx +1 -1
  18. package/site/content/docs/elements/channel/index.mdx +1 -1
  19. package/site/content/docs/elements/flow/http.mdx +4 -4
  20. package/site/content/docs/elements/flow/index.mdx +3 -3
  21. package/site/content/docs/elements/flow/meta.json +1 -1
  22. package/site/content/docs/elements/gate/auth.mdx +4 -1
  23. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  24. package/site/content/docs/elements/store/index.mdx +1 -1
  25. package/site/content/docs/elements/store/kv.mdx +5 -5
  26. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  27. package/site/content/docs/plugins/otp.mdx +31 -31
  28. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  29. package/site/content/docs/recipes/index.mdx +6 -6
  30. package/site/content/docs/recipes/mailpit.mdx +1 -1
  31. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  32. package/site/content/docs/recipes/pgdog.mdx +1 -1
  33. package/site/content/docs/recipes/postgres.mdx +12 -8
  34. package/site/content/docs/recipes/redis.mdx +4 -4
  35. package/site/content/docs/reference/cli.mdx +2 -1
  36. package/site/content/docs/reference/configuration.mdx +11 -9
  37. package/site/content/docs/reference/errors.mdx +12 -2
  38. package/site/content/docs/reference/fx.mdx +35 -21
  39. package/site/content/docs/reference/idempotency.mdx +192 -0
  40. package/site/content/docs/reference/index.mdx +6 -1
  41. package/site/content/docs/reference/meta.json +2 -1
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  44. package/site/content/docs/understand/the-architecture.mdx +14 -12
  45. package/site/content/docs/understand/try-it.mdx +3 -3
  46. package/src/auth/api-key-sql.test.ts +44 -0
  47. package/src/auth/api-key-sql.ts +1 -0
  48. package/src/auth/api-keys.ts +30 -12
  49. package/src/auth/config.ts +6 -2
  50. package/src/auth/gate-auth.test.ts +20 -0
  51. package/src/cli/agents-md.test.ts +25 -0
  52. package/src/cli/ask-vault-gaps.test.ts +38 -1
  53. package/src/cli/ask-vault-gaps.ts +35 -2
  54. package/src/cli/competitor-mention-removal.test.ts +3 -3
  55. package/src/cli/decide.ts +174 -0
  56. package/src/cli/decision-lock-watch.test.ts +45 -0
  57. package/src/cli/decision-lock-watch.ts +51 -0
  58. package/src/cli/dev-app-runner.ts +34 -8
  59. package/src/cli/dev-auth-secret.test.ts +49 -0
  60. package/src/cli/dev-auth-secret.ts +49 -0
  61. package/src/cli/dev-hot.test.ts +41 -0
  62. package/src/cli/dev-hot.ts +48 -0
  63. package/src/cli/dev.test.ts +1 -1
  64. package/src/cli/dev.ts +24 -6
  65. package/src/cli/docker-cli.test.ts +1 -1
  66. package/src/cli/eval.ts +142 -1
  67. package/src/cli/index.ts +14 -1
  68. package/src/cli/load-config.images.test.ts +12 -12
  69. package/src/cli/load-config.ts +2 -2
  70. package/src/cli/mcp-from-console.ts +48 -0
  71. package/src/cli/registry.ts +35 -1
  72. package/src/cli/vault-cmd.test.ts +23 -0
  73. package/src/cli/vault-cmd.ts +7 -0
  74. package/src/client/agent.ts +307 -0
  75. package/src/client/create.ts +18 -1
  76. package/src/client/transport.test.ts +245 -0
  77. package/src/client/transport.ts +133 -19
  78. package/src/client/types.ts +29 -3
  79. package/src/client-react/index.ts +7 -1
  80. package/src/client-react/use-agent-run.test.ts +271 -0
  81. package/src/client-react/use-agent-run.ts +193 -0
  82. package/src/compiler/ai-approval.test.ts +36 -0
  83. package/src/compiler/ai-repair.test.ts +22 -0
  84. package/src/compiler/decisions.extract.test.ts +164 -0
  85. package/src/compiler/effects-infer.ts +753 -17
  86. package/src/compiler/extract.test.ts +37 -0
  87. package/src/compiler/extract.ts +381 -24
  88. package/src/compiler/fixtures/skyport.expected.json +24 -0
  89. package/src/compiler/fx-follow.test.ts +164 -0
  90. package/src/compiler/fx-index.ts +399 -0
  91. package/src/compiler/response.ts +35 -2
  92. package/src/config/index.ts +21 -3
  93. package/src/console/server/ai-runs-flows.ts +269 -0
  94. package/src/console/server/ai-runs.test.ts +323 -0
  95. package/src/console/server/ai-runs.ts +562 -0
  96. package/src/console/server/ai.ts +12 -1
  97. package/src/console/server/app.ts +10 -1
  98. package/src/console/server/decisions-flows.ts +157 -0
  99. package/src/console/server/decisions.test.ts +66 -0
  100. package/src/console/server/decisions.ts +229 -0
  101. package/src/console/server/flows-invoke.test.ts +2 -2
  102. package/src/console/server/flows.ts +50 -2
  103. package/src/console/server/invoke-user-flow.ts +9 -0
  104. package/src/console/server/runs-ingest.ts +16 -0
  105. package/src/console/server/serve.ts +5 -0
  106. package/src/console/server/state.ts +25 -1
  107. package/src/console/server/store-stats.test.ts +47 -0
  108. package/src/console/server/store-stats.ts +92 -18
  109. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  110. package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
  111. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
  112. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
  113. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
  114. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  115. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  116. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  117. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  118. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  119. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  120. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  121. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  122. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  123. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  124. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
  125. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
  126. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  127. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  128. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  129. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
  130. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  131. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  132. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  133. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  134. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  135. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  136. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  137. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  138. package/src/console/ui-next/dist/index.html +4 -3
  139. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  140. package/src/console/ui-next/src/client.ts +224 -1
  141. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  142. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  143. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  144. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  145. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  146. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  147. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  148. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  149. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  150. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  151. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  152. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  153. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  154. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  155. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  156. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  157. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  158. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  159. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  160. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  161. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  162. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  163. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  164. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  165. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  166. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  167. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  168. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  169. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  171. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
  172. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  173. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  174. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  175. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  176. package/src/console/ui-next/src/router.tsx +17 -0
  177. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  178. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  179. package/src/console/ui-next/vite.config.ts +9 -0
  180. package/src/docker/docker.test.ts +13 -13
  181. package/src/docker/images-config.test.ts +12 -12
  182. package/src/docker/recipes/dragonfly.ts +3 -2
  183. package/src/docker/recipes/redis.ts +1 -1
  184. package/src/docker/stack-id.test.ts +1 -1
  185. package/src/drivers/ai-anthropic.ts +212 -21
  186. package/src/drivers/ai-mock.ts +34 -1
  187. package/src/drivers/ai-openai-compatible.ts +67 -26
  188. package/src/drivers/ai-preconnect.ts +18 -0
  189. package/src/drivers/ai-stream.test.ts +62 -0
  190. package/src/drivers/ai-types.ts +20 -0
  191. package/src/drivers/journal-postgres.ts +455 -3
  192. package/src/drivers/meilisearch.ts +2 -0
  193. package/src/drivers/memory-ddl.test.ts +18 -0
  194. package/src/drivers/memory.ts +21 -1
  195. package/src/drivers/pg-rls.test.ts +31 -0
  196. package/src/drivers/pg-rls.ts +15 -0
  197. package/src/drivers/postgres.test.ts +9 -0
  198. package/src/drivers/postgres.ts +49 -11
  199. package/src/drivers/vault-builtin.test.ts +10 -0
  200. package/src/drivers/vault-builtin.ts +13 -1
  201. package/src/drivers/vault-remote-bag.ts +5 -1
  202. package/src/drivers/vault-types.ts +6 -1
  203. package/src/elements/ai/agent-event-slot.ts +27 -0
  204. package/src/elements/ai/approval-http.ts +207 -0
  205. package/src/elements/ai/approval.test.ts +741 -0
  206. package/src/elements/ai/approval.ts +228 -0
  207. package/src/elements/ai/decisions/bind.ts +163 -0
  208. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  209. package/src/elements/ai/decisions/certificate.ts +377 -0
  210. package/src/elements/ai/decisions/certify.ts +314 -0
  211. package/src/elements/ai/decisions/decide.test.ts +953 -0
  212. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  213. package/src/elements/ai/decisions/export.test.ts +120 -0
  214. package/src/elements/ai/decisions/export.ts +146 -0
  215. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  216. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  217. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  218. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  219. package/src/elements/ai/decisions/http.test.ts +154 -0
  220. package/src/elements/ai/decisions/http.ts +240 -0
  221. package/src/elements/ai/decisions/labels.test.ts +282 -0
  222. package/src/elements/ai/decisions/labels.ts +257 -0
  223. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  224. package/src/elements/ai/decisions/openrouter.ts +36 -0
  225. package/src/elements/ai/decisions/provider.ts +102 -0
  226. package/src/elements/ai/decisions/typesafe.ts +36 -0
  227. package/src/elements/ai/declare.ts +252 -1
  228. package/src/elements/ai/events.test.ts +379 -0
  229. package/src/elements/ai/events.ts +138 -0
  230. package/src/elements/ai/run-events.test.ts +461 -0
  231. package/src/elements/ai/run-events.ts +522 -0
  232. package/src/elements/ai/runtime.ts +1186 -105
  233. package/src/elements/ai/stream-turn.ts +214 -0
  234. package/src/elements/ai/stream.live.test.ts +76 -0
  235. package/src/elements/ai/subagent.test.ts +136 -0
  236. package/src/elements/ai.test.ts +497 -3
  237. package/src/elements/ai.ts +16 -1
  238. package/src/elements/clock/durable.ts +27 -1
  239. package/src/elements/store/emit-drizzle.ts +2 -2
  240. package/src/elements/store/live-default.test.ts +2 -0
  241. package/src/elements/store/live-http.test.ts +1 -0
  242. package/src/elements/store/resource-list-docs.test.ts +2 -2
  243. package/src/elements/store/resource.test.ts +3 -3
  244. package/src/elements/store/schema-decl.test.ts +1 -0
  245. package/src/elements/store/sql-condition.ts +24 -7
  246. package/src/elements/store/sql-session.test.ts +51 -0
  247. package/src/elements/store/sql-session.ts +111 -1
  248. package/src/elements/store/upsert-app.test.ts +1 -1
  249. package/src/elements/vault/audit.test.ts +137 -0
  250. package/src/elements/vault/audit.ts +106 -13
  251. package/src/elements/vault/boot-chain.ts +9 -1
  252. package/src/elements/vault/builtin-adapter.ts +33 -3
  253. package/src/i18n/catalogs/ar.ts +9 -0
  254. package/src/i18n/catalogs/en.ts +9 -0
  255. package/src/index.ts +2 -0
  256. package/src/kernel/abort-scope.ts +33 -2
  257. package/src/kernel/agent-event-store.ts +448 -0
  258. package/src/kernel/app.ts +294 -19
  259. package/src/kernel/auto-cache.test.ts +6 -6
  260. package/src/kernel/boot-bind/ai.ts +3 -0
  261. package/src/kernel/boot-bind/store.test.ts +26 -0
  262. package/src/kernel/boot-bind/store.ts +82 -6
  263. package/src/kernel/boot-bind/vault.ts +1 -0
  264. package/src/kernel/boot.ts +34 -5
  265. package/src/kernel/builtin-errors.ts +10 -0
  266. package/src/kernel/capability.ts +3 -0
  267. package/src/kernel/client-descriptor.ts +1 -0
  268. package/src/kernel/concurrency.test.ts +33 -0
  269. package/src/kernel/decision-label-store.ts +441 -0
  270. package/src/kernel/effects-stamping.test.ts +2 -2
  271. package/src/kernel/effects.test.ts +2 -1
  272. package/src/kernel/effects.ts +13 -1
  273. package/src/kernel/element-registries.ts +3 -0
  274. package/src/kernel/errors-compiler.ts +20 -0
  275. package/src/kernel/errors-text.ts +4 -0
  276. package/src/kernel/errors.ts +2 -0
  277. package/src/kernel/external-effects.test.ts +36 -0
  278. package/src/kernel/flow.test.ts +2 -3
  279. package/src/kernel/flow.ts +24 -0
  280. package/src/kernel/fx-decide.ts +761 -0
  281. package/src/kernel/fx-fetch.ts +5 -1
  282. package/src/kernel/fx.test.ts +12 -5
  283. package/src/kernel/fx.ts +316 -29
  284. package/src/kernel/http-frame.test.ts +94 -0
  285. package/src/kernel/http-frame.ts +203 -0
  286. package/src/kernel/idempotency-store.ts +578 -0
  287. package/src/kernel/idempotency.test.ts +556 -0
  288. package/src/kernel/idempotency.ts +304 -0
  289. package/src/kernel/index.ts +1 -0
  290. package/src/kernel/journal.ts +160 -0
  291. package/src/kernel/json-result.ts +10 -0
  292. package/src/kernel/pipeline.test.ts +45 -0
  293. package/src/kernel/sse-id.ts +32 -0
  294. package/src/manifest/diff.ts +1 -0
  295. package/src/manifest/types.ts +37 -2
  296. package/src/mcp/ai-tools.test.ts +202 -0
  297. package/src/mcp/authorization.ts +66 -0
  298. package/src/mcp/session.ts +3 -0
  299. package/src/mcp/tools.ts +168 -0
  300. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  301. package/src/plugins/otp.test.ts +65 -1
  302. package/src/plugins/otp.ts +59 -7
  303. package/src/release/absolute-regression.test.ts +63 -0
  304. package/src/release/http-graph.test.ts +27 -0
  305. package/src/release/http-graph.ts +95 -0
  306. package/src/release/index.ts +3 -0
  307. package/src/release/limits.ts +17 -2
  308. package/src/release/measure.ts +144 -28
  309. package/src/runs/collect.ts +4 -1
  310. package/src/runs/parquet.test.ts +27 -1
  311. package/src/runs/parquet.ts +19 -0
  312. package/src/runs/types.ts +31 -0
  313. package/src/runtime/json-code-block.test.ts +106 -47
  314. package/src/runtime/json-code-block.ts +1209 -46
  315. package/src/term.test.ts +13 -0
  316. package/src/term.ts +81 -49
  317. package/src/test/create-test-app.test.ts +3 -1
  318. package/src/test/create-test-app.ts +40 -0
  319. package/src/test/live-signals.test.ts +1 -0
  320. package/src/test/provisions.integration.test.ts +1 -0
  321. package/src/test/tenant-isolation.test.ts +1 -0
  322. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  323. package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
  324. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  325. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
  326. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
  327. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
  328. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  329. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  330. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
  331. package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
  332. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  333. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  334. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  335. package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
  336. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  337. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  338. package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
  339. package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
  340. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
  341. package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
  342. package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
@@ -77,7 +77,8 @@ export interface OtpProviderModeOptions extends OtpBaseOptions {
77
77
  readonly channels?: never;
78
78
  /** Forbidden in provider mode — code never exists server-side. */
79
79
  readonly exposeDevOtp?: never;
80
- readonly resendCooldownMs?: never;
80
+ /** Spacing between provider SMS resends. Default 60s. */
81
+ readonly resendCooldownMs?: number;
81
82
  readonly from?: never;
82
83
  }
83
84
 
@@ -216,10 +217,7 @@ export function otp(opts: OtpOptions): PluginDef {
216
217
  const identities = resolveSharedIdentities(opts);
217
218
  const verifications = opts.verifications ?? createVerificationStore();
218
219
  const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
219
- const resendCooldownMs =
220
- opts.mode === "app"
221
- ? (opts.resendCooldownMs ?? DEFAULT_RESEND_COOLDOWN_MS)
222
- : DEFAULT_RESEND_COOLDOWN_MS;
220
+ const resendCooldownMs = opts.resendCooldownMs ?? DEFAULT_RESEND_COOLDOWN_MS;
223
221
  const channels = opts.mode === "app" ? opts.channels : ([] as const);
224
222
  const exposeDevOtp = opts.mode === "app" && opts.exposeDevOtp === true;
225
223
  const from = opts.mode === "app" ? opts.from : undefined;
@@ -288,8 +286,61 @@ export function otp(opts: OtpOptions): PluginDef {
288
286
  attempts: 0,
289
287
  phone,
290
288
  sealedOtp: null,
289
+ lastDeliveredAt: now,
290
+ lastChannel: "sms",
291
+ });
292
+ return { ok: true as const, channel: "sms" as const };
293
+ },
294
+ });
295
+
296
+ const resendContract = {
297
+ in: z.object({
298
+ phone: z.string().min(8),
299
+ lang: z.enum(["en", "ar"]).optional(),
300
+ channel: z.enum(["sms", "whatsapp", "email"]).optional(),
301
+ }),
302
+ out: requestOut,
303
+ errors: { AuthFailed, AuthRateLimited },
304
+ };
305
+
306
+ const resend = flow("auth.resendOtp", {
307
+ plane: "user",
308
+ effects: { sends: ["sms-otp"] },
309
+ do: async (input, fx) => {
310
+ if (input.channel !== undefined && input.channel !== "sms") {
311
+ return fail("AuthFailed", { reason: "invalid_channel" });
312
+ }
313
+ const phone = input.phone.trim();
314
+ if (!E164.test(phone)) return fail("AuthFailed", { reason: "invalid_phone" });
315
+ const now = runtime.now();
316
+ const row = findActiveVerification(verifications, `otp:${phone}`, now);
317
+ if (!row || row.sealedOtp) {
318
+ return fail("AuthFailed", { reason: "invalid_credentials" });
319
+ }
320
+ if (row.lastDeliveredAt !== undefined && now - row.lastDeliveredAt < resendCooldownMs) {
321
+ return fail("AuthFailed", { reason: "resend_cooldown" });
322
+ }
323
+
324
+ const requestId = okid();
325
+ await fx.sendOtp({
326
+ to: phone,
327
+ requestId,
328
+ ...(input.lang ? { lang: input.lang } : {}),
329
+ });
330
+ invalidateVerifications(verifications, `otp:${phone}`, now);
331
+ putVerification(verifications, {
332
+ id: okid(),
333
+ identifier: `otp:${phone}`,
334
+ value: `provider:${requestId}`,
335
+ expiresAt: now + ttlMs,
336
+ createdAt: now,
337
+ consumedAt: null,
338
+ attempts: 0,
339
+ phone,
340
+ sealedOtp: null,
341
+ lastDeliveredAt: now,
342
+ lastChannel: "sms",
291
343
  });
292
- // Provider mode: resend-via-different-channel is impossible — provider owns the code.
293
344
  return { ok: true as const, channel: "sms" as const };
294
345
  },
295
346
  });
@@ -373,7 +424,8 @@ export function otp(opts: OtpOptions): PluginDef {
373
424
  return plugin("otp", { version: "0.0.1", config: configSnapshot })
374
425
  .needs("auth")
375
426
  .binding(bindPublicAuth("/otp/request", request, "otp", requestContract))
376
- .binding(bindPublicAuth("/otp/verify", verify, "otp", verifyContract));
427
+ .binding(bindPublicAuth("/otp/verify", verify, "otp", verifyContract))
428
+ .binding(bindPublicAuth("/otp/resend", resend, "otp", resendContract));
377
429
  }
378
430
 
379
431
  // ── App mode ───────────────────────────────────────────────────────────
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Absolute budgets with room under the cap also fail at 2× the last value.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import {
7
+ absoluteBudgetOk,
8
+ absoluteRegressionCeiling,
9
+ formatBudgetsReport,
10
+ type BudgetsSnapshot,
11
+ } from "./measure.ts";
12
+ import {
13
+ ABSOLUTE_REGRESSION_RATIO,
14
+ CLIENT_BUDGET_BYTES,
15
+ COLD_START_BUDGET_MS,
16
+ CONSOLE_BUDGET_BYTES,
17
+ KERNEL_EDGE_BUDGET_BYTES,
18
+ } from "./limits.ts";
19
+
20
+ describe("absolute regression ceilings", () => {
21
+ test("ratio is 2× so a confirmed doubling fails and a single noisy run can be re-measured", () => {
22
+ expect(ABSOLUTE_REGRESSION_RATIO).toBe(2);
23
+ });
24
+
25
+ test("cold start and console initial load grow a ceiling; kernel, client, and a zero baseline do not", () => {
26
+ expect(absoluteRegressionCeiling(undefined, COLD_START_BUDGET_MS, "ms")).toBeUndefined();
27
+ expect(absoluteRegressionCeiling(0, 1, "ms")).toBeUndefined();
28
+ expect(absoluteRegressionCeiling(8.915, COLD_START_BUDGET_MS, "ms")).toBe(17.83);
29
+ expect(absoluteRegressionCeiling(353_621, CONSOLE_BUDGET_BYTES, "bytes")).toBe(707_242);
30
+ expect(absoluteRegressionCeiling(12_556, KERNEL_EDGE_BUDGET_BYTES, "bytes")).toBeUndefined();
31
+ expect(absoluteRegressionCeiling(4598, CLIENT_BUDGET_BYTES, "bytes")).toBeUndefined();
32
+ });
33
+
34
+ test("ok requires both the cap and the ceiling", () => {
35
+ expect(absoluteBudgetOk(8.915, COLD_START_BUDGET_MS, 17.83)).toBe(true);
36
+ expect(absoluteBudgetOk(17.83, COLD_START_BUDGET_MS, 17.83)).toBe(false);
37
+ expect(absoluteBudgetOk(20, COLD_START_BUDGET_MS, 17.83)).toBe(false);
38
+ expect(absoluteBudgetOk(80, COLD_START_BUDGET_MS, undefined)).toBe(false);
39
+ expect(absoluteBudgetOk(12_556, KERNEL_EDGE_BUDGET_BYTES, undefined)).toBe(true);
40
+ });
41
+
42
+ test("the CI log prints the 2× ceiling and the absolute cap", () => {
43
+ const snapshot: BudgetsSnapshot = {
44
+ measuredAt: "2026-09-23T00:00:00.000Z",
45
+ version: "0.0.0",
46
+ budgets: [
47
+ {
48
+ id: "coldStartMedianMs",
49
+ label: "Cold start on Bun",
50
+ value: 20,
51
+ limit: COLD_START_BUDGET_MS,
52
+ regressionLimit: 17.83,
53
+ unit: "ms",
54
+ gate: "absolute",
55
+ group: "core",
56
+ ok: false,
57
+ },
58
+ ],
59
+ };
60
+ const report = formatBudgetsReport(snapshot);
61
+ expect(report).toContain("[FAIL] Cold start on Bun: 20.000 ms < 17.830 ms (cap 75.000 ms)");
62
+ });
63
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `okengine/http` must not statically import heavy modules.
3
+ * Forbidden ids live in {@link HTTP_STATIC_GRAPH_FORBIDDEN}.
4
+ */
5
+
6
+ import { describe, expect, test } from "bun:test";
7
+ import { forbiddenHttpGraphHits, readStaticGraphSpecifiers } from "./http-graph.ts";
8
+
9
+ describe("okengine/http static import graph", () => {
10
+ test("zod and the hybrid-search runtime are not in the graph", async () => {
11
+ const specifiers = await readStaticGraphSpecifiers();
12
+ expect(specifiers.length).toBeGreaterThan(10);
13
+ expect(forbiddenHttpGraphHits(specifiers)).toEqual([]);
14
+ });
15
+
16
+ test("the forbidden list matches zod and search-runtime, and nothing nearby", () => {
17
+ expect(forbiddenHttpGraphHits(["node_modules/zod/index.js"])).toEqual(["zod"]);
18
+ expect(forbiddenHttpGraphHits(["zod"])).toEqual(["zod"]);
19
+ expect(forbiddenHttpGraphHits(["src/elements/store/search-runtime.ts"])).toEqual([
20
+ "hybrid-search runtime",
21
+ ]);
22
+ expect(forbiddenHttpGraphHits(["src/kernel/fx.ts"])).toEqual(["fx runtime"]);
23
+ expect(forbiddenHttpGraphHits(["src/kernel/fx-decide.ts"])).toEqual([]);
24
+ expect(forbiddenHttpGraphHits(["src/elements/store/search-lsh.ts"])).toEqual([]);
25
+ expect(forbiddenHttpGraphHits(["src/kernel/app.ts"])).toEqual([]);
26
+ });
27
+ });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Modules that must stay off the static `okengine/http` graph.
3
+ *
4
+ * Add a heavy module here when a cold-start regression pulls it in.
5
+ * One list — the import-graph test matches every entry.
6
+ */
7
+
8
+ /** One forbidden module on the `okengine/http` static graph. */
9
+ export interface HttpGraphForbidden {
10
+ /** Stable id. Printed when the build pulls this module in. */
11
+ readonly id: string;
12
+ /**
13
+ * True when `specifier` is this module.
14
+ * Specifiers are Bun metafile input paths and import paths, including externals.
15
+ *
16
+ * @param specifier - Input path or import specifier
17
+ */
18
+ readonly test: (specifier: string) => boolean;
19
+ }
20
+
21
+ /**
22
+ * Forbidden static imports for `okengine/http`.
23
+ * `zod` and the hybrid-search runtime (`search-runtime`) load on first use
24
+ * elsewhere; a static edge puts them on every cold start.
25
+ */
26
+ export const HTTP_STATIC_GRAPH_FORBIDDEN: readonly HttpGraphForbidden[] = [
27
+ {
28
+ id: "zod",
29
+ test: (specifier) =>
30
+ specifier === "zod" ||
31
+ specifier.startsWith("zod/") ||
32
+ specifier.includes("node_modules/zod/") ||
33
+ specifier.includes("node_modules/zod@"),
34
+ },
35
+ {
36
+ id: "hybrid-search runtime",
37
+ test: (specifier) =>
38
+ specifier.includes("search-runtime.ts") ||
39
+ specifier.includes("search-runtime.js") ||
40
+ /(^|\/)search-runtime$/.test(specifier),
41
+ },
42
+ {
43
+ id: "fx runtime",
44
+ test: (specifier) =>
45
+ specifier.endsWith("/fx.ts") || specifier.endsWith("/fx.js") || /(^|\/)fx$/.test(specifier),
46
+ },
47
+ ];
48
+
49
+ /** Metafile slice this gate reads. */
50
+ interface GraphMetafile {
51
+ readonly inputs: Readonly<
52
+ Record<string, { readonly imports?: readonly { readonly path: string }[] }>
53
+ >;
54
+ }
55
+
56
+ /**
57
+ * Ids from {@link HTTP_STATIC_GRAPH_FORBIDDEN} that appear in `specifiers`.
58
+ *
59
+ * @param specifiers - Bundled input paths and import specifiers
60
+ */
61
+ export function forbiddenHttpGraphHits(specifiers: readonly string[]): readonly string[] {
62
+ const hits: string[] = [];
63
+ for (const rule of HTTP_STATIC_GRAPH_FORBIDDEN) {
64
+ if (specifiers.some((specifier) => rule.test(specifier))) hits.push(rule.id);
65
+ }
66
+ return hits;
67
+ }
68
+
69
+ /**
70
+ * Static module specifiers for an entry, from a Bun metafile.
71
+ * Includes bundled inputs and every import path (external and dynamic).
72
+ *
73
+ * @param entry - Absolute path to the entry file
74
+ */
75
+ export async function readStaticGraphSpecifiers(
76
+ entry = `${import.meta.dir}/../http.ts`,
77
+ ): Promise<readonly string[]> {
78
+ const result = await Bun.build({
79
+ entrypoints: [entry],
80
+ target: "bun",
81
+ format: "esm",
82
+ metafile: true,
83
+ });
84
+ if (!result.success) {
85
+ throw new Error(`${entry} build failed:\n${result.logs.map(String).join("\n")}`);
86
+ }
87
+ const metafile = result.metafile as GraphMetafile | undefined;
88
+ if (!metafile) throw new Error(`${entry} build returned no metafile`);
89
+ const specifiers = new Set<string>();
90
+ for (const [input, info] of Object.entries(metafile.inputs)) {
91
+ specifiers.add(input);
92
+ for (const imp of info.imports ?? []) specifiers.add(imp.path);
93
+ }
94
+ return [...specifiers];
95
+ }
@@ -18,6 +18,7 @@ export {
18
18
  } from "./exports.ts";
19
19
 
20
20
  export {
21
+ ABSOLUTE_REGRESSION_RATIO,
21
22
  CLIENT_BUDGET_BYTES,
22
23
  COLD_START_BUDGET_MS,
23
24
  CONSOLE_BUDGET_BYTES,
@@ -28,6 +29,8 @@ export {
28
29
  } from "./limits.ts";
29
30
 
30
31
  export {
32
+ absoluteBudgetOk,
33
+ absoluteRegressionCeiling,
31
34
  budgetsPass,
32
35
  exportRegressionLimitBytes,
33
36
  formatBudgetsMarkdown,
@@ -7,8 +7,13 @@
7
7
  /** Kernel (edge profile) — gzipped minified bundle. */
8
8
  export const KERNEL_EDGE_BUDGET_BYTES = 17 * 1024;
9
9
 
10
- /** Client runtime — gzipped minified bundle. */
11
- export const CLIENT_BUDGET_BYTES = 5 * 1024;
10
+ /**
11
+ * Client runtime — gzipped minified bundle.
12
+ *
13
+ * 6 kB (was 5 kB). Idempotency inlines the okid alphabet; measured gzip is
14
+ * 5194, which sat over 90% of 5120.
15
+ */
16
+ export const CLIENT_BUDGET_BYTES = 6 * 1024;
12
17
 
13
18
  /** Console initial load — gzipped html + entry js/css (ui-next SPA, no panel-* split). */
14
19
  export const CONSOLE_BUDGET_BYTES = 700 * 1024;
@@ -27,3 +32,13 @@ export const EXPORT_REGRESSION_TOLERANCE_RATIO = 0.02;
27
32
 
28
33
  /** Minimum allowed growth (bytes) before a subpath export fails regression. */
29
34
  export const EXPORT_REGRESSION_TOLERANCE_FLOOR_BYTES = 256;
35
+
36
+ /**
37
+ * Growth multiple for an absolute budget that still has room under its cap.
38
+ *
39
+ * The sample fails when it reaches this multiple of the last committed value
40
+ * and that multiple is still below the absolute cap. Two is the line: one
41
+ * cold-start run has already printed a phantom jump of about this size, so
42
+ * the probe re-runs before failing instead of tightening the ratio.
43
+ */
44
+ export const ABSOLUTE_REGRESSION_RATIO = 2;
@@ -13,6 +13,7 @@ import {
13
13
  resolveExportBudgetTargets,
14
14
  } from "./exports.ts";
15
15
  import {
16
+ ABSOLUTE_REGRESSION_RATIO,
16
17
  CLIENT_BUDGET_BYTES,
17
18
  COLD_START_BUDGET_MS,
18
19
  CONSOLE_BUDGET_BYTES,
@@ -80,8 +81,18 @@ export interface BudgetSample {
80
81
  readonly label: string;
81
82
  /** Measured value. */
82
83
  readonly value: number;
83
- /** Upper bound used for `ok` (absolute cap or regression ceiling). */
84
+ /**
85
+ * Absolute cap, or the regression ceiling for export samples.
86
+ * Landing copy reads this as the published cap — do not replace it
87
+ * with {@link regressionLimit}.
88
+ */
84
89
  readonly limit: number;
90
+ /**
91
+ * Tighter growth ceiling (`ABSOLUTE_REGRESSION_RATIO` × last committed
92
+ * value) when that multiple is still under {@link limit}. Absent when
93
+ * the absolute cap is the only gate.
94
+ */
95
+ readonly regressionLimit?: number;
85
96
  /** Unit for display / snapshot. */
86
97
  readonly unit: "bytes" | "ms";
87
98
  /** Gate mode. */
@@ -277,7 +288,7 @@ if (!artifact) { console.error("no output"); process.exit(1); }
277
288
  const raw = await artifact.arrayBuffer();
278
289
  process.stdout.write(String(Bun.gzipSync(new Uint8Array(raw)).byteLength));
279
290
  `;
280
- const proc = Bun.spawn(["bun", "-e", probe], {
291
+ const proc = Bun.spawn([process.execPath, "-e", probe], {
281
292
  cwd: ROOT,
282
293
  stdout: "pipe",
283
294
  stderr: "pipe",
@@ -298,23 +309,52 @@ process.stdout.write(String(Bun.gzipSync(new Uint8Array(raw)).byteLength));
298
309
  }
299
310
 
300
311
  /**
301
- * Gzip size of the client runtime bundle.
312
+ * Gzip size of the client runtime bundle (one file, no split).
313
+ *
314
+ * If this build ever uses `splitting`, the gate must count the entry plus
315
+ * every chunk it imports statically. The entry file alone under-counts the
316
+ * first download when a shared chunk loads with it.
302
317
  */
303
318
  export async function measureClientGzipBytes(): Promise<number> {
304
- const entry = `${import.meta.dir}/../client/budget-entry.ts`;
305
- const result = await Bun.build({
306
- entrypoints: [entry],
307
- minify: true,
308
- target: "browser",
309
- format: "esm",
319
+ const entry = resolve(ROOT, "src/client/budget-entry.ts");
320
+ // `bun test` sets NODE_ENV=test. An in-process Bun.build then inlines that
321
+ // and the gzip drifts from `bun run budgets`. Spawn once, production, so
322
+ // both callers share this number.
323
+ const probe = `
324
+ const result = await Bun.build({
325
+ entrypoints: ${JSON.stringify([entry])},
326
+ minify: true,
327
+ target: "browser",
328
+ format: "esm",
329
+ });
330
+ if (!result.success) {
331
+ console.error(result.logs.map(String).join("\\n"));
332
+ process.exit(1);
333
+ }
334
+ const artifact = result.outputs[0];
335
+ if (!artifact) { console.error("no output"); process.exit(1); }
336
+ const raw = await artifact.arrayBuffer();
337
+ process.stdout.write(String(Bun.gzipSync(new Uint8Array(raw)).byteLength));
338
+ `;
339
+ const proc = Bun.spawn([process.execPath, "-e", probe], {
340
+ cwd: ROOT,
341
+ stdout: "pipe",
342
+ stderr: "pipe",
343
+ env: { ...process.env, NODE_ENV: "production" },
310
344
  });
311
- if (!result.success) {
312
- throw new Error(`client build failed:\n${result.logs.map(String).join("\n")}`);
345
+ const [stdout, stderr, exitCode] = await Promise.all([
346
+ new Response(proc.stdout).text(),
347
+ new Response(proc.stderr).text(),
348
+ proc.exited,
349
+ ]);
350
+ if (exitCode !== 0) {
351
+ throw new Error(`client build failed:\n${stderr || stdout}`);
313
352
  }
314
- const artifact = result.outputs[0];
315
- if (!artifact) throw new Error("client build produced no output");
316
- const raw = await artifact.arrayBuffer();
317
- return Bun.gzipSync(new Uint8Array(raw)).byteLength;
353
+ const n = Number(stdout.trim());
354
+ if (!Number.isFinite(n) || n <= 0) {
355
+ throw new Error(`client measure produced ${stdout}`);
356
+ }
357
+ return n;
318
358
  }
319
359
 
320
360
  /**
@@ -423,7 +463,7 @@ process.stdout.write(String(ms));
423
463
  `;
424
464
  const samples: number[] = [];
425
465
  for (let i = 0; i < 7; i++) {
426
- const proc = Bun.spawn(["bun", "-e", probe], {
466
+ const proc = Bun.spawn([process.execPath, "-e", probe], {
427
467
  cwd: ROOT,
428
468
  stdout: "pipe",
429
469
  stderr: "pipe",
@@ -451,17 +491,66 @@ process.stdout.write(String(ms));
451
491
  /**
452
492
  * Median cold-start ms across Bun subprocesses.
453
493
  *
454
- * Takes the best of up to five rounds (early-exit when under budget) so
455
- * noisy shared CI runners do not fail a real sub-budget cold start.
494
+ * Takes the best of up to five rounds and returns as soon as one round is
495
+ * under `exitBelowMs`. If every round is still at or above that line, runs
496
+ * one confirmation round and keeps the minimum. A single noisy run has
497
+ * printed a phantom jump of about 2×; the confirmation is what keeps that
498
+ * from failing the regression gate.
499
+ *
500
+ * @param exitBelowMs - Stop when a round is under this (regression ceiling, or the 75 ms cap)
456
501
  */
457
- export async function measureColdStartMedianMs(): Promise<number> {
502
+ export async function measureColdStartMedianMs(
503
+ exitBelowMs = COLD_START_BUDGET_MS,
504
+ ): Promise<number> {
458
505
  let best = Number.POSITIVE_INFINITY;
459
506
  for (let round = 0; round < 5; round++) {
460
507
  const median = await measureColdStartMedianMsOnce();
461
508
  if (median < best) best = median;
462
- if (best < COLD_START_BUDGET_MS) return best;
509
+ if (best < exitBelowMs) return best;
463
510
  }
464
- return best;
511
+ const confirm = await measureColdStartMedianMsOnce();
512
+ return Math.min(best, confirm);
513
+ }
514
+
515
+ /**
516
+ * Growth ceiling for an absolute budget that still has room under its cap.
517
+ *
518
+ * Undefined when there is no positive baseline, or when doubling would
519
+ * already miss the absolute cap (kernel and client today). Zero baselines
520
+ * (routing p99 reads as 0 ms) stay on the absolute cap — a ratio of zero
521
+ * would fail the next positive sample.
522
+ *
523
+ * @param previous - Last committed value, if the snapshot has this id
524
+ * @param absoluteLimit - Published cap
525
+ * @param unit - Rounding: whole bytes, or milliseconds to 0.001
526
+ */
527
+ export function absoluteRegressionCeiling(
528
+ previous: number | undefined,
529
+ absoluteLimit: number,
530
+ unit: BudgetSample["unit"],
531
+ ): number | undefined {
532
+ if (previous === undefined || !(previous > 0)) return undefined;
533
+ const raw = previous * ABSOLUTE_REGRESSION_RATIO;
534
+ const ceiling = unit === "bytes" ? Math.round(raw) : Math.round(raw * 1000) / 1000;
535
+ if (ceiling >= absoluteLimit) return undefined;
536
+ return ceiling;
537
+ }
538
+
539
+ /**
540
+ * True when a rounded absolute sample is under the cap and any regression ceiling.
541
+ *
542
+ * @param value - Rounded measurement
543
+ * @param absoluteLimit - Published cap
544
+ * @param regressionLimit - Growth ceiling, when the cap would still allow it
545
+ */
546
+ export function absoluteBudgetOk(
547
+ value: number,
548
+ absoluteLimit: number,
549
+ regressionLimit: number | undefined,
550
+ ): boolean {
551
+ if (value >= absoluteLimit) return false;
552
+ if (regressionLimit !== undefined && value >= regressionLimit) return false;
553
+ return true;
465
554
  }
466
555
 
467
556
  /**
@@ -542,8 +631,15 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
542
631
  const previous = await loadPreviousBudgetValues();
543
632
 
544
633
  // Cold start alone first — parallel gzip work contends for CPU on CI and
545
- // falsely inflates the wall-clock probe.
546
- const coldStartMedianMs = await measureColdStartMedianMs();
634
+ // falsely inflates the wall-clock probe. Exit under the 2× ceiling when
635
+ // that line is tighter than 75 ms, so a phantom jump gets another round
636
+ // instead of being recorded.
637
+ const coldCeiling = absoluteRegressionCeiling(
638
+ previous.get("coldStartMedianMs"),
639
+ COLD_START_BUDGET_MS,
640
+ "ms",
641
+ );
642
+ const coldStartMedianMs = await measureColdStartMedianMs(coldCeiling ?? COLD_START_BUDGET_MS);
547
643
  const [kernelEdgeGzipBytes, clientGzipBytes, consoleInitialGzipBytes, httpPing] =
548
644
  await Promise.all([
549
645
  measureKernelEdgeGzipBytes(),
@@ -562,6 +658,7 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
562
658
  "bytes",
563
659
  "absolute",
564
660
  "core",
661
+ previous,
565
662
  ),
566
663
  sample(
567
664
  "clientGzipBytes",
@@ -571,6 +668,7 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
571
668
  "bytes",
572
669
  "absolute",
573
670
  "core",
671
+ previous,
574
672
  ),
575
673
  sample(
576
674
  "consoleInitialGzipBytes",
@@ -580,6 +678,7 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
580
678
  "bytes",
581
679
  "absolute",
582
680
  "core",
681
+ previous,
583
682
  ),
584
683
  sample(
585
684
  "coldStartMedianMs",
@@ -589,6 +688,7 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
589
688
  "ms",
590
689
  "absolute",
591
690
  "core",
691
+ previous,
592
692
  ),
593
693
  sample(
594
694
  "routingP99Ms",
@@ -598,6 +698,7 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
598
698
  "ms",
599
699
  "absolute",
600
700
  "core",
701
+ previous,
601
702
  ),
602
703
  regressionSample(
603
704
  "httpPingGzipBytes",
@@ -648,9 +749,7 @@ export function formatBudgetsReport(snapshot: BudgetsSnapshot): string {
648
749
  lines.push(GROUP_HEADINGS[group]);
649
750
  for (const b of rows) {
650
751
  const flag = b.ok ? "ok" : "FAIL";
651
- lines.push(
652
- ` [${flag}] ${b.label}: ${formatValue(b.value, b.unit)} < ${formatValue(b.limit, b.unit)}`,
653
- );
752
+ lines.push(` [${flag}] ${b.label}: ${formatValue(b.value, b.unit)} < ${formatBound(b)}`);
654
753
  }
655
754
  }
656
755
  return `${lines.join("\n")}\n`;
@@ -692,7 +791,7 @@ export function formatBudgetsMarkdown(snapshot: BudgetsSnapshot): string {
692
791
  "",
693
792
  `_okengine v${snapshot.version} · measured ${snapshot.measuredAt}_`,
694
793
  "",
695
- "Core rows are absolute AGENTS caps (plus HTTP-ping regression samples). Exports, Plugins, and Drivers fail on regression vs the prior [`budgets.json`](budgets.json) (max +256 B or +2%). Export gzip excludes hard/optional externals (`zod`, `sently`, `oxc-parser`, `ajv`, DuckDB, FormatJS). The `okengine` export row is the **thin root** (gzip); use `okengine/full` for the legacy mega-barrel and `okengine/http` for HTTP-only apps.",
794
+ "Core rows are absolute AGENTS caps (plus HTTP-ping regression samples). An absolute sample also fails when it reaches 2× its last committed value and that multiple is still under the cap (cold start, and any other absolute row with the same headroom). Cold start keeps the best of five rounds and confirms a failure once, so one noisy run does not fail. Exports, Plugins, and Drivers fail on regression vs the prior [`budgets.json`](budgets.json) (max +256 B or +2%). Export gzip excludes hard/optional externals (`zod`, `sently`, `oxc-parser`, `ajv`, DuckDB, FormatJS). The `okengine` export row is the **thin root** (gzip); use `okengine/full` for the legacy mega-barrel and `okengine/http` for HTTP-only apps.",
696
795
  ];
697
796
  const order: readonly BudgetGroup[] = ["core", "exports", "plugins", "drivers"];
698
797
  for (const group of order) {
@@ -758,20 +857,37 @@ function sample(
758
857
  unit: BudgetSample["unit"],
759
858
  gate: BudgetGate,
760
859
  group: BudgetGroup,
860
+ previous?: ReadonlyMap<string, number>,
761
861
  ): BudgetSample {
762
862
  const rounded = unit === "bytes" ? Math.round(value) : Math.round(value * 1000) / 1000;
863
+ const regressionLimit =
864
+ gate === "absolute" ? absoluteRegressionCeiling(previous?.get(id), limit, unit) : undefined;
763
865
  return {
764
866
  id,
765
867
  label,
766
868
  value: rounded,
767
869
  limit,
870
+ ...(regressionLimit !== undefined ? { regressionLimit } : {}),
768
871
  unit,
769
872
  gate,
770
873
  group,
771
- ok: rounded < limit,
874
+ ok: absoluteBudgetOk(rounded, limit, regressionLimit),
772
875
  };
773
876
  }
774
877
 
878
+ /**
879
+ * CI log bound. When the 2× ceiling is tighter than the cap, print both.
880
+ *
881
+ * @param sample - Measured row
882
+ */
883
+ function formatBound(row: BudgetSample): string {
884
+ const ceiling = row.regressionLimit;
885
+ if (ceiling !== undefined && ceiling < row.limit) {
886
+ return `${formatValue(ceiling, row.unit)} (cap ${formatValue(row.limit, row.unit)})`;
887
+ }
888
+ return formatValue(row.limit, row.unit);
889
+ }
890
+
775
891
  /**
776
892
  * Byte sample gated against the previous committed value (+ tolerance).
777
893
  * First appearance records a baseline (`ok: true`, `limit` = value + floor).
@@ -13,7 +13,7 @@ import type { Trigger } from "../kernel/triggers.ts";
13
13
  import type { FlowPlane } from "../manifest/types.ts";
14
14
  import { cacheDimensionOf, type RunTelemetry } from "../kernel/run-telemetry.ts";
15
15
  import { okid } from "../okid.ts";
16
- import type { RunError, WideEvent } from "./types.ts";
16
+ import type { RunError, RunHttpFrame, WideEvent } from "./types.ts";
17
17
 
18
18
  /** Inputs for {@link collectWideEvent}. */
19
19
  export interface CollectWideEventInput {
@@ -42,6 +42,8 @@ export interface CollectWideEventInput {
42
42
  readonly input?: unknown;
43
43
  /** Flow return value when the run completed with an output. */
44
44
  readonly output?: unknown;
45
+ /** HTTP wire frame when the trigger was `http`. */
46
+ readonly http?: RunHttpFrame;
45
47
  /** Optional parent run id (causal chain). */
46
48
  readonly parentId?: string;
47
49
  /** Optional run id (generated when omitted). */
@@ -123,6 +125,7 @@ export function collectWideEvent(input: CollectWideEventInput): WideEvent {
123
125
  error,
124
126
  ...(input.input !== undefined ? { input: input.input } : {}),
125
127
  ...(input.output !== undefined ? { output: input.output } : {}),
128
+ ...(input.http !== undefined ? { http: input.http } : {}),
126
129
  effects: [...input.ledger.entries],
127
130
  logs: [...input.telemetry.logs],
128
131
  durationMs,