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
package/src/plugins/otp.ts
CHANGED
|
@@ -77,7 +77,8 @@ export interface OtpProviderModeOptions extends OtpBaseOptions {
|
|
|
77
77
|
readonly channels?: never;
|
|
78
78
|
/** Forbidden in provider mode — code never exists server-side. */
|
|
79
79
|
readonly exposeDevOtp?: never;
|
|
80
|
-
|
|
80
|
+
/** Spacing between provider SMS resends. Default 60s. */
|
|
81
|
+
readonly resendCooldownMs?: number;
|
|
81
82
|
readonly from?: never;
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -216,10 +217,7 @@ export function otp(opts: OtpOptions): PluginDef {
|
|
|
216
217
|
const identities = resolveSharedIdentities(opts);
|
|
217
218
|
const verifications = opts.verifications ?? createVerificationStore();
|
|
218
219
|
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
219
|
-
const resendCooldownMs =
|
|
220
|
-
opts.mode === "app"
|
|
221
|
-
? (opts.resendCooldownMs ?? DEFAULT_RESEND_COOLDOWN_MS)
|
|
222
|
-
: DEFAULT_RESEND_COOLDOWN_MS;
|
|
220
|
+
const resendCooldownMs = opts.resendCooldownMs ?? DEFAULT_RESEND_COOLDOWN_MS;
|
|
223
221
|
const channels = opts.mode === "app" ? opts.channels : ([] as const);
|
|
224
222
|
const exposeDevOtp = opts.mode === "app" && opts.exposeDevOtp === true;
|
|
225
223
|
const from = opts.mode === "app" ? opts.from : undefined;
|
|
@@ -288,8 +286,61 @@ export function otp(opts: OtpOptions): PluginDef {
|
|
|
288
286
|
attempts: 0,
|
|
289
287
|
phone,
|
|
290
288
|
sealedOtp: null,
|
|
289
|
+
lastDeliveredAt: now,
|
|
290
|
+
lastChannel: "sms",
|
|
291
|
+
});
|
|
292
|
+
return { ok: true as const, channel: "sms" as const };
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const resendContract = {
|
|
297
|
+
in: z.object({
|
|
298
|
+
phone: z.string().min(8),
|
|
299
|
+
lang: z.enum(["en", "ar"]).optional(),
|
|
300
|
+
channel: z.enum(["sms", "whatsapp", "email"]).optional(),
|
|
301
|
+
}),
|
|
302
|
+
out: requestOut,
|
|
303
|
+
errors: { AuthFailed, AuthRateLimited },
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const resend = flow("auth.resendOtp", {
|
|
307
|
+
plane: "user",
|
|
308
|
+
effects: { sends: ["sms-otp"] },
|
|
309
|
+
do: async (input, fx) => {
|
|
310
|
+
if (input.channel !== undefined && input.channel !== "sms") {
|
|
311
|
+
return fail("AuthFailed", { reason: "invalid_channel" });
|
|
312
|
+
}
|
|
313
|
+
const phone = input.phone.trim();
|
|
314
|
+
if (!E164.test(phone)) return fail("AuthFailed", { reason: "invalid_phone" });
|
|
315
|
+
const now = runtime.now();
|
|
316
|
+
const row = findActiveVerification(verifications, `otp:${phone}`, now);
|
|
317
|
+
if (!row || row.sealedOtp) {
|
|
318
|
+
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
319
|
+
}
|
|
320
|
+
if (row.lastDeliveredAt !== undefined && now - row.lastDeliveredAt < resendCooldownMs) {
|
|
321
|
+
return fail("AuthFailed", { reason: "resend_cooldown" });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const requestId = okid();
|
|
325
|
+
await fx.sendOtp({
|
|
326
|
+
to: phone,
|
|
327
|
+
requestId,
|
|
328
|
+
...(input.lang ? { lang: input.lang } : {}),
|
|
329
|
+
});
|
|
330
|
+
invalidateVerifications(verifications, `otp:${phone}`, now);
|
|
331
|
+
putVerification(verifications, {
|
|
332
|
+
id: okid(),
|
|
333
|
+
identifier: `otp:${phone}`,
|
|
334
|
+
value: `provider:${requestId}`,
|
|
335
|
+
expiresAt: now + ttlMs,
|
|
336
|
+
createdAt: now,
|
|
337
|
+
consumedAt: null,
|
|
338
|
+
attempts: 0,
|
|
339
|
+
phone,
|
|
340
|
+
sealedOtp: null,
|
|
341
|
+
lastDeliveredAt: now,
|
|
342
|
+
lastChannel: "sms",
|
|
291
343
|
});
|
|
292
|
-
// Provider mode: resend-via-different-channel is impossible — provider owns the code.
|
|
293
344
|
return { ok: true as const, channel: "sms" as const };
|
|
294
345
|
},
|
|
295
346
|
});
|
|
@@ -373,7 +424,8 @@ export function otp(opts: OtpOptions): PluginDef {
|
|
|
373
424
|
return plugin("otp", { version: "0.0.1", config: configSnapshot })
|
|
374
425
|
.needs("auth")
|
|
375
426
|
.binding(bindPublicAuth("/otp/request", request, "otp", requestContract))
|
|
376
|
-
.binding(bindPublicAuth("/otp/verify", verify, "otp", verifyContract))
|
|
427
|
+
.binding(bindPublicAuth("/otp/verify", verify, "otp", verifyContract))
|
|
428
|
+
.binding(bindPublicAuth("/otp/resend", resend, "otp", resendContract));
|
|
377
429
|
}
|
|
378
430
|
|
|
379
431
|
// ── App mode ───────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absolute budgets with room under the cap also fail at 2× the last value.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
absoluteBudgetOk,
|
|
8
|
+
absoluteRegressionCeiling,
|
|
9
|
+
formatBudgetsReport,
|
|
10
|
+
type BudgetsSnapshot,
|
|
11
|
+
} from "./measure.ts";
|
|
12
|
+
import {
|
|
13
|
+
ABSOLUTE_REGRESSION_RATIO,
|
|
14
|
+
CLIENT_BUDGET_BYTES,
|
|
15
|
+
COLD_START_BUDGET_MS,
|
|
16
|
+
CONSOLE_BUDGET_BYTES,
|
|
17
|
+
KERNEL_EDGE_BUDGET_BYTES,
|
|
18
|
+
} from "./limits.ts";
|
|
19
|
+
|
|
20
|
+
describe("absolute regression ceilings", () => {
|
|
21
|
+
test("ratio is 2× so a confirmed doubling fails and a single noisy run can be re-measured", () => {
|
|
22
|
+
expect(ABSOLUTE_REGRESSION_RATIO).toBe(2);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("cold start and console initial load grow a ceiling; kernel, client, and a zero baseline do not", () => {
|
|
26
|
+
expect(absoluteRegressionCeiling(undefined, COLD_START_BUDGET_MS, "ms")).toBeUndefined();
|
|
27
|
+
expect(absoluteRegressionCeiling(0, 1, "ms")).toBeUndefined();
|
|
28
|
+
expect(absoluteRegressionCeiling(8.915, COLD_START_BUDGET_MS, "ms")).toBe(17.83);
|
|
29
|
+
expect(absoluteRegressionCeiling(353_621, CONSOLE_BUDGET_BYTES, "bytes")).toBe(707_242);
|
|
30
|
+
expect(absoluteRegressionCeiling(12_556, KERNEL_EDGE_BUDGET_BYTES, "bytes")).toBeUndefined();
|
|
31
|
+
expect(absoluteRegressionCeiling(4598, CLIENT_BUDGET_BYTES, "bytes")).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("ok requires both the cap and the ceiling", () => {
|
|
35
|
+
expect(absoluteBudgetOk(8.915, COLD_START_BUDGET_MS, 17.83)).toBe(true);
|
|
36
|
+
expect(absoluteBudgetOk(17.83, COLD_START_BUDGET_MS, 17.83)).toBe(false);
|
|
37
|
+
expect(absoluteBudgetOk(20, COLD_START_BUDGET_MS, 17.83)).toBe(false);
|
|
38
|
+
expect(absoluteBudgetOk(80, COLD_START_BUDGET_MS, undefined)).toBe(false);
|
|
39
|
+
expect(absoluteBudgetOk(12_556, KERNEL_EDGE_BUDGET_BYTES, undefined)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("the CI log prints the 2× ceiling and the absolute cap", () => {
|
|
43
|
+
const snapshot: BudgetsSnapshot = {
|
|
44
|
+
measuredAt: "2026-09-23T00:00:00.000Z",
|
|
45
|
+
version: "0.0.0",
|
|
46
|
+
budgets: [
|
|
47
|
+
{
|
|
48
|
+
id: "coldStartMedianMs",
|
|
49
|
+
label: "Cold start on Bun",
|
|
50
|
+
value: 20,
|
|
51
|
+
limit: COLD_START_BUDGET_MS,
|
|
52
|
+
regressionLimit: 17.83,
|
|
53
|
+
unit: "ms",
|
|
54
|
+
gate: "absolute",
|
|
55
|
+
group: "core",
|
|
56
|
+
ok: false,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
const report = formatBudgetsReport(snapshot);
|
|
61
|
+
expect(report).toContain("[FAIL] Cold start on Bun: 20.000 ms < 17.830 ms (cap 75.000 ms)");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `okengine/http` must not statically import heavy modules.
|
|
3
|
+
* Forbidden ids live in {@link HTTP_STATIC_GRAPH_FORBIDDEN}.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { forbiddenHttpGraphHits, readStaticGraphSpecifiers } from "./http-graph.ts";
|
|
8
|
+
|
|
9
|
+
describe("okengine/http static import graph", () => {
|
|
10
|
+
test("zod and the hybrid-search runtime are not in the graph", async () => {
|
|
11
|
+
const specifiers = await readStaticGraphSpecifiers();
|
|
12
|
+
expect(specifiers.length).toBeGreaterThan(10);
|
|
13
|
+
expect(forbiddenHttpGraphHits(specifiers)).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("the forbidden list matches zod and search-runtime, and nothing nearby", () => {
|
|
17
|
+
expect(forbiddenHttpGraphHits(["node_modules/zod/index.js"])).toEqual(["zod"]);
|
|
18
|
+
expect(forbiddenHttpGraphHits(["zod"])).toEqual(["zod"]);
|
|
19
|
+
expect(forbiddenHttpGraphHits(["src/elements/store/search-runtime.ts"])).toEqual([
|
|
20
|
+
"hybrid-search runtime",
|
|
21
|
+
]);
|
|
22
|
+
expect(forbiddenHttpGraphHits(["src/kernel/fx.ts"])).toEqual(["fx runtime"]);
|
|
23
|
+
expect(forbiddenHttpGraphHits(["src/kernel/fx-decide.ts"])).toEqual([]);
|
|
24
|
+
expect(forbiddenHttpGraphHits(["src/elements/store/search-lsh.ts"])).toEqual([]);
|
|
25
|
+
expect(forbiddenHttpGraphHits(["src/kernel/app.ts"])).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modules that must stay off the static `okengine/http` graph.
|
|
3
|
+
*
|
|
4
|
+
* Add a heavy module here when a cold-start regression pulls it in.
|
|
5
|
+
* One list — the import-graph test matches every entry.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** One forbidden module on the `okengine/http` static graph. */
|
|
9
|
+
export interface HttpGraphForbidden {
|
|
10
|
+
/** Stable id. Printed when the build pulls this module in. */
|
|
11
|
+
readonly id: string;
|
|
12
|
+
/**
|
|
13
|
+
* True when `specifier` is this module.
|
|
14
|
+
* Specifiers are Bun metafile input paths and import paths, including externals.
|
|
15
|
+
*
|
|
16
|
+
* @param specifier - Input path or import specifier
|
|
17
|
+
*/
|
|
18
|
+
readonly test: (specifier: string) => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Forbidden static imports for `okengine/http`.
|
|
23
|
+
* `zod` and the hybrid-search runtime (`search-runtime`) load on first use
|
|
24
|
+
* elsewhere; a static edge puts them on every cold start.
|
|
25
|
+
*/
|
|
26
|
+
export const HTTP_STATIC_GRAPH_FORBIDDEN: readonly HttpGraphForbidden[] = [
|
|
27
|
+
{
|
|
28
|
+
id: "zod",
|
|
29
|
+
test: (specifier) =>
|
|
30
|
+
specifier === "zod" ||
|
|
31
|
+
specifier.startsWith("zod/") ||
|
|
32
|
+
specifier.includes("node_modules/zod/") ||
|
|
33
|
+
specifier.includes("node_modules/zod@"),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "hybrid-search runtime",
|
|
37
|
+
test: (specifier) =>
|
|
38
|
+
specifier.includes("search-runtime.ts") ||
|
|
39
|
+
specifier.includes("search-runtime.js") ||
|
|
40
|
+
/(^|\/)search-runtime$/.test(specifier),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "fx runtime",
|
|
44
|
+
test: (specifier) =>
|
|
45
|
+
specifier.endsWith("/fx.ts") || specifier.endsWith("/fx.js") || /(^|\/)fx$/.test(specifier),
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Metafile slice this gate reads. */
|
|
50
|
+
interface GraphMetafile {
|
|
51
|
+
readonly inputs: Readonly<
|
|
52
|
+
Record<string, { readonly imports?: readonly { readonly path: string }[] }>
|
|
53
|
+
>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Ids from {@link HTTP_STATIC_GRAPH_FORBIDDEN} that appear in `specifiers`.
|
|
58
|
+
*
|
|
59
|
+
* @param specifiers - Bundled input paths and import specifiers
|
|
60
|
+
*/
|
|
61
|
+
export function forbiddenHttpGraphHits(specifiers: readonly string[]): readonly string[] {
|
|
62
|
+
const hits: string[] = [];
|
|
63
|
+
for (const rule of HTTP_STATIC_GRAPH_FORBIDDEN) {
|
|
64
|
+
if (specifiers.some((specifier) => rule.test(specifier))) hits.push(rule.id);
|
|
65
|
+
}
|
|
66
|
+
return hits;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Static module specifiers for an entry, from a Bun metafile.
|
|
71
|
+
* Includes bundled inputs and every import path (external and dynamic).
|
|
72
|
+
*
|
|
73
|
+
* @param entry - Absolute path to the entry file
|
|
74
|
+
*/
|
|
75
|
+
export async function readStaticGraphSpecifiers(
|
|
76
|
+
entry = `${import.meta.dir}/../http.ts`,
|
|
77
|
+
): Promise<readonly string[]> {
|
|
78
|
+
const result = await Bun.build({
|
|
79
|
+
entrypoints: [entry],
|
|
80
|
+
target: "bun",
|
|
81
|
+
format: "esm",
|
|
82
|
+
metafile: true,
|
|
83
|
+
});
|
|
84
|
+
if (!result.success) {
|
|
85
|
+
throw new Error(`${entry} build failed:\n${result.logs.map(String).join("\n")}`);
|
|
86
|
+
}
|
|
87
|
+
const metafile = result.metafile as GraphMetafile | undefined;
|
|
88
|
+
if (!metafile) throw new Error(`${entry} build returned no metafile`);
|
|
89
|
+
const specifiers = new Set<string>();
|
|
90
|
+
for (const [input, info] of Object.entries(metafile.inputs)) {
|
|
91
|
+
specifiers.add(input);
|
|
92
|
+
for (const imp of info.imports ?? []) specifiers.add(imp.path);
|
|
93
|
+
}
|
|
94
|
+
return [...specifiers];
|
|
95
|
+
}
|
package/src/release/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export {
|
|
|
18
18
|
} from "./exports.ts";
|
|
19
19
|
|
|
20
20
|
export {
|
|
21
|
+
ABSOLUTE_REGRESSION_RATIO,
|
|
21
22
|
CLIENT_BUDGET_BYTES,
|
|
22
23
|
COLD_START_BUDGET_MS,
|
|
23
24
|
CONSOLE_BUDGET_BYTES,
|
|
@@ -28,6 +29,8 @@ export {
|
|
|
28
29
|
} from "./limits.ts";
|
|
29
30
|
|
|
30
31
|
export {
|
|
32
|
+
absoluteBudgetOk,
|
|
33
|
+
absoluteRegressionCeiling,
|
|
31
34
|
budgetsPass,
|
|
32
35
|
exportRegressionLimitBytes,
|
|
33
36
|
formatBudgetsMarkdown,
|
package/src/release/limits.ts
CHANGED
|
@@ -7,8 +7,13 @@
|
|
|
7
7
|
/** Kernel (edge profile) — gzipped minified bundle. */
|
|
8
8
|
export const KERNEL_EDGE_BUDGET_BYTES = 17 * 1024;
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Client runtime — gzipped minified bundle.
|
|
12
|
+
*
|
|
13
|
+
* 6 kB (was 5 kB). Idempotency inlines the okid alphabet; measured gzip is
|
|
14
|
+
* 5194, which sat over 90% of 5120.
|
|
15
|
+
*/
|
|
16
|
+
export const CLIENT_BUDGET_BYTES = 6 * 1024;
|
|
12
17
|
|
|
13
18
|
/** Console initial load — gzipped html + entry js/css (ui-next SPA, no panel-* split). */
|
|
14
19
|
export const CONSOLE_BUDGET_BYTES = 700 * 1024;
|
|
@@ -27,3 +32,13 @@ export const EXPORT_REGRESSION_TOLERANCE_RATIO = 0.02;
|
|
|
27
32
|
|
|
28
33
|
/** Minimum allowed growth (bytes) before a subpath export fails regression. */
|
|
29
34
|
export const EXPORT_REGRESSION_TOLERANCE_FLOOR_BYTES = 256;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Growth multiple for an absolute budget that still has room under its cap.
|
|
38
|
+
*
|
|
39
|
+
* The sample fails when it reaches this multiple of the last committed value
|
|
40
|
+
* and that multiple is still below the absolute cap. Two is the line: one
|
|
41
|
+
* cold-start run has already printed a phantom jump of about this size, so
|
|
42
|
+
* the probe re-runs before failing instead of tightening the ratio.
|
|
43
|
+
*/
|
|
44
|
+
export const ABSOLUTE_REGRESSION_RATIO = 2;
|
package/src/release/measure.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
resolveExportBudgetTargets,
|
|
14
14
|
} from "./exports.ts";
|
|
15
15
|
import {
|
|
16
|
+
ABSOLUTE_REGRESSION_RATIO,
|
|
16
17
|
CLIENT_BUDGET_BYTES,
|
|
17
18
|
COLD_START_BUDGET_MS,
|
|
18
19
|
CONSOLE_BUDGET_BYTES,
|
|
@@ -80,8 +81,18 @@ export interface BudgetSample {
|
|
|
80
81
|
readonly label: string;
|
|
81
82
|
/** Measured value. */
|
|
82
83
|
readonly value: number;
|
|
83
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Absolute cap, or the regression ceiling for export samples.
|
|
86
|
+
* Landing copy reads this as the published cap — do not replace it
|
|
87
|
+
* with {@link regressionLimit}.
|
|
88
|
+
*/
|
|
84
89
|
readonly limit: number;
|
|
90
|
+
/**
|
|
91
|
+
* Tighter growth ceiling (`ABSOLUTE_REGRESSION_RATIO` × last committed
|
|
92
|
+
* value) when that multiple is still under {@link limit}. Absent when
|
|
93
|
+
* the absolute cap is the only gate.
|
|
94
|
+
*/
|
|
95
|
+
readonly regressionLimit?: number;
|
|
85
96
|
/** Unit for display / snapshot. */
|
|
86
97
|
readonly unit: "bytes" | "ms";
|
|
87
98
|
/** Gate mode. */
|
|
@@ -277,7 +288,7 @@ if (!artifact) { console.error("no output"); process.exit(1); }
|
|
|
277
288
|
const raw = await artifact.arrayBuffer();
|
|
278
289
|
process.stdout.write(String(Bun.gzipSync(new Uint8Array(raw)).byteLength));
|
|
279
290
|
`;
|
|
280
|
-
const proc = Bun.spawn([
|
|
291
|
+
const proc = Bun.spawn([process.execPath, "-e", probe], {
|
|
281
292
|
cwd: ROOT,
|
|
282
293
|
stdout: "pipe",
|
|
283
294
|
stderr: "pipe",
|
|
@@ -298,23 +309,52 @@ process.stdout.write(String(Bun.gzipSync(new Uint8Array(raw)).byteLength));
|
|
|
298
309
|
}
|
|
299
310
|
|
|
300
311
|
/**
|
|
301
|
-
* Gzip size of the client runtime bundle.
|
|
312
|
+
* Gzip size of the client runtime bundle (one file, no split).
|
|
313
|
+
*
|
|
314
|
+
* If this build ever uses `splitting`, the gate must count the entry plus
|
|
315
|
+
* every chunk it imports statically. The entry file alone under-counts the
|
|
316
|
+
* first download when a shared chunk loads with it.
|
|
302
317
|
*/
|
|
303
318
|
export async function measureClientGzipBytes(): Promise<number> {
|
|
304
|
-
const entry =
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
319
|
+
const entry = resolve(ROOT, "src/client/budget-entry.ts");
|
|
320
|
+
// `bun test` sets NODE_ENV=test. An in-process Bun.build then inlines that
|
|
321
|
+
// and the gzip drifts from `bun run budgets`. Spawn once, production, so
|
|
322
|
+
// both callers share this number.
|
|
323
|
+
const probe = `
|
|
324
|
+
const result = await Bun.build({
|
|
325
|
+
entrypoints: ${JSON.stringify([entry])},
|
|
326
|
+
minify: true,
|
|
327
|
+
target: "browser",
|
|
328
|
+
format: "esm",
|
|
329
|
+
});
|
|
330
|
+
if (!result.success) {
|
|
331
|
+
console.error(result.logs.map(String).join("\\n"));
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
const artifact = result.outputs[0];
|
|
335
|
+
if (!artifact) { console.error("no output"); process.exit(1); }
|
|
336
|
+
const raw = await artifact.arrayBuffer();
|
|
337
|
+
process.stdout.write(String(Bun.gzipSync(new Uint8Array(raw)).byteLength));
|
|
338
|
+
`;
|
|
339
|
+
const proc = Bun.spawn([process.execPath, "-e", probe], {
|
|
340
|
+
cwd: ROOT,
|
|
341
|
+
stdout: "pipe",
|
|
342
|
+
stderr: "pipe",
|
|
343
|
+
env: { ...process.env, NODE_ENV: "production" },
|
|
310
344
|
});
|
|
311
|
-
|
|
312
|
-
|
|
345
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
346
|
+
new Response(proc.stdout).text(),
|
|
347
|
+
new Response(proc.stderr).text(),
|
|
348
|
+
proc.exited,
|
|
349
|
+
]);
|
|
350
|
+
if (exitCode !== 0) {
|
|
351
|
+
throw new Error(`client build failed:\n${stderr || stdout}`);
|
|
313
352
|
}
|
|
314
|
-
const
|
|
315
|
-
if (!
|
|
316
|
-
|
|
317
|
-
|
|
353
|
+
const n = Number(stdout.trim());
|
|
354
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
355
|
+
throw new Error(`client measure produced ${stdout}`);
|
|
356
|
+
}
|
|
357
|
+
return n;
|
|
318
358
|
}
|
|
319
359
|
|
|
320
360
|
/**
|
|
@@ -423,7 +463,7 @@ process.stdout.write(String(ms));
|
|
|
423
463
|
`;
|
|
424
464
|
const samples: number[] = [];
|
|
425
465
|
for (let i = 0; i < 7; i++) {
|
|
426
|
-
const proc = Bun.spawn([
|
|
466
|
+
const proc = Bun.spawn([process.execPath, "-e", probe], {
|
|
427
467
|
cwd: ROOT,
|
|
428
468
|
stdout: "pipe",
|
|
429
469
|
stderr: "pipe",
|
|
@@ -451,17 +491,66 @@ process.stdout.write(String(ms));
|
|
|
451
491
|
/**
|
|
452
492
|
* Median cold-start ms across Bun subprocesses.
|
|
453
493
|
*
|
|
454
|
-
* Takes the best of up to five rounds
|
|
455
|
-
*
|
|
494
|
+
* Takes the best of up to five rounds and returns as soon as one round is
|
|
495
|
+
* under `exitBelowMs`. If every round is still at or above that line, runs
|
|
496
|
+
* one confirmation round and keeps the minimum. A single noisy run has
|
|
497
|
+
* printed a phantom jump of about 2×; the confirmation is what keeps that
|
|
498
|
+
* from failing the regression gate.
|
|
499
|
+
*
|
|
500
|
+
* @param exitBelowMs - Stop when a round is under this (regression ceiling, or the 75 ms cap)
|
|
456
501
|
*/
|
|
457
|
-
export async function measureColdStartMedianMs(
|
|
502
|
+
export async function measureColdStartMedianMs(
|
|
503
|
+
exitBelowMs = COLD_START_BUDGET_MS,
|
|
504
|
+
): Promise<number> {
|
|
458
505
|
let best = Number.POSITIVE_INFINITY;
|
|
459
506
|
for (let round = 0; round < 5; round++) {
|
|
460
507
|
const median = await measureColdStartMedianMsOnce();
|
|
461
508
|
if (median < best) best = median;
|
|
462
|
-
if (best <
|
|
509
|
+
if (best < exitBelowMs) return best;
|
|
463
510
|
}
|
|
464
|
-
|
|
511
|
+
const confirm = await measureColdStartMedianMsOnce();
|
|
512
|
+
return Math.min(best, confirm);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Growth ceiling for an absolute budget that still has room under its cap.
|
|
517
|
+
*
|
|
518
|
+
* Undefined when there is no positive baseline, or when doubling would
|
|
519
|
+
* already miss the absolute cap (kernel and client today). Zero baselines
|
|
520
|
+
* (routing p99 reads as 0 ms) stay on the absolute cap — a ratio of zero
|
|
521
|
+
* would fail the next positive sample.
|
|
522
|
+
*
|
|
523
|
+
* @param previous - Last committed value, if the snapshot has this id
|
|
524
|
+
* @param absoluteLimit - Published cap
|
|
525
|
+
* @param unit - Rounding: whole bytes, or milliseconds to 0.001
|
|
526
|
+
*/
|
|
527
|
+
export function absoluteRegressionCeiling(
|
|
528
|
+
previous: number | undefined,
|
|
529
|
+
absoluteLimit: number,
|
|
530
|
+
unit: BudgetSample["unit"],
|
|
531
|
+
): number | undefined {
|
|
532
|
+
if (previous === undefined || !(previous > 0)) return undefined;
|
|
533
|
+
const raw = previous * ABSOLUTE_REGRESSION_RATIO;
|
|
534
|
+
const ceiling = unit === "bytes" ? Math.round(raw) : Math.round(raw * 1000) / 1000;
|
|
535
|
+
if (ceiling >= absoluteLimit) return undefined;
|
|
536
|
+
return ceiling;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* True when a rounded absolute sample is under the cap and any regression ceiling.
|
|
541
|
+
*
|
|
542
|
+
* @param value - Rounded measurement
|
|
543
|
+
* @param absoluteLimit - Published cap
|
|
544
|
+
* @param regressionLimit - Growth ceiling, when the cap would still allow it
|
|
545
|
+
*/
|
|
546
|
+
export function absoluteBudgetOk(
|
|
547
|
+
value: number,
|
|
548
|
+
absoluteLimit: number,
|
|
549
|
+
regressionLimit: number | undefined,
|
|
550
|
+
): boolean {
|
|
551
|
+
if (value >= absoluteLimit) return false;
|
|
552
|
+
if (regressionLimit !== undefined && value >= regressionLimit) return false;
|
|
553
|
+
return true;
|
|
465
554
|
}
|
|
466
555
|
|
|
467
556
|
/**
|
|
@@ -542,8 +631,15 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
|
|
|
542
631
|
const previous = await loadPreviousBudgetValues();
|
|
543
632
|
|
|
544
633
|
// Cold start alone first — parallel gzip work contends for CPU on CI and
|
|
545
|
-
// falsely inflates the wall-clock probe.
|
|
546
|
-
|
|
634
|
+
// falsely inflates the wall-clock probe. Exit under the 2× ceiling when
|
|
635
|
+
// that line is tighter than 75 ms, so a phantom jump gets another round
|
|
636
|
+
// instead of being recorded.
|
|
637
|
+
const coldCeiling = absoluteRegressionCeiling(
|
|
638
|
+
previous.get("coldStartMedianMs"),
|
|
639
|
+
COLD_START_BUDGET_MS,
|
|
640
|
+
"ms",
|
|
641
|
+
);
|
|
642
|
+
const coldStartMedianMs = await measureColdStartMedianMs(coldCeiling ?? COLD_START_BUDGET_MS);
|
|
547
643
|
const [kernelEdgeGzipBytes, clientGzipBytes, consoleInitialGzipBytes, httpPing] =
|
|
548
644
|
await Promise.all([
|
|
549
645
|
measureKernelEdgeGzipBytes(),
|
|
@@ -562,6 +658,7 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
|
|
|
562
658
|
"bytes",
|
|
563
659
|
"absolute",
|
|
564
660
|
"core",
|
|
661
|
+
previous,
|
|
565
662
|
),
|
|
566
663
|
sample(
|
|
567
664
|
"clientGzipBytes",
|
|
@@ -571,6 +668,7 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
|
|
|
571
668
|
"bytes",
|
|
572
669
|
"absolute",
|
|
573
670
|
"core",
|
|
671
|
+
previous,
|
|
574
672
|
),
|
|
575
673
|
sample(
|
|
576
674
|
"consoleInitialGzipBytes",
|
|
@@ -580,6 +678,7 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
|
|
|
580
678
|
"bytes",
|
|
581
679
|
"absolute",
|
|
582
680
|
"core",
|
|
681
|
+
previous,
|
|
583
682
|
),
|
|
584
683
|
sample(
|
|
585
684
|
"coldStartMedianMs",
|
|
@@ -589,6 +688,7 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
|
|
|
589
688
|
"ms",
|
|
590
689
|
"absolute",
|
|
591
690
|
"core",
|
|
691
|
+
previous,
|
|
592
692
|
),
|
|
593
693
|
sample(
|
|
594
694
|
"routingP99Ms",
|
|
@@ -598,6 +698,7 @@ export async function measureAllBudgets(): Promise<BudgetsSnapshot> {
|
|
|
598
698
|
"ms",
|
|
599
699
|
"absolute",
|
|
600
700
|
"core",
|
|
701
|
+
previous,
|
|
601
702
|
),
|
|
602
703
|
regressionSample(
|
|
603
704
|
"httpPingGzipBytes",
|
|
@@ -648,9 +749,7 @@ export function formatBudgetsReport(snapshot: BudgetsSnapshot): string {
|
|
|
648
749
|
lines.push(GROUP_HEADINGS[group]);
|
|
649
750
|
for (const b of rows) {
|
|
650
751
|
const flag = b.ok ? "ok" : "FAIL";
|
|
651
|
-
lines.push(
|
|
652
|
-
` [${flag}] ${b.label}: ${formatValue(b.value, b.unit)} < ${formatValue(b.limit, b.unit)}`,
|
|
653
|
-
);
|
|
752
|
+
lines.push(` [${flag}] ${b.label}: ${formatValue(b.value, b.unit)} < ${formatBound(b)}`);
|
|
654
753
|
}
|
|
655
754
|
}
|
|
656
755
|
return `${lines.join("\n")}\n`;
|
|
@@ -692,7 +791,7 @@ export function formatBudgetsMarkdown(snapshot: BudgetsSnapshot): string {
|
|
|
692
791
|
"",
|
|
693
792
|
`_okengine v${snapshot.version} · measured ${snapshot.measuredAt}_`,
|
|
694
793
|
"",
|
|
695
|
-
"Core rows are absolute AGENTS caps (plus HTTP-ping regression samples). Exports, Plugins, and Drivers fail on regression vs the prior [`budgets.json`](budgets.json) (max +256 B or +2%). Export gzip excludes hard/optional externals (`zod`, `sently`, `oxc-parser`, `ajv`, DuckDB, FormatJS). The `okengine` export row is the **thin root** (gzip); use `okengine/full` for the legacy mega-barrel and `okengine/http` for HTTP-only apps.",
|
|
794
|
+
"Core rows are absolute AGENTS caps (plus HTTP-ping regression samples). An absolute sample also fails when it reaches 2× its last committed value and that multiple is still under the cap (cold start, and any other absolute row with the same headroom). Cold start keeps the best of five rounds and confirms a failure once, so one noisy run does not fail. Exports, Plugins, and Drivers fail on regression vs the prior [`budgets.json`](budgets.json) (max +256 B or +2%). Export gzip excludes hard/optional externals (`zod`, `sently`, `oxc-parser`, `ajv`, DuckDB, FormatJS). The `okengine` export row is the **thin root** (gzip); use `okengine/full` for the legacy mega-barrel and `okengine/http` for HTTP-only apps.",
|
|
696
795
|
];
|
|
697
796
|
const order: readonly BudgetGroup[] = ["core", "exports", "plugins", "drivers"];
|
|
698
797
|
for (const group of order) {
|
|
@@ -758,20 +857,37 @@ function sample(
|
|
|
758
857
|
unit: BudgetSample["unit"],
|
|
759
858
|
gate: BudgetGate,
|
|
760
859
|
group: BudgetGroup,
|
|
860
|
+
previous?: ReadonlyMap<string, number>,
|
|
761
861
|
): BudgetSample {
|
|
762
862
|
const rounded = unit === "bytes" ? Math.round(value) : Math.round(value * 1000) / 1000;
|
|
863
|
+
const regressionLimit =
|
|
864
|
+
gate === "absolute" ? absoluteRegressionCeiling(previous?.get(id), limit, unit) : undefined;
|
|
763
865
|
return {
|
|
764
866
|
id,
|
|
765
867
|
label,
|
|
766
868
|
value: rounded,
|
|
767
869
|
limit,
|
|
870
|
+
...(regressionLimit !== undefined ? { regressionLimit } : {}),
|
|
768
871
|
unit,
|
|
769
872
|
gate,
|
|
770
873
|
group,
|
|
771
|
-
ok: rounded
|
|
874
|
+
ok: absoluteBudgetOk(rounded, limit, regressionLimit),
|
|
772
875
|
};
|
|
773
876
|
}
|
|
774
877
|
|
|
878
|
+
/**
|
|
879
|
+
* CI log bound. When the 2× ceiling is tighter than the cap, print both.
|
|
880
|
+
*
|
|
881
|
+
* @param sample - Measured row
|
|
882
|
+
*/
|
|
883
|
+
function formatBound(row: BudgetSample): string {
|
|
884
|
+
const ceiling = row.regressionLimit;
|
|
885
|
+
if (ceiling !== undefined && ceiling < row.limit) {
|
|
886
|
+
return `${formatValue(ceiling, row.unit)} (cap ${formatValue(row.limit, row.unit)})`;
|
|
887
|
+
}
|
|
888
|
+
return formatValue(row.limit, row.unit);
|
|
889
|
+
}
|
|
890
|
+
|
|
775
891
|
/**
|
|
776
892
|
* Byte sample gated against the previous committed value (+ tolerance).
|
|
777
893
|
* First appearance records a baseline (`ok: true`, `limit` = value + floor).
|
package/src/runs/collect.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { Trigger } from "../kernel/triggers.ts";
|
|
|
13
13
|
import type { FlowPlane } from "../manifest/types.ts";
|
|
14
14
|
import { cacheDimensionOf, type RunTelemetry } from "../kernel/run-telemetry.ts";
|
|
15
15
|
import { okid } from "../okid.ts";
|
|
16
|
-
import type { RunError, WideEvent } from "./types.ts";
|
|
16
|
+
import type { RunError, RunHttpFrame, WideEvent } from "./types.ts";
|
|
17
17
|
|
|
18
18
|
/** Inputs for {@link collectWideEvent}. */
|
|
19
19
|
export interface CollectWideEventInput {
|
|
@@ -42,6 +42,8 @@ export interface CollectWideEventInput {
|
|
|
42
42
|
readonly input?: unknown;
|
|
43
43
|
/** Flow return value when the run completed with an output. */
|
|
44
44
|
readonly output?: unknown;
|
|
45
|
+
/** HTTP wire frame when the trigger was `http`. */
|
|
46
|
+
readonly http?: RunHttpFrame;
|
|
45
47
|
/** Optional parent run id (causal chain). */
|
|
46
48
|
readonly parentId?: string;
|
|
47
49
|
/** Optional run id (generated when omitted). */
|
|
@@ -123,6 +125,7 @@ export function collectWideEvent(input: CollectWideEventInput): WideEvent {
|
|
|
123
125
|
error,
|
|
124
126
|
...(input.input !== undefined ? { input: input.input } : {}),
|
|
125
127
|
...(input.output !== undefined ? { output: input.output } : {}),
|
|
128
|
+
...(input.http !== undefined ? { http: input.http } : {}),
|
|
126
129
|
effects: [...input.ledger.entries],
|
|
127
130
|
logs: [...input.telemetry.logs],
|
|
128
131
|
durationMs,
|