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
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Postgres-backed consent, suppression, and receipt ledger.
3
+ *
4
+ * Reads are synchronous against a cache. Writes hit SQL and the cache.
5
+ * Call {@link PostgresChannelLedger.reload} on another instance to see them.
6
+ */
7
+
8
+ import type { ChannelMedium } from "../../manifest/types.ts";
9
+ import type { ConsentStore, OptOut } from "./consent.ts";
10
+ import type { DeliveryReceipt, ReceiptLedger } from "./receipts.ts";
11
+ import type { SuppressionEntry, SuppressionReason, SuppressionStore } from "./suppression.ts";
12
+
13
+ /** SQL surface the channel ledger needs (`?` placeholders). */
14
+ export interface ChannelLedgerSql {
15
+ /**
16
+ * Run a query and return rows.
17
+ *
18
+ * @param sql - Statement
19
+ * @param params - Bindings
20
+ */
21
+ query(sql: string, params?: readonly unknown[]): Promise<Record<string, unknown>[]>;
22
+ /**
23
+ * Run a statement.
24
+ *
25
+ * @param sql - Statement
26
+ * @param params - Bindings
27
+ */
28
+ exec(sql: string, params?: readonly unknown[]): Promise<{ changes: number }>;
29
+ }
30
+
31
+ /** Shared channel ledger (consent + suppression + receipts). */
32
+ export interface PostgresChannelLedger {
33
+ readonly consent: ConsentStore;
34
+ readonly suppression: SuppressionStore;
35
+ readonly receipts: ReceiptLedger;
36
+ /** Wait for queued writes. */
37
+ flush(): Promise<void>;
38
+ /** Wait for queued writes, then replace the cache from SQL. */
39
+ reload(): Promise<void>;
40
+ }
41
+
42
+ const CONSENT = "oke_channel_consent";
43
+ const BOUNCE = "oke_channel_bounce";
44
+ const RECEIPT = "oke_channel_receipt";
45
+
46
+ /**
47
+ * Open the ledger tables and load the current rows.
48
+ *
49
+ * @param sql - App SQL client
50
+ */
51
+ export async function openPostgresChannelLedger(
52
+ sql: ChannelLedgerSql,
53
+ ): Promise<PostgresChannelLedger> {
54
+ await sql.exec(
55
+ `CREATE TABLE IF NOT EXISTS ${CONSENT} (subject TEXT NOT NULL, medium TEXT NOT NULL, at BIGINT NOT NULL, PRIMARY KEY (subject, medium))`,
56
+ );
57
+ await sql.exec(
58
+ `CREATE TABLE IF NOT EXISTS ${BOUNCE} (subject TEXT NOT NULL, medium TEXT NOT NULL, at BIGINT NOT NULL, PRIMARY KEY (subject, medium))`,
59
+ );
60
+ await sql.exec(`CREATE TABLE IF NOT EXISTS ${RECEIPT} (id TEXT PRIMARY KEY, body TEXT NOT NULL)`);
61
+
62
+ const consents: OptOut[] = [];
63
+ const bounces: OptOut[] = [];
64
+ const receipts: DeliveryReceipt[] = [];
65
+ let chain: Promise<void> = Promise.resolve();
66
+
67
+ function enqueue(work: () => Promise<void>): void {
68
+ chain = chain.then(work);
69
+ }
70
+
71
+ async function flush(): Promise<void> {
72
+ await chain;
73
+ }
74
+
75
+ function matches(rows: readonly OptOut[], subject: string, medium: ChannelMedium): boolean {
76
+ return rows.some(
77
+ (row) => row.subject === subject && (row.medium === "all" || row.medium === medium),
78
+ );
79
+ }
80
+
81
+ function remember(
82
+ rows: OptOut[],
83
+ subject: string,
84
+ medium: ChannelMedium | "all",
85
+ at: number,
86
+ ): void {
87
+ const idx = rows.findIndex((row) => row.subject === subject && row.medium === medium);
88
+ const next: OptOut = { subject, medium, at };
89
+ if (idx >= 0) rows[idx] = next;
90
+ else rows.push(next);
91
+ }
92
+
93
+ function forget(rows: OptOut[], subject: string, medium: ChannelMedium | "all"): void {
94
+ for (let i = rows.length - 1; i >= 0; i--) {
95
+ const row = rows[i]!;
96
+ if (row.subject === subject && row.medium === medium) rows.splice(i, 1);
97
+ }
98
+ }
99
+
100
+ const consent: ConsentStore = {
101
+ isOptedOut(subject, medium) {
102
+ return matches(consents, subject, medium);
103
+ },
104
+ optOut(subject, medium) {
105
+ const at = Date.now();
106
+ remember(consents, subject, medium, at);
107
+ enqueue(() =>
108
+ sql
109
+ .exec(
110
+ `INSERT INTO ${CONSENT} (subject, medium, at) VALUES (?, ?, ?) ON CONFLICT (subject, medium) DO UPDATE SET at = excluded.at`,
111
+ [subject, medium, at],
112
+ )
113
+ .then(() => undefined),
114
+ );
115
+ },
116
+ optIn(subject, medium) {
117
+ forget(consents, subject, medium);
118
+ enqueue(() =>
119
+ sql
120
+ .exec(`DELETE FROM ${CONSENT} WHERE subject = ? AND medium = ?`, [subject, medium])
121
+ .then(() => undefined),
122
+ );
123
+ },
124
+ list() {
125
+ return [...consents];
126
+ },
127
+ };
128
+
129
+ function reasonFor(
130
+ subject: string,
131
+ medium: ChannelMedium,
132
+ ):
133
+ | { readonly suppressed: true; readonly reason: SuppressionReason }
134
+ | { readonly suppressed: false } {
135
+ if (consent.isOptedOut(subject, medium)) return { suppressed: true, reason: "opted-out" };
136
+ if (matches(bounces, subject, medium)) return { suppressed: true, reason: "prior-bounce" };
137
+ return { suppressed: false };
138
+ }
139
+
140
+ const suppression: SuppressionStore = {
141
+ consent,
142
+ isSuppressed: reasonFor,
143
+ optOut(subject, medium) {
144
+ consent.optOut(subject, medium);
145
+ },
146
+ addPriorBounce(subject, medium) {
147
+ const at = Date.now();
148
+ remember(bounces, subject, medium, at);
149
+ enqueue(() =>
150
+ sql
151
+ .exec(
152
+ `INSERT INTO ${BOUNCE} (subject, medium, at) VALUES (?, ?, ?) ON CONFLICT (subject, medium) DO UPDATE SET at = excluded.at`,
153
+ [subject, medium, at],
154
+ )
155
+ .then(() => undefined),
156
+ );
157
+ },
158
+ clear(subject, medium) {
159
+ consent.optIn(subject, medium);
160
+ forget(bounces, subject, medium);
161
+ enqueue(() =>
162
+ sql
163
+ .exec(`DELETE FROM ${BOUNCE} WHERE subject = ? AND medium = ?`, [subject, medium])
164
+ .then(() => undefined),
165
+ );
166
+ },
167
+ list() {
168
+ const rows: SuppressionEntry[] = [];
169
+ for (const row of consents) {
170
+ rows.push({ subject: row.subject, medium: row.medium, reason: "opted-out", at: row.at });
171
+ }
172
+ for (const row of bounces) {
173
+ rows.push({ subject: row.subject, medium: row.medium, reason: "prior-bounce", at: row.at });
174
+ }
175
+ return rows;
176
+ },
177
+ };
178
+
179
+ const receiptLedger: ReceiptLedger = {
180
+ record(receipt) {
181
+ receipts.push(receipt);
182
+ enqueue(() =>
183
+ sql
184
+ .exec(`INSERT INTO ${RECEIPT} (id, body) VALUES (?, ?)`, [
185
+ receipt.id,
186
+ JSON.stringify(receipt),
187
+ ])
188
+ .then(() => undefined),
189
+ );
190
+ },
191
+ all() {
192
+ return [...receipts];
193
+ },
194
+ forTemplate(template) {
195
+ return receipts.filter((row) => row.template === template);
196
+ },
197
+ byMessageId(messageId) {
198
+ for (let i = receipts.length - 1; i >= 0; i--) {
199
+ const row = receipts[i]!;
200
+ if (row.messageId === messageId || row.id === messageId) return row;
201
+ }
202
+ return undefined;
203
+ },
204
+ updateStatus(messageId, patch) {
205
+ const idx = receipts.findIndex((row) => row.messageId === messageId || row.id === messageId);
206
+ if (idx < 0) return undefined;
207
+ const prev = receipts[idx]!;
208
+ const next: DeliveryReceipt = {
209
+ ...prev,
210
+ status: patch.status,
211
+ at: patch.at ?? prev.at,
212
+ ...(patch.error !== undefined ? { error: patch.error } : {}),
213
+ };
214
+ receipts[idx] = next;
215
+ enqueue(() =>
216
+ sql
217
+ .exec(`UPDATE ${RECEIPT} SET body = ? WHERE id = ?`, [JSON.stringify(next), next.id])
218
+ .then(() => undefined),
219
+ );
220
+ return next;
221
+ },
222
+ };
223
+
224
+ async function loadOptOuts(table: string, into: OptOut[]): Promise<void> {
225
+ const rows = await sql.query(`SELECT subject, medium, at FROM ${table}`);
226
+ into.length = 0;
227
+ for (const row of rows) {
228
+ into.push({
229
+ subject: String(row.subject),
230
+ medium: String(row.medium) as ChannelMedium | "all",
231
+ at: Number(row.at),
232
+ });
233
+ }
234
+ }
235
+
236
+ async function reload(): Promise<void> {
237
+ await flush();
238
+ await loadOptOuts(CONSENT, consents);
239
+ await loadOptOuts(BOUNCE, bounces);
240
+ const rows = await sql.query(`SELECT id, body FROM ${RECEIPT}`);
241
+ receipts.length = 0;
242
+ for (const row of rows) {
243
+ const parsed: unknown = JSON.parse(String(row.body));
244
+ if (parsed && typeof parsed === "object") receipts.push(parsed as DeliveryReceipt);
245
+ }
246
+ }
247
+
248
+ await reload();
249
+
250
+ return { consent, suppression, receipts: receiptLedger, flush, reload };
251
+ }
@@ -11,6 +11,7 @@ import { failureCodeOf, rebindUndosFromDo, runCompensationPhase } from "../../ke
11
11
  import type { AnyFlowDef } from "../../kernel/flow.ts";
12
12
  import { createFxContext, type CreateFxOptions, type Fx } from "../../kernel/fx.ts";
13
13
  import { isFlowFailure } from "../../kernel/hooks.ts";
14
+ import { isJsonStreamResult } from "../../kernel/json-result.ts";
14
15
  import {
15
16
  createJournal,
16
17
  isJournalSuspend,
@@ -142,7 +143,32 @@ export async function runDurable<O = unknown>(
142
143
  session.rewind();
143
144
  return options.flow.do(options.input as never, fx);
144
145
  };
145
- const output = await (options.flow.retry ? fxRetry(run, options.flow.retry) : run());
146
+ const produced = await (options.flow.retry ? fxRetry(run, options.flow.retry) : run());
147
+ if (isJsonStreamResult(produced)) {
148
+ try {
149
+ for await (const chunk of produced.chunks) {
150
+ void chunk;
151
+ }
152
+ } catch (err) {
153
+ if (isJournalSuspend(err)) {
154
+ return {
155
+ status: "sleeping",
156
+ runId: session.runId,
157
+ wakeAt: err.wakeAt,
158
+ label: err.label,
159
+ };
160
+ }
161
+ throw err;
162
+ }
163
+ const settled = await produced.result;
164
+ await session.commit("completed", { output: settled });
165
+ return {
166
+ status: "completed",
167
+ runId: session.runId,
168
+ output: settled as O,
169
+ };
170
+ }
171
+ const output = produced;
146
172
  if (isFlowFailure(output)) {
147
173
  await runCompensationPhase({
148
174
  flow: options.flow,
@@ -26,6 +26,12 @@ export interface CreateSignalRuntimeOptions {
26
26
  readonly sql?: unknown;
27
27
  readonly redis?: import("../../drivers/signal-types.ts").SignalRedisClientLike;
28
28
  readonly nats?: import("../../drivers/signal-types.ts").SignalNatsClientLike;
29
+ /** Forwarded to the driver. Postgres poll when `listen` is absent. */
30
+ readonly pollMs?: number;
31
+ /** Redis consumer-group consume for `once`. */
32
+ readonly compete?: boolean;
33
+ /** Redis consumer name. */
34
+ readonly consumerId?: string;
29
35
  }
30
36
 
31
37
  /** Signal runtime. */
@@ -106,6 +112,9 @@ export function createSignalRuntime(options: CreateSignalRuntimeOptions): Signal
106
112
  sql: options.sql,
107
113
  redis: options.redis,
108
114
  nats: options.nats,
115
+ pollMs: options.pollMs,
116
+ compete: options.compete,
117
+ consumerId: options.consumerId,
109
118
  });
110
119
  return bus;
111
120
  },
@@ -267,6 +267,7 @@ describe("postgres transactional emit (dual-write fix)", () => {
267
267
  });
268
268
 
269
269
  const notified: string[] = [];
270
+ if (sql.listen === undefined) throw new Error("postgres fake has no listen");
270
271
  await sql.listen("oke_signal", (p) => {
271
272
  notified.push(p);
272
273
  });
@@ -17,6 +17,7 @@ import {
17
17
  isInvalidatedByWrite,
18
18
  resolveCacheEffects,
19
19
  tier1DimsByResource,
20
+ tier1FlowDims,
20
21
  tier1KeysForReads,
21
22
  tier1Lookup,
22
23
  } from "./cache.ts";
@@ -79,21 +80,41 @@ describe("tier-1 cache — exact per-resource invalidation (path b)", () => {
79
80
  });
80
81
 
81
82
  describe("autoCacheEligible", () => {
82
- test("read-only flows cache by default; mutations / asks / durable / cache:false do not", () => {
83
- expect(autoCacheEligible({ effects: { reads: ["sql:notes"] } })).toBe(true);
84
- expect(autoCacheEligible({ cache: true, effects: { reads: ["sql:notes"] } })).toBe(true);
85
- expect(autoCacheEligible({ cache: "30s", effects: { reads: ["sql:notes"] } })).toBe(true);
86
- expect(autoCacheEligible({ cache: false, effects: { reads: ["sql:notes"] } })).toBe(false);
87
- expect(autoCacheEligible({ durable: true, effects: { reads: ["sql:notes"] } })).toBe(false);
88
- expect(autoCacheEligible({ effects: { reads: ["sql:notes"], writes: ["sql:notes"] } })).toBe(
89
- false,
90
- );
91
- expect(autoCacheEligible({ effects: { reads: ["sql:notes"], asks: ["task-suggest"] } })).toBe(
83
+ test("pure store reads cache by default; side effects never cache", () => {
84
+ const reads: Effects = { reads: ["sql:notes"] };
85
+ expect(autoCacheEligible({ effects: reads })).toBe(true);
86
+ expect(autoCacheEligible({ auto: false, effects: reads })).toBe(false);
87
+ expect(autoCacheEligible({ auto: false, cache: true, effects: reads })).toBe(true);
88
+ expect(autoCacheEligible({ auto: true, effects: reads })).toBe(true);
89
+ expect(autoCacheEligible({ cache: true, effects: reads })).toBe(true);
90
+ expect(autoCacheEligible({ cache: "30s", effects: reads })).toBe(true);
91
+ expect(autoCacheEligible({ auto: true, cache: false, effects: reads })).toBe(false);
92
+ expect(autoCacheEligible({ cache: true, durable: true, effects: reads })).toBe(false);
93
+ expect(
94
+ autoCacheEligible({ cache: true, effects: { reads: ["sql:notes"], writes: ["sql:notes"] } }),
95
+ ).toBe(false);
96
+ expect(
97
+ autoCacheEligible({ cache: true, effects: { reads: ["sql:notes"], asks: ["task-suggest"] } }),
98
+ ).toBe(false);
99
+ for (const extra of [
100
+ { emits: ["note.created"] },
101
+ { sends: ["welcome"] },
102
+ { fetches: ["api.example.com"] },
103
+ { secrets: ["stripe"] },
104
+ { calls: ["notes.create"] },
105
+ { embeds: ["embed-small"] },
106
+ { decides: ["route"] },
107
+ ]) {
108
+ expect(autoCacheEligible({ auto: true, effects: { reads: ["sql:notes"], ...extra } })).toBe(
109
+ false,
110
+ );
111
+ }
112
+ expect(autoCacheEligible({ auto: true, effects: { reads: ["runs"] } })).toBe(false);
113
+ expect(autoCacheEligible({ auto: true, effects: { reads: ["signal:notify"] } })).toBe(false);
114
+ expect(autoCacheEligible({ auto: true, effects: { reads: ["sql:notes", "runs"] } })).toBe(
92
115
  false,
93
116
  );
94
- expect(autoCacheEligible({ effects: { reads: ["runs"] } })).toBe(false);
95
- expect(autoCacheEligible({ effects: { reads: ["signal:notify"] } })).toBe(false);
96
- expect(autoCacheEligible({ effects: {} })).toBe(false);
117
+ expect(autoCacheEligible({ auto: true, effects: {} })).toBe(false);
97
118
  });
98
119
 
99
120
  test("flow+input dims keep list and get from colliding; invalidation still uses the resource", () => {
@@ -107,7 +128,7 @@ describe("autoCacheEligible", () => {
107
128
  expect(isInvalidatedByWrite(getKeys[0]!, { writes: ["sql:views"] })).toBe(true);
108
129
  });
109
130
 
110
- test("effectsFromLedger keeps store reads/writes and asks; drops runs", () => {
131
+ test("effectsFromLedger keeps store effects and side effects, including non-store reads", () => {
111
132
  expect(
112
133
  effectsFromLedger([
113
134
  { kind: "read", resource: "sql:views" },
@@ -117,14 +138,98 @@ describe("autoCacheEligible", () => {
117
138
  { kind: "write", resource: "sql:views" },
118
139
  { kind: "ask", resource: "summarize" },
119
140
  { kind: "emit", resource: "view-changed" },
141
+ { kind: "send", resource: "welcome" },
142
+ { kind: "fetch", resource: "api.example.com" },
143
+ { kind: "secret", resource: "stripe" },
144
+ { kind: "call", resource: "notes.create" },
145
+ { kind: "embed", resource: "embed-small" },
146
+ { kind: "decide", resource: "route" },
120
147
  ]),
121
148
  ).toEqual({
122
- reads: ["sql:views"],
149
+ reads: ["sql:views", "runs", "signal:notify"],
123
150
  writes: ["sql:views"],
124
151
  asks: ["summarize"],
152
+ emits: ["view-changed"],
153
+ sends: ["welcome"],
154
+ fetches: ["api.example.com"],
155
+ secrets: ["stripe"],
156
+ calls: ["notes.create"],
157
+ embeds: ["embed-small"],
158
+ decides: ["route"],
125
159
  });
126
160
  });
127
161
 
162
+ test("tier-1 dims always include tenant, locale, scopes, and roles", () => {
163
+ const effects: Effects = { reads: ["sql:notes"] };
164
+ const a = tier1FlowDims(
165
+ "notes.list",
166
+ {},
167
+ {
168
+ userId: "u1",
169
+ tenantId: "ta",
170
+ locale: "en",
171
+ scopes: ["b", "a"],
172
+ roles: ["admin"],
173
+ },
174
+ );
175
+ const b = tier1FlowDims(
176
+ "notes.list",
177
+ {},
178
+ {
179
+ userId: "u1",
180
+ tenantId: "tb",
181
+ locale: "en",
182
+ scopes: ["a", "b"],
183
+ roles: ["admin"],
184
+ },
185
+ );
186
+ const locale = tier1FlowDims(
187
+ "notes.list",
188
+ {},
189
+ {
190
+ userId: "u1",
191
+ tenantId: "ta",
192
+ locale: "ar",
193
+ scopes: ["a", "b"],
194
+ roles: ["member"],
195
+ },
196
+ );
197
+ expect(a).toContain("t:ta");
198
+ expect(a).toContain("l:en");
199
+ expect(a).toContain("s:a,b");
200
+ expect(a).toContain("r:admin");
201
+ expect(a).not.toEqual(b);
202
+ expect(
203
+ tier1DimsByResource(
204
+ effects,
205
+ "notes.list",
206
+ {},
207
+ {
208
+ userId: "u1",
209
+ tenantId: "ta",
210
+ locale: "en",
211
+ scopes: ["a"],
212
+ roles: ["admin"],
213
+ },
214
+ )["sql:notes"],
215
+ ).not.toEqual(
216
+ tier1DimsByResource(
217
+ effects,
218
+ "notes.list",
219
+ {},
220
+ {
221
+ userId: "u1",
222
+ tenantId: "ta",
223
+ locale: "ar",
224
+ scopes: ["a"],
225
+ roles: ["member"],
226
+ },
227
+ )["sql:notes"],
228
+ );
229
+ expect(locale).toContain("l:ar");
230
+ expect(locale).toContain("r:member");
231
+ });
232
+
128
233
  test("resolveCacheEffects prefers stamped reads, then learned reads", () => {
129
234
  expect(resolveCacheEffects({ reads: ["sql:views"] }, ["sql:tasks"])).toEqual({
130
235
  reads: ["sql:views"],