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
@@ -0,0 +1,304 @@
1
+ /**
2
+ * HTTP idempotency policy.
3
+ *
4
+ * A flow honors `Idempotency-Key` when it is reached over HTTP as REST
5
+ * non-GET or RPC, its inferred effects mutate or it uses `fx.raw`, and it
6
+ * is not a stream, live feed, or binary response. The IETF draft this header
7
+ * follows expired without becoming an RFC.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import { parseDurationMs } from "../elements/clock/duration.ts";
13
+ import type { Effects } from "../manifest/types.ts";
14
+ import { fail } from "./errors.ts";
15
+ import type { EffectKind } from "./effects.ts";
16
+ import type { IdempotencyRow, StoredResponse } from "./idempotency-store.ts";
17
+
18
+ /** Request header. */
19
+ export const IDEMPOTENCY_HEADER = "idempotency-key";
20
+
21
+ /** Set on a replayed response. */
22
+ export const REPLAYED_HEADER = "idempotent-replayed";
23
+
24
+ /** Default TTL stamped when the flow omits one. */
25
+ export const IDEMPOTENCY_DEFAULT_TTL = "24h";
26
+
27
+ /** Author option on `flow({ idempotency })`. */
28
+ export type FlowIdempotencyOption =
29
+ | false
30
+ | "required"
31
+ | { readonly required?: boolean; readonly ttl?: string };
32
+
33
+ /** Manifest / client stamp. */
34
+ export interface ResolvedIdempotency {
35
+ readonly mode: "off" | "auto" | "required";
36
+ readonly ttl: string;
37
+ }
38
+
39
+ const MUTATING: ReadonlySet<EffectKind> = new Set([
40
+ "write",
41
+ "emit",
42
+ "send",
43
+ "call",
44
+ "fetch",
45
+ "ask",
46
+ "embed",
47
+ ]);
48
+
49
+ const KEY_PATTERN = /^[\x20-\x7E]{16,255}$/;
50
+
51
+ /**
52
+ * True when inferred effects include anything besides reads and secrets.
53
+ *
54
+ * @param effects - Declared or inferred effect set
55
+ */
56
+ export function hasMutatingEffects(effects: Effects | undefined): boolean {
57
+ if (effects === undefined) return false;
58
+ return (
59
+ (effects.writes?.length ?? 0) > 0 ||
60
+ (effects.emits?.length ?? 0) > 0 ||
61
+ (effects.sends?.length ?? 0) > 0 ||
62
+ (effects.calls?.length ?? 0) > 0 ||
63
+ (effects.fetches?.length ?? 0) > 0 ||
64
+ (effects.asks?.length ?? 0) > 0 ||
65
+ (effects.embeds?.length ?? 0) > 0
66
+ );
67
+ }
68
+
69
+ /**
70
+ * True when this HTTP entry should look at `Idempotency-Key`.
71
+ *
72
+ * @param args - Trigger shape and inferred effects
73
+ */
74
+ export function idempotencyEligible(args: {
75
+ readonly hasRequest: boolean;
76
+ /** HTTP method. RPC is `POST`. */
77
+ readonly method: string | undefined;
78
+ readonly stream: boolean;
79
+ readonly live: boolean;
80
+ readonly effects: Effects | undefined;
81
+ readonly usesRaw: boolean;
82
+ }): boolean {
83
+ if (!args.hasRequest || args.stream || args.live) return false;
84
+ const method = (args.method ?? "POST").toUpperCase();
85
+ if (method === "GET") return false;
86
+ return args.usesRaw || hasMutatingEffects(args.effects);
87
+ }
88
+
89
+ /** What this request should do with the header. */
90
+ export interface IdempotencyAttempt {
91
+ readonly claiming: boolean;
92
+ readonly mode: "off" | "auto" | "required";
93
+ readonly ttlMs: number;
94
+ readonly usesRaw: boolean;
95
+ }
96
+
97
+ /**
98
+ * Decide whether this request claims a key.
99
+ *
100
+ * A stamped `required` flow still ignores the header when this entry is not
101
+ * an eligible HTTP call (signal delivery, GET, stream, live, read-only).
102
+ *
103
+ * @param args - Flow stamp plus this request
104
+ */
105
+ export function prepareIdempotencyAttempt(args: {
106
+ readonly hasRequest: boolean;
107
+ readonly method: string | undefined;
108
+ readonly stream: boolean;
109
+ readonly live: boolean;
110
+ readonly effects: Effects | undefined;
111
+ readonly usesRaw: boolean;
112
+ readonly option: FlowIdempotencyOption | undefined;
113
+ readonly resolved: ResolvedIdempotency | undefined;
114
+ readonly header: string | null;
115
+ }): IdempotencyAttempt {
116
+ const eligible = idempotencyEligible(args);
117
+ const resolved = args.resolved ?? resolveIdempotencyMode(args.option, eligible);
118
+ const mode = eligible ? resolved.mode : "off";
119
+ const headerPresent = args.header !== null && args.header.trim().length > 0;
120
+ const ttlMs = idempotencyTtlMs(resolved.ttl);
121
+ return {
122
+ claiming: mode === "required" || (mode === "auto" && headerPresent),
123
+ mode,
124
+ ttlMs: ttlMs > 0 ? ttlMs : 24 * 60 * 60 * 1000,
125
+ usesRaw: args.usesRaw,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Resolve `off | auto | required` and the TTL string.
131
+ *
132
+ * Ineligible flows are `off` even when the option says required. Callers
133
+ * that parse source should reject that combination before calling this.
134
+ *
135
+ * @param option - Author option
136
+ * @param eligible - {@link idempotencyEligible}
137
+ */
138
+ export function resolveIdempotencyMode(
139
+ option: FlowIdempotencyOption | undefined,
140
+ eligible: boolean,
141
+ ): ResolvedIdempotency {
142
+ const ttl = ttlOf(option);
143
+ if (!eligible || option === false) return { mode: "off", ttl };
144
+ if (option === "required") return { mode: "required", ttl };
145
+ if (typeof option === "object" && option.required === true) return { mode: "required", ttl };
146
+ return { mode: "auto", ttl };
147
+ }
148
+
149
+ /**
150
+ * Parse a TTL string. `0` means the string is not a duration.
151
+ *
152
+ * @param ttl - Duration such as `24h`
153
+ */
154
+ export function idempotencyTtlMs(ttl: string): number {
155
+ return parseDurationMs(ttl);
156
+ }
157
+
158
+ /**
159
+ * `user:<id>`, else `apikey:<id>`, else `anon`.
160
+ *
161
+ * @param auth - Principal after the gate
162
+ */
163
+ export function idempotencyPrincipal(auth: {
164
+ readonly userId: string | null;
165
+ readonly apiKeyId?: string | null;
166
+ }): string {
167
+ if (auth.userId !== null && auth.userId.length > 0) return `user:${auth.userId}`;
168
+ if (auth.apiKeyId !== null && auth.apiKeyId !== undefined && auth.apiKeyId.length > 0) {
169
+ return `apikey:${auth.apiKeyId}`;
170
+ }
171
+ return "anon";
172
+ }
173
+
174
+ /**
175
+ * SHA-256 of canonical JSON `{ flow, input }`. Computed once at claim.
176
+ *
177
+ * @param flow - Flow name
178
+ * @param input - Validated input
179
+ */
180
+ export function idempotencyFingerprint(flow: string, input: unknown): string {
181
+ const hasher = new Bun.CryptoHasher("sha256");
182
+ hasher.update(canonicalJson({ flow, input }));
183
+ return hasher.digest("hex");
184
+ }
185
+
186
+ /**
187
+ * Classify a raw header value.
188
+ *
189
+ * @param header - Header value, or null when absent
190
+ */
191
+ export function classifyIdempotencyKey(header: string | null): "missing" | "invalid" | string {
192
+ if (header === null) return "missing";
193
+ const key = header.trim();
194
+ if (!KEY_PATTERN.test(key)) return "invalid";
195
+ return key;
196
+ }
197
+
198
+ /**
199
+ * True when this attempt recorded a non-read effect. Secrets do not count.
200
+ * Failed attempts stay on the ledger.
201
+ *
202
+ * @param entries - This run's ledger
203
+ */
204
+ export function attemptedMutation(entries: readonly { readonly kind: EffectKind }[]): boolean {
205
+ return entries.some((entry) => MUTATING.has(entry.kind));
206
+ }
207
+
208
+ /**
209
+ * Replay a stored response and mark it `Idempotent-Replayed`.
210
+ *
211
+ * @param row - Completed row
212
+ */
213
+ export function replayResponse(row: IdempotencyRow): Response {
214
+ const headers = new Headers();
215
+ const contentType = row.responseHeaders?.contentType;
216
+ const location = row.responseHeaders?.location;
217
+ if (contentType !== undefined && contentType.length > 0) headers.set("content-type", contentType);
218
+ if (location !== undefined && location.length > 0) headers.set("location", location);
219
+ headers.set(REPLAYED_HEADER, "true");
220
+ const status = row.responseStatus ?? 200;
221
+ if (status === 204) return new Response(null, { status, headers });
222
+ return new Response(row.responseBody ?? "", { status, headers });
223
+ }
224
+
225
+ /**
226
+ * Protocol failure. `IdempotencyInProgress` also sets `Retry-After`.
227
+ *
228
+ * @param code - Builtin idempotency code
229
+ * @param retryAfterSeconds - Seconds until the lease expires, for 409
230
+ */
231
+ export function idempotencyErrorResponse(
232
+ code:
233
+ | "IdempotencyKeyMissing"
234
+ | "IdempotencyKeyInvalid"
235
+ | "IdempotencyKeyReused"
236
+ | "IdempotencyInProgress",
237
+ retryAfterSeconds?: number,
238
+ ): Response {
239
+ const failure = fail(
240
+ code,
241
+ code === "IdempotencyInProgress" ? { retryAfter: retryAfterSeconds } : {},
242
+ );
243
+ const headers = new Headers();
244
+ if (code === "IdempotencyInProgress") {
245
+ headers.set("retry-after", String(Math.max(1, retryAfterSeconds ?? 1)));
246
+ }
247
+ const status =
248
+ code === "IdempotencyKeyReused" ? 422 : code === "IdempotencyInProgress" ? 409 : 400;
249
+ return Response.json({ data: null, error: failure.error }, { status, headers });
250
+ }
251
+
252
+ /**
253
+ * Buffer a JSON envelope for storage. Streams and non-JSON bodies return
254
+ * undefined so the caller deletes the in-progress row.
255
+ *
256
+ * @param res - Encoded response (not the live stream)
257
+ */
258
+ export async function storedFromResponse(res: {
259
+ readonly status: number;
260
+ readonly headers: { get(name: string): string | null };
261
+ text(): Promise<string>;
262
+ }): Promise<StoredResponse | undefined> {
263
+ const contentType = res.headers.get("content-type") ?? undefined;
264
+ if (contentType?.includes("text/event-stream")) return undefined;
265
+ if (
266
+ contentType !== undefined &&
267
+ !contentType.includes("json") &&
268
+ !contentType.includes("text/") &&
269
+ res.status !== 204
270
+ ) {
271
+ return undefined;
272
+ }
273
+ const location = res.headers.get("location") ?? undefined;
274
+ const body = res.status === 204 ? "" : await res.text();
275
+ return {
276
+ status: res.status,
277
+ body,
278
+ ...(contentType !== undefined ? { contentType } : {}),
279
+ ...(location !== undefined ? { location } : {}),
280
+ };
281
+ }
282
+
283
+ /**
284
+ * Author TTL, or {@link IDEMPOTENCY_DEFAULT_TTL}.
285
+ *
286
+ * @param option - Author option
287
+ */
288
+ export function ttlOf(option: FlowIdempotencyOption | undefined): string {
289
+ if (typeof option === "object" && option !== null && typeof option.ttl === "string") {
290
+ return option.ttl;
291
+ }
292
+ return IDEMPOTENCY_DEFAULT_TTL;
293
+ }
294
+
295
+ function canonicalJson(value: unknown): string {
296
+ if (value === undefined) return "null";
297
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
298
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
299
+ const obj = value as Record<string, unknown>;
300
+ const keys = Object.keys(obj)
301
+ .filter((key) => obj[key] !== undefined)
302
+ .sort();
303
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(obj[key])}`).join(",")}}`;
304
+ }
@@ -212,6 +212,7 @@ export {
212
212
  currentAbortSignal,
213
213
  isAbortError,
214
214
  linkAbort,
215
+ requestSignal,
215
216
  withAbortSignal,
216
217
  } from "./abort-scope.ts";
217
218
 
@@ -10,7 +10,10 @@
10
10
  import { mkdir } from "node:fs/promises";
11
11
  import { dirname } from "node:path";
12
12
  import { okid } from "../okid.ts";
13
+ import type { DecisionLabelStore } from "./decision-label-store.ts";
14
+ import type { IdempotencyStore } from "./idempotency-store.ts";
13
15
  import { JournalSuspend } from "./journal-suspend.ts";
16
+ import { lazyRequire } from "./lazy-require.ts";
14
17
 
15
18
  export { JournalSuspend, isJournalSuspend } from "./journal-suspend.ts";
16
19
 
@@ -126,6 +129,24 @@ export interface JournalLeaseStore {
126
129
  * @param leaseMs - Lease duration
127
130
  */
128
131
  acquireLease(runId: string, instanceId: string, now: number, leaseMs: number): Promise<boolean>;
132
+ /**
133
+ * Take the lease and write `update(run)` in that same hold.
134
+ * `undefined` from `update` keeps the previous entries and still holds the lease.
135
+ * `"lease"` means another holder won. `"missing"` means no run.
136
+ *
137
+ * @param runId - Run id
138
+ * @param instanceId - Claimant instance
139
+ * @param now - Epoch-ms
140
+ * @param leaseMs - Lease duration
141
+ * @param update - Next snapshot, or undefined to leave entries
142
+ */
143
+ cas(
144
+ runId: string,
145
+ instanceId: string,
146
+ now: number,
147
+ leaseMs: number,
148
+ update: (run: JournalRun) => JournalRun | undefined,
149
+ ): Promise<"ok" | "missing" | { readonly lease: true; readonly leaseExpiresAt?: number }>;
129
150
  /**
130
151
  * Release a lease held by `instanceId` (no-op for other holders).
131
152
  *
@@ -151,8 +172,45 @@ export interface JournalLeaseStore {
151
172
  listOrphans(now: number): Promise<readonly JournalRun[]>;
152
173
  }
153
174
 
175
+ /**
176
+ * Load the idempotency table implementation without a static import.
177
+ * Computed stem so the edge profile does not inline it.
178
+ */
179
+ function loadIdempotencyStore(): typeof import("./idempotency-store.ts") {
180
+ return lazyRequire(import.meta.dir, ["idempotency", "store"].join("-"));
181
+ }
182
+
183
+ /**
184
+ * Load the decision-label table without a static import.
185
+ */
186
+ function loadDecisionLabelStore(): typeof import("./decision-label-store.ts") {
187
+ return lazyRequire(import.meta.dir, ["decision", "label", "store"].join("-"));
188
+ }
189
+
190
+ /**
191
+ * Load the agent-event table without a static import.
192
+ */
193
+ function loadAgentEventStore(): typeof import("./agent-event-store.ts") {
194
+ return lazyRequire(import.meta.dir, ["agent", "event", "store"].join("-"));
195
+ }
196
+
154
197
  /** Persistence backend for journal runs. */
155
198
  export interface JournalStore extends Partial<JournalLeaseStore> {
199
+ /**
200
+ * Idempotency records on this same driver. Absent on a custom store that
201
+ * only implements run `get` / `put`.
202
+ */
203
+ readonly idempotency?: IdempotencyStore;
204
+ /**
205
+ * Decision labels and the drift flag on this same driver. Opened only when
206
+ * the app declares decisions.
207
+ */
208
+ readonly decisions?: DecisionLabelStore;
209
+ /**
210
+ * Agent-run event log on this same driver. Followers resume from these rows
211
+ * after a restart and from another instance.
212
+ */
213
+ readonly agentEvents?: import("./agent-event-store.ts").AgentEventStore;
156
214
  /**
157
215
  * Load a run by id.
158
216
  *
@@ -219,6 +277,7 @@ function leaseMethods(
219
277
  load: () => Promise<Map<string, JournalRun>>,
220
278
  flush?: (map: Map<string, JournalRun>) => Promise<void>,
221
279
  ): JournalLeaseStore {
280
+ let gate: Promise<void> = Promise.resolve();
222
281
  return {
223
282
  async acquireLease(runId, instanceId, now, leaseMs) {
224
283
  const map = await load();
@@ -229,6 +288,31 @@ function leaseMethods(
229
288
  await flush?.(map);
230
289
  return true;
231
290
  },
291
+ async cas(runId, instanceId, now, leaseMs, update) {
292
+ let release!: () => void;
293
+ const slot = new Promise<void>((resolve) => {
294
+ release = resolve;
295
+ });
296
+ const prev = gate;
297
+ gate = slot;
298
+ await prev;
299
+ try {
300
+ const map = await load();
301
+ const run = map.get(runId);
302
+ if (!run) return "missing";
303
+ if (!claimable(run, instanceId, now)) {
304
+ return { lease: true, leaseExpiresAt: run.leaseExpiresAt };
305
+ }
306
+ const next = update(cloneRun(run)) ?? run;
307
+ next.lockedBy = instanceId;
308
+ next.leaseExpiresAt = now + leaseMs;
309
+ map.set(runId, next);
310
+ await flush?.(map);
311
+ return "ok";
312
+ } finally {
313
+ release();
314
+ }
315
+ },
232
316
  async releaseLease(runId, instanceId) {
233
317
  const map = await load();
234
318
  const run = map.get(runId);
@@ -286,6 +370,21 @@ export function createMemoryJournalStore(seed?: readonly JournalRun[]): JournalS
286
370
  return [...runs.values()].map(cloneRun);
287
371
  },
288
372
  ...leaseMethods(load),
373
+ get idempotency(): IdempotencyStore {
374
+ const created = loadIdempotencyStore().createMemoryIdempotencyStore();
375
+ Object.defineProperty(this, "idempotency", { value: created });
376
+ return created;
377
+ },
378
+ get decisions(): DecisionLabelStore {
379
+ const created = loadDecisionLabelStore().createMemoryDecisionLabelStore();
380
+ Object.defineProperty(this, "decisions", { value: created });
381
+ return created;
382
+ },
383
+ get agentEvents(): import("./agent-event-store.ts").AgentEventStore {
384
+ const created = loadAgentEventStore().createMemoryAgentEventStore();
385
+ Object.defineProperty(this, "agentEvents", { value: created });
386
+ return created;
387
+ },
289
388
  };
290
389
  }
291
390
 
@@ -332,6 +431,27 @@ export function createFileJournalStore(path: string): JournalStore {
332
431
  },
333
432
  // Single-host file: leases coordinate same-machine processes only.
334
433
  ...leaseMethods(load, flush),
434
+ get idempotency(): IdempotencyStore {
435
+ const created = loadIdempotencyStore().createFileIdempotencyStore(
436
+ `${dirname(path)}/idempotency.json`,
437
+ );
438
+ Object.defineProperty(this, "idempotency", { value: created });
439
+ return created;
440
+ },
441
+ get decisions(): DecisionLabelStore {
442
+ const created = loadDecisionLabelStore().createFileDecisionLabelStore(
443
+ `${dirname(path)}/decision-labels.json`,
444
+ );
445
+ Object.defineProperty(this, "decisions", { value: created });
446
+ return created;
447
+ },
448
+ get agentEvents(): import("./agent-event-store.ts").AgentEventStore {
449
+ const created = loadAgentEventStore().createFileAgentEventStore(
450
+ `${dirname(path)}/agent-events`,
451
+ );
452
+ Object.defineProperty(this, "agentEvents", { value: created });
453
+ return created;
454
+ },
335
455
  };
336
456
  }
337
457
 
@@ -405,6 +525,11 @@ export interface JournalSession {
405
525
  * instead of treating the cursor as past them. Clears the in-memory undo stack
406
526
  * so frames re-bind on the next walk.
407
527
  */
528
+ /**
529
+ * Increments on {@link JournalSession.rewind}. Agent run ids key off this
530
+ * so `flow.retry` replays the same id.
531
+ */
532
+ readonly epoch: number;
408
533
  rewind(): void;
409
534
  /** Registered per-step undos in persist/replay order (LIFO compensate). */
410
535
  undoStack(): readonly JournalUndoFrame[];
@@ -428,6 +553,36 @@ export interface JournalSession {
428
553
  ): Promise<void>;
429
554
  }
430
555
 
556
+ /**
557
+ * A journal session that forwards to a real session once one is assigned.
558
+ *
559
+ * Idempotent durable flows start the journal only after the claim, which is
560
+ * after `fx` has already been created.
561
+ */
562
+ export function createJournalSlot(): {
563
+ readonly slot: { session?: JournalSession };
564
+ readonly facade: JournalSession;
565
+ } {
566
+ const slot: { session?: JournalSession } = {};
567
+ const facade = new Proxy({} as JournalSession, {
568
+ get(_target, prop, receiver) {
569
+ const session = slot.session;
570
+ if (!session) {
571
+ if (prop === "runId") return "";
572
+ if (prop === "epoch") return 0;
573
+ if (prop === "stampTenant") return async () => undefined;
574
+ if (prop === "rewind") return () => undefined;
575
+ return undefined;
576
+ }
577
+ const value = Reflect.get(session, prop, receiver);
578
+ return typeof value === "function"
579
+ ? (value as (...args: unknown[]) => unknown).bind(session)
580
+ : value;
581
+ },
582
+ });
583
+ return { slot, facade };
584
+ }
585
+
431
586
  /** Journal facade. */
432
587
  export interface Journal {
433
588
  readonly store: JournalStore;
@@ -460,6 +615,7 @@ export function createJournal(options: CreateJournalOptions): Journal {
460
615
  function openSession(run: JournalRun, leased: boolean): JournalSession {
461
616
  /** Next entry index to consume on replay. */
462
617
  let cursor = 0;
618
+ let epoch = 0;
463
619
  let leaseHeld = leased;
464
620
  let registrationPass = false;
465
621
  let undoExecution = false;
@@ -609,9 +765,13 @@ export function createJournal(options: CreateJournalOptions): Journal {
609
765
  await persist();
610
766
  return value;
611
767
  },
768
+ get epoch() {
769
+ return epoch;
770
+ },
612
771
  rewind() {
613
772
  cursor = 0;
614
773
  undos.length = 0;
774
+ epoch += 1;
615
775
  },
616
776
  undoStack() {
617
777
  return undos;
@@ -28,6 +28,16 @@ export interface JsonStreamResult {
28
28
  ready?: () => Promise<void>;
29
29
  /** Set by the kernel to commit journal / Runs after the stream settles. */
30
30
  finalize?: () => Promise<void>;
31
+ /**
32
+ * Agent result once a streamed `fx.run` settles.
33
+ * Absent for text and live streams.
34
+ */
35
+ result?: Promise<unknown>;
36
+ /**
37
+ * Set when iteration parked a durable run, after `[DONE]` is queued.
38
+ * Finalize commits the journal as sleeping from this.
39
+ */
40
+ parked?: { wakeAt: number; label: string };
31
41
  }
32
42
 
33
43
  /**
@@ -14,6 +14,7 @@ import { createClockRuntime } from "../elements/clock.ts";
14
14
  import { gate } from "../elements/gate.ts";
15
15
  import { createRunsRuntime, memoryRunsDriver } from "../runs/index.ts";
16
16
  import { oke } from "./app.ts";
17
+ import { HTTP_FRAME_REDACTED } from "./http-frame.ts";
17
18
  import { flow, resetFlowSeq } from "./flow.ts";
18
19
  import { on, resetBindings } from "./on.ts";
19
20
  import { http } from "./triggers.ts";
@@ -196,6 +197,50 @@ describe("pipeline — Unauthorized for anonymous", () => {
196
197
  });
197
198
  });
198
199
 
200
+ describe("pipeline — HTTP wire frame", () => {
201
+ test("fetch stores query, headers, and status beside the body", async () => {
202
+ resetBindings();
203
+ resetFlowSeq();
204
+
205
+ on(
206
+ http.get("/tasks", { out: z.object({ ok: z.literal(true) }) }).public(),
207
+ flow("tasks.list", {
208
+ do: () => ({ ok: true as const }),
209
+ }),
210
+ );
211
+
212
+ const runs = createRunsRuntime({ driver: memoryRunsDriver });
213
+ await runs.open();
214
+ const app = oke({
215
+ name: "http-frame",
216
+ gate: { policies: [gate.public] },
217
+ runs,
218
+ env: "test",
219
+ });
220
+ await app.boot({ env: "test", gates: [gate.public], runs });
221
+
222
+ const res = await app.fetch(
223
+ new Request("http://localhost/tasks?limit=20", {
224
+ headers: { accept: "application/json", cookie: "sid=secret" },
225
+ }),
226
+ );
227
+ expect(res.status).toBe(200);
228
+
229
+ const event = (await runs.all()).find((row) => row.flow === "tasks.list");
230
+ expect(event?.input).toBeUndefined();
231
+ expect(event?.http).toMatchObject({
232
+ request: {
233
+ method: "GET",
234
+ path: "/tasks",
235
+ query: { limit: "20" },
236
+ headers: { cookie: HTTP_FRAME_REDACTED, accept: "application/json" },
237
+ },
238
+ response: { status: 200 },
239
+ });
240
+ await app.bootResult?.close();
241
+ });
242
+ });
243
+
199
244
  describe("pipeline — evaluated gates on the run", () => {
200
245
  test("recorded run contains evaluated chain, not just declared names", async () => {
201
246
  resetBindings();
@@ -0,0 +1,32 @@
1
+ /**
2
+ * SSE id carried on a live agent event without changing its JSON payload.
3
+ *
4
+ * The follow log stores a seq. The live `fx.run` frame for that row uses the
5
+ * same seq as `id:` so a client can resume with `Last-Event-ID`.
6
+ */
7
+
8
+ const sseIdBrand: unique symbol = Symbol.for("oke.sse.id");
9
+
10
+ /**
11
+ * Copy `value` and attach an SSE id the encoder writes as `id:`.
12
+ * The id is not enumerable, so it is not part of the JSON payload.
13
+ *
14
+ * @param value - Event object
15
+ * @param id - SSE id (the stored log seq)
16
+ */
17
+ export function withSseId<T extends object>(value: T, id: string): T {
18
+ const copy: T = { ...value };
19
+ Object.defineProperty(copy, sseIdBrand, { value: id, enumerable: false });
20
+ return copy;
21
+ }
22
+
23
+ /**
24
+ * SSE id attached by {@link withSseId}, if any.
25
+ *
26
+ * @param value - Stream chunk
27
+ */
28
+ export function readSseId(value: unknown): string | undefined {
29
+ if (typeof value !== "object" || value === null) return undefined;
30
+ const id = (value as { readonly [sseIdBrand]?: unknown })[sseIdBrand];
31
+ return typeof id === "string" ? id : undefined;
32
+ }
@@ -52,6 +52,7 @@ const EFFECT_KEYS = [
52
52
  "secrets",
53
53
  "calls",
54
54
  "fetches",
55
+ "decides",
55
56
  ] as const;
56
57
 
57
58
  /** Isolation strength: higher = stronger tenant separation. */