# Changelog 0.22

## v0.22.0 — 2026-09-24

### ✨ Added

- HTTP idempotency. A mutating call can send `Idempotency-Key`. The server claims it after the gate and validation, replays the stored response, and holds a lease while `do` runs. Rows live in `oke_idempotency` on the journal driver until the TTL (default 24h). The client sends one key per non-GET call and surfaces `Idempotent-Replayed: true` as `meta.idempotentReplayed`. The key is 21 characters from `crypto.getRandomValues` through the okid alphabet, so a page on plain HTTP still sends one. If `crypto` is missing, the call sends no key and runs once.

- Client gzip budget is **6 kB** (was 5 kB). The growth is the idempotency feature, not waste: measured client gzip is 5194, which was over 90% of 5120.

- Effect inference follows a project helper whose parameter is named `fx`, and a same-function `fx.store` chain alias. `liveQuery` and `applySearchEmbedCdc` record their effects as package intrinsics. The shorter starter's link flows stamp the SQL, KV, and signal effects those helpers perform.

- Vault `audit.sink: "webhook"` POSTs each operation as secret-free JSON to `vault.audit.webhookUrl`. `oke vault audit` (list, verify, purge) stays on the SQL hash chain and refuses `stdout` / `webhook`.

- `otp({ mode: "provider" })` exposes `POST /auth/otp/resend` for the same SMS number. The provider issues a new code; `resendCooldownMs` applies. Another channel is rejected.

- Absolute budget samples that can still double and stay under their cap (cold start, and console initial load while it sits this far under 700 kB) fail CI at 2× the last committed value. The 75 ms cold-start cap stays. The probe keeps the best of five rounds and confirms a failure once, so one noisy run does not fail the build.

- The `okengine/http` static import graph fails CI when `zod` or the hybrid-search runtime is included. Forbidden modules live in one list, so the next heavy module is a single entry.

- Agent skill `oke-perf` — before a release, measure budgets, flag drift the gates still allow, bisect to the commit, and fix it with a lazy import, `createDebouncedRunner`, or a memo (`/oke-perf`).

### 💥 Breaking Changes

- With `retry` configured, a call retries when it is GET/QUERY or it carries an idempotency key (automatic on every non-GET call). `idempotencyKey: false` runs once unless that call passes `{ retry: true }`. A `409 IdempotencyInProgress` waits for `Retry-After` and retries within those attempts.

- `createTestApp` enforces Manifest tokens. Inline tests that are not an app pass `capability: "open"`. App suites pick the tokens up from the project root.

- Code that renames, destructures, or lets `fx` (or a chain alias) escape fails extract with **OKE1900**. Name the parameter `fx` and keep chain aliases in the same function. An explicit `effects` block must include every effect inference can see. It may add keys. It no longer replaces the inferred set.

- Store RLS emit calls `pgTable.withRLS` whenever `rls: true`. After upgrading, a command without a policy returns zero rows, with no error, and there is no runtime warning. Shorter adds an open SELECT policy in the same change.

### ♻️ Changed

#### Runtime

- Dependency refresh: Console and tooling patches and minors (`zod` `^4.6.5`, TanStack Query `^5.103.2`, Router `^1.170.39`, oxlint `^1.85.0`, motion `^13.4.2`, and the matching CodeMirror, DuckDB, and Happy DOM releases). Drizzle `rc5` moves to `1.0.0-rc.5-5935859` for both orm and kit. The SQL condition compiler accepts the new `and()` / `or()` wrappers (`"(("`, `") and ("`).

- `fx.fetch`, Meilisearch, and remote Vault HTTP abort with the ambient `fx.signal` (`fx.race` / `fx.all`). An effect that has not started throws `AbortError`. SQL, Redis, and an in-flight sently Channel send still finish.

#### Dev, Keel & create-oke

- Compose pins: Mailpit `v1.31.2`, PgDog `v0.1.59`, Meilisearch `v1.54`. A Meilisearch minor can need a dump and restore. create-oke, templates, and Keel follow the same zod / DuckDB / `@clack/prompts` bumps, and the same Drizzle `rc5` hash.

- okengine and create-oke are licensed under Apache-2.0.

#### Docs

- Docs site dependencies: `next` `16.3.6`, fumadocs `16.15.13` (core and `@fumadocs/base-ui` paired), `fumadocs-mdx` `15.4.3`, `lucide-react` `^1.48.0`. Tenancy and Reference sidebar icons use `Building` and `BookBookmark` (`Building2` and `BookMarked` left the Lucide icon map). Recipe and configuration examples use the new Mailpit, PgDog, and Meilisearch pins.

- Landing hero rail no longer shows the client gzip cell — cold start and routing p99 stay; package size stays in budgets / CI, not the stage strip.

- [Routing](/docs/understand/routing) moved from Flow into Understand (Architecture → Routing → Try it).

### 🐛 Fixed

#### Runtime

- Typecheck stays green on current Bun `fetch` (`preconnect`) and stricter `AstNode` narrowing in effect inference, vault audit tests, the idempotency client test, and OTP provider resend.

- Scaffold tests expect `@duckdb/node-api` `^1.5.5-r.5`. The competitor-name gate matches the router peer on a word boundary so `Honored` / `canHonor` are not hits. The JSR dry-run uses an installed Deno binary when one is on `PATH`, so the npm wrapper does not stall downloading its own.

#### Docs

- README quick start puts the health contract on `http.get({ out: HealthOut })`, matching the create-oke starter. `out` stays on the exposure; the Flow is `do`.
