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
|
@@ -174,44 +174,158 @@ export function isStoreResourceRef(ref: string): ref is ResourceRef {
|
|
|
174
174
|
return STORE_REF.test(ref) && ref !== "runs";
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/** Effect list keys that disqualify tier-1 auto-cache when non-empty. */
|
|
178
|
+
const DISQUALIFYING_EFFECTS = [
|
|
179
|
+
"writes",
|
|
180
|
+
"emits",
|
|
181
|
+
"sends",
|
|
182
|
+
"asks",
|
|
183
|
+
"embeds",
|
|
184
|
+
"secrets",
|
|
185
|
+
"calls",
|
|
186
|
+
"fetches",
|
|
187
|
+
"decides",
|
|
188
|
+
] as const satisfies readonly (keyof Effects)[];
|
|
189
|
+
|
|
177
190
|
/**
|
|
178
|
-
* Store
|
|
191
|
+
* Store and non-store effects recorded on one invocation's ledger.
|
|
179
192
|
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
193
|
+
* Side effects stay on the result so auto-cache can refuse a flow that
|
|
194
|
+
* also fetched, emitted, or called — a learned store read is not enough.
|
|
182
195
|
*
|
|
183
196
|
* @param entries - Ledger entries from the invocation
|
|
184
197
|
*/
|
|
185
198
|
export function effectsFromLedger(
|
|
186
199
|
entries: readonly { readonly kind: string; readonly resource: string }[],
|
|
187
200
|
): Effects {
|
|
188
|
-
const reads:
|
|
189
|
-
const writes:
|
|
201
|
+
const reads: string[] = [];
|
|
202
|
+
const writes: string[] = [];
|
|
190
203
|
const asks: string[] = [];
|
|
191
|
-
const
|
|
192
|
-
const
|
|
204
|
+
const emits: string[] = [];
|
|
205
|
+
const sends: string[] = [];
|
|
206
|
+
const embeds: string[] = [];
|
|
207
|
+
const secrets: string[] = [];
|
|
208
|
+
const calls: string[] = [];
|
|
209
|
+
const fetches: string[] = [];
|
|
210
|
+
const decides: string[] = [];
|
|
211
|
+
const seen = new Set<string>();
|
|
212
|
+
const push = (bucket: string[], kind: string, resource: string): void => {
|
|
213
|
+
const key = `${kind}:${resource}`;
|
|
214
|
+
if (seen.has(key)) return;
|
|
215
|
+
seen.add(key);
|
|
216
|
+
bucket.push(resource);
|
|
217
|
+
};
|
|
193
218
|
for (const entry of entries) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
219
|
+
switch (entry.kind) {
|
|
220
|
+
case "read":
|
|
221
|
+
push(reads, entry.kind, entry.resource);
|
|
222
|
+
break;
|
|
223
|
+
case "write":
|
|
224
|
+
push(writes, entry.kind, entry.resource);
|
|
225
|
+
break;
|
|
226
|
+
case "ask":
|
|
227
|
+
push(asks, entry.kind, entry.resource);
|
|
228
|
+
break;
|
|
229
|
+
case "emit":
|
|
230
|
+
push(emits, entry.kind, entry.resource);
|
|
231
|
+
break;
|
|
232
|
+
case "send":
|
|
233
|
+
push(sends, entry.kind, entry.resource);
|
|
234
|
+
break;
|
|
235
|
+
case "embed":
|
|
236
|
+
push(embeds, entry.kind, entry.resource);
|
|
237
|
+
break;
|
|
238
|
+
case "secret":
|
|
239
|
+
push(secrets, entry.kind, entry.resource);
|
|
240
|
+
break;
|
|
241
|
+
case "call":
|
|
242
|
+
push(calls, entry.kind, entry.resource);
|
|
243
|
+
break;
|
|
244
|
+
case "fetch":
|
|
245
|
+
push(fetches, entry.kind, entry.resource);
|
|
246
|
+
break;
|
|
247
|
+
case "decide":
|
|
248
|
+
push(decides, entry.kind, entry.resource);
|
|
249
|
+
break;
|
|
250
|
+
default:
|
|
251
|
+
break;
|
|
206
252
|
}
|
|
207
253
|
}
|
|
254
|
+
return {
|
|
255
|
+
...(reads.length > 0 ? { reads: reads as Effects["reads"] } : {}),
|
|
256
|
+
...(writes.length > 0 ? { writes: writes as Effects["writes"] } : {}),
|
|
257
|
+
...(asks.length > 0 ? { asks } : {}),
|
|
258
|
+
...(emits.length > 0 ? { emits } : {}),
|
|
259
|
+
...(sends.length > 0 ? { sends } : {}),
|
|
260
|
+
...(embeds.length > 0 ? { embeds } : {}),
|
|
261
|
+
...(secrets.length > 0 ? { secrets } : {}),
|
|
262
|
+
...(calls.length > 0 ? { calls } : {}),
|
|
263
|
+
...(fetches.length > 0 ? { fetches } : {}),
|
|
264
|
+
...(decides.length > 0 ? { decides } : {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* True when every effect is a store read (`sql:` / `kv:` / `files:` / `index:`).
|
|
270
|
+
*
|
|
271
|
+
* Sends, emits, fetches, vault reads, asks, decides, calls, writes, and
|
|
272
|
+
* non-store reads (`runs`, `signal:`) are not cacheable.
|
|
273
|
+
*
|
|
274
|
+
* @param effects - Declared or ledgered effects
|
|
275
|
+
*/
|
|
276
|
+
export function autoCachePure(effects: Effects | undefined): boolean {
|
|
277
|
+
if (!effects) return false;
|
|
278
|
+
for (const key of DISQUALIFYING_EFFECTS) {
|
|
279
|
+
if ((effects[key]?.length ?? 0) > 0) return false;
|
|
280
|
+
}
|
|
281
|
+
const reads = effects.reads ?? [];
|
|
282
|
+
if (reads.length === 0) return false;
|
|
283
|
+
return reads.every((ref) => isStoreResourceRef(ref));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Union two effect bags. Used so a ledgered fetch cannot be dropped
|
|
288
|
+
* before the auto-cache eligibility check.
|
|
289
|
+
*
|
|
290
|
+
* @param left - Declared or previously resolved effects
|
|
291
|
+
* @param right - Ledgered effects
|
|
292
|
+
*/
|
|
293
|
+
export function mergeEffects(left: Effects, right: Effects): Effects {
|
|
294
|
+
const reads = union(left.reads, right.reads);
|
|
295
|
+
const writes = union(left.writes, right.writes);
|
|
296
|
+
const emits = union(left.emits, right.emits);
|
|
297
|
+
const sends = union(left.sends, right.sends);
|
|
298
|
+
const asks = union(left.asks, right.asks);
|
|
299
|
+
const embeds = union(left.embeds, right.embeds);
|
|
300
|
+
const secrets = union(left.secrets, right.secrets);
|
|
301
|
+
const calls = union(left.calls, right.calls);
|
|
302
|
+
const fetches = union(left.fetches, right.fetches);
|
|
303
|
+
const decides = union(left.decides, right.decides);
|
|
208
304
|
return {
|
|
209
305
|
...(reads.length > 0 ? { reads } : {}),
|
|
210
306
|
...(writes.length > 0 ? { writes } : {}),
|
|
307
|
+
...(emits.length > 0 ? { emits } : {}),
|
|
308
|
+
...(sends.length > 0 ? { sends } : {}),
|
|
211
309
|
...(asks.length > 0 ? { asks } : {}),
|
|
310
|
+
...(embeds.length > 0 ? { embeds } : {}),
|
|
311
|
+
...(secrets.length > 0 ? { secrets } : {}),
|
|
312
|
+
...(calls.length > 0 ? { calls } : {}),
|
|
313
|
+
...(fetches.length > 0 ? { fetches } : {}),
|
|
314
|
+
...(decides.length > 0 ? { decides } : {}),
|
|
212
315
|
};
|
|
213
316
|
}
|
|
214
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Stable union of two effect lists.
|
|
320
|
+
*
|
|
321
|
+
* @param left - First list
|
|
322
|
+
* @param right - Second list
|
|
323
|
+
*/
|
|
324
|
+
function union<T extends string>(left?: readonly T[], right?: readonly T[]): T[] {
|
|
325
|
+
if ((left?.length ?? 0) === 0 && (right?.length ?? 0) === 0) return [];
|
|
326
|
+
return [...new Set([...(left ?? []), ...(right ?? [])])];
|
|
327
|
+
}
|
|
328
|
+
|
|
215
329
|
/**
|
|
216
330
|
* Effects the auto-cache lookup should use: stamped reads when present,
|
|
217
331
|
* otherwise reads learned from a previous run's ledger.
|
|
@@ -223,6 +337,9 @@ export function resolveCacheEffects(
|
|
|
223
337
|
declared: Effects | undefined,
|
|
224
338
|
learnedReads: readonly ResourceRef[] | undefined,
|
|
225
339
|
): Effects {
|
|
340
|
+
if (declared && !autoCachePure(declared) && hasAnyEffect(declared)) {
|
|
341
|
+
return declared;
|
|
342
|
+
}
|
|
226
343
|
const declaredReads = (declared?.reads ?? []).filter((r) => isStoreResourceRef(r));
|
|
227
344
|
if (declaredReads.length > 0 || (declared?.writes?.length ?? 0) > 0) {
|
|
228
345
|
return declared ?? {};
|
|
@@ -233,6 +350,16 @@ export function resolveCacheEffects(
|
|
|
233
350
|
return declared ?? {};
|
|
234
351
|
}
|
|
235
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Whether the effect bag records anything.
|
|
355
|
+
*
|
|
356
|
+
* @param effects - Declared or ledgered effects
|
|
357
|
+
*/
|
|
358
|
+
function hasAnyEffect(effects: Effects): boolean {
|
|
359
|
+
const keys = ["reads", ...DISQUALIFYING_EFFECTS] as const;
|
|
360
|
+
return keys.some((key) => (effects[key]?.length ?? 0) > 0);
|
|
361
|
+
}
|
|
362
|
+
|
|
236
363
|
/**
|
|
237
364
|
* Tier-1 hit only when every key is still present (a write to any
|
|
238
365
|
* contributing resource must miss).
|
|
@@ -255,48 +382,82 @@ export function tier1Lookup<T>(
|
|
|
255
382
|
return value;
|
|
256
383
|
}
|
|
257
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Caller dimensions stamped into a tier-1 cache key.
|
|
387
|
+
*
|
|
388
|
+
* Segments are always present so an empty tenant cannot collide with a set one.
|
|
389
|
+
*/
|
|
390
|
+
export interface Tier1Caller {
|
|
391
|
+
/** Authenticated user id. Empty when anonymous. */
|
|
392
|
+
readonly userId?: string | null;
|
|
393
|
+
/** Active tenant id. Empty when tenancy is off. */
|
|
394
|
+
readonly tenantId?: string | null;
|
|
395
|
+
/** Resolved request locale. */
|
|
396
|
+
readonly locale?: string | null;
|
|
397
|
+
/** Effective scopes, including the tenant-role union. */
|
|
398
|
+
readonly scopes?: Iterable<string>;
|
|
399
|
+
/**
|
|
400
|
+
* Membership role names. Cache identity only — gates do not read this.
|
|
401
|
+
*/
|
|
402
|
+
readonly roles?: Iterable<string>;
|
|
403
|
+
}
|
|
404
|
+
|
|
258
405
|
/**
|
|
259
406
|
* Whether a flow should use automatic tier-1 cache.
|
|
260
407
|
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
408
|
+
* On by default for a pure store read that is not durable. `auto: false`
|
|
409
|
+
* turns the app default off; a flow can still opt in with `cache: true` or a
|
|
410
|
+
* duration. `cache: false` always disables. Sends, emits, fetches, secrets,
|
|
411
|
+
* calls, asks, embeds, decides, and writes are never cached.
|
|
265
412
|
*
|
|
266
|
-
* @param options - Flow cache flag, durability, and effect set
|
|
413
|
+
* @param options - Flow cache flag, app switch, durability, and effect set
|
|
267
414
|
*/
|
|
268
415
|
export function autoCacheEligible(options: {
|
|
269
416
|
readonly cache?: boolean | string;
|
|
417
|
+
/** App-level switch. Omitted means on. `false` turns auto-cache off. */
|
|
418
|
+
readonly auto?: boolean;
|
|
270
419
|
readonly durable?: boolean;
|
|
271
420
|
readonly effects?: Effects;
|
|
272
421
|
}): boolean {
|
|
273
422
|
if (options.cache === false) return false;
|
|
274
423
|
if (options.durable === true) return false;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return reads.length > 0;
|
|
424
|
+
if (options.auto === false && options.cache !== true && typeof options.cache !== "string") {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
return autoCachePure(options.effects);
|
|
280
428
|
}
|
|
281
429
|
|
|
282
430
|
/**
|
|
283
431
|
* Dimension suffixes for a flow-scoped tier-1 key.
|
|
284
432
|
*
|
|
285
|
-
* Format after {@link computedCacheKey}:
|
|
433
|
+
* Format after {@link computedCacheKey}:
|
|
434
|
+
* `computed:{resource}/{flow}/{input}/{userId}/t:{tenant}/l:{locale}/s:{scopes}/r:{roles}`.
|
|
286
435
|
* Invalidation still keys off the resource segment.
|
|
287
436
|
*
|
|
437
|
+
* A string third argument is the user id (older call shape).
|
|
438
|
+
*
|
|
288
439
|
* @param flowName - Flow id
|
|
289
440
|
* @param input - Validated flow input
|
|
290
|
-
* @param
|
|
441
|
+
* @param caller - Caller identity, or a user id string
|
|
291
442
|
*/
|
|
292
443
|
export function tier1FlowDims(
|
|
293
444
|
flowName: string,
|
|
294
445
|
input: unknown,
|
|
295
|
-
|
|
446
|
+
caller?: Tier1Caller | string | null,
|
|
296
447
|
): readonly string[] {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
448
|
+
const identity: Tier1Caller =
|
|
449
|
+
typeof caller === "string" || caller == null ? { userId: caller ?? null } : caller;
|
|
450
|
+
const scopes = [...(identity.scopes ?? [])].sort();
|
|
451
|
+
const roles = [...(identity.roles ?? [])].sort();
|
|
452
|
+
return [
|
|
453
|
+
flowName,
|
|
454
|
+
fingerprintInput(input),
|
|
455
|
+
identity.userId ?? "",
|
|
456
|
+
`t:${identity.tenantId ?? ""}`,
|
|
457
|
+
`l:${identity.locale ?? ""}`,
|
|
458
|
+
`s:${scopes.join(",")}`,
|
|
459
|
+
`r:${roles.join(",")}`,
|
|
460
|
+
];
|
|
300
461
|
}
|
|
301
462
|
|
|
302
463
|
/**
|
|
@@ -305,18 +466,18 @@ export function tier1FlowDims(
|
|
|
305
466
|
* @param effects - Read effects
|
|
306
467
|
* @param flowName - Flow id
|
|
307
468
|
* @param input - Validated flow input
|
|
308
|
-
* @param
|
|
469
|
+
* @param caller - Caller identity, or a user id string
|
|
309
470
|
*/
|
|
310
471
|
export function tier1DimsByResource(
|
|
311
472
|
effects: Effects,
|
|
312
473
|
flowName: string,
|
|
313
474
|
input: unknown,
|
|
314
|
-
|
|
475
|
+
caller?: Tier1Caller | string | null,
|
|
315
476
|
): Readonly<Record<string, readonly string[]>> {
|
|
316
|
-
const dims = tier1FlowDims(flowName, input,
|
|
477
|
+
const dims = tier1FlowDims(flowName, input, caller);
|
|
317
478
|
const out: Record<string, readonly string[]> = {};
|
|
318
479
|
for (const resource of effects.reads ?? []) {
|
|
319
|
-
if (resource
|
|
480
|
+
if (!isStoreResourceRef(resource)) continue;
|
|
320
481
|
out[resource] = dims;
|
|
321
482
|
}
|
|
322
483
|
return out;
|
|
@@ -142,8 +142,12 @@ describe("SqlStoreHandle upsert — epoch-ms into timestamp (Postgres)", () => {
|
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
test("select/update WHERE coerces epoch-ms on timestamp columns", async () => {
|
|
145
|
-
await handle
|
|
146
|
-
|
|
145
|
+
await handle
|
|
146
|
+
.insert(notesTs)
|
|
147
|
+
.values({ id: "old", title: "old", createdAt: 1 as unknown as Date });
|
|
148
|
+
await handle
|
|
149
|
+
.insert(notesTs)
|
|
150
|
+
.values({ id: "new", title: "new", createdAt: 100 as unknown as Date });
|
|
147
151
|
|
|
148
152
|
// Drizzle types timestamp `{ mode: "date" }` as Date; the store still
|
|
149
153
|
// coerces epoch-ms binds (`fx.clock.now()`) at WHERE compile time.
|
|
@@ -69,6 +69,8 @@ describe("SqlStoreHandle — no relational query surface (path b)", () => {
|
|
|
69
69
|
"upsert",
|
|
70
70
|
"increment",
|
|
71
71
|
"raw",
|
|
72
|
+
"run",
|
|
73
|
+
"transaction",
|
|
72
74
|
"count",
|
|
73
75
|
"page",
|
|
74
76
|
"ensureTable",
|
|
@@ -156,6 +158,57 @@ describe("SqlStoreHandle — upsert", () => {
|
|
|
156
158
|
expect(row).toEqual({ id: "welcome", title: "Hello", createdAt: 1 });
|
|
157
159
|
});
|
|
158
160
|
|
|
161
|
+
test("primary-key insert and update each hit the connection once", async () => {
|
|
162
|
+
let queries = 0;
|
|
163
|
+
const counting = {
|
|
164
|
+
driverId: sharedConn.driverId,
|
|
165
|
+
role: sharedConn.role,
|
|
166
|
+
query(sql: string, params?: readonly unknown[]) {
|
|
167
|
+
queries += 1;
|
|
168
|
+
return sharedConn.query(sql, params);
|
|
169
|
+
},
|
|
170
|
+
exec: (sql: string, params?: readonly unknown[]) => sharedConn.exec(sql, params),
|
|
171
|
+
close: () => sharedConn.close(),
|
|
172
|
+
} satisfies SqlConnection;
|
|
173
|
+
const handle = createSqlStoreHandle("sql:app", {
|
|
174
|
+
connection: counting,
|
|
175
|
+
classifications: new Map(),
|
|
176
|
+
routedRole: "primary",
|
|
177
|
+
domainDdl: "off",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
queries = 0;
|
|
181
|
+
const inserted = await handle.upsert(
|
|
182
|
+
posts,
|
|
183
|
+
{ id: "once" },
|
|
184
|
+
{ id: "once", title: "a", createdAt: 1 },
|
|
185
|
+
);
|
|
186
|
+
expect(inserted.status).toBe("upserted");
|
|
187
|
+
expect(queries).toBe(1);
|
|
188
|
+
|
|
189
|
+
queries = 0;
|
|
190
|
+
const updated = await handle.upsert(
|
|
191
|
+
posts,
|
|
192
|
+
{ id: "once" },
|
|
193
|
+
{ id: "once", title: "b", createdAt: 2 },
|
|
194
|
+
{ onExisting: "update" },
|
|
195
|
+
);
|
|
196
|
+
expect(updated.status).toBe("changed");
|
|
197
|
+
expect(queries).toBe(1);
|
|
198
|
+
|
|
199
|
+
const row = await sharedHandle.findById(posts, "once");
|
|
200
|
+
expect(row).toEqual({ id: "once", title: "b", createdAt: 2 });
|
|
201
|
+
|
|
202
|
+
queries = 0;
|
|
203
|
+
const other = await handle.upsert(
|
|
204
|
+
posts,
|
|
205
|
+
{ title: "missing" },
|
|
206
|
+
{ id: "other", title: "missing", createdAt: 3 },
|
|
207
|
+
);
|
|
208
|
+
expect(other.status).toBe("upserted");
|
|
209
|
+
expect(queries).toBe(2);
|
|
210
|
+
});
|
|
211
|
+
|
|
159
212
|
test("onExisting update changes matched columns", async () => {
|
|
160
213
|
await sharedHandle.upsert(posts, { id: "n1" }, { id: "n1", title: "one", createdAt: 10 });
|
|
161
214
|
const updated = await sharedHandle.upsert(
|