0.23

Every published okengine release, newest first.

all series · 0.22

v0.23.0

✨ 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.