okengine 0.21.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +54 -0
- package/package.json +53 -38
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +2 -2
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.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/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- 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 +11 -9
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +35 -21
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- 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/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/competitor-mention-removal.test.ts +3 -3
- 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/docker-cli.test.ts +1 -1
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +14 -1
- package/src/cli/load-config.images.test.ts +12 -12
- package/src/cli/load-config.ts +2 -2
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/agent.ts +307 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +245 -0
- package/src/client/transport.ts +133 -19
- package/src/client/types.ts +29 -3
- 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-infer.ts +753 -17
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +381 -24
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- 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-invoke.test.ts +2 -2
- 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-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.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-CgkKlA56.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-Y6HLjyJ5.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-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.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-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.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-B54v4Yit.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 +15 -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/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/docker/stack-id.test.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.ts +455 -3
- package/src/drivers/meilisearch.ts +2 -0
- 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/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- 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/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -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/clock/durable.ts +27 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +9 -0
- package/src/i18n/catalogs/en.ts +9 -0
- package/src/index.ts +2 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +294 -19
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +34 -5
- package/src/kernel/builtin-errors.ts +10 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- 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 +20 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +316 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +160 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +37 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +27 -0
- package/src/release/http-graph.ts +95 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +144 -28
- 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/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
|
@@ -51,6 +51,7 @@ export async function bindVault(options: BootOptions, env: ConfigEnv): Promise<V
|
|
|
51
51
|
env,
|
|
52
52
|
cwd: process.cwd(),
|
|
53
53
|
seed: {},
|
|
54
|
+
...(options.config?.vault?.audit === undefined ? {} : { audit: options.config.vault.audit }),
|
|
54
55
|
});
|
|
55
56
|
const vault = createVaultRuntime({
|
|
56
57
|
secrets: vaultSecrets,
|
package/src/kernel/boot.ts
CHANGED
|
@@ -282,9 +282,14 @@ export function resolveElementNeeds(options: BootOptions): ElementNeeds {
|
|
|
282
282
|
const considerFlow = (f: AnyFlowDef): void => {
|
|
283
283
|
const e = f.effects;
|
|
284
284
|
if (f.durable === true) journal = true;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
285
|
+
const touchesStore = [...(e?.reads ?? []), ...(e?.writes ?? [])].some(
|
|
286
|
+
(ref) =>
|
|
287
|
+
ref.startsWith("sql:") ||
|
|
288
|
+
ref.startsWith("kv:") ||
|
|
289
|
+
ref.startsWith("files:") ||
|
|
290
|
+
ref.startsWith("index:"),
|
|
291
|
+
);
|
|
292
|
+
if (touchesStore) store = true;
|
|
288
293
|
if ((e?.emits?.length ?? 0) > 0) signal = true;
|
|
289
294
|
if ((e?.secrets?.length ?? 0) > 0) vault = true;
|
|
290
295
|
if ((e?.sends?.length ?? 0) > 0) channel = true;
|
|
@@ -548,6 +553,12 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
548
553
|
if (clockRt) void Promise.resolve(clockRt.tick()).catch(ignoreBenignSql);
|
|
549
554
|
if (journal && durableResume) void Promise.resolve(durableResume()).catch(ignoreBenignSql);
|
|
550
555
|
if (fleet) void fleet.maybeHeartbeat().catch(ignoreBenignSql);
|
|
556
|
+
const stem = ["run", "events"].join("-");
|
|
557
|
+
void import(`../elements/ai/${stem}.ts`)
|
|
558
|
+
.then((mod: typeof import("../elements/ai/run-events.ts")) =>
|
|
559
|
+
mod.sweepInstalledAgentEvents(),
|
|
560
|
+
)
|
|
561
|
+
.catch(ignoreBenignSql);
|
|
551
562
|
}, period);
|
|
552
563
|
schedulerTimer.unref?.();
|
|
553
564
|
}
|
|
@@ -567,7 +578,7 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
567
578
|
// 6. AI
|
|
568
579
|
let ai = pre.ai;
|
|
569
580
|
if (needs.ai && !ai) {
|
|
570
|
-
ai = aiBind!.bindAi(options, gate, now, env, docker, vault);
|
|
581
|
+
ai = aiBind!.bindAi(options, gate, now, env, docker, vault, journal?.store);
|
|
571
582
|
}
|
|
572
583
|
|
|
573
584
|
// 7. Runs
|
|
@@ -601,6 +612,15 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
|
|
|
601
612
|
docker,
|
|
602
613
|
});
|
|
603
614
|
|
|
615
|
+
const needsIdempotencyJournal = (options.flows ?? []).some((f) => {
|
|
616
|
+
const mode = f.resolvedIdempotency?.mode;
|
|
617
|
+
return mode === "auto" || mode === "required";
|
|
618
|
+
});
|
|
619
|
+
if (!journal && needsIdempotencyJournal) {
|
|
620
|
+
journalBind ??= await loadBind<JournalBind>("journal");
|
|
621
|
+
journal = (await journalBind.bindJournal(options, env)).journal;
|
|
622
|
+
}
|
|
623
|
+
|
|
604
624
|
// 9. `.adopt()` barrel freshness — opt-in only (same `rootDir` gate as
|
|
605
625
|
// capability minting above; never a filesystem read the kernel performs
|
|
606
626
|
// on its own).
|
|
@@ -712,7 +732,12 @@ export async function mintCapabilities(
|
|
|
712
732
|
|
|
713
733
|
let manifest = options.manifest;
|
|
714
734
|
let extractError: string | undefined;
|
|
715
|
-
const needsManifest =
|
|
735
|
+
const needsManifest =
|
|
736
|
+
manifest === undefined &&
|
|
737
|
+
flows.some(
|
|
738
|
+
(f) =>
|
|
739
|
+
f.effects === undefined || (f.idempotency !== false && f.resolvedIdempotency === undefined),
|
|
740
|
+
);
|
|
716
741
|
if (needsManifest) {
|
|
717
742
|
const rootDir = options.rootDir ?? process.env["OKE_ROOT_DIR"];
|
|
718
743
|
if (rootDir) {
|
|
@@ -723,6 +748,10 @@ export async function mintCapabilities(
|
|
|
723
748
|
}
|
|
724
749
|
|
|
725
750
|
for (const f of flows) {
|
|
751
|
+
const manifestFlow = manifest?.flows?.[f.name];
|
|
752
|
+
if (manifestFlow?.idempotency) f.resolvedIdempotency = manifestFlow.idempotency;
|
|
753
|
+
if (manifestFlow?.usesRaw === true) f.usesRaw = true;
|
|
754
|
+
|
|
726
755
|
if (f.effects !== undefined) {
|
|
727
756
|
map.set(f.name, createCapabilityToken(f.name, f.effects));
|
|
728
757
|
continue;
|
|
@@ -21,6 +21,11 @@ export const BUILTIN_ERROR_STATUS = {
|
|
|
21
21
|
AuthFailed: 400,
|
|
22
22
|
InternalError: 500,
|
|
23
23
|
ServiceUnavailable: 503,
|
|
24
|
+
IdempotencyKeyMissing: 400,
|
|
25
|
+
IdempotencyKeyInvalid: 400,
|
|
26
|
+
IdempotencyKeyReused: 422,
|
|
27
|
+
IdempotencyInProgress: 409,
|
|
28
|
+
JournalLeaseBusy: 409,
|
|
24
29
|
} as const;
|
|
25
30
|
|
|
26
31
|
/** Built-in error code (including `DatabaseError`). */
|
|
@@ -86,6 +91,11 @@ export type BuiltinErrorMap = {
|
|
|
86
91
|
};
|
|
87
92
|
readonly InternalError: Record<string, never>;
|
|
88
93
|
readonly ServiceUnavailable: { readonly retryAfter?: number };
|
|
94
|
+
readonly IdempotencyKeyMissing: Record<string, never>;
|
|
95
|
+
readonly IdempotencyKeyInvalid: Record<string, never>;
|
|
96
|
+
readonly IdempotencyKeyReused: Record<string, never>;
|
|
97
|
+
readonly IdempotencyInProgress: { readonly retryAfter?: number };
|
|
98
|
+
readonly JournalLeaseBusy: { readonly retryAfter?: number };
|
|
89
99
|
};
|
|
90
100
|
|
|
91
101
|
/**
|
package/src/kernel/capability.ts
CHANGED
|
@@ -21,6 +21,7 @@ const UNDECLARED_KEY: Readonly<Record<EffectKind, keyof typeof OKE_ERRORS>> = {
|
|
|
21
21
|
secret: "UNDECLARED_SECRET",
|
|
22
22
|
call: "UNDECLARED_CALL",
|
|
23
23
|
fetch: "UNDECLARED_FETCH",
|
|
24
|
+
decide: "UNDECLARED_DECIDE",
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
/** Maps effect kind → the corresponding `Effects` field. */
|
|
@@ -34,6 +35,7 @@ const EFFECTS_FIELD: Readonly<Record<EffectKind, keyof Effects>> = {
|
|
|
34
35
|
secret: "secrets",
|
|
35
36
|
call: "calls",
|
|
36
37
|
fetch: "fetches",
|
|
38
|
+
decide: "decides",
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
/**
|
|
@@ -91,6 +93,7 @@ export function createCapabilityToken(flow: string, declared?: Effects): Capabil
|
|
|
91
93
|
secret: new Set(effects.secrets ?? []),
|
|
92
94
|
call: new Set(effects.calls ?? []),
|
|
93
95
|
fetch: new Set(effects.fetches ?? []),
|
|
96
|
+
decide: new Set(effects.decides ?? []),
|
|
94
97
|
};
|
|
95
98
|
|
|
96
99
|
return {
|
|
@@ -56,6 +56,7 @@ export function buildClientDescriptor(
|
|
|
56
56
|
...(stamp.stream === true ? { stream: true as const } : {}),
|
|
57
57
|
...(stamp.matchKey !== undefined ? { matchKey: stamp.matchKey } : {}),
|
|
58
58
|
...(stamp.gates !== undefined && stamp.gates.length > 0 ? { gates: stamp.gates } : {}),
|
|
59
|
+
...(def?.resolvedIdempotency !== undefined ? { idempotency: def.resolvedIdempotency } : {}),
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
out[unit] = unitOut;
|
|
@@ -90,6 +90,39 @@ describe("fx.race — structured concurrency", () => {
|
|
|
90
90
|
test("empty race throws", async () => {
|
|
91
91
|
await expect(fxRace([])).rejects.toThrow(/empty/);
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
test("a gated effect does not start after the branch is aborted", async () => {
|
|
95
|
+
const { fx } = createFxContext({
|
|
96
|
+
flow: "t",
|
|
97
|
+
effects: { reads: ["sql:notes"] },
|
|
98
|
+
});
|
|
99
|
+
const store = fx.store("sql:notes") as { get(key: string): Promise<unknown> };
|
|
100
|
+
let sawAbort = false;
|
|
101
|
+
|
|
102
|
+
const winner = await fx.race([
|
|
103
|
+
async () => {
|
|
104
|
+
await new Promise<void>((resolve) => {
|
|
105
|
+
const signal = fx.signal;
|
|
106
|
+
if (signal.aborted) {
|
|
107
|
+
resolve();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
await store.get("n1");
|
|
114
|
+
} catch (err) {
|
|
115
|
+
sawAbort = isAbortError(err);
|
|
116
|
+
}
|
|
117
|
+
return "slow";
|
|
118
|
+
},
|
|
119
|
+
async () => "fast",
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
expect(winner).toBe("fast");
|
|
123
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
124
|
+
expect(sawAbort).toBe(true);
|
|
125
|
+
});
|
|
93
126
|
});
|
|
94
127
|
|
|
95
128
|
describe("fx.using — scoped cleanup", () => {
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision labels and the drift flag on the journal driver.
|
|
3
|
+
*
|
|
4
|
+
* A sibling of `oke_idempotency`. Rows are not journal runs. Tables are
|
|
5
|
+
* created only when this store is opened, which boot does when the app
|
|
6
|
+
* declares decisions.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdir } from "node:fs/promises";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
import type { DecisionCandidate, DecisionLabel } from "../elements/ai/decisions/certificate.ts";
|
|
14
|
+
|
|
15
|
+
/** SQL surface the postgres journal client already exposes. */
|
|
16
|
+
export interface DecisionLabelSql {
|
|
17
|
+
query(sql: string, params?: readonly unknown[]): Promise<Record<string, unknown>[]>;
|
|
18
|
+
exec(sql: string, params?: readonly unknown[]): Promise<{ changes: number }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Drift for one decision. A later certificate (`certifiedAt` newer) ignores it. */
|
|
22
|
+
export interface DecisionDriftFlag {
|
|
23
|
+
readonly suspended: boolean;
|
|
24
|
+
/** Epoch ms of the certificate that was current when the flag was written. */
|
|
25
|
+
readonly certifiedAt: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @deprecated Use {@link DecisionDriftFlag}. Kept for stored rows. */
|
|
29
|
+
export interface DecisionDriftRecord extends DecisionDriftFlag {}
|
|
30
|
+
|
|
31
|
+
/** Thrown when the postgres label store cannot open or query. */
|
|
32
|
+
export class DecisionLabelStoreError extends Error {
|
|
33
|
+
/**
|
|
34
|
+
* @param message - What failed
|
|
35
|
+
* @param options - Underlying driver error
|
|
36
|
+
*/
|
|
37
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
38
|
+
super(message, options);
|
|
39
|
+
this.name = "DecisionLabelStoreError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Persistence for review labels and the app drift flag. */
|
|
44
|
+
export interface DecisionLabelStore {
|
|
45
|
+
insert(label: DecisionLabel, at: number): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Labels for one decision. Omit `tenant` to read every tenant.
|
|
48
|
+
*
|
|
49
|
+
* @param decision - Decision name
|
|
50
|
+
* @param tenant - Tenant filter
|
|
51
|
+
*/
|
|
52
|
+
list(decision?: string, tenant?: string | null): Promise<DecisionLabel[]>;
|
|
53
|
+
/** Drift flags keyed by decision name. */
|
|
54
|
+
drift(): Promise<Readonly<Record<string, DecisionDriftFlag>>>;
|
|
55
|
+
/**
|
|
56
|
+
* Persist one decision's suspension flag.
|
|
57
|
+
*
|
|
58
|
+
* @param decision - Decision name
|
|
59
|
+
* @param record - Flag and the certificate time it belongs to
|
|
60
|
+
*/
|
|
61
|
+
setDrift(decision: string, record: DecisionDriftFlag): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Store one candidate on this journal. Any instance can read it after a restart.
|
|
64
|
+
*
|
|
65
|
+
* @param decision - Decision name
|
|
66
|
+
* @param entry - Lock entry the clock fitted
|
|
67
|
+
*/
|
|
68
|
+
putCandidate(decision: string, entry: DecisionCandidate): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Candidate last written for this decision.
|
|
71
|
+
*
|
|
72
|
+
* @param decision - Decision name
|
|
73
|
+
*/
|
|
74
|
+
getCandidate(decision: string): Promise<DecisionCandidate | undefined>;
|
|
75
|
+
/** Decision names that have a candidate. */
|
|
76
|
+
listCandidates(): Promise<readonly string[]>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface StoredLabel extends DecisionLabel {
|
|
80
|
+
readonly at: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* In-memory label store.
|
|
85
|
+
*/
|
|
86
|
+
export function createMemoryDecisionLabelStore(): DecisionLabelStore {
|
|
87
|
+
const rows: StoredLabel[] = [];
|
|
88
|
+
const candidates = new Map<string, DecisionCandidate>();
|
|
89
|
+
const driftFlags = new Map<string, DecisionDriftFlag>();
|
|
90
|
+
return {
|
|
91
|
+
async insert(label, at) {
|
|
92
|
+
rows.push({ ...label, at });
|
|
93
|
+
},
|
|
94
|
+
async list(decision, tenant) {
|
|
95
|
+
return rows
|
|
96
|
+
.filter((row) => decision === undefined || row.decision === decision)
|
|
97
|
+
.filter((row) => tenant === undefined || (row.tenant ?? null) === tenant)
|
|
98
|
+
.map(toLabel);
|
|
99
|
+
},
|
|
100
|
+
async drift() {
|
|
101
|
+
return Object.fromEntries(driftFlags);
|
|
102
|
+
},
|
|
103
|
+
async setDrift(decision, next) {
|
|
104
|
+
if (next.suspended) driftFlags.set(decision, next);
|
|
105
|
+
else driftFlags.delete(decision);
|
|
106
|
+
},
|
|
107
|
+
async putCandidate(decision, entry) {
|
|
108
|
+
candidates.set(decision, entry);
|
|
109
|
+
},
|
|
110
|
+
async getCandidate(decision) {
|
|
111
|
+
return candidates.get(decision);
|
|
112
|
+
},
|
|
113
|
+
async listCandidates() {
|
|
114
|
+
return [...candidates.keys()];
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* File-backed label store beside the journal file.
|
|
121
|
+
*
|
|
122
|
+
* @param path - JSON file path
|
|
123
|
+
*/
|
|
124
|
+
export function createFileDecisionLabelStore(path: string): DecisionLabelStore {
|
|
125
|
+
let rows: StoredLabel[] | undefined;
|
|
126
|
+
let candidates: Record<string, DecisionCandidate> = {};
|
|
127
|
+
let driftFlags: Record<string, DecisionDriftFlag> = {};
|
|
128
|
+
const load = async (): Promise<void> => {
|
|
129
|
+
if (rows) return;
|
|
130
|
+
rows = [];
|
|
131
|
+
const file = Bun.file(path);
|
|
132
|
+
if (await file.exists()) {
|
|
133
|
+
const raw = (await file.json()) as {
|
|
134
|
+
rows?: StoredLabel[];
|
|
135
|
+
suspended?: boolean;
|
|
136
|
+
certifiedAt?: number;
|
|
137
|
+
drifts?: Record<string, DecisionDriftFlag>;
|
|
138
|
+
candidates?: Record<string, DecisionCandidate>;
|
|
139
|
+
};
|
|
140
|
+
rows = raw.rows ?? [];
|
|
141
|
+
candidates = raw.candidates ?? {};
|
|
142
|
+
driftFlags = raw.drifts ?? {};
|
|
143
|
+
const legacy =
|
|
144
|
+
driftFlags["*"] ??
|
|
145
|
+
(raw.suspended === true && Object.keys(driftFlags).length === 0
|
|
146
|
+
? { suspended: true as const, certifiedAt: raw.certifiedAt ?? 0 }
|
|
147
|
+
: undefined);
|
|
148
|
+
if (legacy?.suspended) {
|
|
149
|
+
delete driftFlags["*"];
|
|
150
|
+
for (const name of declaredDriftDecisions) {
|
|
151
|
+
driftFlags[name] = { suspended: legacy.suspended, certifiedAt: legacy.certifiedAt };
|
|
152
|
+
}
|
|
153
|
+
await mkdir(dirname(path), { recursive: true });
|
|
154
|
+
await Bun.write(path, JSON.stringify({ rows, candidates, drifts: driftFlags }));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const flush = async (): Promise<void> => {
|
|
159
|
+
await mkdir(dirname(path), { recursive: true });
|
|
160
|
+
await Bun.write(
|
|
161
|
+
path,
|
|
162
|
+
JSON.stringify({
|
|
163
|
+
rows,
|
|
164
|
+
candidates,
|
|
165
|
+
drifts: driftFlags,
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
async insert(label, at) {
|
|
171
|
+
await load();
|
|
172
|
+
rows!.push({ ...label, at });
|
|
173
|
+
await flush();
|
|
174
|
+
},
|
|
175
|
+
async list(decision, tenant) {
|
|
176
|
+
await load();
|
|
177
|
+
return rows!
|
|
178
|
+
.filter((row) => decision === undefined || row.decision === decision)
|
|
179
|
+
.filter((row) => tenant === undefined || (row.tenant ?? null) === tenant)
|
|
180
|
+
.map(toLabel);
|
|
181
|
+
},
|
|
182
|
+
async drift() {
|
|
183
|
+
await load();
|
|
184
|
+
return driftFlags;
|
|
185
|
+
},
|
|
186
|
+
async setDrift(decision, next) {
|
|
187
|
+
await load();
|
|
188
|
+
if (next.suspended) driftFlags[decision] = next;
|
|
189
|
+
else delete driftFlags[decision];
|
|
190
|
+
await flush();
|
|
191
|
+
},
|
|
192
|
+
async putCandidate(decision, entry) {
|
|
193
|
+
await load();
|
|
194
|
+
candidates[decision] = entry;
|
|
195
|
+
await flush();
|
|
196
|
+
},
|
|
197
|
+
async getCandidate(decision) {
|
|
198
|
+
await load();
|
|
199
|
+
return candidates[decision];
|
|
200
|
+
},
|
|
201
|
+
async listCandidates() {
|
|
202
|
+
await load();
|
|
203
|
+
return Object.keys(candidates);
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Decision names an old app-wide drift flag is copied onto. Boot sets this before open. */
|
|
209
|
+
let declaredDriftDecisions: readonly string[] = [];
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Declare which decisions receive a legacy app-wide drift flag.
|
|
213
|
+
*
|
|
214
|
+
* @param names - Decision names from the manifest
|
|
215
|
+
*/
|
|
216
|
+
export function setDeclaredDriftDecisions(names: readonly string[]): void {
|
|
217
|
+
declaredDriftDecisions = names;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Postgres label store on the journal connection. Creates the tables.
|
|
222
|
+
* A failed init or query throws {@link DecisionLabelStoreError}.
|
|
223
|
+
*
|
|
224
|
+
* @param sql - Journal SQL client
|
|
225
|
+
*/
|
|
226
|
+
export async function createPostgresDecisionLabelStore(
|
|
227
|
+
sql: DecisionLabelSql,
|
|
228
|
+
): Promise<DecisionLabelStore> {
|
|
229
|
+
try {
|
|
230
|
+
await sql.exec(`CREATE TABLE IF NOT EXISTS oke_decision_labels (
|
|
231
|
+
decision_id TEXT NOT NULL,
|
|
232
|
+
question TEXT NOT NULL,
|
|
233
|
+
value TEXT NOT NULL,
|
|
234
|
+
propensity DOUBLE PRECISION NOT NULL,
|
|
235
|
+
reviewer TEXT NOT NULL,
|
|
236
|
+
locale TEXT,
|
|
237
|
+
model TEXT,
|
|
238
|
+
tenant TEXT,
|
|
239
|
+
score DOUBLE PRECISION,
|
|
240
|
+
loss INTEGER,
|
|
241
|
+
raw TEXT,
|
|
242
|
+
at BIGINT NOT NULL,
|
|
243
|
+
input TEXT
|
|
244
|
+
)`);
|
|
245
|
+
await sql.exec(`ALTER TABLE oke_decision_labels ADD COLUMN IF NOT EXISTS input TEXT`);
|
|
246
|
+
await sql.exec(`ALTER TABLE oke_decision_labels ADD COLUMN IF NOT EXISTS review_id TEXT`);
|
|
247
|
+
await migrateDecisionDriftTable(sql, declaredDriftDecisions);
|
|
248
|
+
await sql.exec(`CREATE TABLE IF NOT EXISTS oke_decision_candidates (
|
|
249
|
+
decision_id TEXT PRIMARY KEY,
|
|
250
|
+
body TEXT NOT NULL
|
|
251
|
+
)`);
|
|
252
|
+
} catch (cause) {
|
|
253
|
+
throw new DecisionLabelStoreError("decision label store failed to open", { cause });
|
|
254
|
+
}
|
|
255
|
+
const query = async (
|
|
256
|
+
statement: string,
|
|
257
|
+
params?: readonly unknown[],
|
|
258
|
+
): Promise<Record<string, unknown>[]> => {
|
|
259
|
+
try {
|
|
260
|
+
return await sql.query(statement, params);
|
|
261
|
+
} catch (cause) {
|
|
262
|
+
throw new DecisionLabelStoreError("decision label store query failed", { cause });
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const exec = async (statement: string, params?: readonly unknown[]): Promise<void> => {
|
|
266
|
+
try {
|
|
267
|
+
await sql.exec(statement, params);
|
|
268
|
+
} catch (cause) {
|
|
269
|
+
throw new DecisionLabelStoreError("decision label store query failed", { cause });
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
async insert(label, at) {
|
|
274
|
+
await exec(
|
|
275
|
+
`INSERT INTO oke_decision_labels
|
|
276
|
+
(decision_id, question, value, propensity, reviewer, locale, model, tenant, score, loss, raw, at, input, review_id)
|
|
277
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
278
|
+
[
|
|
279
|
+
label.decision,
|
|
280
|
+
label.question,
|
|
281
|
+
JSON.stringify(label.value),
|
|
282
|
+
label.propensity,
|
|
283
|
+
label.reviewer,
|
|
284
|
+
label.locale ?? null,
|
|
285
|
+
label.model ?? null,
|
|
286
|
+
label.tenant ?? null,
|
|
287
|
+
label.score ?? null,
|
|
288
|
+
label.loss ?? null,
|
|
289
|
+
label.raw === undefined ? null : JSON.stringify(label.raw),
|
|
290
|
+
at,
|
|
291
|
+
label.input === undefined ? null : JSON.stringify(label.input),
|
|
292
|
+
label.reviewId ?? null,
|
|
293
|
+
],
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
async list(decision, tenant) {
|
|
297
|
+
const clauses: string[] = [];
|
|
298
|
+
const args: (string | null)[] = [];
|
|
299
|
+
if (decision !== undefined) {
|
|
300
|
+
clauses.push("decision_id = ?");
|
|
301
|
+
args.push(decision);
|
|
302
|
+
}
|
|
303
|
+
if (tenant !== undefined) {
|
|
304
|
+
clauses.push("tenant IS NOT DISTINCT FROM ?");
|
|
305
|
+
args.push(tenant);
|
|
306
|
+
}
|
|
307
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
308
|
+
const found = await query(
|
|
309
|
+
`SELECT decision_id, question, value, propensity, reviewer, locale, model, tenant, score, loss, raw, at, input, review_id
|
|
310
|
+
FROM oke_decision_labels ${where}`,
|
|
311
|
+
args,
|
|
312
|
+
);
|
|
313
|
+
return found.map((row) => ({
|
|
314
|
+
decision: String(row.decision_id),
|
|
315
|
+
question: String(row.question),
|
|
316
|
+
value: JSON.parse(String(row.value)) as unknown,
|
|
317
|
+
propensity: Number(row.propensity),
|
|
318
|
+
reviewer: String(row.reviewer),
|
|
319
|
+
...(row.locale != null ? { locale: String(row.locale) } : {}),
|
|
320
|
+
...(row.model != null ? { model: String(row.model) } : {}),
|
|
321
|
+
...(row.tenant != null ? { tenant: String(row.tenant) } : {}),
|
|
322
|
+
...(row.score != null ? { score: Number(row.score) } : {}),
|
|
323
|
+
...(row.loss != null ? { loss: Number(row.loss) } : {}),
|
|
324
|
+
...(row.raw != null ? { raw: JSON.parse(String(row.raw)) as unknown } : {}),
|
|
325
|
+
...(row.input != null ? { input: JSON.parse(String(row.input)) as unknown } : {}),
|
|
326
|
+
...(row.review_id != null ? { reviewId: String(row.review_id) } : {}),
|
|
327
|
+
at: Number(row.at),
|
|
328
|
+
}));
|
|
329
|
+
},
|
|
330
|
+
async drift() {
|
|
331
|
+
const rows = await query(
|
|
332
|
+
`SELECT decision_id, suspended, certified_at FROM oke_decision_drift`,
|
|
333
|
+
);
|
|
334
|
+
const flags: Record<string, DecisionDriftFlag> = {};
|
|
335
|
+
for (const row of rows) {
|
|
336
|
+
const name = String(row.decision_id ?? "");
|
|
337
|
+
if (!name) continue;
|
|
338
|
+
flags[name] = {
|
|
339
|
+
suspended: Number(row.suspended) === 1,
|
|
340
|
+
certifiedAt: Number(row.certified_at ?? 0),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return flags;
|
|
344
|
+
},
|
|
345
|
+
async setDrift(decision, next) {
|
|
346
|
+
if (!next.suspended) {
|
|
347
|
+
await exec(`DELETE FROM oke_decision_drift WHERE decision_id = ?`, [decision]);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
await exec(
|
|
351
|
+
`INSERT INTO oke_decision_drift (decision_id, suspended, certified_at) VALUES (?, ?, ?)
|
|
352
|
+
ON CONFLICT (decision_id) DO UPDATE SET suspended = excluded.suspended, certified_at = excluded.certified_at`,
|
|
353
|
+
[decision, 1, next.certifiedAt],
|
|
354
|
+
);
|
|
355
|
+
},
|
|
356
|
+
async putCandidate(decision, entry) {
|
|
357
|
+
await exec(
|
|
358
|
+
`INSERT INTO oke_decision_candidates (decision_id, body) VALUES (?, ?)
|
|
359
|
+
ON CONFLICT (decision_id) DO UPDATE SET body = excluded.body`,
|
|
360
|
+
[decision, JSON.stringify(entry)],
|
|
361
|
+
);
|
|
362
|
+
},
|
|
363
|
+
async getCandidate(decision) {
|
|
364
|
+
const rows = await query(`SELECT body FROM oke_decision_candidates WHERE decision_id = ?`, [
|
|
365
|
+
decision,
|
|
366
|
+
]);
|
|
367
|
+
const body = rows[0]?.body;
|
|
368
|
+
if (typeof body !== "string") return undefined;
|
|
369
|
+
const parsed = JSON.parse(body) as DecisionCandidate;
|
|
370
|
+
if (!parsed || typeof parsed.model !== "string" || !parsed.questions) return undefined;
|
|
371
|
+
return parsed;
|
|
372
|
+
},
|
|
373
|
+
async listCandidates() {
|
|
374
|
+
const rows = await query(`SELECT decision_id FROM oke_decision_candidates`);
|
|
375
|
+
return rows.map((row) => String(row.decision_id));
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function toLabel(row: StoredLabel): DecisionLabel {
|
|
381
|
+
return {
|
|
382
|
+
decision: row.decision,
|
|
383
|
+
question: row.question,
|
|
384
|
+
value: row.value,
|
|
385
|
+
propensity: row.propensity,
|
|
386
|
+
reviewer: row.reviewer,
|
|
387
|
+
...(row.locale !== undefined ? { locale: row.locale } : {}),
|
|
388
|
+
...(row.model !== undefined ? { model: row.model } : {}),
|
|
389
|
+
...(row.tenant !== undefined ? { tenant: row.tenant } : {}),
|
|
390
|
+
...(row.score !== undefined ? { score: row.score } : {}),
|
|
391
|
+
...(row.loss !== undefined ? { loss: row.loss } : {}),
|
|
392
|
+
...(row.raw !== undefined ? { raw: row.raw } : {}),
|
|
393
|
+
...(row.input !== undefined ? { input: row.input } : {}),
|
|
394
|
+
...(row.reviewId !== undefined ? { reviewId: row.reviewId } : {}),
|
|
395
|
+
at: row.at,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Recreate `oke_decision_drift` when it still uses an app-wide `id` row.
|
|
401
|
+
* A suspended flag is copied onto every declared decision.
|
|
402
|
+
*
|
|
403
|
+
* @param sql - Journal SQL client
|
|
404
|
+
* @param names - Declared decision names
|
|
405
|
+
*/
|
|
406
|
+
async function migrateDecisionDriftTable(
|
|
407
|
+
sql: DecisionLabelSql,
|
|
408
|
+
names: readonly string[],
|
|
409
|
+
): Promise<void> {
|
|
410
|
+
const create = `CREATE TABLE IF NOT EXISTS oke_decision_drift (
|
|
411
|
+
decision_id TEXT PRIMARY KEY,
|
|
412
|
+
suspended INTEGER NOT NULL,
|
|
413
|
+
certified_at BIGINT NOT NULL
|
|
414
|
+
)`;
|
|
415
|
+
let columns: string[] = [];
|
|
416
|
+
try {
|
|
417
|
+
const rows = await sql.query(
|
|
418
|
+
`SELECT column_name FROM information_schema.columns WHERE table_name = 'oke_decision_drift'`,
|
|
419
|
+
);
|
|
420
|
+
columns = rows.map((row) => String(row.column_name ?? "").toLowerCase());
|
|
421
|
+
} catch {
|
|
422
|
+
columns = [];
|
|
423
|
+
}
|
|
424
|
+
if (columns.includes("id") && !columns.includes("decision_id")) {
|
|
425
|
+
const old = await sql.query(`SELECT * FROM oke_decision_drift`);
|
|
426
|
+
const flag = old[0];
|
|
427
|
+
await sql.exec(`DROP TABLE oke_decision_drift`);
|
|
428
|
+
await sql.exec(create.replace("IF NOT EXISTS ", ""));
|
|
429
|
+
if (flag && Number(flag.suspended) === 1) {
|
|
430
|
+
const certifiedAt = Number(flag.certified_at ?? 0);
|
|
431
|
+
for (const name of names) {
|
|
432
|
+
await sql.exec(
|
|
433
|
+
`INSERT INTO oke_decision_drift (decision_id, suspended, certified_at) VALUES (?, ?, ?)`,
|
|
434
|
+
[name, 1, certifiedAt],
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
await sql.exec(create);
|
|
441
|
+
}
|
|
@@ -71,7 +71,7 @@ describe("boot-level: undeclared-effects flow, no manifest / rootDir", () => {
|
|
|
71
71
|
const create = buildUnannotatedCreateFlow(db);
|
|
72
72
|
const app = oke({ name: "stamp-open", gate: { policies: [gate.public] } }).adopt({ create });
|
|
73
73
|
Object.assign(app.$options, { stores: [db] });
|
|
74
|
-
await createTestApp(app
|
|
74
|
+
await createTestApp(app, { capability: "open" });
|
|
75
75
|
|
|
76
76
|
const res = await app.fetch(
|
|
77
77
|
new Request("http://localhost/notes", {
|
|
@@ -157,7 +157,7 @@ describe("boot-level: table-ref resolution stays backward compatible", () => {
|
|
|
157
157
|
create,
|
|
158
158
|
});
|
|
159
159
|
Object.assign(app.$options, { stores: [db] });
|
|
160
|
-
await createTestApp(app);
|
|
160
|
+
await createTestApp(app, { capability: "open" });
|
|
161
161
|
|
|
162
162
|
const res = await app.fetch(
|
|
163
163
|
new Request("http://localhost/notes", {
|
|
@@ -21,9 +21,10 @@ describe("effects — reversibility tiers", () => {
|
|
|
21
21
|
secret: "capability",
|
|
22
22
|
call: "portal",
|
|
23
23
|
fetch: "irreversible",
|
|
24
|
+
decide: "irreversible",
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
expect(EFFECT_KIND_TIERS).toHaveLength(
|
|
27
|
+
expect(EFFECT_KIND_TIERS).toHaveLength(10);
|
|
27
28
|
for (const { kind, reversibility } of EFFECT_KIND_TIERS) {
|
|
28
29
|
expect(reversibility).toBe(expected[kind]);
|
|
29
30
|
expect(reversibilityOf(kind)).toBe(expected[kind]);
|