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.
- package/AGENTS.md +7 -0
- package/manifest.v1.schema.json +44 -0
- package/package.json +32 -19
- package/site/content/docs/ai/mcp.mdx +14 -10
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/index.mdx +4 -4
- package/site/content/docs/elements/channel/receipts.mdx +7 -5
- package/site/content/docs/elements/channel/sms.mdx +2 -1
- package/site/content/docs/elements/flow/index.mdx +22 -18
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/signal/index.mdx +15 -14
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/plugins/otp.mdx +3 -3
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +9 -7
- package/site/content/docs/reference/fx.mdx +19 -10
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/auth/tenants.ts +17 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/doctor-diff.test.ts +29 -1
- package/src/cli/doctor-diff.ts +35 -0
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +2 -2
- package/src/cli/load-config.ts +1 -1
- package/src/cli/manifest-pr-diff.ts +16 -0
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/client/agent.ts +307 -0
- package/src/client/transport.test.ts +20 -0
- package/src/client/transport.ts +2 -1
- package/src/client/types.ts +1 -1
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-fetch.test.ts +77 -0
- package/src/compiler/effects-infer.ts +114 -17
- package/src/compiler/effects-join.test.ts +48 -0
- package/src/compiler/extract.test.ts +28 -0
- package/src/compiler/extract.ts +279 -13
- package/src/compiler/fx-index.ts +58 -4
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/index.ts +1 -1
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-C_N4qvai.js +4 -0
- package/src/console/ui-next/dist/assets/{agent-disclosure-Bkohc_8X.js → agent-disclosure-Cq1TxniW.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-rL0lHxat.js → cache-glyph-BaI4uxZz.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-Deo4PP18.js → call-pii-button-DNyK7IIY.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-CoJ6amHf.js → collapsible-CRhBJMZR.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-D9twj8Xy.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-CsHWlVPU.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-ZaTJAhR9.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CPYBzDh0.js → highlighted-json-DQ-EMAfz.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-DTPevKmM.js → http-method-Cn_Fl79s.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-CmRFeWdT.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-Cn6R_dXz.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-BM27lXld.js → replica-lag-Bx6EZhSe.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DmdiUrfh.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-8P6vGXnn.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-BfGvpFSv.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CWdUHbc0.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-CZOFZzay.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-ki_ishmX.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +6 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.test.ts +22 -1
- package/src/drivers/journal-postgres.ts +511 -13
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/signal-compete.test.ts +234 -0
- package/src/drivers/signal-postgres.ts +39 -19
- package/src/drivers/signal-redis.ts +167 -11
- package/src/drivers/signal-types.ts +12 -0
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/mcp-protocol.ts +5 -6
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1187 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/channel/runtime.ts +11 -0
- package/src/elements/channel/sql-ledger.ts +251 -0
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/signal/runtime.ts +9 -0
- package/src/elements/signal.test.ts +1 -0
- package/src/elements/store/cache.test.ts +120 -15
- package/src/elements/store/cache.ts +200 -39
- package/src/elements/store/prepare-row.test.ts +6 -2
- package/src/elements/store/sql-session.test.ts +53 -0
- package/src/elements/store/sql-session.ts +206 -10
- package/src/elements/store.ts +1 -0
- package/src/i18n/catalogs/ar.ts +5 -0
- package/src/i18n/catalogs/en.ts +5 -0
- package/src/index.ts +2 -0
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +135 -17
- package/src/kernel/auto-cache.test.ts +228 -0
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/channel.ts +63 -2
- package/src/kernel/boot-bind/honor-config.test.ts +40 -10
- package/src/kernel/boot-bind/signal.ts +61 -25
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot.ts +31 -6
- package/src/kernel/builtin-errors.ts +2 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +11 -0
- package/src/kernel/errors-text.ts +20 -0
- package/src/kernel/errors.ts +10 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +33 -3
- package/src/kernel/fx-call-types.test.ts +32 -0
- package/src/kernel/fx-decide.ts +765 -0
- package/src/kernel/fx-sql-handle.ts +46 -7
- package/src/kernel/fx.test.ts +13 -8
- package/src/kernel/fx.ts +339 -45
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency.test.ts +2 -1
- package/src/kernel/journal.test.ts +114 -0
- package/src/kernel/journal.ts +471 -51
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline-tenant.ts +5 -1
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/pipeline.ts +5 -0
- package/src/kernel/signal-tx.ts +28 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/kernel/store-transaction.test.ts +95 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +29 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/docs-server.ts +19 -2
- package/src/mcp/mcp.test.ts +83 -0
- package/src/mcp/protocol.ts +8 -2
- package/src/mcp/server.ts +23 -2
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/mcp/versions.ts +60 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/mena.ts +56 -0
- package/src/release/http-graph.test.ts +2 -0
- package/src/release/http-graph.ts +5 -0
- package/src/release/measure.ts +39 -14
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Ze6ckjdd.js +0 -4
- package/src/console/ui-next/dist/assets/copy-inline-button-CWG5_ZOh.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-b_ZzIsmU.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-nwXEO1Ac.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-C8sSCqYT.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-D2E7BtKK.js +0 -1
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +0 -58
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-BjFT7DNl.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Cmitq3da.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CbDIB-7x.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-Cs45Czkt.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-BOMr0go9.js +0 -2
package/src/kernel/app.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* pipeline, and wires `fx.call` to untriggered (and triggered) flows.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { bindAgentApprovalFlows } from "../elements/ai/approval-http.ts";
|
|
9
10
|
import { compileRoute } from "../compiler/dynamic.ts";
|
|
10
11
|
import type { CompiledRoute } from "../compiler/aot.ts";
|
|
11
12
|
import { encodeExecuteResult, encodeFailure } from "../compiler/response.ts";
|
|
@@ -67,6 +68,7 @@ import {
|
|
|
67
68
|
import { GATE_PUBLIC_NAME } from "../elements/gate/flatten.ts";
|
|
68
69
|
import type { JsonCodeAuth } from "../runtime/json-code-block.ts";
|
|
69
70
|
import { resolveDurationMs } from "./elapsed.ts";
|
|
71
|
+
import { captureHttpFrame } from "./http-frame.ts";
|
|
70
72
|
import { fail, throwOke, OkeError } from "./errors.ts";
|
|
71
73
|
import { FLOW_NAME_DUPLICATE, FLOW_UNNAMED } from "./errors-flow-name.ts";
|
|
72
74
|
import { ONCE_SIGNAL_MULTI_FLOW } from "./errors-once-signal.ts";
|
|
@@ -217,6 +219,39 @@ function loadClientDescriptor(): typeof import("./client-descriptor.ts") {
|
|
|
217
219
|
return lazyRequire(import.meta.dir, ["client", "descriptor"].join("-"));
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
/** Decision HTTP flows. A static `import()` still lands on every HTTP bundle. */
|
|
223
|
+
function loadDecisionBind(): typeof import("../elements/ai/decisions/bind.ts") {
|
|
224
|
+
return lazyRequire(`${import.meta.dir}/../elements/ai/decisions`, ["bi", "nd"].join(""));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Decision lockfile. Loaded only when the manifest declares decisions. */
|
|
228
|
+
function loadDecisionCertificate(): typeof import("../elements/ai/decisions/certificate.ts") {
|
|
229
|
+
return lazyRequire(`${import.meta.dir}/../elements/ai/decisions`, ["certificate"].join(""));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Decision label opener. Loaded only when the manifest declares decisions. */
|
|
233
|
+
function loadDecisionLabels(): typeof import("../elements/ai/decisions/labels.ts") {
|
|
234
|
+
return lazyRequire(`${import.meta.dir}/../elements/ai/decisions`, ["lab", "els"].join(""));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Drift-flag registration. Loaded only when the manifest declares decisions. */
|
|
238
|
+
function loadDecisionLabelStore(): typeof import("./decision-label-store.ts") {
|
|
239
|
+
return lazyRequire(import.meta.dir, ["decision", "label", "store"].join("-"));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** `fx.decide` gate hook. Loaded only when the manifest declares decisions. */
|
|
243
|
+
function loadFxDecideModule(): typeof import("./fx-decide.ts") {
|
|
244
|
+
return lazyRequire(import.meta.dir, ["fx", "decide"].join("-"));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Full boot graph (drivers, elements). A static `import()` string is still
|
|
249
|
+
* bundled into every HTTP app; the stem stays computed.
|
|
250
|
+
*/
|
|
251
|
+
function loadBoot(): typeof import("./boot.ts") {
|
|
252
|
+
return lazyRequire(import.meta.dir, ["bo", "ot"].join(""));
|
|
253
|
+
}
|
|
254
|
+
|
|
220
255
|
/** Options for {@link oke}. */
|
|
221
256
|
export interface OkeOptions {
|
|
222
257
|
/** Application name (Manifest `app`). */
|
|
@@ -333,6 +368,15 @@ export interface OkeOptions {
|
|
|
333
368
|
};
|
|
334
369
|
};
|
|
335
370
|
};
|
|
371
|
+
/**
|
|
372
|
+
* Tier-1 read cache. Omitted caches pure store-read flows. `{ auto: false }`
|
|
373
|
+
* turns that off. A flow can still opt in with `cache: true` or a duration,
|
|
374
|
+
* and `cache: false` always disables. The key includes tenant, locale,
|
|
375
|
+
* scopes, and membership roles.
|
|
376
|
+
*/
|
|
377
|
+
readonly cache?: {
|
|
378
|
+
readonly auto?: boolean;
|
|
379
|
+
};
|
|
336
380
|
/** Channel runtime options. */
|
|
337
381
|
readonly channel?: BootOptions["channel"];
|
|
338
382
|
/** AI runtime options. */
|
|
@@ -563,6 +607,8 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
563
607
|
readonly runId?: string;
|
|
564
608
|
/** Tenant identity for cron / `fx.call` (propagated, unlike auth). */
|
|
565
609
|
readonly tenant?: { readonly id: string | null };
|
|
610
|
+
/** Explicit locale override for {@link Fx.t} / channel sends. */
|
|
611
|
+
readonly locale?: string;
|
|
566
612
|
},
|
|
567
613
|
): Promise<ExecuteResult>;
|
|
568
614
|
/**
|
|
@@ -1095,6 +1141,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1095
1141
|
// contribute auth-method Bindings before traffic (plan Phase 2).
|
|
1096
1142
|
const router: Router<Binding> = smart;
|
|
1097
1143
|
|
|
1144
|
+
bindAgentApprovalFlows(adoptBinding);
|
|
1145
|
+
|
|
1098
1146
|
function adoptBinding(b: Binding): void {
|
|
1099
1147
|
adopted.push(b);
|
|
1100
1148
|
registerFlow(b.flow);
|
|
@@ -1115,6 +1163,24 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1115
1163
|
|
|
1116
1164
|
/** Once-per-app: system `_oke_search_embed_*` CDC bindings from Manifest. */
|
|
1117
1165
|
let searchEmbedFlowsBound = false;
|
|
1166
|
+
let decisionFlowsBound = false;
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Register the decision aggregate, drift monitor, and candidate route
|
|
1170
|
+
* when any decision declares autonomy.
|
|
1171
|
+
*
|
|
1172
|
+
* @param manifest - Boot manifest
|
|
1173
|
+
*/
|
|
1174
|
+
async function ensureDecisionFlows(manifest: BootOptions["manifest"] | undefined): Promise<void> {
|
|
1175
|
+
if (decisionFlowsBound || !manifest) return;
|
|
1176
|
+
const { bindDecisionFlows, manifestHasDecisionAutonomy } = loadDecisionBind();
|
|
1177
|
+
if (!manifestHasDecisionAutonomy(manifest)) {
|
|
1178
|
+
decisionFlowsBound = true;
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
bindDecisionFlows(adoptBinding, manifest);
|
|
1182
|
+
decisionFlowsBound = true;
|
|
1183
|
+
}
|
|
1118
1184
|
/**
|
|
1119
1185
|
* Auto-register hybrid-search embed CDC flows when any `.embed()` column
|
|
1120
1186
|
* exists. Lazy-loads search-bind so edge graphs without search stay lean.
|
|
@@ -1339,6 +1405,19 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1339
1405
|
...durableResumeFx(),
|
|
1340
1406
|
...(gateConfig.auth?.tenant ? (loadAppTenant().w(5, gateConfig.auth) as object) : {}),
|
|
1341
1407
|
...(existing?.tenant ? { tenant: { id: existing.tenant } } : {}),
|
|
1408
|
+
callHandler: async (name, callInput) => {
|
|
1409
|
+
const target = flowsByName.get(name);
|
|
1410
|
+
if (!target) return undefined;
|
|
1411
|
+
const inner = await execute(
|
|
1412
|
+
target,
|
|
1413
|
+
callInput,
|
|
1414
|
+
{ kind: "internal" } satisfies InternalTrigger,
|
|
1415
|
+
{ parentId: runId },
|
|
1416
|
+
);
|
|
1417
|
+
if (inner.failure) return inner.failure;
|
|
1418
|
+
if (inner.ctx.error !== undefined) throw inner.ctx.error;
|
|
1419
|
+
return inner.output;
|
|
1420
|
+
},
|
|
1342
1421
|
},
|
|
1343
1422
|
});
|
|
1344
1423
|
} catch (err) {
|
|
@@ -1367,7 +1446,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1367
1446
|
}
|
|
1368
1447
|
|
|
1369
1448
|
async function doBoot(overrides?: Partial<BootOptions>): Promise<BootResult> {
|
|
1370
|
-
const { bootApplication, resolveElementNeeds } =
|
|
1449
|
+
const { bootApplication, resolveElementNeeds } = loadBoot();
|
|
1371
1450
|
const { assertHttpGatePosture } = await import("../elements/gate/boot.ts");
|
|
1372
1451
|
const { assertPluginNeeds, buildAvailableNeedTokens } = await import("./plugin-needs.ts");
|
|
1373
1452
|
const dockerFlag =
|
|
@@ -1388,6 +1467,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1388
1467
|
}
|
|
1389
1468
|
|
|
1390
1469
|
const unguardedHttp = overrides?.unguardedHttp ?? gateConfig.unguardedHttp ?? "deny";
|
|
1470
|
+
await ensureDecisionFlows(overrides?.manifest ?? options.manifest);
|
|
1391
1471
|
// Auth bindings are in `adopted` — missing posture fails with GateBootError.
|
|
1392
1472
|
assertHttpGatePosture(adopted, { unguardedHttp, env: bootEnv });
|
|
1393
1473
|
|
|
@@ -1557,6 +1637,26 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1557
1637
|
// Built-in hybrid search — auto-register durable embed CDC flows when the
|
|
1558
1638
|
// Manifest declares any `.embed()` column (writer flows stay embed-free).
|
|
1559
1639
|
await ensureSearchEmbedFlows(overrides?.manifest ?? options.manifest);
|
|
1640
|
+
const decisionManifest = overrides?.manifest ?? options.manifest;
|
|
1641
|
+
const declaredDecisions = decisionManifest?.ai?.decisions;
|
|
1642
|
+
if (declaredDecisions && Object.keys(declaredDecisions).length > 0) {
|
|
1643
|
+
const { loadDecisionLockfile, setDecisionDrift } = loadDecisionCertificate();
|
|
1644
|
+
const { openDecisionLabelStore } = loadDecisionLabels();
|
|
1645
|
+
const { setDeclaredDriftDecisions } = loadDecisionLabelStore();
|
|
1646
|
+
const { setDecisionGateAllow } = loadFxDecideModule();
|
|
1647
|
+
const root = overrides?.rootDir ?? options.rootDir ?? process.env["OKE_ROOT_DIR"];
|
|
1648
|
+
if (!root) {
|
|
1649
|
+
throw new Error(
|
|
1650
|
+
"oke boot: decisions are declared but rootDir and OKE_ROOT_DIR are unset, so the decision lockfile cannot load.",
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
await loadDecisionLockfile(root);
|
|
1654
|
+
setDeclaredDriftDecisions(Object.keys(declaredDecisions));
|
|
1655
|
+
if (result.journal && result.journal.store.decisions) {
|
|
1656
|
+
await openDecisionLabelStore(result.journal.store, setDecisionDrift);
|
|
1657
|
+
}
|
|
1658
|
+
if (result.gate) setDecisionGateAllow((names, ctx) => result.gate!.allow(names, ctx));
|
|
1659
|
+
}
|
|
1560
1660
|
// otp() provider / app mode capability — fail loud at boot, never silent downgrade.
|
|
1561
1661
|
if (result.channel) {
|
|
1562
1662
|
const { assertOtpPluginCapability } = await import("../auth/otp-capability.ts");
|
|
@@ -1996,6 +2096,14 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1996
2096
|
: undefined;
|
|
1997
2097
|
// Revealed PII must not hit a prior masked entry or land in cache.
|
|
1998
2098
|
const reveal = extras?.trustedInvoke === true && extras.revealPii === true;
|
|
2099
|
+
const cacheAuto = options.cache?.auto !== false;
|
|
2100
|
+
const cacheCaller = {
|
|
2101
|
+
userId: fx.auth.userId,
|
|
2102
|
+
tenantId: fx.tenant.id,
|
|
2103
|
+
locale: fx.locale,
|
|
2104
|
+
scopes: fx.auth.scopes,
|
|
2105
|
+
roles: principals?.cacheRoles ?? [],
|
|
2106
|
+
};
|
|
1999
2107
|
const cacheOk =
|
|
2000
2108
|
cache !== undefined &&
|
|
2001
2109
|
storeRt !== undefined &&
|
|
@@ -2003,12 +2111,13 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2003
2111
|
!reveal &&
|
|
2004
2112
|
cache.autoCacheEligible({
|
|
2005
2113
|
cache: flowDef.cache,
|
|
2114
|
+
auto: cacheAuto,
|
|
2006
2115
|
durable: flowDef.durable,
|
|
2007
2116
|
effects: cacheEffects,
|
|
2008
2117
|
});
|
|
2009
2118
|
const dims =
|
|
2010
2119
|
cacheOk && cache && cacheEffects
|
|
2011
|
-
? cache.tier1DimsByResource(cacheEffects, flowDef.name, input,
|
|
2120
|
+
? cache.tier1DimsByResource(cacheEffects, flowDef.name, input, cacheCaller)
|
|
2012
2121
|
: undefined;
|
|
2013
2122
|
if (cacheOk && cache && dims && storeRt) {
|
|
2014
2123
|
const keys = cache.tier1KeysForReads(cacheEffects, dims);
|
|
@@ -2043,39 +2152,35 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2043
2152
|
const output = isFlowFailure(raw) ? raw : await projectFlowOut(flowDef.out, raw);
|
|
2044
2153
|
if (!isFlowFailure(output) && cache && storeRt && cacheEffects) {
|
|
2045
2154
|
const ledgerFx = cache.effectsFromLedger(ledger.entries);
|
|
2155
|
+
const observed = cache.mergeEffects(cacheEffects, ledgerFx);
|
|
2046
2156
|
const writeEffects: Effects = {
|
|
2047
|
-
writes: [
|
|
2048
|
-
...new Set([...(cacheEffects.writes ?? []), ...(ledgerFx.writes ?? [])]),
|
|
2049
|
-
].filter(cache.isStoreResourceRef),
|
|
2157
|
+
writes: (observed.writes ?? []).filter(cache.isStoreResourceRef),
|
|
2050
2158
|
};
|
|
2051
2159
|
if ((writeEffects.writes?.length ?? 0) > 0) {
|
|
2052
2160
|
storeRt.onWriteEffects(writeEffects);
|
|
2053
2161
|
}
|
|
2054
|
-
const
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
learnedTier1Reads.
|
|
2162
|
+
const storeReads = (observed.reads ?? []).filter(cache.isStoreResourceRef);
|
|
2163
|
+
if (cache.autoCachePure(observed) && storeReads.length > 0) {
|
|
2164
|
+
learnedTier1Reads.set(flowDef.name, storeReads);
|
|
2165
|
+
} else {
|
|
2166
|
+
learnedTier1Reads.delete(flowDef.name);
|
|
2059
2167
|
}
|
|
2060
|
-
const putEffects: Effects = {
|
|
2061
|
-
reads: mergedReads,
|
|
2062
|
-
...(ledgerFx.writes ? { writes: ledgerFx.writes } : {}),
|
|
2063
|
-
...(ledgerFx.asks ? { asks: ledgerFx.asks } : {}),
|
|
2064
|
-
};
|
|
2065
2168
|
const storeAfter =
|
|
2066
2169
|
!reveal &&
|
|
2067
2170
|
cache.autoCacheEligible({
|
|
2068
2171
|
cache: flowDef.cache,
|
|
2172
|
+
auto: cacheAuto,
|
|
2069
2173
|
durable: flowDef.durable,
|
|
2070
|
-
effects:
|
|
2174
|
+
effects: observed,
|
|
2071
2175
|
});
|
|
2072
2176
|
if (storeAfter && output !== undefined && !loadFx().isJsonStreamResult(output)) {
|
|
2073
2177
|
const ttlMs =
|
|
2074
2178
|
typeof flowDef.cache === "string" ? cache.parseTtlMs(flowDef.cache) : undefined;
|
|
2179
|
+
const putEffects: Effects = { reads: storeReads };
|
|
2075
2180
|
storeRt.putTier1(
|
|
2076
2181
|
putEffects,
|
|
2077
2182
|
output,
|
|
2078
|
-
cache.tier1DimsByResource(putEffects, flowDef.name, input,
|
|
2183
|
+
cache.tier1DimsByResource(putEffects, flowDef.name, input, cacheCaller),
|
|
2079
2184
|
ttlMs,
|
|
2080
2185
|
);
|
|
2081
2186
|
if (!cacheOk) telemetry.cacheMisses += 1;
|
|
@@ -2257,6 +2362,14 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2257
2362
|
: durationMs;
|
|
2258
2363
|
if (journalSession) {
|
|
2259
2364
|
inflightRuns.delete(journalSession.runId);
|
|
2365
|
+
const parked = streamOut?.parked;
|
|
2366
|
+
if (parked) {
|
|
2367
|
+
ctx.state.sleeping = {
|
|
2368
|
+
wakeAt: parked.wakeAt,
|
|
2369
|
+
label: parked.label,
|
|
2370
|
+
runId: journalSession.runId,
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2260
2373
|
const sleeping = ctx.state.sleeping as
|
|
2261
2374
|
| { readonly wakeAt: number; readonly label: string; readonly runId: string }
|
|
2262
2375
|
| undefined;
|
|
@@ -2277,6 +2390,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2277
2390
|
fx,
|
|
2278
2391
|
error: terminalErr,
|
|
2279
2392
|
});
|
|
2393
|
+
} else if (parked && sleeping) {
|
|
2394
|
+
await journalSession.commit("sleeping", { wakeAt: sleeping.wakeAt });
|
|
2280
2395
|
} else if (!sleeping) {
|
|
2281
2396
|
await journalSession.commit("completed", {
|
|
2282
2397
|
output: streamOut ? { streamed: true } : result.output,
|
|
@@ -2309,6 +2424,9 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
2309
2424
|
? { output: streamOut ? { streamed: true } : result.output }
|
|
2310
2425
|
: {}),
|
|
2311
2426
|
...(parentId !== undefined ? { parentId } : {}),
|
|
2427
|
+
...(trigger.kind === "http" && extras?.request
|
|
2428
|
+
? { http: captureHttpFrame(extras.request, result.response) }
|
|
2429
|
+
: {}),
|
|
2312
2430
|
},
|
|
2313
2431
|
archiveCleartext,
|
|
2314
2432
|
);
|
|
@@ -9,6 +9,7 @@ import { gate } from "../elements/gate.ts";
|
|
|
9
9
|
import { field, id, now, store } from "../elements/store.ts";
|
|
10
10
|
import { createTestApp, type TestApiCall, type TestApp } from "../test/create-test-app.ts";
|
|
11
11
|
import { isStoreResourceRef } from "../elements/store/cache.ts";
|
|
12
|
+
import type { Effects } from "../manifest/types.ts";
|
|
12
13
|
import { oke } from "./app.ts";
|
|
13
14
|
import { resetNoEffectsWarnForTests } from "./boot.ts";
|
|
14
15
|
import { flow, resetFlowSeq } from "./flow.ts";
|
|
@@ -56,6 +57,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
56
57
|
const app = oke({
|
|
57
58
|
name: "auto-cache",
|
|
58
59
|
stores: [db],
|
|
60
|
+
cache: { auto: true },
|
|
59
61
|
gate: { policies: [gate.public] },
|
|
60
62
|
}).adopt({ list });
|
|
61
63
|
const t = await createTestApp(app, { capability: "open" });
|
|
@@ -88,6 +90,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
88
90
|
const app = oke({
|
|
89
91
|
name: "auto-cache-execute",
|
|
90
92
|
stores: [db],
|
|
93
|
+
cache: { auto: true },
|
|
91
94
|
gate: { policies: [gate.public] },
|
|
92
95
|
}).adopt({ list });
|
|
93
96
|
const t = await createTestApp(app, { capability: "open" });
|
|
@@ -156,6 +159,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
156
159
|
const app = oke({
|
|
157
160
|
name: "auto-cache-ledger",
|
|
158
161
|
stores: [db],
|
|
162
|
+
cache: { auto: true },
|
|
159
163
|
gate: { policies: [gate.public] },
|
|
160
164
|
}).adopt({ list });
|
|
161
165
|
const t = await createTestApp(app, { capability: "open" });
|
|
@@ -207,6 +211,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
207
211
|
const app = oke({
|
|
208
212
|
name: "auto-cache-inv",
|
|
209
213
|
stores: [db],
|
|
214
|
+
cache: { auto: true },
|
|
210
215
|
gate: { policies: [gate.public] },
|
|
211
216
|
}).adopt({ list, create });
|
|
212
217
|
const t = await createTestApp(app, { capability: "open" });
|
|
@@ -249,6 +254,7 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
249
254
|
const app = oke({
|
|
250
255
|
name: "auto-cache-ledger-inv",
|
|
251
256
|
stores: [db],
|
|
257
|
+
cache: { auto: true },
|
|
252
258
|
gate: { policies: [gate.public] },
|
|
253
259
|
}).adopt({ list, create });
|
|
254
260
|
const t = await createTestApp(app, { capability: "open" });
|
|
@@ -261,4 +267,226 @@ describe("automatic tier-1 cache from effects", () => {
|
|
|
261
267
|
expect(lists).toBe(2);
|
|
262
268
|
await t.close();
|
|
263
269
|
});
|
|
270
|
+
|
|
271
|
+
test("omitted cache caches a pure store read", async () => {
|
|
272
|
+
resetBindings();
|
|
273
|
+
resetFlowSeq();
|
|
274
|
+
const db = store.sql("app", { schema: { notes } });
|
|
275
|
+
let lists = 0;
|
|
276
|
+
const list = on(
|
|
277
|
+
http.get("/notes").public(),
|
|
278
|
+
flow("notes.list", {
|
|
279
|
+
effects: { reads: ["sql:notes"] },
|
|
280
|
+
do: () => {
|
|
281
|
+
lists += 1;
|
|
282
|
+
return [{ id: "n1", title: "Harbor" }];
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
const app = oke({
|
|
287
|
+
name: "auto-cache-default-on",
|
|
288
|
+
stores: [db],
|
|
289
|
+
gate: { policies: [gate.public] },
|
|
290
|
+
}).adopt({ list });
|
|
291
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
292
|
+
const notesApi = requireNotesApi(t);
|
|
293
|
+
await notesApi.list({});
|
|
294
|
+
await notesApi.list({});
|
|
295
|
+
expect(lists).toBe(1);
|
|
296
|
+
await t.close();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("cache.auto false turns automatic caching off", async () => {
|
|
300
|
+
resetBindings();
|
|
301
|
+
resetFlowSeq();
|
|
302
|
+
const db = store.sql("app", { schema: { notes } });
|
|
303
|
+
let lists = 0;
|
|
304
|
+
const list = on(
|
|
305
|
+
http.get("/notes").public(),
|
|
306
|
+
flow("notes.list", {
|
|
307
|
+
effects: { reads: ["sql:notes"] },
|
|
308
|
+
do: () => {
|
|
309
|
+
lists += 1;
|
|
310
|
+
return [{ id: "n1", title: "Harbor" }];
|
|
311
|
+
},
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
const app = oke({
|
|
315
|
+
name: "auto-cache-app-off",
|
|
316
|
+
stores: [db],
|
|
317
|
+
cache: { auto: false },
|
|
318
|
+
gate: { policies: [gate.public] },
|
|
319
|
+
}).adopt({ list });
|
|
320
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
321
|
+
const notesApi = requireNotesApi(t);
|
|
322
|
+
await notesApi.list({});
|
|
323
|
+
await notesApi.list({});
|
|
324
|
+
expect(lists).toBe(2);
|
|
325
|
+
await t.close();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("cache: true opts one flow in without the app switch", async () => {
|
|
329
|
+
resetBindings();
|
|
330
|
+
resetFlowSeq();
|
|
331
|
+
const db = store.sql("app", { schema: { notes } });
|
|
332
|
+
let lists = 0;
|
|
333
|
+
const list = on(
|
|
334
|
+
http.get("/notes", { out: z.array(NoteOut) }).public(),
|
|
335
|
+
flow("notes.list", {
|
|
336
|
+
cache: true,
|
|
337
|
+
effects: { reads: ["sql:notes"] },
|
|
338
|
+
do: () => {
|
|
339
|
+
lists += 1;
|
|
340
|
+
return [{ id: "n1", title: "Harbor" }];
|
|
341
|
+
},
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
const app = oke({
|
|
345
|
+
name: "auto-cache-flow-opt-in",
|
|
346
|
+
stores: [db],
|
|
347
|
+
gate: { policies: [gate.public] },
|
|
348
|
+
}).adopt({ list });
|
|
349
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
350
|
+
const notesApi = requireNotesApi(t);
|
|
351
|
+
await notesApi.list({});
|
|
352
|
+
await notesApi.list({});
|
|
353
|
+
expect(lists).toBe(1);
|
|
354
|
+
await t.close();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const sideEffects = [
|
|
358
|
+
{ kind: "emit", effects: { reads: ["sql:notes"], emits: ["note.created"] } },
|
|
359
|
+
{ kind: "send", effects: { reads: ["sql:notes"], sends: ["welcome"] } },
|
|
360
|
+
{ kind: "fetch", effects: { reads: ["sql:notes"], fetches: ["api.example.com"] } },
|
|
361
|
+
{ kind: "secret", effects: { reads: ["sql:notes"], secrets: ["stripe"] } },
|
|
362
|
+
{ kind: "ask", effects: { reads: ["sql:notes"], asks: ["summarize"] } },
|
|
363
|
+
{ kind: "decide", effects: { reads: ["sql:notes"], decides: ["route"] } },
|
|
364
|
+
{ kind: "call", effects: { reads: ["sql:notes"], calls: ["notes.create"] } },
|
|
365
|
+
] satisfies readonly { kind: string; effects: Effects }[];
|
|
366
|
+
|
|
367
|
+
for (const side of sideEffects) {
|
|
368
|
+
test(`${side.kind} effects run the handler on every call`, async () => {
|
|
369
|
+
resetBindings();
|
|
370
|
+
resetFlowSeq();
|
|
371
|
+
const db = store.sql("app", { schema: { notes } });
|
|
372
|
+
let lists = 0;
|
|
373
|
+
const list = on(
|
|
374
|
+
http.get("/notes").public(),
|
|
375
|
+
flow("notes.list", {
|
|
376
|
+
effects: side.effects,
|
|
377
|
+
do: () => {
|
|
378
|
+
lists += 1;
|
|
379
|
+
return [{ id: "n1", title: "Harbor" }];
|
|
380
|
+
},
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
const app = oke({
|
|
384
|
+
name: `auto-cache-${side.kind}`,
|
|
385
|
+
stores: [db],
|
|
386
|
+
cache: { auto: true },
|
|
387
|
+
gate: { policies: [gate.public] },
|
|
388
|
+
}).adopt({ list });
|
|
389
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
390
|
+
const notesApi = requireNotesApi(t);
|
|
391
|
+
await notesApi.list({});
|
|
392
|
+
await notesApi.list({});
|
|
393
|
+
expect(lists).toBe(2);
|
|
394
|
+
await t.close();
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
test("a ledgered call beside a store read is not cached", async () => {
|
|
399
|
+
resetBindings();
|
|
400
|
+
resetFlowSeq();
|
|
401
|
+
const db = store.sql("app", { schema: { notes } });
|
|
402
|
+
let lists = 0;
|
|
403
|
+
const list = on(
|
|
404
|
+
http.get("/notes", { out: z.array(NoteOut) }).public(),
|
|
405
|
+
flow("notes.list", {
|
|
406
|
+
do: async (_input, fx) => {
|
|
407
|
+
lists += 1;
|
|
408
|
+
await fx.store(db).select().from(notes);
|
|
409
|
+
await fx.call("notes.missing", {});
|
|
410
|
+
return [{ id: "n1", title: "Harbor" }];
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
const app = oke({
|
|
415
|
+
name: "auto-cache-ledger-call",
|
|
416
|
+
stores: [db],
|
|
417
|
+
cache: { auto: true },
|
|
418
|
+
gate: { policies: [gate.public] },
|
|
419
|
+
}).adopt({ list });
|
|
420
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
421
|
+
const notesApi = requireNotesApi(t);
|
|
422
|
+
await notesApi.list({});
|
|
423
|
+
await notesApi.list({});
|
|
424
|
+
expect(lists).toBe(2);
|
|
425
|
+
await t.close();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("two tenants do not share a cache hit", async () => {
|
|
429
|
+
resetBindings();
|
|
430
|
+
resetFlowSeq();
|
|
431
|
+
const db = store.sql("app", { schema: { notes } });
|
|
432
|
+
let lists = 0;
|
|
433
|
+
const list = on(
|
|
434
|
+
http.get("/notes").public(),
|
|
435
|
+
flow("notes.list", {
|
|
436
|
+
effects: { reads: ["sql:notes"] },
|
|
437
|
+
do: () => {
|
|
438
|
+
lists += 1;
|
|
439
|
+
return [{ id: "n1", title: "Harbor" }];
|
|
440
|
+
},
|
|
441
|
+
}),
|
|
442
|
+
);
|
|
443
|
+
const app = oke({
|
|
444
|
+
name: "auto-cache-tenant",
|
|
445
|
+
stores: [db],
|
|
446
|
+
cache: { auto: true },
|
|
447
|
+
gate: { policies: [gate.public] },
|
|
448
|
+
}).adopt({ list });
|
|
449
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
450
|
+
const trigger = list.triggers[0];
|
|
451
|
+
if (trigger === undefined) throw new Error("notes.list has no trigger");
|
|
452
|
+
await app.execute(list, {}, trigger, { tenant: { id: "ta" } });
|
|
453
|
+
await app.execute(list, {}, trigger, { tenant: { id: "ta" } });
|
|
454
|
+
expect(lists).toBe(1);
|
|
455
|
+
await app.execute(list, {}, trigger, { tenant: { id: "tb" } });
|
|
456
|
+
expect(lists).toBe(2);
|
|
457
|
+
await t.close();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("two locales do not share a cache hit", async () => {
|
|
461
|
+
resetBindings();
|
|
462
|
+
resetFlowSeq();
|
|
463
|
+
const db = store.sql("app", { schema: { notes } });
|
|
464
|
+
let lists = 0;
|
|
465
|
+
const list = on(
|
|
466
|
+
http.get("/notes").public(),
|
|
467
|
+
flow("notes.list", {
|
|
468
|
+
effects: { reads: ["sql:notes"] },
|
|
469
|
+
do: () => {
|
|
470
|
+
lists += 1;
|
|
471
|
+
return [{ id: "n1", title: "Harbor" }];
|
|
472
|
+
},
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
const app = oke({
|
|
476
|
+
name: "auto-cache-locale",
|
|
477
|
+
stores: [db],
|
|
478
|
+
cache: { auto: true },
|
|
479
|
+
config: { i18n: { locales: ["en", "ar"], default: "en" } },
|
|
480
|
+
gate: { policies: [gate.public] },
|
|
481
|
+
}).adopt({ list });
|
|
482
|
+
const t = await createTestApp(app, { capability: "open" });
|
|
483
|
+
const trigger = list.triggers[0];
|
|
484
|
+
if (trigger === undefined) throw new Error("notes.list has no trigger");
|
|
485
|
+
await app.execute(list, {}, trigger, { locale: "en" });
|
|
486
|
+
await app.execute(list, {}, trigger, { locale: "en" });
|
|
487
|
+
expect(lists).toBe(1);
|
|
488
|
+
await app.execute(list, {}, trigger, { locale: "ar" });
|
|
489
|
+
expect(lists).toBe(2);
|
|
490
|
+
await t.close();
|
|
491
|
+
});
|
|
264
492
|
});
|
|
@@ -14,6 +14,7 @@ import type { AiDriver, AiOpenOptions } from "../../drivers/ai-types.ts";
|
|
|
14
14
|
import { createAiRuntime, type AiRuntime } from "../../elements/ai.ts";
|
|
15
15
|
import type { GateRuntime } from "../../elements/gate.ts";
|
|
16
16
|
import type { VaultRuntime } from "../../elements/vault.ts";
|
|
17
|
+
import type { JournalStore } from "../journal.ts";
|
|
17
18
|
import type { BootOptions } from "../boot.ts";
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -33,6 +34,7 @@ export function bindAi(
|
|
|
33
34
|
env: ConfigEnv = "test",
|
|
34
35
|
docker = false,
|
|
35
36
|
vault?: VaultRuntime,
|
|
37
|
+
journalStore?: JournalStore,
|
|
36
38
|
): AiRuntime {
|
|
37
39
|
const id = resolveAiDriverId(options, env, docker);
|
|
38
40
|
const driver =
|
|
@@ -48,6 +50,7 @@ export function bindAi(
|
|
|
48
50
|
},
|
|
49
51
|
gates: options.ai?.gates ?? gate,
|
|
50
52
|
now,
|
|
53
|
+
...(journalStore ? { journalStore } : {}),
|
|
51
54
|
});
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { resolveDriverId, type ConfigEnv } from "../../config/index.ts";
|
|
6
6
|
import { CHANNEL_EMAIL_DEFAULTS, CHANNEL_SMS_DEFAULTS } from "../../config/driver-defaults.ts";
|
|
7
|
+
import { sharedPostgresClient, toPostgresParams } from "../../drivers/postgres.ts";
|
|
8
|
+
import type { PostgresClientLike } from "../../drivers/postgres.ts";
|
|
7
9
|
import { openConsoleChannel } from "../../drivers/channel-console.ts";
|
|
8
10
|
import { openMsegatChannel } from "../../drivers/channel-msegat.ts";
|
|
9
11
|
import { openResendChannel } from "../../drivers/channel-resend.ts";
|
|
@@ -16,8 +18,59 @@ import { openUnifonicChannel } from "../../drivers/channel-unifonic.ts";
|
|
|
16
18
|
import { openWaCloudChannel } from "../../drivers/channel-wa-cloud.ts";
|
|
17
19
|
import type { ChannelDriver, ChannelOpenOptions } from "../../drivers/channel-types.ts";
|
|
18
20
|
import { createChannelRuntime, type ChannelRuntime } from "../../elements/channel.ts";
|
|
21
|
+
import {
|
|
22
|
+
openPostgresChannelLedger,
|
|
23
|
+
type ChannelLedgerSql,
|
|
24
|
+
type PostgresChannelLedger,
|
|
25
|
+
} from "../../elements/channel/sql-ledger.ts";
|
|
19
26
|
import type { BootOptions } from "../boot.ts";
|
|
20
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Query/exec wrapper over the shared Bun.SQL pool.
|
|
30
|
+
*
|
|
31
|
+
* @param client - Shared postgres client
|
|
32
|
+
*/
|
|
33
|
+
function asChannelSql(client: PostgresClientLike): ChannelLedgerSql {
|
|
34
|
+
return {
|
|
35
|
+
async query(sql, params = []) {
|
|
36
|
+
const result = await client.unsafe(toPostgresParams(sql, params), [...params]);
|
|
37
|
+
if (Array.isArray(result)) return result as Record<string, unknown>[];
|
|
38
|
+
return Array.from(result as ArrayLike<Record<string, unknown>>);
|
|
39
|
+
},
|
|
40
|
+
async exec(sql, params = []) {
|
|
41
|
+
const result = await client.unsafe(toPostgresParams(sql, params), [...params]);
|
|
42
|
+
if (
|
|
43
|
+
result &&
|
|
44
|
+
typeof result === "object" &&
|
|
45
|
+
"changes" in result &&
|
|
46
|
+
typeof result.changes === "number"
|
|
47
|
+
) {
|
|
48
|
+
return { changes: result.changes };
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(result)) return { changes: result.length };
|
|
51
|
+
return { changes: 0 };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Durable ledger when a SQL URL is set outside `test`.
|
|
58
|
+
* Injected stores win. `test` stays in memory.
|
|
59
|
+
*
|
|
60
|
+
* @param options - Boot options
|
|
61
|
+
* @param env - Active environment
|
|
62
|
+
*/
|
|
63
|
+
async function maybeLedger(
|
|
64
|
+
options: BootOptions,
|
|
65
|
+
env: ConfigEnv,
|
|
66
|
+
): Promise<PostgresChannelLedger | undefined> {
|
|
67
|
+
if (env === "test") return undefined;
|
|
68
|
+
if (options.channel?.suppression && options.channel.receipts) return undefined;
|
|
69
|
+
const url = process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL;
|
|
70
|
+
if (!url) return undefined;
|
|
71
|
+
return openPostgresChannelLedger(asChannelSql(sharedPostgresClient(url)));
|
|
72
|
+
}
|
|
73
|
+
|
|
21
74
|
/**
|
|
22
75
|
* Construct a Channel runtime (console inbox default).
|
|
23
76
|
*
|
|
@@ -29,16 +82,24 @@ import type { BootOptions } from "../boot.ts";
|
|
|
29
82
|
* @param now - Clock
|
|
30
83
|
* @param docker - Prefer compose SMTP when active
|
|
31
84
|
*/
|
|
32
|
-
export function bindChannel(
|
|
85
|
+
export async function bindChannel(
|
|
33
86
|
options: BootOptions,
|
|
34
87
|
env: ConfigEnv,
|
|
35
88
|
now: () => number,
|
|
36
89
|
docker = false,
|
|
37
|
-
): ChannelRuntime {
|
|
90
|
+
): Promise<ChannelRuntime> {
|
|
91
|
+
const ledger = await maybeLedger(options, env);
|
|
38
92
|
return createChannelRuntime({
|
|
39
93
|
...(options.channel ?? {}),
|
|
40
94
|
drivers: options.channel?.drivers ?? defaultDrivers(options, env, docker),
|
|
41
95
|
now,
|
|
96
|
+
...(ledger
|
|
97
|
+
? {
|
|
98
|
+
suppression: options.channel?.suppression ?? ledger.suppression,
|
|
99
|
+
receipts: options.channel?.receipts ?? ledger.receipts,
|
|
100
|
+
refresh: () => ledger.reload(),
|
|
101
|
+
}
|
|
102
|
+
: {}),
|
|
42
103
|
});
|
|
43
104
|
}
|
|
44
105
|
|