# Changelog 0.23

## v0.23.0 — 2026-09-25

### ✨ Added

#### Runtime

- Browser JSON page Authorization sits after Headers. Its summary switches No, Bearer, Basic, or API. The strip Auth and Headers tokens each open one row under that strip. The request rail Auth and Headers summaries are Inherit or Custom: Inherit uses the global credentials or headers, Custom sets them on that request. Custom header rows override global headers on the same name. Token, password, and API key value stay masked until the eye control reveals them, and a refresh or a closed tab drops those secrets.

- `prompt({ repair: 1 })` sends one follow-up when the answer misses `out`. That completion counts toward `maxCostPerCall` and is its own journal entry. `repair: 0` still throws `AiSchemaValidationError`.

- `ai.agent({ tools })` may include another agent. The child keeps its own step cap. Its cost cap is the smaller of its budget and the parent's remaining spend. Nesting stops at `maxDepth` (default 3). The child run records `parentRunId`.

- A tool on `ai.agent` may set `approval` and `gate`. The calling Flow must be `durable: true`. The run parks until `fx.agent.approve`, `fx.agent.deny`, or the timeout (default `24h`, then deny). The first resolution wins; a later one is HTTP 409. Approve can replace the tool args. Resume replays the tool output.

- `ai.decision` and `fx.decide` run one provider request for every question. `how` is `auto`, `reviewed`, or `abstained`. `audited` is a flag on an auto value, not a `how`. Review and `onUncertain: "abstain"` are exclusive. A review parks with no default timeout. An HTTP trigger cannot review. Autonomy comes only from `oke-decisions.lock.json` next to the app config. `oke decide promote` fetches the operator candidate. `oke eval --certify` builds a certificate from the seed file and does not run prompt evals. The OpenRouter decisions endpoint is alpha on the provider side.

- Drift is one flag per decision. Promote and recertify clear only that decision. The monitor emits the decisions that failed. Console reads that flag.

- `oke decide labels <name> --export` writes reviewed labels as seed JSONL for the caller's tenant, grouped by review id. Only declared `in` fields are copied. Secret and redacted fields are masked when the label is written. The command prints that the file contains production data.

- App MCP on `:6535` lists agent runs (`oke.ai.runs.list`, `oke.ai.runs.get` with the follow events), pending approvals (`oke.ai.approvals.list`), and decisions (`oke.decisions.list`: state, pending count, certificate metrics, drift). The scopes match the traces tools: one of `mcp:ai:read` or `mcp:decisions:read`, or `console:runs:read`. The token's tenant is the default. Listing another tenant, or every tenant, requires `console:*` or `mcp:*`. Run args, trail, approval args, decision inputs, and follow events mask PII and redacted secrets the same way traces do. There is no resolve or promote tool.

#### Docs

- `fx.run(..., { stream: true })` streams each model turn when the driver implements `stream`. `mock`, `anthropic`, and `openai-compatible` emit text deltas and tool-call argument deltas. `bedrock` and `vertex` stay reserved. The assembled turn is journaled, so a durable replay does not call the model. The default `threadId` is a unique id.

- `GET /agent/runs/:runId/events` resumes with `Last-Event-ID` under every gate on the Flow and the starting principal. An operator may follow. No gate runtime denies a gated run. An approval interrupt does not end the follow. The log lives on the journal driver, keyed by the agent run id. The header stores the agent and, for a nested run, `parentRunId`. The lease holder is the only writer. Past 5,000 rows, deltas stop and one `oke.events.truncated` event is stored. Structural events, interrupts, and the terminal frames stay. The scheduler deletes finished logs after 24 hours and closes an unfinished run after 7 days.

- A failed `fetch.preconnect` is ignored. `anthropic` and `openai-compatible` share that guard.

- `okengine/client/agent` follows a run with `Last-Event-ID`, and `approve` / `deny` retry `JournalLeaseBusy` and surface `Conflict`. `okengine/client-react` exports `useAgentRun`. Neither module is on the `okengine/client` graph.

- `ai.decision` score levels and a whole question held in a same-file const are checked at compile time. An unresolved name fails the build.

- Prompts document `repair: 1`: the follow-up counts toward `maxCostPerCall` and is its own journal entry.

- Agents document tool approval: `approval`, `gate`, `timeout`, and `durable: true`.

- Decisions name the experimental limits: a promoted lockfile applies on restart, one app-wide drift flag, compile-time checks that miss variables, and about 122 error-free labels per threshold at δ = 0.1.

- Decisions document the full loop: a real review gate, emit-then-decide, the `$` result, the learning → candidate → certified → suspended lifecycle, Console review, and `oke eval --certify` / `oke decide promote`.

- Decisions and agent events match the lockfile root, per-question abstain, the operator resolve route, the label counts, and a stream that ends with `[DONE]`.

- The MCP page lists `oke.ai.runs.list`, `oke.ai.runs.get`, `oke.ai.approvals.list`, and `oke.decisions.list`, and says resolve and promote stay human. Skills mentions the AI section of `AGENTS.md`.

#### Console — Flows & traces

- The trace sheet header shows the run id and, when the trace was called from another run, the parent id. Each id copies. Previous and next step through the current list from the open trace (filters included). Overview scrolls that row into view. Observability steps the newest-first buffer.

- Trace Request and Response Fields expand objects and arrays into nested fields. A collapsed row shows a short preview (`id: ENG-12`, `5 items`) instead of the raw JSON string. Array bodies use indexed rows.

- HTTP traces show the rest of the message beside the body: query, request headers, status, and response headers. Credential headers (`authorization`, `cookie`, `set-cookie`, token and secret names) are stored as `[redacted]`. Runs recorded before this change still show the body only. Console Call API invokes record the same frame (method, path, query, `accept` / `content-type`, and the response status) even though they do not arrive as a socket request.

- HTTP traces show who sent the request under the method line: device, platform, and browser or API client, with a version when the user-agent has one (`Chrome 131`, `curl 8.7`). Expanding the strip lists the client IP and the raw user-agent. A Console Call API invoke is labeled Console. Runs recorded before the stamp still show only the headers they stored.

- Flows has a Decisions link and a review queue at `/flows/decisions`. Each decision is `learning`, `candidate ready`, `certified`, or `suspended` from its own drift flag and the lockfile. A candidate row shows fit metrics and `oke decide promote <name>`. The queue shows age. The resolve form accepts only declared options and levels. An unanswered boolean is invalid. An audit row submits `labelOnly`. A second resolve is Conflict. A lease collision waits for `Retry-After`. Label and drift write failures are listed. The sidebar stays six modules.

- Ask, decide, and agent call rows in a trace link to the AI runs view or the decision. An `fx.run` or nested agent call that carries an agent run id opens that run. A row without an id still searches by name.

#### Console — Observability

- Observability has an AI tab. Agent runs show agent, status, stop reason (including error), steps, cost, tokens, thread id, and start. The detail shows the tool trail, the approver, denials, repair attempts, and child runs via `parentRunId`. Follow reads `GET /agent/runs/:runId/events` and resumes with `Last-Event-ID`. The agent and parent run id are on the follow-log header. When the in-memory ledger is on the app runtime, the trail is the journal approval step.

- The same tab lists pending tool approvals: agent, tool, args, age, gate, and tenant. Approve accepts optional edited JSON args. Deny takes a reason. The first resolve wins (`Conflict`). A held lease returns `JournalLeaseBusy` and `Retry-After`. The run detail links to the queue.

#### Dev, Keel & create-oke

- `AGENTS.md`, the `oke` skill, and the create-oke `AGENTS.md` state the AI rules: `fx.ask` / `fx.run` / `fx.decide`, durable tool approval, the follow stream, and lockfile autonomy. The certify command is `oke eval --certify`.

- When `OKE_CONSOLE_PROXY` is set, the Console dev server sends `/agent` to that kernel. `oke dev` still sends follow to the app on `:6530`.

- `dev:console` loads the kernel from source. Vite's config bundle was making Bun `import.meta.dir` point at a temp chunk, so the parked approval could not boot.

#### Console — Units & Call API

- Flows folders can be pinned. The count stays at the end of the row. Hover reveals a pin after the folder name, and a pinned folder keeps that pin. Pinning lifts the unit to the top of its trigger band and stays in this browser.

### ♻️ Changed

#### Dev, Keel & create-oke

- Turborepo runs the workspace tasks. `bun run typecheck` still checks the framework, the Console, create-oke, and Keel, now in parallel. Builds, lint, format check, and the framework test suite cache locally. Dev servers stay uncached. Package tags keep the docs site and the Keel example from being imported by the library or the CLI.

- `oke dev` reloads `oke-decisions.lock.json` when the file changes. Production reads the lockfile once, at boot.

#### Runtime

- Browser JSON page colors the status, latency, cache, and auth marks, and the request verb (GET green, POST sky, PUT amber, PATCH violet, DELETE rose). Params, Path, Body, Headers, Auth, and Cookies each carry a colored icon. Fields, JSON, Auth, and Headers on the strips do too.

- Browser JSON page puts Params, Path, Body, Headers, Auth, and Cookies above the route list. Cookies sits after Auth. Routes stays pinned to the bottom of the Request rail, and its header ends with a control that collapses the list downward.

- Browser JSON page opens the response as fields: status, response headers, and a body that expands objects and arrays. A collapsed row shows a short preview (`id: ENG-12`, `5 items`). Fields and JSON sit on the response header. JSON swaps only the body; status and response headers stay. Credential response headers render as `[redacted]`.

- `fx.run` accepts `{ messages }` (user, assistant, and tool turns) as well as one `message`. Passing both throws. Thread storage stays in the app.

- `fx.run(agent, input, { stream: true })` and tool-using `fx.ask(..., { stream: true })` yield AG-UI events. Pipe them through `fx.json.stream`. Parse with `okengine/client/agent` (not the core client). Subagent notices are `CUSTOM`.

- `fx.run` reports `stopReason`: `completed`, `max_steps`, `budget`, `denied`, `aborted`, or `error`. Hitting `maxSteps` is not a normal completion. `denied` is set only when a deny ends the run. A thrown tool is `error` and still rejects.

- `fx.run` stops when `budget.maxCostPerRun` is reached and returns the partial result. A tool-less `fx.ask` still throws `AiBudgetExceededError` at `maxCostPerCall`.

- Kernel edge gzip is 13590 bytes (was 13289). Stored-row SSE ids are read on the response encoder. The 17 kB cap is unchanged. Client gzip stays 5218.

- Kernel edge gzip is 13289 bytes (was 13132). The `decide` effect kind and SQL pool / replica binding sit on the edge graph. The 17 kB cap is unchanged. Client gzip stays 5218.

- Repeated identical `fx.ask` calls outside a durable run reach the model. Replay stays on that run's journal. Console keeps the newest 500 ask journal entries and agent runs.

- Primary-key `upsert()` on postgres and pglite is one `INSERT … ON CONFLICT`. An update still records the row before and after. A match on any other column, and the memory driver, still reads and then writes.

#### Console — Flows & traces

- Long traces group the waterfall and event list by kind and resource. A run with hundreds of repeated sends and writes shows one lane per operation, with every occurrence on that timeline, instead of one empty track per effect. Shorter runs still list each effect. Expanding a lane opens the individual events.

#### Dev, Keel & create-oke

- Default `store.kv` image is Dragonfly 2.0 (`docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0`, was `redis:8-alpine`). Driver id stays `redis`. Pin `redis:8-alpine` to keep Redis Open Source. The recipe sets `memlock: -1` and does not pass `--maxmemory-policy`.

#### Console — Units & Call API

- Call API is a full-height rail on the right of the flow contract. The contract keeps the main pane. Drag the split to resize the rail. The contract header ends with the same collapse control as the flows tree.

### 🐛 Fixed

#### Runtime

- A streamed `fx.run` resumes a parked tool approval. The ask is ledgered when the run id is known, and the tool steps stay on the journal so replay runs the tool.

- A stored agent event that fails once is retried. The first failure is still recorded on the run, and the stream ends.

- `journal:runs` on `effects.writes` does not open a store. Unknown, other, and none client facts stay off the trace request card.

- Memory SQL accepts `CREATE INDEX IF NOT EXISTS`, so identity tables can be ensured on the in-process driver.

- HTTP cold start no longer evaluates `fx`, boot, or the decision modules. Those load on first use, and `okengine/http` rejects a static `fx` import. The follow log slot stays a tiny module so the AI runtime and the approval route share one log.

- A failed agent-event append does not consume a seq. It retries once. If it still fails, the next stored row is `oke.events.gap`.

- A sweep claim expires after 30 seconds on the memory, file, and Postgres event stores, so another instance can close a run whose sweeper stopped.

- Postgres truncation looks at `event_type` and `event_name`, not a `LIKE` over the payload.

#### Console — Units & Call API

- Call API query and body fields drop a column when the rail is too narrow for a name and its type hint. The last column sits flush with the right edge.

#### Console — Store

- Store → Performance creates `pg_stat_statements` with the initial oke SQL (`oke db push` and the first SQL session). A missing view is no longer reported as an unsupported driver. Preload is still required. Memory, PGlite, and Cockroach still say telemetry is unavailable.

- Store → Performance no longer prints `[object Object]` when Postgres throws a plain object. A missing `pg_stat_statements` relation stays createable. Memory, PGlite, and Cockroach still say telemetry is unavailable, and the sentence names the driver.

#### Console — Flows & traces

- The flows page links to Decisions.

#### Runtime

- A failed agent-event append is recorded on the run and does not stop the stream. The stream still ends with a terminal frame and `data: [DONE]`. Resuming a parked run re-reads the highest seq, including when the same process opens it again. Past the cap, a new process keeps the truncation marker and does not store more deltas. One scheduler claims the close of an abandoned run. The file journal is single-process and is not for large logs.

- Live agent SSE frames for stored rows send `id:` as that row's seq, so `useAgentRun` does not repeat text after an approval.

- Agent run events append in emit order on one queue per run. A repeated seq is an error. Followers read `seq` greater than the last id. The file journal appends one JSONL file per run. `flow.retry` keeps the agent run id. `useAgentRun` follows a run once; approve and deny do not open another follow, and text is not replayed.

- `openai-compatible` streams send `response_format` the same way `complete` does. A tool error's `RUN_FINISHED` keeps the run's `threadId`.

- Decision label export groups rows that have no review id by decision, non-secret input, reviewer, and time. A file journal copies a legacy `*` drift flag onto every declared decision. The journal review record masks secret and redacted fields; the reviewer still sees the other input.

- Browser JSON page replays a direct visit that came back `401` when a Bearer, Basic, or API key is already stored. The address-bar load cannot send that secret; the replay uses the same headers as Send, once per credential set, so a rejected token stays on screen.

- A streamed agent approval inside a durable Flow ends the run as `sleeping` with its wake time. The interrupt frame and `data: [DONE]` go out first. Resume stores the agent result on that run. A tool error puts its message on `RUN_FINISHED.result.error`. `AiDurableRequiredError` ends the stream with `RUN_ERROR` and `data: [DONE]`.

- OpenRouter resolves `typesafe/jev-1.13` to `typesafe/jev-1.13-20260917`. A score answer's `probabilities` are keyed by index, and `legend` names each index. Those indexes map onto the declared levels.

- Browser JSON page Auth keeps the token, password, and API key value when the Auth row closes. A refresh or a closed tab still drops those secrets.

- The compiler writes `autonomy` (`maxError`, `audit`, `risk`) and `evals` onto `manifest.ai.decisions`. `oke eval --certify` loads the app entry, then writes `oke-decisions.lock.json` under `OKE_ROOT_DIR` (the same root as `oke decide promote`).

- Two decision resolves take the journal lease and compare-and-swap the review. The first writer wins. The other gets `JournalLeaseBusy`. A candidate is stored on the journal driver, so another instance can serve it after a restart.

- Abstain drops only the uncertain questions. A certain question in the same call stays `auto`. A reviewer may submit `none_of_these`; the Flow receives that value.

- An approval interrupt and a tool error close the agent stream with the final frame and `data: [DONE]`.

- A decision label keeps the time it was written. A failed label write stays on the trace and the decisions page, and the next write still runs. Postgres matches a null tenant with `IS NOT DISTINCT FROM`. An older drift table gains `certified_at` when the store opens.

- A Bearer API key created in Console verifies on the app. `oke dev` stores one `OKE_AUTH_SECRET` in `.env.local` and gives it to Console and the app, and `gate.auth` uses that value instead of minting a different secret per process. An `oke_` secret missing from memory reloads `oke_api_keys` before the 401. Access refresh reads that table too, so Calls updates after the request. Keel turns on `gate.auth` with `http: false` so member gates accept that key.

- Learn-then-Test certifies a threshold only when the error count is low under `maxError`. Choice and score candidates read the stored label distribution. The postgres label store throws if it cannot open or query, and candidate and drift jobs read labels at run time. One failed label insert does not drop the next write. A drift flag stores the certificate time it was raised against; a newer lockfile certificate ignores it. Boot fails when decisions are declared and `rootDir` / `OKE_ROOT_DIR` is unset. `review: someVar` compiles to the gate's declared name. Choice options must be an object literal.

- `fx.decide` reads the provider key through the non-journaling secret path, so a replay sees a rotated key and the journal does not store it. Learn-then-Test uses a fixed threshold grid and a Bonferroni exact binomial test. Audit drift counts only labels for the pinned model since the certificate. Labels and the drift flag live on the journal driver, created only when the app declares decisions. The lockfile loads from the app root. `oke eval --certify` uses the app provider and pins the resolved model version. An operator resolves every tenant; a tenant reviewer sees only their own. `Retry-After` above 60 seconds takes the outage path.

- `fx.decide` journals the projected answer and reads the resolved review once after wake. A second call in the same run gets its own review id. `none_of_these` is not auto. A boolean value follows the calibrated probability. Pending reviews stamp `fx.tenant`. A missing provider secret is a config error, resolved through the secret capability.

- Resolving a decision checks the declared review Gate. The reviewer is the authenticated operator, and every open question must be a valid option. The tenant must match the parked row. Uncertain reviews record propensity 1; audit rows record the audit rate.

- `oke eval --certify` calls the provider, fits a calibrator per question, and keeps a threshold only when an exact binomial Learn-then-Test passes. The lockfile is loaded from the app root at boot. `oke decide promote` merges that one decision into the existing lockfile.

- Decision labels live in a tenant-scoped table at the app root. The candidate job reads every tenant. Drift is a one-sided binomial on persisted audit labels and writes one app-level suspension flag. A change emits `oke/decision/drift`.

- The decision candidate route requires an operator gate and is bound before the HTTP gate posture check. An unauthenticated request is 401 or 403.

- Decision HTTP retries only 429 and 529, and honors `Retry-After`. The breaker counts network errors, timeouts, 5xx, and 529. A 4xx request error and an abort leave it closed.

- Compiling a decision rejects more than 254 choice options, an author `none_of_these`, a score outside 2–10 levels, and a duplicate name. The review gate keeps its real name.

- Budget measurement spawns `process.execPath` instead of a `bun` binary on `PATH`.

- `drivers.store.sql` `pool.max` sizes the Bun.SQL pool. Unset stays 8. A different max does not reuse a pool opened at another size. `pool.min` is not applied. `replicas` receive read-only flows, each on its own pool at that max.

- Agent tool approval resolves under the journal lease (`get` one run, compare-and-set, `put`). Two instances cannot both win, and the winning write keeps the run's other entries. The approval id is `${runId}.${toolCallId}` (base64url). A finished approval is `Conflict` (HTTP 409, no retry). A held lease is `JournalLeaseBusy` (HTTP 409) with `Retry-After`; the client waits and retries that code only.

#### Console — Flows & traces

- The decisions page lists a failed label write with the decision, question, and error. The review queue is unchanged.

- HTTP traces omit the Body panel when the stored payload is empty (`{}`, `[]`, or nothing). Query, headers, status, and a body that has fields stay.

#### Console — Units & Call API

- A pinned Flows folder keeps its flow count at the end of the row. The pin sits after the folder name and no longer makes that row taller.

#### Dev, Keel & create-oke

- create-oke's minimal Docker stub escapes the Dragonfly `--maxmemory` default, so the scaffold file parses. The command still passes `${OKE_STORE_KV_MAXMEMORY:-0}`.

- `bun test` keeps Playwright specs, starter templates, examples, the site, and `*.live.test.ts` out of the default suite. A CLI `--path-ignore-patterns` flag replaces that list, so the framework script no longer passes one.

- Client gzip is one measure. `bun test` was building with `NODE_ENV=test` (~5323) while `budgets.json` recorded the production build (5194). The probe now spawns with `NODE_ENV=production`.

- `oke dev` soft reload disposes the previous app generation. Each save was leaving that boot's scheduler and Bun.SQL pool open, so one process held dozens of connections against PgDog's pool of 20. Console login and clock flows then waited on checkout (tens of seconds). Restart `oke dev` once to drop pools leaked before this fix.

- `oke dev` secret prompt keeps the masked API key on one line. Each `*` was sent through CLI chrome, so every character printed as its own `│` row. The status board also stays still while that prompt owns the cursor.
