Repository navigation
Added
-
core, thread, runtime, obsdb, Studio: approvals linked (U13.1; UX-E-17).
core.ResumeOf(runID), a new run option, names the parked run a resume continues: itsinvoke_agentspan andrun_startrecord carryweft.resumed_from, and every run fed decisions carriesweft.resume.calls— each parked call as<call id>=approved|denied|resolved|no_decision, in call order. A call the resume denies (Deny, or no decision) gets a zero-lengthexecute_toolspan markedweft.tool.denied/weft.tool.denied_kind(error.typeDENIED, never the reason); the root'sweft.run.deniedstill counts it and the span is not counted again. The decider rides as metadataweft.approval.actor. The core cannot infer the link (a transcript carries no run id):threadpasses it on every resume turn with the decisions' actor (a signed decision's key id, elseWho;""on every other turn),weft/runtimeon the playground's resume with the command's actor (panel:<public id>,server,local); an app resuming a plainGeneratepassesweft.ResumeOf(res.ID). obsdb derivesRunRow.ResumedFrom(SQLite migration 0018'sruns.resumed_from, ClickHouse 0019'sweft_runs.ResumedFrom, restating both run views with the four new contract keys) andRunRow.ResumedByon the parked run — read time on both backends, so a resumer ingested before or after its parked run links it; Studio's run row and document serveresumed_from/resumed_by(omitted when none), the seam U1's status word reads. Model-visible behaviour, the transcript and the approval boundary are unchanged. Contract fixtures:approval_denied(now aDenyand an undecided call, withResumeOf) andresumed(rw.support-desk's shape:Approve+Resolve+ResumeOf+ the actor). Since U13.1-fix:resumed_byis the latest resumer by start time (a tie: the larger id), never the largest id;weft.resume.callsis capped at 32 calls (,…+kbeyond);threadlinks any run that applies recorded decisions, and appends each step's messages before its own reporting, so a panicking tracer no longer delays a step's durable append to the turn's end;weft.approval.actoris metadata and survives content-off — put an opaque id inDecision.Who/KeyID, never an email. -
core: what stopped a run is on its record (U3.1; UX-E-11, UX-A-08, UX-S1-02). The
invoke_agentspan carriesweft.run.stopped_byand therun_finisheventstopped_by(RunFinish.StoppedBy): the firedStopWhencondition's manifest name (has_tool_call:submit,step_count_is:2,output_submitted, a custom condition'sString(), elsecustom),final_answer,parked, or — on the span alone, a failed run having norun_finish—max_steps,usage_limit,loop_detected,model_retries_exceeded,canceled. ADR 0016 amendment 2026-10-12;docs/record-contract.md§3/§4; contract casesstopped_byandcanceled. -
core:
CancelCause, a typed cancellation cause (UX-E-41): a run canceled throughcontext.WithCancelCausewith a cause that has aKind() stringrecords it asweft.cancel.causeon itsinvoke_agentspan (deadlinefor a deadline).threadfells a turn withthread.interrupted(anInterrupt/Rollbacksend) orthread.closed(aClosethat gave up);runtime's cancel command withstudio.cancel, and its link's own shutdown (runtime.Install's shutdown func) the runs still in flight withapp.shutdown. The run's error is unchanged (ctx.Err()). -
obsdb, Studio,
weft runs: what stopped a run is on its row (U3.2; UX-E-11, UX-E-41, UX-A-08, UX-S1-02).RunRow.StoppedBy/CancelCause, columnsstopped_by/cancel_causederived at ingest from theinvoke_agentspan andrun_finish(the larger when they differ) and backfilled (SQLite migration 0010, ClickHouse 0011, which restates both run views);RunQuery.StoppedByand?stopped_by=on/api/runsand/api/search(exact, or a prefix ending in:—has_tool_call:); the run row and run document carrystopped_by/cancel_cause(omitted when empty);weft runsprints a STOPPED BY column and--jsoncarries both. Studio's run header shows a "stopped by …" chip on every status, linking the step where it fired, with the story's end card mirroring it; the stop reason reads "last step stopped on" only where it contradicts; "ended without run_finish" only when neither a terminal event nor astopped_byexists and the run is not failed (a failed run that named no stop — a model error — ends on "■ failed · ", its Error root span the terminal record); the chip's detail is the error's own (the usage limit's counts, a loop's "N identical steps in a row", a retries breach'stool "x" retried N times; a cancel shows its cause only); the runs table's line is skipped for the default exit (a succeededfinal_answer); the panel's turn chips carry the same words (src/lib/stopped-by.ts). Panel +502 B when it landed (the ledger's U3.2 row; 78,912 B after U3-fix and U2-fix). Record contract §6. -
thread, obsdb, Studio, panel,
weft runs: titles, not ids (U6; UX-A-11, UX-S5a-09, UX-B-17, UX-D-10, UX-A-25, UX-B-25).threadstamps the session's title (Session.Title, set throughSetInfo) on every later turn as run metadataweft.session.title— a label, so it survives a content-off destination.obsdb.SessionRow.Title(the newest turn's non-empty title) andFirstPrompt(the first turn's prompt preview),obsdb.SessionTitleKey, andSessionQuery.Q(every term in the title, the first turn's whole prompt, the id, public id or agent; both backends, no migration);/api/sessionsrows and documents carrytitle/first_promptand take?q=(the sessions page's text box is now server-side). Studio leads every run row, session turn ("t3" + prompt, "resume of t1 · 1 decided call" for a resume — the previous turn parked and this one carries no new prompt), session row, ⌘K item, run page heading ("agent · prompt") and browser tab (director · How many units… · succeeded · weft studio; a sessionAcme support desk · 9 turns) with its title —src/lib/title.ts'srunTitle/sessionTitle: the prompt, else the agent, else "(content not captured)" for a content-off run, else the id — the id a copyable mono line under it. The panel's turn row reads "t3 ", its narrow picker "● parked t3 How many units… · 2 steps", ids in the title; its filter reads each row'sprompt_preview(the per-turn prompt memo is gone).weft runsprints a PROMPT column (40 runes, "…") before STOPPED BY. Record contract §2/§6; contract casesession_title, and the fixtures'sessions. Panel +323 B (79,347 B). -
core, otel, obsdb, Studio,
weft doctor: emitted counts and drop accounting (U5; UX-E-04, UX-E-05, UX-E-38, UX-A-41). Theinvoke_agentspan carriesweft.run.eventsandweft.run.messagesat its end — the durable event records and growth messages records the run handed to its LoggerProvider, 0 with no logger. obsdb derivesevents_emitted/messages_emittedon the run row (RunRow.EventsEmitted/MessagesEmitted; SQLite migration 0012, ClickHouse 0013 restating the traces view; both backfilled) and the readers count the loss: fewer events stored than emitted isgap, causeevents_lost— "N of M events were lost", split into positions missing below the last stored one and "the last K events were lost" (obsdb.EventsLostHole,EventsLostReason; throughTailLostHole, any status); a root that emitted records none of which was stored reads the same gap ("every one"), andnot_recorded/no_loggeris reserved for a root that emitted none.tail_loststays for roots without the count. The cause of the stress runs' silent loss, proven byotel.TestSDKBatchQueueDropsOldestSilently: the SDK's log BatchProcessor overwrites its oldest queued record when full (run_start first) and tells nobody. Each batch destination now admits its records ahead of the SDK queue (otel/admission.go): newotel.QueueSize(n)(Studio's default 16,384, others 2,048) and dev-onlyotel.Blocking()(refused outsideWEFT_ENV=devwith one WARN, likeRawBodies); a full queue sheds deltas first, then ordinary records,run_start/run_finish/the root span/heartbeats last; every refusal is counted, warned once per destination ("weft/otel: destination queue full", naming the destination and the knob) and carried asweft.records.droppedon that destination's heartbeats. Studio keeps the largest value per service (/api/meta'singest.dropped, the Settings page's "ingest health",weft doctor'singestline); the run row servesevents_emitted/messages_emitted; every gap's fix names the knob. The Local destination writes synchronously and loses nothing (TestLocalFloodLosesNothing). Record contract §3/§4/§6/§7; contract fixtures carry the counts (tracer_only: emitted 0 →no_logger;tail_lost: 4 of 8 →events_lost). -
core, mcp, obsdb, Studio, panel: error kinds and problem counters (U4; UX-S1-01, UX-E-26, UX-E-12, UX-E-13, UX-S2-05, UX-A-40). The
execute_toolspan types a tool failure —error.typepanicwithweft.panic.layerhandler|middleware(wastool_error),mcp_transport/mcp_tool_errorfromweft/mcp(through an error'sErrorType()string, a convention core reads after the context errors),weft.tool.input_errorstrict|schema|syntaxbesideINVALID_INPUT— andweft.tool.truncated_byteswhen the result cap cut (on thetool_finishrecord too); theinvoke_agentspan counts a resume's denied calls inweft.run.denied. The model-visible text is unchanged. The run row gainserror_type(the root'serror.type, the context errors read ascancelled) and problem counters derived at ingest from the spans as a de-duplicated token set —attempts_retried,fallbacks,tool_errors,denied,truncated,repaired(always 0 in this release: the core does not report a repair yet, U23.1); the run document addslog_warn/log_error, counted at read (SQLite 0013, ClickHouse 0015 restating both run views, both backfilled).?error_type=on/api/runsand/api/search. Studio's runs table badges problems only (↻2,⇄,✋,✗,✂,[max_steps]), the run header adds the kind chip (acancelledtone) and the problems beside the step count, the span drawer names a tool's failure kind and cut; the panel's turn row draws the same chips (lib/error-kind.ts, +806 B). Contract casestool_problems,retry_fallback,approval_denied. -
core, obsdb, Studio, panel: phase 7 polish, batch 2 (U32.2; UX-E-64, UX-S1-07, UX-S1-08, UX-S1-09, UX-S1-10, UX-S1-12, UX-S2-14, UX-S2-15, UX-S4-04, UX-S4-14). SQLite migration 0019 adds
spans_start(spans bystart_ns,trace_id): the traces list'ssincewindow narrows to the traces with a span in it before grouping, instead of scanning every span (ClickHouse already prunes it by itstoDate(Timestamp)partitions: no migration). Aloop_detectedrun's repeated step cards carry "repeats step K", linking the first of the N stepsDetectLoopscounted (the error's count, ending at the step it failed at). A tool result whose text isRepair's synthesisedno result recorded: the call was interruptedis badged "synthesised by Repair" on every request message line — the one surface that can see it: Repair writes the text into a later run's input transcript only, so notool_finishcarries it and the story's pills and the panel's call rows never badge it (P7-fix2 dropped those two paths). Matched on the text: the record carries no other signal; recordingweft.messages.repairedis not done. A failed model attempt reads a phrase for itserror.type(stream_idle"no bytes from the provider within the adapter's idle timeout",model_requests_denied,model_error, the context errors,model_contract; an unknown type as itself, "no description"), the raw type in the hover. The run header's public id and metadata chips link to the runs list filtered by them (?public_id=,?tag=k=v). Settings names how app logs reach a run (the LoggerProvider; slog needs a bridge — a slog→OTel bridge (otel.Slog) is planned; an out-of-loopAgent.CallToolrecords nothing — also in its godoc), and the ingest-health line names the run_start and run_finish carriers. docs/record-contract.md §6 lists what the record attributes to user code today (effects only; the hooks are U23's and U25's). Themetadata_capreason names its three limits ("limit 64 keys, 128-byte keys, 1 KiB values"). Not changed, accepted: a server-sideparkedstatus filter (the client-side pick and the session's state cover it); a livePUT /api/settings/pricing(download + restart); ingest health persisted across a restart. -
Studio, panel: one unpriced-cost reason per page (U7; UX-A-12, UX-D-21, UX-S4-07, UX-H-33, UX-B-27, UX-C-17). A scripted model no pricing row matches — every
wefttestmodel, the playground'sweft/runtimescripted engine — is an exact$0by a rule in Go (cost_knowntrue,cacheexact, newcost_badgevaluescripted), drawn "$0 · scripted"; a row that matches still wins. A pricing row with noprovider(or"*") matches on its model glob alone, any transport (an OpenAI-compatible model behind the openai adapter); a file's missing provider loads as"*"instead of failing. A summed figure's reason (delegated, session, experiment) is one line counting its runs — "2 of 3 subagent runs unpriced: no price for openai/x" — never a clause per run; an unpriced run's reads "openai/x has no price". Newunpriced_models/delegated_unpriced_modelslist the models behind a figure; Studio's runs list, sessions list and run header, and the panel's header, say "N runs unpriced: no row for …" once, beside a "price this model…" link to Settings (/settings?price=, a prefilled row). An unpriced cell is "—" with a small "unpriced" chip (the sessions list's a number or "—"); a partial sum reads "≥ $x · k of n runs priced", "—" when its known part is $0 (never "≥ $0.00", never "<" under "≥"). The livePUT /api/settings/pricingwas not added: the table is still applied by download and restart. -
obsdb, Studio: manifests stored by hash, and indexes for the run lists (U27; UX-E-16, UX-E-23, UX-E-35, UX-E-36). New optional
obsdb.ManifestStore(PutManifest,Manifest;StoredManifest,ManifestHash,CheckStoredManifest,ErrInvalidManifest), implemented by both backends (SQLite migration 0014, ClickHouse 0016): each agent's one-agentcore.Manifestdocument under its sha256 — theweft.manifest.hashits runs carry — content-addressed, the first write kept, kept forever (DeleteRun, DeleteSession, Sweep and Vacuum never touch it). Studio writes it from the weft.json it serves (atNew) and from every runtime registration, and servesGET /api/manifests/{hash}(capabilitymanifests;{manifest_hash, agent, source, first_seen, current_manifest_hash, manifest}; a read-scoped panel token 403 hidden, as/api/manifest); the run document carriesmanifest_available: truewhen its hash resolves. SQLite: the public-id, session and session-list predicates state their partial index's<> ''so the planner usesruns_public/runs_session(each session's newest-turn, title, prompt and content-off subqueries walked every top-level run before);runs_publicis rebuilt over the list's whole key,runs_session_startedis new,runs_startedandruns_experimentgain therun_idtie-break;runs_allservesall=1,runs_last_seenthe live backfill window and the running filter, partialruns_failedthe failed filter (runWhere adds each status's implied condition beside the CASE); migration 0015 adds partialruns_failed_allfor the all-runs failed page (status=failed&all=1, which walkedruns_alluntil the page filled), and the all-runs running page and the live backfill write their order+started_nssoruns_last_seenfinds the few matches instead ofruns_allwalking every row (U27-fix). Cost of the upgrade: migration 0014 rebuildsruns_public,runs_startedandruns_experimentand builds four new indexes (runs_session_started,runs_all,runs_last_seen,runs_failed; 0015 a fifth,runs_failed_all) — a one-time synchronous rebuild insideOpen, seconds on a large file before Studio serves; andruns_last_seenis rewritten on everylast_seenbump, which is every record batch a run writes — one more index entry per batch. ClickHouse: a run's spans are read byTraceId = ? AND RunId = ?when its row names the trace (falling back toRunIdalone), andotel_tracesgains theidx_weft_run_idbloom filter (materialized in the background). The run-header "config at run time" view is left to U26. -
Studio, panel: one status vocabulary on both surfaces (U1; UX-A-06, UX-B-04, UX-D-16, UX-F-07, UX-S3-03).
src/lib/status.tswords every run row — running · succeeded · failed · interrupted · parked · cancelled, each with a hint and a colour role — and the runs list, the run header ("parked · 1 call awaiting a decision"), a session's turns and its header (its newest turn's word), the live table, ⌘K, and the panel's turn list, narrow picker and turn filter all draw it.parkedandcancelledare read-time words, never stored (record contract §7): cancelled is failed witherror_typecancelled (U4's tone folded in), parked is succeeded with pending calls while no later turn of its thread session exists — then it reads "parked → t2", a link to the resume; a plainGenerateresume has no link yet and reads parked. The panel's "parked" filter returns open parks only and gainsinterruptedandcancelled; its hostrunevent keeps the plain words.src/lib/palette.ts'sROLEgives each status one colour on both surfaces: running is Studio's--status-runin the panel too (new--weft-status-run; was the interrupted mustard), interrupted the mustard (was the tool blue), parked Studio's new--status-parked(the panel's purple), the live dot the succeeded green (was the accent); the panel's chip gains Studio's dot. Panel +589 B (81,716 B). -
core, otel, obsdb, Studio, panel: phase 7 polish, batch 1 (U32.1; UX-S2-10, UX-S2-11, UX-S4-09, UX-S4-15, UX-E-42, UX-E-48, UX-E-50, UX-E-51, UX-E-58). A RETRY the loop counts carries
weft.tool.retry(the tool's consecutive count with it) andweft.tool.retry_budget(MaxModelRetries) on itsexecute_toolspan, and the budget's error names both (tool "x" retried 3 times, past MaxModelRetries(2); the model-visibleRETRY: …text is unchanged); Studio's story pill reads "retry 1/2", "retry 3/2 — budget spent", and "strict input" for aStrictInputrefusal, from the call's span;model_retries_exceededreads "tool retry budget spent". Theinvoke_agentspan carriesweft.tap.panics— the run's contained observer panics, besideAgent.TapPanics— counted as the run row'stap_panicsproblem ("⚠N tap panics"). The prompt record's body gainssegments(instructions, thentool:<name>per snippet; content-gated, redacted per segment, dropped under a cap that cut the text), drawn under labels in Studio's Request pane (obsdb.PromptRecord.Segments,obsdb.PromptSegment; breaking for a caller comparingPromptRecordvalues with==: the struct is no longer comparable).weft.metadata.droppedbecomes the run document'struncatedhole, causemetadata_cap("N metadata pairs dropped (limit 64 keys / 1 KiB values)"). New problem tokenstap_panics:/metadata_dropped:(SQLite migration 0017 backfills the second; ClickHouse 0018 restates both run views with the three new keys in the meta tuple and backfills). The kill switch's kind chip reads "denied by the kill switch (WEFT_MODEL_REQUESTS)"; Settings › Token namesweft mcp --url <this Studio>anddocs/mcp-recipe.md; the run header shows the tool concurrency ("parallelism: 2" / "sequential") as the run ran: itsweft.override.parallelism, else the request record'ssequential_tools, else the run's stored manifest; the Wire tab is labelled "loop request → wire bytes".
Fixed
-
cmd/weft, obsdb, Studio, panel,
weft doctor, docs: the pre-release review of the Go side (R7-prep).weft runsandweft doctorprint every free-text cell — the prompt preview, agent names, stop causes, ids, Studio's error text, service names — with each control character (an escape sequence, a carriage return, C1 and bidi controls) as a space (internal/termsafe): a prompt or an OTLP-ingested record no longer drives the operator's terminal. A content-on run that lost some of its transcript batches badgesgap, causemessages_lost— "K of N transcript batches were lost" (obsdb.MessagesLostHole,MessagesLostReason,obsdb.CauseMessagesLost;lib/honesty.ts), joined to anevents_lostgap on the one badge; a store holding no messages record is never called lost (a content-off destination stores none; record contract §7 says what that cannot tell apart); contract casemessages_lost, andtail_lostgains the cause. A failed root never settles: its row readssettlingfalse while its event page reads done, one answer from the first read (DeriveSettlingis passedrootEnded && !failedon both backends).weft doctor's ingest fix names the queue and failing exports ("queue full or exports failing — the app's WARN names which"). Record contract §10 listssession_title, and a test holds §10's rows toobsdb/testdata/contractboth ways. Godoc examples forotel.QueueSizeandotel.Blocking(its dev-only gate). Panel +41 B (82,373 B, measured on landing over P7-fix3). -
obsdb, Studio, panel, docs: the phase 7 gate's dry run (P7-fix3). The run header's tool-errors entry names the failed calls' kinds from the run's loaded
execute_toolspans, in lib/error-kind.ts's words — "✗3 3 tool errors: tool error ×1, timeout ×1, pending child ×1" (SUBAGENT_PENDINGreads "pending child") — the count alone while the spans are not loaded, and "N not in the loaded spans" for calls the counter holds and the spans lack; the panel's turn chips stay counts. The panel's in-place edit fields (plan F2: the prompt, the insert boundaries, a reply, args, a result) take their visible text as their accessible name, "edit …" as the title (axe'slabel-content-name-mismatch); an empty field keeps the aria-label. The a11y budget gains the fields at rest with the rule checked by hand (jsdom leaves it incomplete). Arecords_droppedcontract case pinsweft.records.droppedonrun_start/run_finish: out ofmeta, no row field (record contract §10). The concurrency hedge reads "…read when its spans are available" (a run whose spans never load was promised a read). -
thread, obsdb, Studio, panel, docs: the review of P7-fix, U32.2 and U13.1-fix (P7-fix2). The run header's tool-concurrency chip names the manifest's value as that — "the manifest's value; the run's own setting is read once its spans load" — until the run's own
invoke_agentspan is read (a running run, or a token without the requests capability, never reads itsweft.override.parallelism); once it is, "the run's manifest". The Repair badge is drawn on the request message lines only (see U32.2 below). A failed attempt'smodel_requests_deniedreads the run chip's own words ("denied by the kill switch (WEFT_MODEL_REQUESTS)") andunsupportedreads "the model cannot honour part of this request (ErrUnsupported)". Themwgodoc rate-limit example pulls a past slot up to now, so idle time is never banked into a burst. A decided park whose resumer is a thread turn not in the loaded runs readst<n>from its<session>-t<n>id instead of a cut that loses it. Comments only: the approval link's "any turn the decisions ride" path is marked defensive (today only resume turns carry decisions, ADR 0021 §1); SQLite 0018 / ClickHouse 0019 say the reader's rule (the latest resumer by start time, a tie: the larger id) — both runners track migrations by version, no content hash, and ClickHouse's skips--lines, so editing them changes no applied database. The approval-actor fixtures use an opaque id (op_42), as contract §2 asks. -
core, obsdb, Studio, panel: the combined review of U27-fix, U4-fix, U32.1 and U1-fix (P7-fix).
weft.tool.retryis assigned after the step's tool batch, in call order, exactly as the guard counts — a RETRY in a batch that also holds a success of the same tool carries no mark (the count resets; it no longer reads "budget spent" on a run that goes on), and parallel RETRYs of one tool are numbered by call order, not completion order; the mark is on theexecute_toolspan only (its span ends after the batch, stamped with the call's own end), no longer on thetool_finishrecord, which is written before the batch is over. The run header's concurrency is the run's own (itsweft.override.parallelism, thensequential_tools, then the manifest; "parallelism: 4" no longer shows for a run aParallelism(1)run option made sequential). Themetadata_caphole's fix names all three limits ("64 keys, 128-byte keys, 1 KiB values"). ClickHouse 0018 has an upgrade test. The runs list under a loaded-rows filter (theparkedpick, a duration) heads "N listed of M loaded", never the server's "matching" total; the parked chip says a resume that did not succeed is not looked for. SQLite'srunOrderno longer applies under a public-id or session equality. The step route's attempts carrylayer(weft.attempt.layer), and both renderers' attempts lists read "attempt 2 · fallback" (src/lib/attempts.ts'sattemptLabel). -
Studio, panel: the U1 review. "awaiting a decision" is said only where the resume was looked for — a session turn read against its session's whole turn list (the session page, the run header once its session loaded, the panel's session-scoped list); a plain
Generate, a runs page under a filter or sort, the live table and an experiment's grid read "N calls pending · no resume recorded here" (a plainGenerate: "a resume, if any, is not linked yet"), the word staying parked.statusOfreads a row'sresumed_byfirst (U13.1's stored link, served on the run row). The runs list's status filter gainsparked(succeeded asked of the API, the loaded rows kept where the word reads parked, its chip says so); the live table's filter matches status words on the key; the panel's filter gains "parked → (decided)". The experiment grid, a subagent's status, the flow strip's end mark and the live table's cancel toast and parked cell drawstatusOf's word and role (a parked or cancelled run no longer reads succeeded / failed). The running word is drawn in a new--status-run-ink(#d13c10light, 4.61:1 on paper; dark unchanged at 6.07:1) — the dot, pulse and pill border keep--status-run; the panel's theme test holds the word to 4.5:1 again. The[cancelled]kind chip is dropped where the status word already says cancelled. The panel's decided-park word selects its resume turn on click. Record contract §7 no longer spells a key no code writes (TestRecordContractDocNamespasses) and says the next turn decided or dropped the calls. Panel +170 B (81,909 B). -
core, obsdb, Studio, panel,
weft runs: the U4 review.attempts_retriedcounts only a failed attempt a later attempt of the same model call followed (an exhausted three-trymw.Retryreads 2, a non-retryable failure 0; the words "model attempts retried");fallbackscounts the attemptsmw.Fallbackmarks —core.AttemptInfogainsLayer(core.AttemptLayerFallback), theattemptspanweft.attempt.layer— never a model-name comparison;RETRY(ModelRetry, the repair loop) is no tool error (NO_SUCH_TOOLstill is); the denied-approval token is keyed by itsinvoke_agentspan, so two roots of one run id with the same count both count. The problem set is re-derived from the stored spans (SQLite 0016, ClickHouse 0017 restating the traces view and backfilling; U4's token kinds count nowhere). A result-cap cut is forgotten at itstool_finishwith no logger wired (it stayed until run end), and a middleware that recovers a handler's panic and later panics readsweft.panic.layermiddleware. The run document'slog_warn/log_errorcome from one severity-narrowed scan (obsdb.DB.CountLogs, both backends) instead of up to ten paged reads,logs_cappedmarking floors ("≥" on both surfaces). Breaking for anobsdb.DBimplemented outside the two shipped backends: the interface gainsCountLogs(obsdb.CountOtherLogsis the shared body over a severity-narrowedLogCandidates). Record contract §3 says which denialsweft.run.deniedcounts. -
otel, obsdb, Studio, panel: the U5-fix and U7 review. A scripted parent whose subagents bill is no longer "$0 · scripted" where runs are summed (the panel's result pane and header, the experiment grid and totals, the playground): a run's whole bill must be $0 for the scripted words, as Go's set rule already required. A partial sum whose known part is under $0.0001 reads "> $0 · k of n priced" (the exact figure in its title), never a "≥ $0.0001" above the real figure; a paged sum whose loaded rows are all known at $0 reads "$0 · scripted" or "$0 · k of n priced", never "—". A session or experiment made only of the playground's scripted-engine runs (zero usage) is badged scripted, not a bare "$0.00". A session's or experiment's cost reason names no turn id ("1 of 3 turns' cost not known yet ()"; a delegated reason still names the child). The built-in $0 rule covers
wefttest/*andweft/runtime+scriptedonly:weftarena/*, a wrapper that may wrap any model, is unpriced without a row of its own. A Go-builtstudio.Pricingrow with noProvideris the any-provider row, as a file's missing provider loads.unpriced_models,delegated_unpriced_modelsand a reason's model list stop at five, a sixth and more one last element"(+n)". An unpriced run's reason is the model alone, "openai/x has no price". The?price=prefill escapes*,?,[and\in a model name, so the row prices that name alone. The span chain's Blocking latch now sets on the first timed-out wait (a context deadline of the same length fired first and skipped the latch, so a hung exporter cost a wait per span until the timer won), and the latch's first setting logs one WARN per destination: "destination : exporter stalled — records are refused until it recovers (Blocking)". ClickHouse: migration 0014 restates the logs run view withweft.records.droppedin its contract tuple, so storedweft_runs.Metano longer keeps it (reads were already right);obsdb.NonMetaKeyslists MetaOf's exclusion set, and a test holds the ClickHouse copy to it whole. -
otel, Studio,
weft doctor: the U5 review. The span batch queue is built eight places above admission's size, so aForceFlushmarker under a full queue no longer makes the SDK drop an admitted span (likely the run's root) silently and leak its admission place for good.weft.records.droppedalso rides each run'srun_startandrun_finishrecords (record contract §4), not only heartbeats of runs open 10 s, and Studio's ingest health reads it from them; the key is not caller metadata.otel.Blocking()is bounded per stall: after one timed-out wait the chain refuses at once (counted) until an export begins, and a span's End waits under its own 5 s deadline. A detached flush ends when its chain shuts down./api/meta'singestis the dev token's and setup A's loopback only (manifest_check's rule), omitted for a panel token. The ingest max stays per service: the pipeline's resource names no process instance. -
panel: a thread resume across a gap in turn numbers (a queued turn's number dropped by
ClearQueue) is titled "t5 resume of t3": the turn before is the highest loaded turn below, as Studio's session page reads it (U6 re-review c1). -
obsdb, Studio, panel: the U6 review. The run page's heading of a content-off run of a named agent says the agent once (
director · (content not captured), notdirector · director · …). ⌘K finds a session by its title or first prompt and a run by its prompt (cmdk keywords), thesearchcapability or not. A session row carriescontent_off(obsdb.SessionRow.ContentOff, both backends, no migration: the first turn stored events and nomessagesrecord, not derived), so the sessions list titles an untitled content-off session "(content not captured)" instead of its id; an id-titled row no longer repeats the id under it. The panel's turn row and narrow picker title a thread resume withturnTitleas Studio's session page does ("resume of t3 · 1 decided call"); the picker adds the turn's duration and reads an experiment in its card's order (↳ replay of t3 · scripted · ● failed · 1 steps · 2.5s). Record contract §2 (weft.session.titleacross Fork, Branch and the compaction marker) and §6 (content_off); contract casesession_title. Panel +218 B (79,565 B). -
Studio: a settling run raises no counter gap on the server (its missing positions may still land) and none on the client (
statusHolesreadsisLive); a settling run's story foot reads "stream in progress…", never "the stream ended without run_finish"; a failed run's last step stayserrorunder the counted gap (the U2 re-review's carried items). -
Studio: the settling window reaches the UI (the U2 re-review). The run row and run document serve
settling(omitted when false); the run page's polls and event tail, and the panel's turn, follow a settling run as a running one (isLiveinsrc/lib/honesty.ts) and draw asettlingnote beside the status word, so a run_finish or atail_lostgap landing after the root shows without a reload. A failed spans-only root's steps readerrorat once, neverrunning; a settling step names no request gap in its Request and Messages-in panes; a run with a dropped mid-run batch and a lost tail says both reasons on its onegapbadge.stopped_by: step_count_is:with no argument reads "stopped by a step count (StopWhen)". Record contract §6/§7. -
obsdb, Studio: the U2 review (status from the root span). A succeeded root with events stored and no
run_finishbadgesgap, causetail_lost("the run's root span ended but its run_finish record never arrived: a destination dropped the record tail, so the last steps' records are missing") on the run and on the step whosestep_finishwas dropped (that step reads ok, not error) — no longer a clean success (obsdb.TailLostHole,RunRow.FinishRecorded; fixturetail_lost's holes). A root stored before the records behind it settles for 30 s (RunRow.Settling,obsdb.DeriveSettling): no hole calls the records lost and the events route keepsdonefalse while it does.EventPage.Doneis true for a root-ended run once it stopped settling, so a poll that waits for it ends. A spans-only run's step documents and requests/tools panes readnot_recorded, causeno_logger, not a step gap or an empty list (RunRow.RequestsHoleCause).weft_versioncomes from therun_startrecord and the root span alone (its attribute, else its resource), the larger string when they differ, on both backends — never another span's resource (ClickHouse migration 0012 restates the traces view). Derived runs all readroot_ended(SQLite 0011, ClickHouse 0012). Record contract §6/§7. -
obsdb:
weft.run.stopped_byandweft.cancel.causeare contract keys, no longer caller metadata in the run row'smeta(the U3.1 leak; on ClickHousemax(Meta)also lost them, failingTestContractClickHouse/tool_calls). SQLite's 0010 removes them from stored rows; ClickHouse's reader drops them throughobsdb.MetaOf. -
obsdb: a run whose
invoke_agentspan is stored is over —succeeded(orfailedon anErrorroot) without arun_finish, neverinterrupted(UX-E-01). A tracer-only app's runs and runs whose record tail a destination dropped read the crash signal 30 s after they ended. New columnroot_ended, set at span ingest and backfilled from the stored spans (SQLite migration 0009, ClickHouse 0010;RunRow.RootEnded); the status filter, sessions and the sweep read the same rule. The run row'sweft_versionandmanifest_hashnow fill from the span (the version also from its resource) when no record names them (UX-E-02). Record contract §6 and §7, fixturestracer_only,tracer_only_failed,tail_lost; ADR 0024 amended. -
Studio: a weft run with spans and no stored record badges
not_recorded, causeno_logger("the app exported spans only", the fix namingweft.LoggerProvider/otel.Install) instead ofderived, which is the stock-span run's badge alone (UX-E-03;obsdb.LostRecordsHole,obsdb.CauseNoLogger,lib/honesty.ts). Panel +103 B (78,201 B).
Changed
- Devtools panel: the panel's size cap is 96 KiB (98,304 B), up from 80 KiB (audit decision 1); every delta still appends a ledger row. The ledger's
cap(studio/web/panel-budget.json) is now the one source the build (vite.panel.config.ts) andscripts/panel-asset.tsfail on — the build still fails over it; one row, "A5.4 cap raised to 96 KiB", records the change at the then unchanged 78,098 B (20,206 B of headroom at the cap change; the ledger's rows since carry every delta). - Studio, panel: the pre-release review of the web side (W7-prep), UI words only. The problem badges (Studio's and the panel's turn chips) carry
role="img"and their words as the accessible name. The problems chip draws no "parked" part for a decided park: its pending calls read "parked" only under the parked status word, "held" under any other. The resume fallback reads "next turn tN (decided or dropped)" — it no longer claims decided. The run header's concurrency chip, read from the manifest, says so in its visible text ("parallelism: 4 (manifest)"), not only in its hover. Step counts read "1 step" (onestepCounthelper). An unpriced-runs line over a paged list adds "· the loaded rows". "(content not captured)" (contentOff) is gated onisLiveand on a gap / not_recorded hole. Beside a cancelled status word the stop chip reads the cause alone. A tool failure ofcontext.DeadlineExceededreads "deadline". The kind chip reads "[retry budget spent]". The unusedstatsStriphelper is removed. The panel's subagent row and the playground's compare table take their status words throughstatusOf. Panel +176 B (82,549 B). - Studio, panel: the pre-tag re-review (D7). Beside a cancelled status word, a stop chip with no recorded cause reads the step it fired at ("· step N"), so the run header keeps its link to that step. "(content not captured)" no longer shows for a list row whose events were lost (
event_countbelowevents_emitted). A session row'scontent_offis gated like the client's — not while the first turn is running or settling — on both backends' session list and document and in a panel token's scoped document, through one rule,obsdb.SessionContentOff(new; its Status and Settling derived byDeriveStatus/DeriveSettling); conformance case inSessionTitles. The approvals block uses the sharedAWAITINGwords. A test holdsstudio/README.md's panel figures to the ledger's last row. Panel +59 B (82,608 B).
Breaking
- Breaking (record): an
execute_toolspan'serror.typefor a contained panic ispanic(withweft.panic.layer), and forweft/mcpfailuresmcp_transport/mcp_tool_error— all three weretool_error. A query or alert keyed ontool_errormisses them (U4). - Breaking (reader): a tracer-only app's runs, and runs whose record tail a destination dropped, read
succeeded/failedfrom their storedinvoke_agentspan, no longerinterrupted;?status=interrupted/?status=succeeded/?status=failedresults change accordingly (UX-E-01). - Breaking (API): a weft run with spans and no stored record badges
not_recorded/ causeno_logger(orgap/events_lostwhen its root counted records), no longerderived—derivedis a stock-span run's badge alone (UX-E-03). - Breaking (Go): the
obsdb.DBinterface gainsCountLogs(U4): an implementation outside this module must add it. - Breaking (Go):
obsdb.PromptRecordcarriesSegments(a slice) and is no longer comparable:==on it, or its use as a map key, no longer compiles (U32.1). - Breaking (API): the run row's
metano longer carriesweft.run.stopped_byorweft.cancel.cause— they are contract keys, served asstopped_by/cancel_cause(U3.1 fix). - Breaking (API values):
cost_badgegains the valuescripted(an exact $0 by rule forwefttest/*and theweft/runtimescripted engine);cost_reasontexts changed (one reason per page, "openai/x has no price");weftarena/*gets no $0 rule and stays unpriced without a row of its own (U7). - Breaking (Go): an
INVALID_INPUTtool error'sToolError.Erris now an unexported*inputErrorwrappingErrInvalidToolInput, no longer the sentinel itself:errors.Is(err, ErrInvalidToolInput)and the model-visible text are unchanged, butte.Err == ErrInvalidToolInputis false (U4). - Breaking (CLI): the
weft runstable gains the PROMPT and STOPPED BY columns after STEPS: a script that splits rows by position must move toweft runs --json(U3.2, U6). - Changed: the
ErrModelRetriesExceededrun error's text adds the budget —tool "x" retried 3 times, past MaxModelRetries(2)(wastool "x" retried 3 times);errors.Ison the sentinel is unchanged (U32.1).