okengine 0.22.0 → 0.23.1

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 (312) hide show
  1. package/AGENTS.md +7 -0
  2. package/manifest.v1.schema.json +44 -0
  3. package/package.json +32 -19
  4. package/site/content/docs/ai/mcp.mdx +14 -10
  5. package/site/content/docs/ai/skills.mdx +1 -1
  6. package/site/content/docs/elements/ai/agents.mdx +61 -13
  7. package/site/content/docs/elements/ai/decide.mdx +536 -0
  8. package/site/content/docs/elements/ai/events.mdx +373 -0
  9. package/site/content/docs/elements/ai/index.mdx +9 -3
  10. package/site/content/docs/elements/ai/meta.json +1 -1
  11. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  12. package/site/content/docs/elements/channel/index.mdx +4 -4
  13. package/site/content/docs/elements/channel/receipts.mdx +7 -5
  14. package/site/content/docs/elements/channel/sms.mdx +2 -1
  15. package/site/content/docs/elements/flow/index.mdx +22 -18
  16. package/site/content/docs/elements/gate/auth.mdx +4 -1
  17. package/site/content/docs/elements/signal/index.mdx +15 -14
  18. package/site/content/docs/elements/store/index.mdx +1 -1
  19. package/site/content/docs/elements/store/kv.mdx +5 -5
  20. package/site/content/docs/plugins/otp.mdx +3 -3
  21. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  22. package/site/content/docs/recipes/index.mdx +6 -6
  23. package/site/content/docs/recipes/postgres.mdx +12 -8
  24. package/site/content/docs/recipes/redis.mdx +4 -4
  25. package/site/content/docs/reference/cli.mdx +2 -1
  26. package/site/content/docs/reference/configuration.mdx +9 -7
  27. package/site/content/docs/reference/fx.mdx +19 -10
  28. package/src/auth/api-key-sql.test.ts +44 -0
  29. package/src/auth/api-key-sql.ts +1 -0
  30. package/src/auth/api-keys.ts +30 -12
  31. package/src/auth/config.ts +6 -2
  32. package/src/auth/gate-auth.test.ts +20 -0
  33. package/src/auth/tenants.ts +17 -0
  34. package/src/cli/agents-md.test.ts +25 -0
  35. package/src/cli/ask-vault-gaps.test.ts +38 -1
  36. package/src/cli/ask-vault-gaps.ts +35 -2
  37. package/src/cli/decide.ts +174 -0
  38. package/src/cli/decision-lock-watch.test.ts +45 -0
  39. package/src/cli/decision-lock-watch.ts +51 -0
  40. package/src/cli/dev-app-runner.ts +34 -8
  41. package/src/cli/dev-auth-secret.test.ts +49 -0
  42. package/src/cli/dev-auth-secret.ts +49 -0
  43. package/src/cli/dev-hot.test.ts +41 -0
  44. package/src/cli/dev-hot.ts +48 -0
  45. package/src/cli/dev.test.ts +1 -1
  46. package/src/cli/dev.ts +24 -6
  47. package/src/cli/doctor-diff.test.ts +29 -1
  48. package/src/cli/doctor-diff.ts +35 -0
  49. package/src/cli/eval.ts +142 -1
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/load-config.images.test.ts +2 -2
  52. package/src/cli/load-config.ts +1 -1
  53. package/src/cli/manifest-pr-diff.ts +16 -0
  54. package/src/cli/mcp-from-console.ts +48 -0
  55. package/src/cli/registry.ts +35 -1
  56. package/src/client/agent.ts +307 -0
  57. package/src/client/transport.test.ts +20 -0
  58. package/src/client/transport.ts +2 -1
  59. package/src/client/types.ts +1 -1
  60. package/src/client-react/index.ts +7 -1
  61. package/src/client-react/use-agent-run.test.ts +271 -0
  62. package/src/client-react/use-agent-run.ts +193 -0
  63. package/src/compiler/ai-approval.test.ts +36 -0
  64. package/src/compiler/ai-repair.test.ts +22 -0
  65. package/src/compiler/decisions.extract.test.ts +164 -0
  66. package/src/compiler/effects-fetch.test.ts +77 -0
  67. package/src/compiler/effects-infer.ts +114 -17
  68. package/src/compiler/effects-join.test.ts +48 -0
  69. package/src/compiler/extract.test.ts +28 -0
  70. package/src/compiler/extract.ts +279 -13
  71. package/src/compiler/fx-index.ts +58 -4
  72. package/src/compiler/response.ts +35 -2
  73. package/src/config/index.ts +21 -3
  74. package/src/console/index.ts +1 -1
  75. package/src/console/server/ai-runs-flows.ts +269 -0
  76. package/src/console/server/ai-runs.test.ts +323 -0
  77. package/src/console/server/ai-runs.ts +562 -0
  78. package/src/console/server/ai.ts +12 -1
  79. package/src/console/server/app.ts +10 -1
  80. package/src/console/server/decisions-flows.ts +157 -0
  81. package/src/console/server/decisions.test.ts +66 -0
  82. package/src/console/server/decisions.ts +229 -0
  83. package/src/console/server/flows.ts +50 -2
  84. package/src/console/server/invoke-user-flow.ts +9 -0
  85. package/src/console/server/runs-ingest.ts +16 -0
  86. package/src/console/server/serve.ts +5 -0
  87. package/src/console/server/state.ts +25 -1
  88. package/src/console/server/store-stats.test.ts +47 -0
  89. package/src/console/server/store-stats.ts +92 -18
  90. package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
  91. package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
  92. package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
  94. package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
  95. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  96. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  97. package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
  98. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  99. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  100. package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
  101. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  102. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  103. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  104. package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
  105. package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
  107. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  108. package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
  109. package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
  110. package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
  111. package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
  112. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  113. package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
  114. package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
  115. package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
  116. package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
  117. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  118. package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
  119. package/src/console/ui-next/dist/index.html +4 -3
  120. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  121. package/src/console/ui-next/src/client.ts +224 -1
  122. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  123. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  124. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  125. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  126. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  127. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  128. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  129. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  130. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  131. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  132. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  133. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  134. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  135. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  136. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  137. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  138. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  139. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  140. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  141. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  142. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  143. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  144. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  145. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  146. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  147. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  148. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  149. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  150. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  151. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  152. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
  153. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  154. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  155. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  156. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  157. package/src/console/ui-next/src/router.tsx +17 -0
  158. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  159. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  160. package/src/console/ui-next/vite.config.ts +9 -0
  161. package/src/docker/recipes/dragonfly.ts +3 -2
  162. package/src/docker/recipes/redis.ts +1 -1
  163. package/src/drivers/ai-anthropic.ts +212 -21
  164. package/src/drivers/ai-mock.ts +34 -1
  165. package/src/drivers/ai-openai-compatible.ts +67 -26
  166. package/src/drivers/ai-preconnect.ts +18 -0
  167. package/src/drivers/ai-stream.test.ts +62 -0
  168. package/src/drivers/ai-types.ts +20 -0
  169. package/src/drivers/journal-postgres.test.ts +22 -1
  170. package/src/drivers/journal-postgres.ts +511 -13
  171. package/src/drivers/memory-ddl.test.ts +18 -0
  172. package/src/drivers/memory.ts +21 -1
  173. package/src/drivers/pg-rls.test.ts +31 -0
  174. package/src/drivers/pg-rls.ts +15 -0
  175. package/src/drivers/postgres.test.ts +9 -0
  176. package/src/drivers/postgres.ts +49 -11
  177. package/src/drivers/signal-compete.test.ts +234 -0
  178. package/src/drivers/signal-postgres.ts +39 -19
  179. package/src/drivers/signal-redis.ts +167 -11
  180. package/src/drivers/signal-types.ts +12 -0
  181. package/src/elements/ai/agent-event-slot.ts +27 -0
  182. package/src/elements/ai/approval-http.ts +207 -0
  183. package/src/elements/ai/approval.test.ts +741 -0
  184. package/src/elements/ai/approval.ts +228 -0
  185. package/src/elements/ai/decisions/bind.ts +163 -0
  186. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  187. package/src/elements/ai/decisions/certificate.ts +377 -0
  188. package/src/elements/ai/decisions/certify.ts +314 -0
  189. package/src/elements/ai/decisions/decide.test.ts +953 -0
  190. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  191. package/src/elements/ai/decisions/export.test.ts +120 -0
  192. package/src/elements/ai/decisions/export.ts +146 -0
  193. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  194. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  195. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  196. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  197. package/src/elements/ai/decisions/http.test.ts +154 -0
  198. package/src/elements/ai/decisions/http.ts +240 -0
  199. package/src/elements/ai/decisions/labels.test.ts +282 -0
  200. package/src/elements/ai/decisions/labels.ts +257 -0
  201. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  202. package/src/elements/ai/decisions/openrouter.ts +36 -0
  203. package/src/elements/ai/decisions/provider.ts +102 -0
  204. package/src/elements/ai/decisions/typesafe.ts +36 -0
  205. package/src/elements/ai/declare.ts +252 -1
  206. package/src/elements/ai/events.test.ts +379 -0
  207. package/src/elements/ai/events.ts +138 -0
  208. package/src/elements/ai/mcp-protocol.ts +5 -6
  209. package/src/elements/ai/run-events.test.ts +461 -0
  210. package/src/elements/ai/run-events.ts +522 -0
  211. package/src/elements/ai/runtime.ts +1187 -105
  212. package/src/elements/ai/stream-turn.ts +214 -0
  213. package/src/elements/ai/stream.live.test.ts +76 -0
  214. package/src/elements/ai/subagent.test.ts +136 -0
  215. package/src/elements/ai.test.ts +497 -3
  216. package/src/elements/ai.ts +16 -1
  217. package/src/elements/channel/runtime.ts +11 -0
  218. package/src/elements/channel/sql-ledger.ts +251 -0
  219. package/src/elements/clock/durable.ts +27 -1
  220. package/src/elements/signal/runtime.ts +9 -0
  221. package/src/elements/signal.test.ts +1 -0
  222. package/src/elements/store/cache.test.ts +120 -15
  223. package/src/elements/store/cache.ts +200 -39
  224. package/src/elements/store/prepare-row.test.ts +6 -2
  225. package/src/elements/store/sql-session.test.ts +53 -0
  226. package/src/elements/store/sql-session.ts +206 -10
  227. package/src/elements/store.ts +1 -0
  228. package/src/i18n/catalogs/ar.ts +5 -0
  229. package/src/i18n/catalogs/en.ts +5 -0
  230. package/src/index.ts +2 -0
  231. package/src/kernel/agent-event-store.ts +448 -0
  232. package/src/kernel/app.ts +135 -17
  233. package/src/kernel/auto-cache.test.ts +228 -0
  234. package/src/kernel/boot-bind/ai.ts +3 -0
  235. package/src/kernel/boot-bind/channel.ts +63 -2
  236. package/src/kernel/boot-bind/honor-config.test.ts +40 -10
  237. package/src/kernel/boot-bind/signal.ts +61 -25
  238. package/src/kernel/boot-bind/store.test.ts +26 -0
  239. package/src/kernel/boot-bind/store.ts +82 -6
  240. package/src/kernel/boot.ts +31 -6
  241. package/src/kernel/builtin-errors.ts +2 -0
  242. package/src/kernel/capability.ts +3 -0
  243. package/src/kernel/decision-label-store.ts +441 -0
  244. package/src/kernel/effects.test.ts +2 -1
  245. package/src/kernel/effects.ts +13 -1
  246. package/src/kernel/element-registries.ts +3 -0
  247. package/src/kernel/errors-compiler.ts +11 -0
  248. package/src/kernel/errors-text.ts +20 -0
  249. package/src/kernel/errors.ts +10 -0
  250. package/src/kernel/flow.test.ts +2 -3
  251. package/src/kernel/flow.ts +33 -3
  252. package/src/kernel/fx-call-types.test.ts +32 -0
  253. package/src/kernel/fx-decide.ts +765 -0
  254. package/src/kernel/fx-sql-handle.ts +46 -7
  255. package/src/kernel/fx.test.ts +13 -8
  256. package/src/kernel/fx.ts +339 -45
  257. package/src/kernel/http-frame.test.ts +94 -0
  258. package/src/kernel/http-frame.ts +203 -0
  259. package/src/kernel/idempotency.test.ts +2 -1
  260. package/src/kernel/journal.test.ts +114 -0
  261. package/src/kernel/journal.ts +471 -51
  262. package/src/kernel/json-result.ts +10 -0
  263. package/src/kernel/pipeline-tenant.ts +5 -1
  264. package/src/kernel/pipeline.test.ts +45 -0
  265. package/src/kernel/pipeline.ts +5 -0
  266. package/src/kernel/signal-tx.ts +28 -0
  267. package/src/kernel/sse-id.ts +32 -0
  268. package/src/kernel/store-transaction.test.ts +95 -0
  269. package/src/manifest/diff.ts +1 -0
  270. package/src/manifest/types.ts +29 -2
  271. package/src/mcp/ai-tools.test.ts +202 -0
  272. package/src/mcp/authorization.ts +66 -0
  273. package/src/mcp/docs-server.ts +19 -2
  274. package/src/mcp/mcp.test.ts +83 -0
  275. package/src/mcp/protocol.ts +8 -2
  276. package/src/mcp/server.ts +23 -2
  277. package/src/mcp/session.ts +3 -0
  278. package/src/mcp/tools.ts +168 -0
  279. package/src/mcp/versions.ts +60 -0
  280. package/src/plugins/index.ts +1 -0
  281. package/src/plugins/mena.ts +56 -0
  282. package/src/release/http-graph.test.ts +2 -0
  283. package/src/release/http-graph.ts +5 -0
  284. package/src/release/measure.ts +39 -14
  285. package/src/runs/collect.ts +4 -1
  286. package/src/runs/parquet.test.ts +27 -1
  287. package/src/runs/parquet.ts +19 -0
  288. package/src/runs/types.ts +31 -0
  289. package/src/runtime/json-code-block.test.ts +106 -47
  290. package/src/runtime/json-code-block.ts +1209 -46
  291. package/src/term.test.ts +13 -0
  292. package/src/term.ts +81 -49
  293. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  294. package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
  295. package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
  296. package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
  297. package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
  298. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
  299. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
  300. package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
  301. package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
  302. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  303. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
  304. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
  305. package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
  306. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
  307. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
  308. package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
  309. package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
  310. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
  311. package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
  312. package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
@@ -28,6 +28,7 @@ import { SearchConfigError } from "../elements/store/search-errors.ts";
28
28
  import type {
29
29
  Ai,
30
30
  AiAgent,
31
+ AiDecision,
31
32
  AiMcpServer,
32
33
  AiModel,
33
34
  AiPrompt,
@@ -257,11 +258,18 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
257
258
  if (Object.keys(gates).length > 0) manifest.gates = gates;
258
259
  if (Object.keys(vault).length > 0) manifest.vault = vault;
259
260
  if (Object.keys(channels).length > 0) manifest.channels = channels;
260
- if (scope.ai.models || scope.ai.prompts || scope.ai.agents || scope.ai.mcpServers) {
261
+ if (
262
+ scope.ai.models ||
263
+ scope.ai.prompts ||
264
+ scope.ai.agents ||
265
+ scope.ai.decisions ||
266
+ scope.ai.mcpServers
267
+ ) {
261
268
  manifest.ai = {
262
269
  ...(scope.ai.models ? { models: sortRecord(scope.ai.models) } : {}),
263
270
  ...(scope.ai.prompts ? { prompts: sortRecord(scope.ai.prompts) } : {}),
264
271
  ...(scope.ai.agents ? { agents: sortRecord(scope.ai.agents) } : {}),
272
+ ...(scope.ai.decisions ? { decisions: sortRecord(scope.ai.decisions) } : {}),
265
273
  ...(scope.ai.mcpServers ? { mcpServers: sortRecord(scope.ai.mcpServers) } : {}),
266
274
  };
267
275
  }
@@ -1426,7 +1434,11 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1426
1434
  }
1427
1435
 
1428
1436
  if (obj === "ai" && prop === "agent") {
1429
- collectAgent(call, scope);
1437
+ collectAgent(call, program, scope);
1438
+ }
1439
+
1440
+ if (obj === "ai" && prop === "decision") {
1441
+ collectDecision(call, program, scope);
1430
1442
  }
1431
1443
 
1432
1444
  if (obj === "ai" && prop === "mcpServer") {
@@ -1494,6 +1506,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1494
1506
  : {}),
1495
1507
  ...(via && via.length > 0 ? { via } : {}),
1496
1508
  ...(timeout !== undefined ? { timeout } : {}),
1509
+ ...(numberProp(opts, "repair") === 1 ? { repair: 1 as const } : {}),
1497
1510
  };
1498
1511
  scope.ai.prompts = scope.ai.prompts ?? {};
1499
1512
  scope.ai.prompts[promptName] = prompt;
@@ -1648,27 +1661,267 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
1648
1661
  }
1649
1662
  }
1650
1663
 
1651
- function collectAgent(call: CallExpression, scope: ProjectScope): void {
1664
+ function agentToolEntry(
1665
+ el: AstNode,
1666
+ scope: ProjectScope,
1667
+ ): { name: string; approval: boolean } | undefined {
1668
+ if (el.type === "ObjectExpression") {
1669
+ const nameNode = objectProp(el, "name");
1670
+ const raw =
1671
+ (nameNode ? resolveCallTarget(nameNode, scope.bindings) : undefined) ??
1672
+ stringArg(nameNode) ??
1673
+ identifierName(nameNode);
1674
+ if (!raw) return undefined;
1675
+ const name = scope.flowExports.get(raw) ?? scope.bindings.get(raw)?.ref ?? raw;
1676
+ const approvalNode = objectProp(el, "approval");
1677
+ return { name, approval: approvalNode !== undefined && boolProp(el, "approval") !== false };
1678
+ }
1679
+ const resolved = resolveCallTarget(el, scope.bindings);
1680
+ const raw = resolved ?? identifierName(el) ?? stringArg(el);
1681
+ if (!raw) return undefined;
1682
+ const name = scope.flowExports.get(raw) ?? scope.bindings.get(raw)?.ref ?? raw;
1683
+ return { name, approval: false };
1684
+ }
1685
+
1686
+ function assertDecisionPlacement(flowName: string, flow: Flow, scope: ProjectScope): void {
1687
+ const names = flow.effects?.decides ?? [];
1688
+ if (names.length === 0) return;
1689
+ const http = flow.trigger !== undefined && "http" in flow.trigger;
1690
+ for (const decisionName of names) {
1691
+ const decision = scope.ai.decisions?.[decisionName];
1692
+ if (!decision || decision.mode !== "review") continue;
1693
+ if (http) {
1694
+ throw new Error(
1695
+ `ai.decision("${decisionName}") review cannot run in HTTP flow "${flowName}" — a park answers 204. fx.emit a signal and decide in a consumer.`,
1696
+ );
1697
+ }
1698
+ if (flow.durable !== true) {
1699
+ throw new Error(
1700
+ `ai.decision("${decisionName}"): flow "${flowName}" must set durable: true to review`,
1701
+ );
1702
+ }
1703
+ }
1704
+ }
1705
+
1706
+ function decisionReviewName(node: AstNode | undefined, scope: ProjectScope): string {
1707
+ const literal = stringArg(node);
1708
+ if (literal) return literal;
1709
+ if (node?.type === "Identifier") {
1710
+ const name = (node as Identifier).name;
1711
+ return scope.gateIds.get(name) ?? name;
1712
+ }
1713
+ if (node?.type === "CallExpression") {
1714
+ const arg = stringArg((node as CallExpression).arguments[0]);
1715
+ if (arg) return arg;
1716
+ }
1717
+ if (node?.type === "MemberExpression") {
1718
+ const member = node as AstNode & { property: AstNode; computed?: boolean };
1719
+ if (member.property.type === "Identifier" && member.computed !== true) {
1720
+ return (member.property as Identifier).name;
1721
+ }
1722
+ }
1723
+ throw new Error("ai.decision: review gate name could not be resolved");
1724
+ }
1725
+
1726
+ function assertDecisionQuestion(
1727
+ decisionName: string,
1728
+ key: string,
1729
+ value: AstNode | undefined,
1730
+ program: AstNode,
1731
+ ): void {
1732
+ const resolved = resolveDecisionExpr(value, program, decisionName, key);
1733
+ if (!resolved || resolved.type !== "CallExpression") {
1734
+ throw new Error(
1735
+ `ai.decision("${decisionName}"): question "${key}" must be ai.choice, ai.score, or ai.boolean`,
1736
+ );
1737
+ }
1738
+ const callee = (resolved as CallExpression).callee;
1739
+ const member = callee as AstNode & { property?: AstNode };
1740
+ const fn =
1741
+ callee.type === "MemberExpression" && member.property?.type === "Identifier"
1742
+ ? (member.property as Identifier).name
1743
+ : undefined;
1744
+ const args = (resolved as CallExpression).arguments;
1745
+ if (fn === "choice") {
1746
+ const options = args[1];
1747
+ if (!options || options.type !== "ObjectExpression") {
1748
+ throw new Error(
1749
+ `ai.decision("${decisionName}"): choice "${key}" options must be an object literal`,
1750
+ );
1751
+ }
1752
+ const keys = objectProperties(options)
1753
+ .map((prop) => propKey(prop))
1754
+ .filter((name): name is string => typeof name === "string");
1755
+ if (keys.includes("none_of_these")) {
1756
+ throw new Error(`ai.decision("${decisionName}"): choice "${key}" must not set none_of_these`);
1757
+ }
1758
+ if (keys.length > 254) {
1759
+ throw new Error(
1760
+ `ai.decision("${decisionName}"): choice "${key}" has ${keys.length} options; max 254`,
1761
+ );
1762
+ }
1763
+ }
1764
+ if (fn === "score") {
1765
+ const levels = resolveScoreLevels(args[1], program, decisionName, key);
1766
+ const count = ((levels as AstNode & { elements?: unknown[] }).elements ?? []).length;
1767
+ if (count < 2 || count > 10) {
1768
+ throw new Error(`ai.decision("${decisionName}"): score "${key}" needs 2–10 levels`);
1769
+ }
1770
+ }
1771
+ }
1772
+
1773
+ function resolveDecisionExpr(
1774
+ value: AstNode | undefined,
1775
+ program: AstNode,
1776
+ decisionName: string,
1777
+ key: string,
1778
+ ): AstNode | undefined {
1779
+ if (!value) return undefined;
1780
+ if (value.type !== "Identifier") return value;
1781
+ const name = (value as Identifier).name;
1782
+ const init = sameFileConstInit(name, program);
1783
+ if (!init) {
1784
+ throw new Error(
1785
+ `ai.decision("${decisionName}"): question "${key}" is "${name}", which is not a same-file const`,
1786
+ );
1787
+ }
1788
+ return init;
1789
+ }
1790
+
1791
+ function resolveScoreLevels(
1792
+ levels: AstNode | undefined,
1793
+ program: AstNode,
1794
+ decisionName: string,
1795
+ key: string,
1796
+ ): AstNode {
1797
+ const resolved =
1798
+ levels?.type === "Identifier"
1799
+ ? sameFileConstInit((levels as Identifier).name, program)
1800
+ : levels;
1801
+ if (!resolved || resolved.type !== "ArrayExpression") {
1802
+ const label = levels?.type === "Identifier" ? (levels as Identifier).name : "the levels";
1803
+ throw new Error(
1804
+ `ai.decision("${decisionName}"): score "${key}" levels (${label}) must be an array literal or a same-file const`,
1805
+ );
1806
+ }
1807
+ return resolved;
1808
+ }
1809
+
1810
+ function sameFileConstInit(name: string, program: AstNode): AstNode | undefined {
1811
+ let found: AstNode | undefined;
1812
+ walk(program, (node) => {
1813
+ if (found || node.type !== "VariableDeclarator") return;
1814
+ const decl = node as AstNode & { id?: AstNode; init?: AstNode };
1815
+ if (decl.id?.type === "Identifier" && (decl.id as Identifier).name === name) {
1816
+ found = decl.init;
1817
+ }
1818
+ });
1819
+ return found;
1820
+ }
1821
+
1822
+ function collectDecision(call: CallExpression, program: AstNode, scope: ProjectScope): void {
1823
+ const decisionName = stringArg(call.arguments[0]);
1824
+ const opts = objectArg(call.arguments[1]);
1825
+ if (!decisionName || !opts) return;
1826
+ const ask = objectProp(opts, "ask");
1827
+ const questionProps = ask?.type === "ObjectExpression" ? objectProperties(ask) : [];
1828
+ const questionNames = questionProps
1829
+ .map((prop) => propKey(prop))
1830
+ .filter((name): name is string => typeof name === "string" && name.length > 0);
1831
+ if (questionNames.length === 0) {
1832
+ throw new Error(`ai.decision("${decisionName}"): ask is empty`);
1833
+ }
1834
+ for (const prop of questionProps) {
1835
+ const key = propKey(prop);
1836
+ if (!key) continue;
1837
+ if (key === "meta" || key === "$") {
1838
+ throw new Error(`ai.decision("${decisionName}"): question id "${key}" is reserved`);
1839
+ }
1840
+ assertDecisionQuestion(
1841
+ decisionName,
1842
+ key,
1843
+ (prop as AstNode & { value?: AstNode }).value,
1844
+ program,
1845
+ );
1846
+ }
1847
+ if (scope.ai.decisions?.[decisionName]) {
1848
+ throw new Error(`ai.decision("${decisionName}"): duplicate decision name`);
1849
+ }
1850
+ const hasReview = objectProp(opts, "review") !== undefined;
1851
+ const uncertain = stringProp(opts, "onUncertain");
1852
+ const abstain = uncertain === "abstain";
1853
+ if (hasReview === abstain) {
1854
+ throw new Error(
1855
+ `ai.decision("${decisionName}"): declare exactly one of review or onUncertain: "abstain"`,
1856
+ );
1857
+ }
1858
+ const autonomyNode = objectProp(opts, "autonomy");
1859
+ let autonomy: AiDecision["autonomy"];
1860
+ if (autonomyNode?.type === "ObjectExpression") {
1861
+ if (objectProp(autonomyNode, "audit") === undefined) {
1862
+ throw new Error(`ai.decision("${decisionName}"): autonomy requires audit`);
1863
+ }
1864
+ const maxError = numberProp(autonomyNode, "maxError");
1865
+ const audit = numberProp(autonomyNode, "audit");
1866
+ const risk = numberProp(autonomyNode, "risk");
1867
+ if (maxError === undefined || audit === undefined) {
1868
+ throw new Error(`ai.decision("${decisionName}"): autonomy requires maxError and audit`);
1869
+ }
1870
+ autonomy = { maxError, audit, ...(risk !== undefined ? { risk } : {}) };
1871
+ }
1872
+ const evals = stringProp(opts, "evals");
1873
+ const decision: AiDecision = {
1874
+ mode: abstain ? "abstain" : "review",
1875
+ questions: questionNames,
1876
+ ...(hasReview ? { review: decisionReviewName(objectProp(opts, "review"), scope) } : {}),
1877
+ ...(stringProp(opts, "model") ? { model: stringProp(opts, "model") } : {}),
1878
+ driverId: stringProp(opts, "driverId") === "typesafe" ? "typesafe" : "openrouter",
1879
+ ...(evals !== undefined ? { evals } : {}),
1880
+ ...(autonomy !== undefined ? { autonomy } : {}),
1881
+ };
1882
+ scope.ai.decisions = scope.ai.decisions ?? {};
1883
+ scope.ai.decisions[decisionName] = decision;
1884
+ const bindingName = enclosingConstName(call, program);
1885
+ if (bindingName) {
1886
+ scope.bindings.set(bindingName, { kind: "decision", ref: decisionName });
1887
+ }
1888
+ }
1889
+
1890
+ function collectAgent(call: CallExpression, program: AstNode, scope: ProjectScope): void {
1652
1891
  const agentName = stringArg(call.arguments[0]);
1653
1892
  const opts = objectArg(call.arguments[1]);
1654
1893
  if (!agentName || !opts) return;
1655
1894
  const toolsArr = arrayProp(opts, "tools");
1656
- const tools = toolsArr
1657
- ?.map((el) => {
1658
- const resolved = resolveCallTarget(el, scope.bindings);
1659
- if (resolved) return resolved;
1660
- return identifierName(el) ?? stringArg(el);
1661
- })
1662
- .filter((x): x is string => typeof x === "string")
1663
- .map((id) => scope.flowExports.get(id) ?? scope.bindings.get(id)?.ref ?? id);
1895
+ const entries = (toolsArr ?? [])
1896
+ .map((el) => agentToolEntry(el, scope))
1897
+ .filter((entry): entry is { name: string; approval: boolean } => entry !== undefined);
1898
+ const tools = entries.map((entry) => entry.name);
1899
+ const agentCalls = tools.filter(
1900
+ (name) => scope.bindings.get(name)?.kind === "agent" || scope.ai.agents?.[name],
1901
+ );
1902
+ const approvals = Object.fromEntries(
1903
+ entries.filter((entry) => entry.approval).map((entry) => [entry.name, true as const]),
1904
+ );
1664
1905
  const agent: AiAgent = {
1665
- ...(tools && tools.length > 0 ? { tools } : {}),
1906
+ ...(tools.length > 0 ? { tools } : {}),
1907
+ ...(Object.keys(approvals).length > 0 ? { approvals } : {}),
1666
1908
  ...(numberProp(opts, "maxSteps") !== undefined
1667
1909
  ? { maxSteps: numberProp(opts, "maxSteps") }
1668
1910
  : {}),
1911
+ ...(numberProp(opts, "maxDepth") !== undefined
1912
+ ? { maxDepth: numberProp(opts, "maxDepth") }
1913
+ : {}),
1669
1914
  };
1670
1915
  scope.ai.agents = scope.ai.agents ?? {};
1671
1916
  scope.ai.agents[agentName] = agent;
1917
+ const bindingName = enclosingConstName(call, program);
1918
+ if (bindingName) {
1919
+ scope.bindings.set(bindingName, {
1920
+ kind: "agent",
1921
+ ref: agentName,
1922
+ ...(agentCalls.length > 0 ? { agentCalls } : {}),
1923
+ });
1924
+ }
1672
1925
  }
1673
1926
 
1674
1927
  function collectMcpServer(call: CallExpression, program: AstNode, scope: ProjectScope): void {
@@ -1941,6 +2194,7 @@ function registerFlow(args: {
1941
2194
  flowName: name,
1942
2195
  file: args.file.path,
1943
2196
  resolveCallee: (callee, file) => args.scope.fxIndex.resolve(file, callee),
2197
+ resolveFlowRef: (localName, file) => args.scope.fxIndex.flowRef(file, localName),
1944
2198
  })
1945
2199
  : {
1946
2200
  effects: {} as Effects,
@@ -1949,6 +2203,7 @@ function registerFlow(args: {
1949
2203
  cacheIneligible: false,
1950
2204
  nondeterministic: false,
1951
2205
  readsUserId: false,
2206
+ bareIrreversible: [] as ("fetch" | "send")[],
1952
2207
  };
1953
2208
 
1954
2209
  let effects: Effects | undefined;
@@ -2009,7 +2264,16 @@ function registerFlow(args: {
2009
2264
  const line = lineAt(args.file.source, args.flowCall.start ?? 0);
2010
2265
  flow.source = `${args.file.path}:${line}`;
2011
2266
 
2012
- if (boolProp(opts, "durable")) flow.durable = true;
2267
+ if (boolProp(opts, "durable")) {
2268
+ flow.durable = true;
2269
+ const bare = inferred.bareIrreversible[0];
2270
+ if (bare !== undefined) {
2271
+ throw new Error(
2272
+ `OKE1901: durable flow "${name}" calls fx.${bare} outside fx.step. Wrap fx.fetch and fx.send in fx.step.`,
2273
+ );
2274
+ }
2275
+ }
2276
+ assertDecisionPlacement(name, flow, args.scope);
2013
2277
  if (typeof liveFromTrigger === "string") {
2014
2278
  flow.live = liveFromTrigger;
2015
2279
  // Manual live surface — `.live(tableBinding)` on the trigger. Synthesize
@@ -2908,6 +3172,7 @@ function parseEffectsObject(node: AstNode | undefined): Effects | undefined {
2908
3172
  const secrets = stringArrayProp(node, "secrets");
2909
3173
  const calls = stringArrayProp(node, "calls");
2910
3174
  const fetches = stringArrayProp(node, "fetches");
3175
+ const decides = stringArrayProp(node, "decides");
2911
3176
  if (reads) effects.reads = reads as Effects["reads"];
2912
3177
  if (writes) effects.writes = writes as Effects["writes"];
2913
3178
  if (emits) effects.emits = emits;
@@ -2917,6 +3182,7 @@ function parseEffectsObject(node: AstNode | undefined): Effects | undefined {
2917
3182
  if (secrets) effects.secrets = secrets;
2918
3183
  if (calls) effects.calls = calls;
2919
3184
  if (fetches) effects.fetches = fetches;
3185
+ if (decides) effects.decides = decides;
2920
3186
  return effects;
2921
3187
  }
2922
3188
 
@@ -26,6 +26,14 @@ export interface FxIndex {
26
26
  * @param callee - Call callee node
27
27
  */
28
28
  resolve(file: string, callee: AstNode): FxCalleeResolution;
29
+ /**
30
+ * Flow name behind an imported handle (`import { create as createTask }`).
31
+ * Absent when `localName` is not a named import of a `flow()` / `on()` export.
32
+ *
33
+ * @param file - File that contains the `fx.call`
34
+ * @param localName - Local binding (`createTask`)
35
+ */
36
+ flowRef(file: string, localName: string): string | undefined;
29
37
  }
30
38
 
31
39
  interface NamedImport {
@@ -61,6 +69,8 @@ interface FileFx {
61
69
  readonly functions: Map<string, AstNode>;
62
70
  readonly imports: Map<string, ImportBinding>;
63
71
  readonly exports: Map<string, LocalExport | Reexport>;
72
+ /** Local binding → `flow("name")` / `on(…, flow("name"))` literal. */
73
+ readonly flowNames: Map<string, string>;
64
74
  }
65
75
 
66
76
  const INTRINSIC_SOURCES: Readonly<
@@ -176,7 +186,29 @@ export function buildFxIndex(
176
186
  return resolveExport(target, imported.imported, seen);
177
187
  }
178
188
 
189
+ function flowExportName(file: string, exportName: string, seen: Set<string>): string | undefined {
190
+ const key = `${file}#flow:${exportName}`;
191
+ if (seen.has(key)) return undefined;
192
+ seen.add(key);
193
+ const indexed = byFile.get(file);
194
+ if (!indexed) return undefined;
195
+ const exported = indexed.exports.get(exportName);
196
+ if (!exported) return indexed.flowNames.get(exportName);
197
+ if (exported.kind === "local") return indexed.flowNames.get(exported.binding);
198
+ const target = resolveModule(file, exported.from);
199
+ if (!target) return undefined;
200
+ return flowExportName(target, exported.imported, seen);
201
+ }
202
+
179
203
  return {
204
+ flowRef(file, localName) {
205
+ const from = normalizePosix(file);
206
+ const imported = byFile.get(from)?.imports.get(localName);
207
+ if (!imported || imported.kind !== "named") return undefined;
208
+ const target = resolveModule(from, imported.from);
209
+ if (!target) return undefined;
210
+ return flowExportName(target, imported.imported, new Set());
211
+ },
180
212
  resolve(file, callee) {
181
213
  const from = normalizePosix(file);
182
214
  const seen = new Set<string>();
@@ -212,9 +244,10 @@ function indexFile(program: AstNode): FileFx {
212
244
  const functions = new Map<string, AstNode>();
213
245
  const imports = new Map<string, ImportBinding>();
214
246
  const exports = new Map<string, LocalExport | Reexport>();
247
+ const flowNames = new Map<string, string>();
215
248
  const body = (program as AstNode & { body?: AstNode[] }).body ?? [];
216
- for (const stmt of body) indexStmt(stmt, functions, imports, exports);
217
- return { functions, imports, exports };
249
+ for (const stmt of body) indexStmt(stmt, functions, imports, exports, flowNames);
250
+ return { functions, imports, exports, flowNames };
218
251
  }
219
252
 
220
253
  function indexStmt(
@@ -222,6 +255,7 @@ function indexStmt(
222
255
  functions: Map<string, AstNode>,
223
256
  imports: Map<string, ImportBinding>,
224
257
  exports: Map<string, LocalExport | Reexport>,
258
+ flowNames: Map<string, string>,
225
259
  ): void {
226
260
  if (stmt.type === "ImportDeclaration") {
227
261
  indexImport(stmt, imports);
@@ -232,7 +266,7 @@ function indexStmt(
232
266
  return;
233
267
  }
234
268
  if (stmt.type === "VariableDeclaration") {
235
- rememberVars(stmt, functions, exports, false);
269
+ rememberVars(stmt, functions, exports, flowNames, false);
236
270
  return;
237
271
  }
238
272
  if (stmt.type === "ExportDefaultDeclaration") {
@@ -247,7 +281,7 @@ function indexStmt(
247
281
  const decl = (stmt as AstNode & { declaration?: AstNode | null }).declaration;
248
282
  const source = stringArg((stmt as AstNode & { source?: AstNode }).source);
249
283
  if (decl?.type === "FunctionDeclaration") rememberFunction(decl, functions, exports, true);
250
- if (decl?.type === "VariableDeclaration") rememberVars(decl, functions, exports, true);
284
+ if (decl?.type === "VariableDeclaration") rememberVars(decl, functions, exports, flowNames, true);
251
285
  const specifiers = (stmt as AstNode & { specifiers?: AstNode[] }).specifiers ?? [];
252
286
  for (const spec of specifiers) {
253
287
  const local = exportedLocalName(spec);
@@ -309,6 +343,7 @@ function rememberVars(
309
343
  decl: AstNode,
310
344
  functions: Map<string, AstNode>,
311
345
  exports: Map<string, LocalExport | Reexport>,
346
+ flowNames: Map<string, string>,
312
347
  exportIt: boolean,
313
348
  ): void {
314
349
  const declarations = (decl as AstNode & { declarations?: AstNode[] }).declarations ?? [];
@@ -317,10 +352,29 @@ function rememberVars(
317
352
  if (!name) continue;
318
353
  const init = (item as AstNode & { init?: AstNode | null }).init;
319
354
  if (init && isFunctionNode(unwrapValue(init))) functions.set(name, unwrapValue(init));
355
+ const flowName = init ? flowLiteralName(unwrapValue(init)) : undefined;
356
+ if (flowName) flowNames.set(name, flowName);
320
357
  if (exportIt) exports.set(name, { kind: "local", binding: name });
321
358
  }
322
359
  }
323
360
 
361
+ /**
362
+ * `flow("name")` / `call("name")` / `on(trigger, flow("name"))` literal.
363
+ *
364
+ * @param node - Initializer
365
+ */
366
+ function flowLiteralName(node: AstNode): string | undefined {
367
+ if (node.type !== "CallExpression") return undefined;
368
+ const call = node as AstNode & { callee: AstNode; arguments?: AstNode[] };
369
+ const callee = identifierName(call.callee);
370
+ if (callee === "flow" || callee === "call") return stringArg(call.arguments?.[0]);
371
+ if (callee === "on") {
372
+ const body = call.arguments?.[1];
373
+ return body ? flowLiteralName(unwrapValue(body)) : undefined;
374
+ }
375
+ return undefined;
376
+ }
377
+
324
378
  function exportedLocalName(spec: AstNode): string | undefined {
325
379
  return (
326
380
  identifierName((spec as AstNode & { local?: AstNode }).local) ??
@@ -70,6 +70,11 @@ export function encodeFailure(failure: FlowFailure): Response {
70
70
  });
71
71
  }
72
72
 
73
+ /** SSE id reader. Kept off the edge bundle until a stream chunk is encoded. */
74
+ function loadSseId(): typeof import("../kernel/sse-id.ts") {
75
+ return lazyRequire(`${import.meta.dir}/../kernel`, ["sse", "id"].join("-"));
76
+ }
77
+
73
78
  function loadFxLiveStream(): typeof import("../kernel/fx-live-stream.ts") {
74
79
  return lazyRequire(`${import.meta.dir}/../kernel`, ["fx", "live", "stream"].join("-"));
75
80
  }
@@ -133,6 +138,24 @@ export async function encodeExecuteResult(result: {
133
138
  return encodeSuccess(result.output);
134
139
  }
135
140
 
141
+ /**
142
+ * Durable park thrown from a streamed agent after the interrupt frame.
143
+ *
144
+ * Duck-typed so this encoder does not import the journal module.
145
+ *
146
+ * @param err - Value thrown by the chunk iterator
147
+ */
148
+ function isStreamPark(err: unknown): err is { readonly wakeAt: number; readonly label: string } {
149
+ return (
150
+ err instanceof Error &&
151
+ err.name === "JournalSuspend" &&
152
+ "wakeAt" in err &&
153
+ typeof err.wakeAt === "number" &&
154
+ "label" in err &&
155
+ typeof err.label === "string"
156
+ );
157
+ }
158
+
136
159
  function encodeSseStream(carrier: JsonStreamResult): Response {
137
160
  const encoder = new TextEncoder();
138
161
  let finalized = false;
@@ -147,8 +170,12 @@ function encodeSseStream(carrier: JsonStreamResult): Response {
147
170
  for await (const chunk of carrier.chunks) {
148
171
  const frame = loadFxJson().isSseFrame(chunk)
149
172
  ? chunk
150
- : { data: chunk as unknown, id: undefined as string | undefined };
173
+ : { data: chunk as unknown, id: loadSseId().readSseId(chunk) };
151
174
  const lines: string[] = [];
175
+ if ("comment" in frame && frame.comment && frame.data === undefined) {
176
+ controller.enqueue(encoder.encode(`: ${frame.comment}\n\n`));
177
+ continue;
178
+ }
152
179
  if (frame.id !== undefined) lines.push(`id: ${frame.id}`);
153
180
  lines.push(`data: ${JSON.stringify(frame.data)}`);
154
181
  controller.enqueue(encoder.encode(`${lines.join("\n")}\n\n`));
@@ -156,7 +183,13 @@ function encodeSseStream(carrier: JsonStreamResult): Response {
156
183
  controller.enqueue(encoder.encode("data: [DONE]\n\n"));
157
184
  controller.close();
158
185
  } catch (err) {
159
- controller.error(err);
186
+ if (isStreamPark(err)) {
187
+ carrier.parked = { wakeAt: err.wakeAt, label: err.label };
188
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
189
+ controller.close();
190
+ } else {
191
+ controller.error(err);
192
+ }
160
193
  } finally {
161
194
  await finish();
162
195
  }
@@ -508,12 +508,30 @@ export function resolveDriverId(
508
508
  env: ConfigEnv,
509
509
  defaults?: EnvDriverMap,
510
510
  ): string | undefined {
511
+ return driverRefId(resolveDriverRef(map, env, defaults));
512
+ }
513
+
514
+ /**
515
+ * Resolve the rich driver ref for an env (`pool`, `replicas`, `url`).
516
+ *
517
+ * Same merge rules as {@link resolveDriverId}. A string pin has no pool or
518
+ * replicas. With `defaults`, an unset env key keeps that driver's default
519
+ * pin rather than a sibling environment.
520
+ *
521
+ * @param map - Env → driver map (or bare ref)
522
+ * @param env - Active environment
523
+ * @param defaults - Real default map for this specific driver (per-key merge)
524
+ */
525
+ export function resolveDriverRef(
526
+ map: EnvDriverInput | undefined,
527
+ env: ConfigEnv,
528
+ defaults?: EnvDriverMap,
529
+ ): DriverRef | undefined {
511
530
  const normalized = normalizeEnvDriverMap(map);
512
531
  if (defaults) {
513
532
  const merged = mergeEnvDriverMap(normalized, defaults);
514
- return driverRefId(merged[env]);
533
+ return merged[env];
515
534
  }
516
535
  if (!normalized) return undefined;
517
- const ref = normalized[env] ?? normalized.dev ?? normalized.prod ?? normalized.test;
518
- return driverRefId(ref);
536
+ return normalized[env] ?? normalized.dev ?? normalized.prod ?? normalized.test;
519
537
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Console — operator plane UI + server (port 6533).
2
+ * Console — operator plane UI + server (port 6533). Public import: `okengine/console`.
3
3
  *
4
4
  * Built on `createClient<ConsoleApp>`. Every action is a real flow through `fx`;
5
5
  * the audit log is the trace (docs/spec/console.md).