Skip to content

v0.17.0

Choose a tag to compare

@wajihtba wajihtba released this 17 Oct 16:47
· 239 commits to main since this release

Added

  • otel, obsdb, Studio: the slog bridge (U11.1; UX-E-07). otel.Slog(), an Install option, wraps slog.Default (restored at shutdown, with the log package's writer and flags; a program on slog's built-in handler keeps its lines on the log package's writer through a slog.TextHandler at the level slog.SetLogLoggerLevel set when Install ran — read once — each line after log.Prefix(), in its key=value format: the built-in handler cannot be wrapped; one Slog pipeline at a time, and an out-of-order shutdown of two restores what the first found, never a closed bridge's handler); otel.SlogHandler(next) bridges any handler and follows the installed pipeline; Pipeline.SlogHandler(next) binds one pipeline. Every line logged on a span-carrying context — weft.Logger's Debug run lines, mw.Log, mw.Audit, the app's own — becomes an app log record (scope github.com/weftgo/weft/otel/slog, the context's trace and span ids, severity level + 9, the slog level's name, attributes flattened with groups joined by ., every app key starting weft. renamed slog.weft.…, past 120 attributes the rest counted in weft.log.attrs_dropped); a line with no span is not bridged, nor the pipeline's own WARNs. New per-destination otel.AppLogs(on): on by default for Local and Studio with content on, off for Datadog, Langfuse, OTLP and Exporters unless opted in. It admits, never bypasses the destination's content policy: an app log line is content — a content-on destination applies Redact (new kind otel.ContentLog) and MaxBytes (default 32 KiB, weft.content.truncated_bytes) to the message and each string attribute and drops the line when Redact panics; a content-off one that opted in gets it stripped (message and string values emptied, weft.content=stripped, time, severity and span kept). An agent's core.Content(false) reaches its own lines first: core.Logger's tool call line names an error result's size (err_bytes) instead of its text, and mw.Audit's err_bytes and the *core.ToolError's code instead of err and cause (a child run without Content(false) logs them again). Bridged lines are shed with the deltas under a full queue. While a bridge is live — a Pipeline.SlogHandler, Slog's wrap while it is still slog.Default's handler (a line logged through it does not keep the pipeline marked once the program replaced the default), or any other handler that has teed a line into the pipeline (building a package-level SlogHandler marks nothing) — heartbeats and each run's invoke_agent span carry weft.logs.bridged (the lines bridged so far), on the destinations that take the lines only (a content-off sink, an AppLogs(false) one or a third party not opted in stores the root unmarked, so its empty page carries the hint); it is not metadata — obsdb.MetaOf skips it, ClickHouse migration 0021 restates both run views. obsdb.LogPage.NoBridge: a run whose root is stored without the mark and has no attributed line, no gap and no cut; Studio's GET /api/runs/{id}/logs then holds a not_recorded hint — "no app logs: no weft slog bridge was recorded for this run … if the app bridges slog itself, it logged nothing under this run", the fix naming otel.Slog() — instead of an empty page that reads "logged nothing". examples/studio-local installs it and wraps its model with mw.Log(nil): its lines arrive under each step's chat span (TestModelLogLinesArriveUnderTheStep). Settings' app-log line names otel.Slog(). Record contract §3, §4 (app log records), §5, §7; contract fixtures slog_bridged, bridge_overrides (a redacted line, weft.log.attrs_dropped) and bridge_stripped (a content-off destination's stripped line).

  • Studio, panel: a run's app logs on both surfaces (U11.2; UX-E-06, UX-A-07, UX-S2-03). The run page gains a logs view (?view=logs, key l; RunView gains "logs"): every line GET /api/runs/{id}/logs attributes to the run, a severity filter (?lsev=info|warn|error, RunSearch.lsev, the route's severity=), each line's offset from the run's start, severity, body and attributes, and jump to span — ?sel=t:<span id>&tab=logs, the run-tree row that span is behind, or (resolveRowSel / expandFor, an optional span-parents map) its nearest drawn ancestor's. A tree row a WARN / ERROR line landed on carries a tick ("E1 W2", read at severity=warn only when the run document's log_warn / log_error count such lines; "≥E1 W2" and "at least …" over a cut read); its click opens the row's drawer Logs tab. The drawer's Logs tab and the view read every page (src/lib/logs.ts's fetchAllLogs / allLogsQuery, 1,000 a page up to 10,000 lines; lib/api.ts's single-page logsQuery is gone) and badge a longer list truncated ("the first 10,000 lines are shown … nothing was lost"); while the run runs they are read on from the line after the last held, every 5 s, show the page's partial_reason, and stop once the read is cut; a finished run's read is whole, once; a row with no line yet says "this row's spans are not reported yet", never "no app log line". The panel's Story step heads carry "📄 ⤢" under capability logs for no token or the dev token (the logs route refuses every panel token): Studio's run page on that step's row with its Logs tab open (lib/links.ts's logsLink; runLink gains tab); the panel reads no line.

  • Studio, weft studio, weft dev, weft doctor: a server log of refused ingests and 5xx (U11.2; UX-E-37, audit row D10). Every response carries X-Request-Id (the client's well-formed one echoed, else generated; in Access-Control-Expose-Headers); a 5xx error body carries error.request_id. slog.Default gets one WARN line per refused ingest batch (studio: ingest batch refused: request id, status, path, remote, code, reason, bytes), at most one a minute per (remote, route class — /v1/traces, /v1/logs, else /v1/* — code, status), the next one counting the rest as suppressed; a full limiter (1,024 keys) drops expired windows first (their counts on the next line, suppressed_expired) and otherwise logs no new key until a window turns (suppressed_overflow) — behind a reverse proxy every client shares the proxy's remote, documented. One ERROR line per 5xx (studio: request failed, same fields; a panel token's masked body still logs the real error as reason) and per handler panic (studio: handler panicked, then the panic goes on; http.ErrAbortHandler is not logged). The request wrapper is an http.Flusher only when the connection's writer is. weft studio / weft dev --log-level debug|info|warn|error (WEFT_LOG_LEVEL) and --log-format text|json (WEFT_LOG_FORMAT) set the server log's handler on stderr (a level alone installs a text handler; neither leaves Go's default). /api/meta's ingest gains accepted / refused (OTLP POSTs answered 2xx / 4xx–5xx since start, in memory) and last_error / last_at (the last refusal, omitted before one; a refused Content-Encoding is echoed at most 64 bytes, in the body and the log line alike); weft doctor's ingest line prints them — "N OTLP batches accepted, M refused" — and the last refusal with its time and a fix, a warning while that refusal is within the hour.

  • obsdb, Studio: the run tree (U9.1; UX-A-01, UX-E-22, UX-E-34, UX-E-24). GET /api/runs/{id}/spans?tree=1 (capability spans_tree) answers the run's spans and every descendant run's — the runs whose parent chain reaches it (a run row's parent_run_id, which a span's weft.parent.run.id fills, so a running child's finished spans count; a stored parent always wins) and, for a run no row or span names a parent of, every run core's exact child id shape names a descendant (<id>/<digits>/<call> or <id>/resume/<call>, obsdb.TreeChildID: a child whose run_start was lost is still placed, under its nearest ancestor that is a member of the tree — an app's own job/1 is no member, so job/1/0/c under job hangs under job, alike on both backends; job/1 and job/1/retry/2 are not each other's tree) — found from the run rows first and read by run id in any trace (a child stored under another trace is in it), never by reading the run's whole trace, in start order, each keyed by run_id + span_id with its run's parent_run_id, at most 10,000 spans (obsdb.TreeSpanCap) of at most 10,000 runs (obsdb.TreeRowCap); a sibling run of the same app trace and a run-less span are not in it. A derived run's tree is cut from its trace before the span cap, the trace read itself capped at 50,000 spans (obsdb.TreeTraceCap, RunTree.TraceTruncated). Its holes (always a list): truncated, cause response_cap, past any cap; not_recorded, cause no_spans, naming (at most 100 (obsdb.TreeMissingCap), with a count) the finished descendant runs with a row and no span — a running child with no span yet is not missing (TreeRun.Status); hidden, with a count (RunTree.HiddenCount, subtrees included), when a panel token's scope left runs out — a panel token reads the run's own spans and its public id's descendants, every other descendant (a nested Generate or thread turn inside a tool handler, a child whose metadata cleared weft.public_id) left out with its whole subtree and unnamed before the cap (obsdb.TreeQuery.PublicID; hidden runs are not read, never take a visible span's place), never a 403 for the tree. Every /spans answer carries run_id (the run's); the TS mirror is Span.run_id? / parent_run_id?. New optional obsdb.TreeReader (RunTreeSpans(ctx, obsdb.TreeQuery) → obsdb.RunTree{Spans, Truncated, TraceTruncated, Missing, MissingCount, Hidden, HiddenCount}; ChildRuns, at most obsdb.ChildRunsCap (10,000) rows a call, past it obsdb.ErrChildRunsCapped), obsdb.TreeQuery, obsdb.TreeSpan, obsdb.TreeRun, obsdb.TreeIDRule, obsdb.TreeChildID, obsdb.TreeMembers, obsdb.TreeMemberIDs, obsdb.TreeReadIDs, obsdb.CutRunTree, obsdb.CapTree, implemented by both backends: SQLite finds the rows in one recursive statement (runs_started, runs' primary key for the id rule, its rows that name no parent kept only in core's child shape) and reads the spans on spans_run; migration 0020 adds spans_trace_start (a trace in start order, no sort: Trace and a derived run's tree); ClickHouse finds the rows on weft_runs' primary key and migration 0020's idx_parent_run bloom filter on weft_runs.ParentRunID (materialized in the background), and reads the spans on idx_weft_run_id. A database without a TreeReader leaves spans_tree off (capabilities_off says why) and answers ?tree=1 with 501. Record contract §6 "A run tree"; the subagent fixture pins the parent's tree, studio/testdata/api/spans-tree.golden.json the answer and its TS mirror.

  • Studio: the child subtree in place, the graph scale and the trace link (U9.2; UX-A-01, UX-D-13, UX-A-17, UX-S3-14). On /runs/$id a child run row is one collapsed row with "open run ↗", and its chevron (→/←) expands its steps, model calls and tools in place from the same ?tree=1 spans, grandchildren again one collapsed row each, drawn on expand (an expanded child's own ?tree=1 is read only when the tree's truncated hole says it was cut). An expanded child's document and stream are read again only while it or the run runs, its stream followed from the position past the last one held by next_after (childStreamQuery, up to 20,000 events, then a truncated badge on its row; from the start again only when that read held gaps) and its gaps a gap badge. A running child row carries no loss badge (spans export when they end); a finished one's not_recorded badge reads the server's reason. A ?sel= naming a row inside a child — its rowKeys run-qualified key, or <child run>:<span id> — expands the children above it and selects it (a row no child draws selects the child's row); folding a child that holds the selection selects the child's row (lib/run-tree.ts: expandFor, ALL_RUNS, qualifyKey; the implied expansion rebuilt when the selection, the scale or the known runs change, never per event batch). The tree's holes (SpansDoc.holes, the TS mirror of treeSpansDoc: TreeHole with cause, runs and count) are badges above it, not_recorded / no_spans also on the child rows it names. "whole trace ↗" links /traces/<trace id> with the selection kept, the run's own keys run-qualified. ?scale=graph (RunScale gains "graph"; src/lib/graph.ts callGraph, components/studio/call-graph.tsx, inline SVG) draws agents and MCP servers as nodes in call-depth columns, edges with their call count and slowest call (not a p50), a self-call as a loop over its node, a resumed call's child under its parent (<parent>/resume/<call>), "MCP calls of N child runs not shown" when the page did not read their catalogs, each node with a focus ring and its accessible name starting with its visible words, the edge hint on the edge group's <title>, the selection shared with the tree; pool children (separate sessions) are not drawn, and nodes carry tokens when the spans do, no cost. Studio only: the panel's Timeline draws one level (U8.3).

  • obsdb, Studio: the live lane's attempt frame (H9, audit row O17; UX-E-30). GET /api/live?kinds=…,attempt streams event: attempt {run_id, session_id, public_id, step, attempt, layer?, status ok|error, error_type?, provider?, model?, start, end, wait_ms?, retry_after_ms?, trace_id, span_id} — one try inside a model call, read from its attempt span the moment the span lands (OTLP ingest, and a self-publishing database's write; deduped on run, step and attempt), so a step that is retrying or falling back is seen before its chat span ends; wait_ms is the wait before the try (weft.attempt.wait_ms, the step route's attempts[].wait_ms), retry_after_ms the provider's retry-after ask, each absent when not recorded. Live only: never backfilled, nothing new is stored; each attempt frame is held to a panel token's public id. New obsdb.FrameAttempt, obsdb.Frame.Span, obsdb.AttemptFrame; lib/live.ts's openLive gains a frames option (readers of other SSE event names) and src/lib/live-attempts.ts reads the attempt frames. The Live page asks for them: a failed try reads "↻ attempt 1 failed" under its step while the run runs (the error type and the provider's ask in its title); an earlier step's note is dropped.

  • Studio: the runs list's delegation shape (H9; UX-A-19). Every priced run row carries children_count / children_failed (the runs whose parent it is, one level deep, and the failed ones — counted from the children read the cost already makes, one grouped read per level of a page, never a read per row; omitted when 0) and children_unknown when that read failed or passed its bound (10,000 child rows a request; the cost under it reads not known too), drawn "⑂ ?"; /api/sessions/{id}'s turn rows count every parent-linked run. The row draws "⑂ n" (the failed count in red), a disclosure that reads ?parent=<id> and draws the child runs under it, indented, each with its own chip (grandchildren read when opened; past the 100 read, "+N more · open the list"); "with child runs" draws the loaded rows as trees, each child under its parent (src/lib/delegation.ts's nest). /compare opens with a run picker (src/lib/compare-picker.ts: the newest runs as a base, then the base's replay source, the base's conversation (same public id) and its agent's runs, with a find box — id prefix or agent, past the newest 50) and b=r1,r2 reads like the array form; ?b= keeps a JSON array's elements whole.

  • Studio, panel: step and tool cards tell the whole truth (U25.2, U25.3; UX-S1-03, UX-S1-06, UX-S2-02, UX-S2-06, UX-S2-09, UX-S3-12, UX-S3-13, UX-S3-15, UX-H-18, UX-S5b-03, UX-S1-13, UX-E-14). One shared module, src/lib/cards.ts, words what both surfaces draw (the run page's story and the run tree's drawer read the same RunCards): a max_tokens step's call the loop never ran (rule 11) is placed on its step from the transcript (lib/events.ts applyTranscript, FoldedToolCall.unrun) with its args as sent ({"msg":"never) and the "call never executed" badge, drawn "not executed" in the run tree and the panel's Timeline (no span by design, never a not_recorded hole) and counted "1 not executed" in the answer's tools line; an OUTPUT card on the run header (the last valid submit_output call's JSON, "typed output (submit_output)", "repaired after N invalid submissions"; "no output submitted" only when the root's weft.output.submitted is false — a submission whose arguments the record lacks, content off or a read token, reads "typed output (submit_output)" with the stripped badge) and the panel's typed-output chip; the Request pane's catalog marks the output tool; attempt rows name their HTTP status, the provider's cause and the wait before them ("503 · Service Unavailable: overloaded · waited 1 ms" — GET /api/runs/{id}/steps/{n} attempts[] gains http_status, wait_ms, error), the panel's attempts picker too (attemptTag); a tool card's execution line ("12 ms / 50 ms limit · queued 3 ms", its origin when not local, callFactsOf over the execute_tool spans); a parked call reads "awaiting a decision" (or "· ParkOn", "· ParkAllExcept", "· side effects", "· middleware"), a ParkSource park "paused at breakpoint" on a playground run or when the run page knows its service's runtime registered breakpoints_own, else "paused by ParkSource" (ADR 0030) — never "never completed", its "finished at" fact the same word, and the answer's tools line counts it "N awaiting a decision"; "overridden by experiment" on the first step's Request pane for any weft.override.* knob, its hover naming them (lib/cards.ts paramsOverride reads every stored weft.override.params shape); a "PrepareStep: system, tool_choice, params" chip from the request record's prepared; the thinking row reads "low → reasoning_effort" (thinking.wire); the prompt's file parts as attachments ("file image/png · url", the request's message lines too); the header's own / delegated token split ("60 in = 20 own + 40 via 1 child"; the panel's turn line), counting the core's <id>/… Subagent children only (a pool child session or a nested Generate is not delegated); a Subagent call's typed outcome ("child failed", "child awaiting a decision · refund_order", "cycle refused", "child timed out · 2.0 s limit"), kept in the drawer's call rows with the tool limit. Proven on the named seeded run shapes recorded through the real pipeline (studio/cards_test.go → studio/testdata/api/cards.golden.json; routes/run-cards.test.tsx, panel/cards.test.ts, lib/cards.test.ts, parity).

  • core, mw, adapters, runtime, obsdb: the run cards' facts on the record (U25.1; O6, O9, O16; UX-S2-02, UX-S2-06, UX-S2-09, UX-S3-15, UX-H-18, UX-S1-06, UX-E-14). core.AttemptInfo gains Wait and HTTPStatus: the attempt span carries weft.attempt.wait_ms (the backoff slept before the try, rounded up, never 0 — mw.Retry reports it; through a Fallback beneath it on the primary's next request only, and under Retry(Fallback(Retry(x))) the inner Retry's first try keeps the outer backoff; a Fallback's backups carry none) and http.response.status_code (mw.Retry and mw.Fallback read it off the error with mw.HTTPStatus). The execute_tool span carries weft.tool.timeout_ms (the deadline in force, rounded up: a Timeout(500µs) reads 1), weft.tool.queue_ms (a call that actually waited, behind a Sequential barrier or for a Parallelism slot), weft.tool.pending.source beside weft.tool.pending (approval | park_on | park_source | park_all_except | middleware — a middleware or the handler returning ErrApprovalRequired: an agent ParkSource — runtime.Breakpoints() — is no longer indistinguishable from ParkOn) and, on SUBAGENT_PENDING, weft.subagent.pending (the parked child's calls by tool name). An Output agent's invoke_agent span carries weft.output.submitted (false is the run GenerateAs answers ErrNoOutput), and the tools record marks submit_output output: true. The request record gains prepared (O9: the parts the PrepareStep chain changed — system, messages, tools, tool_choice, params, thinking, sequential_tools; names only) and thinking.wire (O16: how the adapter sends the level — a model declares it with the optional ThinkingWire(core.ThinkingConfig) string method, found along the Unwrap chain; openai reasoning_effort | thinking | none by dialect, anthropic thinking.adaptive | thinking.budget_tokens | thinking.disabled, google thinking_level | thinking_budget), both derived inside the request kind's gate: a run whose destinations want no request record compares no request and asks no ThinkingWire (ADR 0016; TestRequestFactsOffComputeNothing). weft/runtime names a playground run's breakpoints in its weft.playground.breakpoints metadata (record contract §2). The new span keys are contract, not metadata: obsdb.MetaOf drops them and ClickHouse migration 0022 restates both run views. ADR 0013 and ADR 0028 amended (the optional ThinkingWire method; prepared[], thinking.wire, the tools record's output); core.ThinkingConfig's godoc names the convention. Record contract §2, §3, §4, §10 (new conformance cases run_cards, queue_breakpoints — weft.tool.queue_ms and weft.playground.breakpoints — and bridge_overrides — weft.override.params' null shape and weft.override.tool_choice; parked, approval_denied, resumed gain pending_source, compacted its request's prepared, retry_fallback its attempts' wait_ms).

  • Studio: the API's run verbs on the run page (U26; UX-A-15, UX-S1-04). The header offers export json · jsonl · otlp beside save as test (capability export, downloaded with the bearer; GET /api/runs/{id}/export serves json, jsonl, otlp and wefttest only — xml is a 400 and is not offered; a read-scoped token gets json and jsonl) and delete run… (capability delete): an in-page confirm saying the run goes with every child run under it at any depth ("N known", "N+ known" past the children read) and with the derived runs read from its tools' spans, and that the rows leave Studio's database only, then DELETE /api/runs/{id} and the runs list (deleting a child reads its parent's document again); where the server would refuse it (a panel token, a non-loopback Host without the server token — meta live_all — or the capability off) one faint line says why instead. The Agents card shows stop_when and usage_limit chips and titles every policy chip with what it does and the option that set it (src/lib/policy.ts; the cap reads "cap 64 KiB").

  • obsdb, Studio: obsdb.RunQuery.IDPrefix / obsdb.SearchQuery.IDPrefix and GET /api/runs?id_prefix= (U26; UX-A-13): the runs whose id starts with the text, byte for byte (no wildcard, no case folding; a prefix ending inside a UTF-8 sequence matches), on both backends — SQLite as a closed primary-key range. The ⌘K palette looks a pasted id up with it (all=1, children included; a panel token finds its own public id's runs only), a single word also as a public id (an app-chosen one like arena:run-1:rw.deep-delegation, not only pub_…), a key=value text by tag.key= (a bare word says tags are matched as key=value), and ranks those runs first under "Exact match": Enter opens the first, and nothing when no exact match answers until the selection is moved.

  • Studio: the polish batch U29.2. GET /api/runs/{id}/export?format=json|jsonl|otlp&tree=1 exports a delegation whole (UX-A-31): every subagent run under the run, breadth first, at most 200, each its own export — json a subagents list beside subagent_holes (truncated/response_cap past the cap, hidden with a count for the runs a panel token's public id does not reach, left out with their subtrees), jsonl each run's lines after its own run record then subagent_hole lines, otlp every run in the same two requests (re-ingests as the tree); the file is <run>.tree.<ext>; wefttest with tree is a 400 (its replay substitutes the subagents' recorded answers); the run header offers "with child runs" beside the formats on a run with children. /api/traces rows carry root_run {id, agent, prompt_preview, status} (read by its id, however many runs the trace holds) and failed_runs (failed_runs_capped when that count stopped at 500: a floor) (UX-A-39): the trace list titles a row "agent · prompt" and its runs cell reads "3 runs · 1 failed", one link to the top run, never the hex id list. /api/runtimes carries breakpoints_set_at (in memory: when this Studio delivered the set) (UX-A-33): Settings says when the set was set, or that it came from before this Studio started, warns when every tool is a breakpoint, and "clear all" PUTs an empty set. Settings has a section index — sticky beside the sections on a wide window, a row above them on a phone (UX-A-32; lib/settings.ts's SETTINGS_SECTIONS). Every page's first tab stop is "Skip to content", landing past the runs list's filter bar (UX-F-25). Under ?sort=cost the runs list says how many rows have no known cost and that they are listed last, newest first, not sorted (UX-A-35; lib/cost.ts's costSortNote). One not-found state (UX-F-31; lib/not-found.ts, components/studio/not-found.tsx): a run, session, experiment or trace 404 and an unknown page read "No in this Studio" with the way back, a run's with "search for it" (the ⌘K palette with the id typed in), and no retry.

  • Studio: the polish batch U29.3 (the playground, the replay drawer, the experiment form, toasts). A bare /playground over several registered agents asks instead of taking the first one (UX-C-35): "Pick an agent (N registered on M runtimes) or paste a run id in Source run", each agent one click, the five latest failed runs as one-click sources. A rejected or lost replay command offers "send again" (a new command; held by the form's own rules) (UX-C-31). A kept tool result's edit field is a textarea labelled by its call — lookup_order(order=42) · c1, so parallel calls read apart — with "start from recorded", "reset to recorded" and a size line (UX-C-25), and "edit this result" focuses that step's field even when call ids repeat across steps (UX-C-24). The replay drawer holds back its command's finish toast while it shows the outcome (UX-C-32; lib/notify.ts's quietCommand). The playground card's "save as fixture" (one download per fixture file) is the run page's "save as test" (one wefttest zip), the panel's result pane hands off to it too (UX-C-29). The preview's prepare_step warning is said only when a request record of the source names what a PrepareStep changed (the parts listed), or none was recorded; new warnings catalog (the registered tools differ from the recorded step's) and agent (the replay's agent is not the source run's) (UX-C-26). A parked app turn's refused verbs carry the refusal's head as their tooltip and point at the one line under the calls, no longer repeating it per verb (UX-A-36). At phone width the page under the open replay drawer does not scroll sideways, and the shell's place word truncates instead of pushing Search past the edge (UX-C-33).

Changed

  • core: weft.override.params is the run's Params replacement by the request record's lower-case names — breaking for readers of the attribute: it was the Go struct's JSON ({"Temperature":0.4,…,"MaxTokens":null}). A Params run option replaces the agent's struct whole, so the attribute holds each knob the replacement sets and null for each knob the agent set and the replacement leaves unset ({"temperature":0.4,"top_p":null} over an agent with TopP); weft.override.hash takes the set knobs alone (equal overrides hash equal), and a params override's hash differs from 0.16.0's. A named tool choice with no name reads weft.override.tool_choice tool, never tool: (U25.1; UX-S5b-03). Contract fixture bridge_overrides.
  • obsdb, Studio, weft: one status filter for /api/runs and /api/search (I3, issue 003) — breaking for /api/runs callers that relied on an unknown word: ?status= takes running, succeeded, failed, interrupted and the read-time parked (an open park: succeeded, pending calls, no run whose resumed_from names it) and cancelled (failed, error_type cancelled), mapped onto stored columns on both backends, so total, limit and the cursor describe the filtered set; any other word is a 400 naming them on both routes (in 0.16.0 /api/runs?status=parked was an empty 200, /api/search a 400). New obsdb.StatusParked, obsdb.StatusCancelled, obsdb.StatusWords, obsdb.ParseStatus, obsdb.ErrInvalidStatus, obsdb.Status.Stored; obsdb/clickhouse's README states the parked filter's cost (a column scan of ResumedFrom). weft runs --status <word> (beside --failed, with which --status cancelled is accepted; a word outside them exits 2) and weft mcp's runs / search tools document the six words. The runs list's status filter is the server's and reads lib/status.ts's words (statusPick, ANY_STATUS): the parked pick's count is the API's total, and cancelled is offered; a row listed under status=parked that the page reads decided through a later turn (no stored resume link: pre-U13.1 data, or a resume without weft.ResumeOf) says "decided by a later turn the record does not link". Revises U32.2's "Not changed, accepted".
  • Studio: one step for the playground page (U26; UX-C-21) — breaking for share links and drafts: the matrix's own "from step" input is gone; the source block's step picker sets the step Run and every matrix cell continue from, and the page's transcript edits go with each cell (and are saved with the experiment) past step 0. The form format is v2 (FORM_VERSION; matrix.from_step removed, a cloned experiment's step is the form's step): a v1 share link or draft is refused, not read with one of its two steps dropped, and says it is from an older Studio with the way out — re-create it from the experiment ("clone into the form").
  • Studio: the ⌘K palette matches by contiguous substrings, never a subsequence (U26; UX-A-13): "1c06797f" does not list a trace whose id merely holds those characters in order, and Enter on text nothing matches opens nothing (src/lib/finder.ts's paletteFilter); a run's metadata pairs are keywords.
  • Studio: /compare links write ?b= as one comma list (b=r_2,r_3), not a JSON array (UX-F-30; lib/links.ts's runsParam: the array kept only when an id holds a comma); the old array links still read. The live page's "ingest open" chip reads "accepting OTLP from loopback without a token" and links to Settings, whose ingest fact says loopback too (UX-A-34). The token wall has a heading, "Sign in to this Studio", and the sidebar keeps Traces, Agents, Playground and Experiments listed, disabled with "needs the token", until it is entered (UX-F-26).
  • Studio: runs list, compare and live polish (H9; UX-A-09, UX-A-10, UX-A-16, UX-A-18, UX-A-21). No page scrolls sideways at 1440 or 360 px (scripts/ux-gate/h9.js measures it; the run page's stacked tree and drawer sit in one minmax(0,1fr) column): the shell's main column is min-w-0, a wide table scrolls inside its own frame (positioned, so an sr-only cell word does not widen the page), the runs table folds its quieter columns on a narrow window, a run header's long subagent id ellipsizes. The runs toolbar is one line — search, agent, status, "+ filter", the top-level toggle, a "? keys" chip in place of the key hint; the other twelve filters are folded behind "+ filter" (its button counts the set ones, each a chip as before). /live draws a subagent under its parent, indented; a run that ended stays a minute (was 30 s), dimmed, and the first load lists the runs that ended in the last minute, so a reload keeps them; the session cell is the session's short id with the public id under it.
  • Studio: the run page's default view is the run tree (U8.2; UX-A-02, UX-A-04). /runs/$id draws lib/waterfall.ts's rows (src/lib/run-tree.ts projects them into the waterfall's): run › step › model call (› attempt) / tool lane › child run, each row's label by the label rule ("model · script", the tool's name — no OTel span names, no per-row service), its gist ("1 tool: ops_manager", "→ 1 call", a reply's or result's first 60 characters; content-off, a tool row's result size from weft.tool.result_bytes), its badge (finish reason, error kind, ok), its duration under "ms" with the offset on hover, a step's lanes when it ran calls side by side, and the critical path as an accent under the bars with a legend. A step or call the events have and no span covers is drawn, never left out: "spans not reported yet" while the run goes on, else badged not_recorded in its own words (truncated when the tree was cut) — WaterfallRow.untimed, UNTIMED_HOLE. The time scale is linear in the run's size (a chat span's attempts grouped once by parent span, the fold's steps and resumed calls indexed once; run-long.test.tsx pins attribute reads per span flat). The views are renamed waterfall · story · raw (?view= and the w/s/r keys; e is gone, RunView is "waterfall" | "story" | "raw"); "trace" names the /traces page only. The switch is scale: time | order | graph, written as ?scale= (the order value round-trips, UX-A-02; RunSearch.axis / RunLinkOptions.axis are now scale; a pre-U8.2 ?axis=time|events link reads as time|order; the t key steps the scale); the dim filters are chips, "dim all but: errors · slow · tools", round-tripping ?dim=errors,slow,tools (RunSearch.dim). With the spans_tree capability the page reads /api/runs/{id}/spans?tree=1 once at load (spansQuery(id, tree), on the waterfall only) and a delegation's child run is one collapsed row under its tool call with "open run ↗"; the run's own spans alone feed the header and the Request pane. A row's detail reads the fold's step, call or run (the story's bodies) where the fold has one, a child row its tool call's, a model call or any other span its span's facts; a time-scale row offers no "replay here" (its extent is milliseconds). The tree is accessible: the axis row (the head, the fold-all buttons, the ms axis) sits outside its role=tree, a call row's toggle is a button beside its verbs, the drawer's subagent button's name starts with its visible words (jsdom axe tests over the waterfall tree, the logs view, the compare page and a status-filtered runs list). Old ?sel= forms resolve: run, the root's run-qualified keys, a pre-U8.2 t:<span id> (the row that span is behind), a bare c:<call id>, a denied call's c:<step>:<call> (as c:resume:<call>). 999.6 ms reads "1.0s" (spanMs).
  • Studio, panel: one run-tree row model for both renderers (U8.1; UX-F-18, UX-A-04, UX-A-22, UX-A-23, UX-S2-07). src/lib/waterfall.ts's waterfallRows() builds run › step › model call (› attempt) / tool (lane) › child run on a time scale (from /spans, sub-millisecond digits kept) or an order scale (stream positions, for a run without spans, from the same row constructors), each row with its selection key, offset and duration, a gist from the folded events and transcript ("→ 1 call", the first 60 characters of a reply or tool result), its status, tool lanes per step, the critical path and the label rule "model · script"; dimRows dims by errors / slow / tools. A child run hangs under the tool span its invoke_agent span names as parent (core numbers call_1 every step, so (parent run, call id) alone is ambiguous); without that span the only child under the tool's (run, call) that starts inside it, never overwritten when two steps repeat a call id. A live delegation (the child's spans before its invoke_agent and the parent's tool end) is a running child row under the fold's open call, else under the root, and a running run's in-flight steps and calls are open rows; a nested Generate's run (no weft.parent.*, no fold link) is a collapsed child row. String-typed attributes (ClickHouse's fallback map) read: "true" for weft.tool.approved / denied / pending, a finish list stored as a JSON string. Span maps are keyed by trace and span id; a child run's t: keys, a step's second model call (s<step>:m2) and its attempts (s<step>:m2:a<n>) are run-qualified and stable; a step's model calls are ordered by start, the step's status and finish from the last. One selection-key scheme (rowKeys): the trace page's keys are the run tree's — a chat span s<step>:m (was s<step>), a denied call c:resume:<call> (was c:<step>:<call>), the trace's root run bare — and the old ?sel= forms still resolve. The descendant-span rules (a child's tool by span or by (run, call), a collapsed child's claim) live in lib/waterfall-tree.ts (treeRows, what Studio's run page builds with); the panel reads a run's own spans and bundles none of it — the same rows for that input. The panel's spanWindow / waterfall (src/panel/render.ts) and its Timeline's inline seq builder are gone: the Timeline draws the model's rows (on a spanless run a step precedes its calls, instead of the order they closed in); lib/trace.ts's spansFromTimed / timeDomain walk the model's span tree. Studio's waterfall axis reads "ms" (was "MSS") and "events", its time ticks and its right column humanised (7.7s, a duration per row, the offset on hover; the first tick "0", took picking its unit on the rounded value — 99.96ms reads "100ms") instead of raw millisecond ranges.
  • Panel: the Timeline draws the run tree (U8.3; UX-D-34, UX-F-18). The rows are Studio's run page's, one level deep: the same keys, labels by the label rule (the gist as the label's title; a child run row's label one rule on both scales and surfaces — its agent, from the child's invoke_agent span, else its fold, else the run document's children[].agent (waterfallRows' agents input) — else its tool's name), kinds and glyphs (lib/waterfall.ts's ROW_KIND, KIND_ICON) and tones (rowTone, moved from lib/run-tree.ts; coloured through lib/palette.ts's TONE / TONE_STUDIO and a --weft-result token equal to Studio's --ev-result), indented by depth, the critical path under the bar, the extent (took) with the offset as its title, untimed rows in UNTIMED_PENDING / UNTIMED_HOLE's words, a step's and a never-finished row's bar at Studio's 60% (color-mix). A child run is one row with ⤢ to Studio's run page at that row (runLink's sel option: ?sel=r:<child>); the step rows are buttons on one roving tab stop (↑/↓; a click, Enter or Space selects the step in Story); the run row is not clickable. Studio's waterfall rows carry data-kind / data-tone. renderWaterfall is gone.
  • Studio: the answer-first drawer on the run page (U10.1; UX-A-03, UX-A-05, UX-A-42, UX-C-23). The detail pane beside the run tree is one drawer for both scales: a kind label (a model call reads MODEL, never TOOL; ATTEMPT, RUN, STEP, CHILD RUN), the verbs drawn with their words — replay from here ▸, edit…, compare… (landing on the step) — not hover-only, and where they are not offered one faint line says why (lib/replay.ts's replayOffWhy, REPLAY_OFF: "replay needs the playground — <capabilities_off's reason>", "a read token cannot replay", "replay after the run ends", a derived run's stock_spans words); for a step (or its model call, attempt or tool) the answer from src/lib/summarize.ts — verdict (the stop and its detail where the run failed at this step, then finish, tokens, latency, first token), sent, replied, tools ran, then; and the lazy tabs Request · Response · Tool I/O · Logs · Raw OTel (?tab=, the tabs pattern: aria-controls / aria-labelledby, roving tabindex, ←/→ wrapping, Home/End; the attrs dump moved to Raw OTel; Logs reads runs/{id}/logs on open, filtered to the row's spans; a pre-U10.1 ?d= reads as Raw OTel). The landing selection is chosen on the rows drawn: the first failed row, else a tool that never finished, else the step a failed run failed at, else step 0 — a model error or a cancel during step N lands on step N (core appends a step's record only after its model call and tools, so weft.run.steps N names the open step), a max_steps run and a cancel before a step began on the run's last drawn step (failedAtStep, landStep), opt.usage-limit on step 2 with its verdict; the verdict, the then line, the end card and the header's stopped-by chip and its link name the same step; "— the loop refused step N+1" is said for a guard's stop only (usage_limit, loop_detected, model_retries_exceeded, max_steps). A derived run lands by the same rule and its step's answer reads its spans — finish and tokens from the chat span's gen_ai attributes, its execute_tool spans as the calls (lib/run-tree.ts's derivedStep) — what no span carries reading "— a derived run has no event record". "past the playhead" is said only while replaying. "read in the story" opens the story on the selection's step, and a ?sel= highlights its card there. The A3 hole words are shorter on both trees (obsdb.HoleNote / HoleNoteFor, lib/honesty.ts, holes.golden.json and the API goldens; tail_lost's fix is the queue fix).
  • Panel: the Story's step reads answer first (U10.2; UX-D-12, UX-D-13's panel half). The selected step — else Studio's landing step: the failed one, else step 0, so a succeeded or running turn reads its first step's answer too — opens with the run page's drawer at panel width: lib/summarize.ts's verdict ([data-verdict], no measured span Studio's drawer does not add) and sent / replied / tools ran / then lines ([data-answer-line]), the same words as Studio's drawer (parity.test.ts: succeeded, settling, max_steps, a model error, a cancel during and before a step, opt.usage-limit's step 2; a derived turn draws no answer — it hands the steps off), and its Logs and Raw OTel hand off — the step head's 📄 ⤢ (U11.2's logsLink) and the answer's Raw OTel ⤢ open the run page at the step's row (?sel=s<n>&tab=logs|raw; no token in the URL). Every [data-weft-handoff] link in the turn view opens through the panel token's hand-off (G3) when there is one. A step's request is one line by default (the attempts, the changed marks, the badges) and opens to the prompt, the catalog and the params; params every one the adapter's default are not listed; the "insert a message" lines show on hover or focus. Subagents expand inline at any depth, each read only once opened (a grandchild's row from its parent's document; an expanded child's stream read again while its last page was not terminal); past two levels the indent stops and the row says "depth N" (lib/waterfall.ts's depthWord); a child's final words that equal its call's result are drawn once, on the call.
  • Studio, panel: phase 8's polish on the run page, run tree and header (U29.1; UX-A-24, UX-A-26, UX-A-27, UX-A-28, UX-A-29, UX-A-30, UX-A-37, UX-A-38, UX-F-15, UX-F-35). The replay bar says "live" only while the run runs; a finished run's reads "end · all N events" and its way back "back to the end" (lib/summarize.ts replayEnd). A nameless agent is "unnamed agent" (muted, "set weft.Name(…)" on hover) on the run header, the runs list, the run tree, a subagent row and the panel's Timeline (lib/format.ts UNNAMED_AGENT). The runs list's j/k selection carries aria-current. A disabled "open in app" is aria-disabled and focusable, its reason read through aria-describedby. A 404 is U29.2's one not-found state (no retry); any other failed run or trace read keeps retry and gains the way back to its list (LoadFailed, lib/not-found.ts notFoundBack). A failed tool row on the run tree reads the story's taxonomy ("panic · handler", "timeout", "retry 1/3", a ToolError code — lib/run-tree.ts treeBadge). Focus starts at the run page's title — the skip link's target there — and the tree's row chevrons leave the tab order (the tree is one stop). The story has "collapse all steps" and "only problems" (dims the steps that went smoothly; stepHasProblem). Delegation words: the header lists "child runs", the delete confirm counts "child runs", the runs list's toggle reads "with child runs" (lib/delegation.ts RUNS_TREE) and so does the export's tree toggle, a pool child parks as "a pool session's turn … started by turn" — "delegated" stays cost and usage's word. Under 480 px the run tree's expand / collapse are two icons and the axis two ticks.
  • Studio, panel: the polish batch U29.4 (UX-C-36, UX-C-39, UX-C-42, UX-C-44, UX-C-46, UX-C-47, UX-F-13, UX-F-19, UX-F-20). The experiments list says how each experiment went and what it ran: passed ("3 / 4 passed", the top-level runs that succeeded), engine(s), source run and step, last run — GET /api/experiments and /api/experiments/{id} rows gain succeeded and last_run (omitted past the summary's walk cap) — and a filter by agent or source run (lib/experiments.ts). A matrix's shape is pluralised, "1 variant × 1 input" (matrixSize), on the playground's Matrix header and the experiment page. A scripted run carries the panel's chip, "scripted · 0 tokens" (plain "scripted" when its usage is not zero), on the run header and the experiment grid too (lib/cost.ts scriptedChip; the panel's turn chip reads the same words, was "scripted (0 tokens)"). With the replay drawer open the story rings the step it replays from ("replaying from here", scrolled to) and marks the steps before it "kept", drawn faint (lib/replay.ts replayMarkOf). The playground's transcript-edit fields show the recorded value as a muted "recorded: …" line, never as the placeholder, and an "edited" chip on a field holding a draft. A read token's replay line names the token that may act ("… it needs the dev token or a playground panel token"). The panel's header counts "runs" unless a session scopes it ("1 run", "3 turns"; was "1 turns" for any list). The panel sets prose — a step's text, the prompt, the notes — in a system sans stack (--weft-font-prose, a new host token; mono stays for ids, JSON and numbers); its running dot and the pill's "● step" pulse (@keyframes weft-pulse; minify-css.test.ts parses the stylesheet raw and minified and fails on a parse error or an animation with no @keyframes). Both surfaces draw a diff in one pair (lib/palette.ts DIFF): added green, removed red and struck, a changed cell mustard — the panel's prompt diff, result-pane diff and compare values were blue / mustard.
  • Studio, panel: the polish batch U29.5 (UX-F-21, UX-F-22, UX-F-32, UX-F-33). A note-toned honesty badge (stripped, redacted, hidden, …) is a neutral outline on both surfaces — Studio's was the accent, the panel's blue; a loss keeps its error outline. The panel header's actions say their word beside the glyph once the dock is 480 px wide ("Studio ⤢", "layout ⇆", "theme ◐"; the glyph and its title below, a container query on the header), and its "raw" button is gone — r and the Raw tab remain. A turn row's accessible name is its fields in words ("turn 3, parked, <title>, 2 steps, 60 in 30 out, 3h ago"). With no turn the header's tokens read "—" (not "0→0"), no "select a turn" is drawn, and the dev list says "no runs yet — connect your app (otel.Install)" (was "no runs yet (dev)"). The raw events explorer's row toggle is a button beside the row's jump and copy verbs, not a role=button row around them (axe nested-interactive, on the Raw OTel tab and the raw view).
  • Panel: 90,091 B gzipped at this release (82,608 B at 0.16.0; the cap is 96 KiB) — the shared row model, the run cards' words and the answer-first drawer are now in panel.js (studio/web/panel-budget.json has the per-item ledger). Studio's dist budget is 700 KiB gzipped (was 600; ADR 0018 §7 amended 2026-10-16 — phase 8's run-screen items; the panel.js copy counts here and under its own cap).

Breaking

  • Breaking (record): weft.override.params on invoke_agent is the run's Params replacement by the request record's lower-case names, with null for each knob the agent set and the replacement leaves unset — it was the Go struct's JSON ({"Temperature":0.4,…,"MaxTokens":null}); a params override's weft.override.hash changes with it (U25; UX-S5b-03). Studio reads both stored shapes.
  • Breaking (API): GET /api/runs?status= with a word outside running, succeeded, failed, interrupted, parked and cancelled is a 400 naming them, as /api/search already was — it listed nothing before (I3, issue 003).
  • Breaking (UI links): playground share links and drafts made before 0.17.0 (form v1, with the matrix's own "from step") are not read; the page says so and how to re-create them from the experiment (U26; UX-C-21).

Fixed

  • Studio: a panel token's run document listed child runs of other public ids (P8-scope-fix) — in 0.16.0 GET /api/runs/{id}'s children[], and so the export's run block, carried every direct child's row (its id, public id, prompt preview, holes) to a token scoped to the parent's public id, and every row's delegated cost priced them. Now one rule (inScope: the child row carries the token's public id — the tree scope's) cuts them at the coster every route prices with: the token reads its public id's children alone — in children[], a step's children and its calls' child_run_id, every children_count / children_failed (runs list, search, a session's turns, the live lane's run frames) — and the rest as one hidden hole with a count on the run document's and the step's holes (drawn "hidden by your token scope · 2 child runs" on both surfaces, StepHole.count; the reason in the right number, "1 child run carries …"), never named; the run's own cost still takes their usage out, its delegated figure reads not known with the reason counting them; /compare's system cell is hidden only by the system prompt's own hole (systemHidden). The server token and setup A are unchanged (TestAuthMatrixChildScope: read and playground tokens over a pub_a run with pub_b, cleared and pool-session children and a pub_b grandchild, across the run document, /api/runs, ?all=1, ?parent=, search, the session, step 0, the diff, the json/jsonl exports with and without tree=1, and a live run frame).
  • Studio: the runs list is no longer N+1 (U9.1; UX-E-24): a page's subagent trees (runs list, search, an experiment's runs, a run document's and a step's children) are read for the own/delegated cost split one ChildRuns read per tree level instead of a COUNT + page per row, at most 10,000 child rows a request (past it the affected runs' cost and counts read not known, never read one by one); the amounts are unchanged, and the delegated reasons and unpriced models list a run's children oldest-first on the runs list too, as the run page did, so a row and its run document say the same words.
  • obsdb/sqlite: RunSpans uses the partial index spans_run (UX-E-22): its read states the index's run_id <> '' beside the equality, without which every waterfall scanned the whole spans table.
  • Studio: g r / g a work on every page, as the keys help said (P8-pre-web; in 0.16.0 only on the runs list): a shell-level chord (lib/keys.ts's goChord), whose r does not toggle the run page's raw view.