Repository navigation
weft v0.10.0 — the request record, Studio's step and export routes, the honesty table, one version from the module
The request record (ADR 0028): every model call's system prompt, tool
catalog, parameters and attempts recorded beside the transcript, read
back by Studio and the devtools panel with a closed table of honesty
badges; app logs, the step route, the export in four formats; one
version from the module. Tags core/v0.10.0 (compatible additions
only) and v0.10.0 (breaking in the pre-freeze layers listed below).
Changed — breaking
- One version, the module's. The new
github.com/weftgo/weft/version
package (standard library only) holds it:version.Versionis the
release tag,version.Runtime()reads the framework module's version
from the build info (the main module or thegithub.com/weftgo/weft
dependency, honouringreplace) and falls back toVersionunder
(devel).studio.Versionis nowversion.Version—v0.4.1→
v0.9.0— so/api/meta'sstudio_versionand the devtools panel's
embedded version (stamped fromversion/version.goat build) move
with the module. A panel built forv0.4.1refuses a Studio that
reportsv0.9.0as newer. /api/meta'sweft_versionis the framework module's build-info
version (version.Runtime()), no longer thecoredependency's;
inside the workspace it reads the tag instead of(devel).obsdb.DBgainsTranscriptBatches(ADR 0028 §8): a run's
messages records with what each stored — index, step
(weft.step.index, -1 when absent), input flag. A third-partyDB
reads pos, step, body and theweft.messages.inputattribute per
messagesrecord, returns them throughobsdb.DedupBatches, and
implementsTranscriptasobsdb.TranscriptBodies(batches). A
backend with no attribute column infers the input flag on index 0 and
setsTranscriptBatch.InputDerived./api/runs/{id}/transcriptrows carry what the record stored: the
storedindex, the storedstep(or-1with"badge": "not_recorded") and the storedinput("badge": "derived"where
the backend inferred it) — no longer derived from assistant-message
order. Playgroundtranscript_edits/from_stepvalidation and the
web client read the same stored step; only a batch without one is
placed by inference, marked derived.obsdb.DBgainsRequests,Prompt,ToolsandCatalogs
(ADR 0028 §10, the request record's read side). A third-partyDB
reads itsrequest,promptandtoolsrecords (stored under
(run, kind, index)) asobsdb.StoredRecords through
obsdb.RequestRecordOf,obsdb.PromptRecordOfand
obsdb.ToolsRecordOf, returns one tools record per hash (the lowest
index) fromCatalogs, and answers a missing hash with
obsdb.ExplainMissing.obsdb.RunRowgainsInstructionsHash,
CatalogHashandRequestCount, which a backend fills with the
larger ofrun_start's and theinvoke_agentspan's
weft.instructions.hash, request index 0'sweft.catalog.hashand
maxweft.request.index+ 1.obsdb/clickhouse's unreleased migration 0004 gained
weft_records.Input,Content,TruncatedBytes,SystemHashand
CatalogHash: a database
that applied 0004's earlier text (only a development build wrote one)
lacks them and must be recreated. ClickHouse now reads the transcript
input flag as stored; only rows written before the column read
TranscriptBatch.InputDerived.obsdb.DBgainsCompactions(ADR 0028 §8, plan A9): a run's
compactions asobsdb.Compaction— each run-scope view in index
order with its step, half-open range, hash and body, then thread's
session markers in emission order. A third-partyDBbuilds each through
obsdb.CompactionOf(from amessagesrecord whose
weft.messages.reasonis set, or a record of kindcompaction) and
orders them withobsdb.SortCompactions. ItsTranscriptand
TranscriptBatchesmust skip everymessagesrecord whose
weft.messages.reasonis set, and its messages count must not count
them (obsdb.Weft.Reasoncarries the attribute).obsdb.DBgainsOtherLogs(plan A7): a run's app log records —
the non-weft records (noweft.run.id) the writers keep beside
weft's, attributed to the run through the span they were emitted
under (its own spans and the non-weft spans below them, never another
run's) — as anobsdb.LogPageofobsdb.OtherLogs, paged by
obsdb.LogQuery(Frominclusive,Limit0 = 100 max 1000,
MinSeverityfiltering without renumbering), withPartial(the run
is running: lines under in-flight spans appear when those spans end,
and indexes may shift),Gap(lines naming a span never stored) and
Truncated(more thanobsdb.MaxLogCandidateslines in the run's
traces, counted before attribution). A third-partyDBimplements it as
obsdb.ReadOtherLogs(ctx, db, runID, q, candidates), where
candidatesreturns the firstlimitnon-weft records of the run's
traces within a time window, in time order.obsdb.HoleError.Kindgains"logs": a finished run
with no span has nothing to attribute through and answers
HoleNotRecorded.
Changed
- The system prompt and the tool catalog now reach content-on
destinations (otel.Local,otel.Studio) under the content policy:
Redact-able, capped withweft.content.truncated_bytes, dropped by
content-off destinations. ADR 0028 reverses the old stance that no
record carries the instructions text. An application whose prompt must
not leave the process redacts it (core.ContentPrompt) or turns
content off for that destination.
Added
-
The compaction marker on the run page and in the panel (plan
A9.2, ADR 0028 §8).GET /api/runs/{id}gainscompactions: each
compaction the run's records name, counts and hash only, never a
message body — a run-scope view{scope: "run", index, step, from_seq, to_seq, hash, replaced, entries}and thread's session
marker{scope: "session", hash, replaced, entries, tokens_before?, tokens_after?, reason};[]for a run that never compacted or was
written before A9 (the record is optional: no badge). A read-scoped
panel token reads it. The run page's step card whose request saw a
view draws "2 messages rewritten into 1 by PrepareStep" ("1 message
inserted by PrepareStep" when nothing was replaced) with the
compactedbadge; a session marker sits at the top of the run it is
filed under — "12 messages compacted into 2 · 8.1k → 1.2k tokens".
"show original" (collapsed, no fetch) expands a view's replaced range
[from_seq, to_seq)from the transcript route's growth records the
page already holds (seq = position in their concatenation); a range
that cannot be placed is thegapbadge. The replacement shows its
count — and, once the step route is loaded, the request's message
count — and says where its body is (the export's
compactions[].messages). The devtools panel draws the same marker:
session markers at the top of the turn, views on their step line. -
A run's app logs and its delta count in Studio (plan A7).
GET /api/runs/{id}/logs?from=&limit=&severity=, under a newlogs
capability in/api/meta, pages the app's own log lines (anslog
bridge or the OTel Logs API on the pipeline'sLoggerProvider) that
were emitted under the run's spans — a tool handler's lines are the
run's — in time order:{logs: [{index, time, severity, severity_number, body, attrs, span_id?}], next_from?};severity
keeps a level and above (trace…fatal, or 1–24).holeslists
every hole of the page (badge/reason/fixrepeat the first):
not_recordedfor a run recorded without a tracer,truncatedwhen
the run's traces hold more than 10 000 log lines in its window (the
cap applies before attribution: later lines of this run may be
missing),gapfor lines in the run's trace naming a span that is
not stored (they may be another run's). A running run's page carries
partial: trueand apartial_reason(lines under in-flight spans
appear once those spans end; indexes may shift) — not a hole.
App logs may carry anything the app logged, prompts included, so a
read-scoped panel token is refused them (403,badge: "hidden"); a
playground-scoped token, the server token and loopback read them. The
run row (runs,runs/{id}, sessions, the export) gains
delta_count, the streamed deltas counted and never stored. The web
client gainsfetchLogsand the types (no UI yet). -
Subagents on the page (plan A10).
GET /api/runstakesall=1
(every run, subagent children included — the same asparent=*; a
parent=<run id>beside it wins; a malformed value is a 400) beside
the existingparent=filter, whose absence keeps the list top-level
only. On the run page a step whose tool call started a child run
shows it as a nested row — agent, status, usage, its holes, a link to
its own page (from the run document's children, or the step route's
children[]when cached) — and opening it folds the child's steps
with the child's own request record, read by the child's id
(/api/runs/<child id>/requests, under therequestscapability:
the child's prompt, never the parent's; a read-scoped token sees
hidden). A call joins its child by the child's id, which names the
step (<parent>/<step>/<call id>— call ids may repeat across steps),
in the story and the trace view's call detail alike; a child id of
another form falls back to the first unlinked call with its
parent_call_id. The run document'schildren[]rows carry each
child's ownholes; a child's usage shows once it ended ("usage at
finish" while it runs, "—" beside the interrupted badge). The runs table is "top-level only" by default with a toggle
(?subagents=all) and a?parent=filter chip; a child row links its
parent. The devtools panel's subagent badge opens the child inline,
one level: its row (agent, status, usage), its steps with its request
line, and an "open in Studio" hand-off carrying the child's id; a
grandchild is its badge and the hand-off only. A child opened while
it ran is read again when the parent's reload finds its status moved. -
Studio's run export (plan A7, A9's byte-faithful fixtures).
GET /api/runs/{id}/export?format=json|jsonl|otlp|wefttest, under a
newexportcapability in/api/meta, downloads the whole run
(Content-Disposition: attachment; filename="<run id>.<ext>", the
quoted name plain ASCII — a child's slashes, quotes, backslashes,
control and non-ASCII characters spelled_— plus RFC 6266's
filename*for a non-ASCII id; HEAD answers the headers alone).jsonis oneweft.run.export/1
document —run(as/api/runs/{id}),events(withgaps),
transcript(the batches as/transcriptserves them),
compactions,requests(the rows withrefs=1, pluspromptsand
catalogskeyed by hash: the record, or{hash, badge}),spans
andholes; every absent block carries its badge, reason and fix
(not_recordedfor a run written before ADR 0028,strippedfor a
content-off run,gapfor records a destination dropped).jsonlis
the same records one per line,{"record": <kind>, …}, in a stable
order (run, badges, events by pos, messages by index, compactions,
requests by index, prompts, catalogs, spans).otlpis
{"logs": ExportLogsServiceRequest, "traces": ExportTraceServiceRequest}
in OTLP/JSON (ids as hex): the records are rebuilt from what obsdb
reads, with the run's identity and metadata on each, the events'
weft.contentmarks, and one heartbeat at its last-seen, so
POST /v1/logsand/v1/tracesre-ingest the same run, its
stripped,truncatedandderivedholes included (an inferred
input flag is left unstamped, a derived prompt or tools record goes
out with its hash and an empty body — the malformed original is not
kept by obsdb). The log records' resource carries only
service.name: obsdb keeps no record's own resource, and the spans
keep theirs verbatim.wefttestis a zip of replay fixtures for
wefttest.Replay. A read-scoped panel token getsjson/jsonlwith
the request block{badge: "hidden"}(the transcript still badged
strippedfor a content-off run);otlpandwefttestare 403
with the hidden badge. An unknown format is 400, an unknown run 404,
a run with nothing to fixture 409 with its badge (stripped,gap
— no transcript, or a step whose request record was dropped — or
derived).
web/src/lib/api.tsgainsexportUrl(runId, format). -
Studio's replay fixtures key on the stored step and the request
record.POST /api/playground/fixturesand the wefttest export
share one builder: each fixture is the step its records were stamped
with (weft.step.index), not the Nth assistant message; its key takes
the tool names, thinking, tool choice and sequential flag from the
step's answering request record (the caller'stoolsonly for a run
written before the record); a step whose request carried a run-scope
compaction view keys on the compacted messages the model saw and
carries acompacted_atheader (index, step, range, hash); the
finish event carries the step's recorded reason, raw reason and
usage; tool-call signatures are kept; and the system prompt is
filled for the reviewer. One fixture per step, from its answering
attempt: failed retry attempts get no fixture, so a replay answers
the step's first call under any middleware.POST /api/playground/fixturesnow refuses a read-scoped panel token (403,
badgehidden: fixtures are request-derived) and answers "nothing to
fixture" with 409 and the badge, like the export (it was 400). -
Every hole is a badge from one closed table, on both surfaces
(plan A3, ADR 0028 §11).obsdb.HoleNote(h)holds each of the ten
holes' one-line reason and, where one exists, its fix — Studio's
routes read it (no second copy of the words), and its golden,
studio/testdata/holes.golden.json, is what the web's
lib/honesty.ts(the run page and the devtools panel's one table) is
checked against, key by key. The events route's rows and the live
record frames carryattrs, limited to theweft.content.*keys
(weft.contentwhen notfull—stripped, or the core's own
none— andweft.content.truncated_bytes), absent when the chain
left the content as emitted;obsdb.PosEventgainsContentand
TruncatedBytes(both backends read them; a ClickHouse row from
before migration 0004 has neither).GET /api/runs/{id}gains
holes: [{hole, reason, fix?}]— the run's own:not_recorded
(written before the request record),interrupted,derived(no
stored event: a row built from spans),stripped(a content-off
run) andgap(event positions missing, once the run is over). The
fold turnsattrsinto badges on the event, its call, its step and
the run ("shortened by the recorder: 12.3 KiB cut", "content not
captured by this app" with its fix); the run header, every step card,
the raw view's event rows and the panel's turn header and step lines
draw them; transcript words whose step has no events render as a
gaprow under the last step, and the replay playhead goes through
the transcript overlay (only steps finished at the playhead take
their final words), so holes survive scrubbing. The step route's
holes read its events' attrs too (stripped, truncated with the bytes
cut). The request routes' reasons are now the table's words.
redactedstays reserved: weft's pipeline does not mark a redaction.
studio/testdata/v0.9.0.dbis a database weft v0.9.0 wrote; Studio's
tests open it (migrated to the current schema) and every pane of its
run says why it is empty. -
Studio's step route (plan A7, with A4's attempts and A10's
children).GET /api/runs/{id}/steps/{n}(nthe step ordinal)
answers one step assembled server-side fromobsdb:status(ok,
error,parked,running),started/finished,latency_ms/
ttft_ms,model(requested, andanswered— the model that
answered undermw.Retry/mw.Fallback),request(the requests
route's row for attempt 1, prompt and catalog inline),attempts
(each request record joined to itsattemptspan on the attempt
number: model, provider,outcome,error_type,retry_after_ms,
times, span id, request index; attempt 1 falls back to thechat
span),messages_in(the request'smessages_refas a count),
events(as the events route serves them),tool_calls(args,
result with bytes and truncation, theexecute_toolspan, the child
run, pending),children(the child runs the step's calls started:
id, call id, agent, status, usage),usage,compaction(the
run-scope view the step's request carried) andholes— every hole
that applies, deduplicated, from ADR 0028 §11's table with a reason
and fix. A block missing for a reason carries its badge
(attempts_badge: not_recordedfor a run without attempt spans or
A4 timing,request: {badge: "not_recorded" | "gap"}). Scoped like
the events route; a read-scoped panel token reads the whole step with
request: {badge: "hidden", reason, fix}. A step past the run's
last, or a running run's next, is 404./api/metalists the new
stepscapability; the web client gainsfetchStepand the
StepDoctypes (no UI change yet).children[].costis omitted
until A5 adds costs (absent, not zero); the answering model and the
attempts' outcomes are read from spans, so a run recorded without a
tracer badges its attemptsnot_recordedwith the fix to install
one —obsdb.HoleNoteFor(h, cause)words it (CauseNoSpans), the
holes golden lists such causes under their hole. Each call is badged
strippedfrom its own events'weft.contenttoo, so a content-off
run written before the request record says so per call. With neither
content nor a tracer, a max_tokens step's calls are not listed; the
max_tokens hole says the step made some. -
Compaction in the record (ADR 0028 §8, plan A9.1). When a
PrepareStepsends a request whose messages are not the run's
transcript, the core emits onemessagesrecord with
weft.messages.reason = compactedright before that request's
record:weft.messages.from_seq/to_seq(the half-open range of the
transcript it replaced, by longest common prefix and suffix over wire
bytes),weft.compaction.scope = run,weft.compaction.hash, the
replacement messages as its body. The request'smessages_refnames
it; the next request names the growth records again. Nothing is
emitted when nothing changed or with capture off, and what the model
receives is unchanged.weft/threadreports every session compaction
(threshold,Compact,ApplyCompaction, a trim, the overflow
re-run) as one record of kindcompaction, emitted when it lands
under the last run that produced the compacted context — scope
session, the compaction entry's hash, the messages replaced →
summary entries and tokens before → after; no messages. Studio's live
lane never forwards a view, and its catch-up places messages records
by their stored index. Bothobsdbbackends keep the plain
transcript the growth records alone and read both through
DB.Compactions. -
(*core.Agent).LoggerProvider()(andweft.Agent's): the OTel
logger provider the agent's runs emit records through — the option's,
or the global one resolved atNew— for satellites whose records
must land beside the run's. -
studio --versionprintsversion.Runtime()and exits. -
Studio's request record routes (ADR 0028 §10, plan A1).
GET /api/runs/{id}/requests?step=&from=&limit=&refs=1: one row per
model-call attempt (index,step,attempt,time,system_hash,
catalog_hash,content—"",strippedorderived—,
truncated_bytes, the parsedbody), each row'spromptandtools
resolved inline by hash unlessrefs=1({hash, badge}when the
record is missing),next_fromwhile pages are full.GET /api/runs/{id}/tools:{catalogs: [{hash, tools, content, truncated_bytes}]}in index order. A run written before the record
answers both withbadge: "not_recorded"(and a reason and fix)
beside the empty list; a content-off run's tools answerbadge: "stripped". Both are refused to a read-scoped panel token (403 with
badge: "hidden"), as/api/manifestis. -
GET /api/runs/{id}(and every run row) gainsinstructions_hash,
catalog_hash,request_countand, for a run written before ADR
0028,requests_badge: "not_recorded". -
/api/metalists therequestscapability: the UIs gate the Request
pane on it. -
obsdb.TranscriptBatch,obsdb.TranscriptBodies,obsdb.DedupBatches
(ADR 0028 §8). -
obsdb.Holeand its ten constants (obsdb.Holes(), ADR 0028 §11's
closed badge table);obsdb.RequestQuery(the zero value reads every
step from index 0;Step *int,From,Limit,PageLimit()),
RequestRecord,RequestBody(and its parts),PromptRecord,
ToolsRecord,ToolEntry,StoredRecord,HoleError,
RunRow.RequestsHole(ADR 0028 §10's reading table). A request,
prompt or tools record whose body does not parse reads
obsdb.HoleDerived, its hashes from the record's attributes. -
The request record (ADR 0028). Three OTel log record kinds beside
event,deltaandmessages:request(one per model-call attempt:
step, attempt, system and catalog hashes, messages reference, tool
names, tool choice, thinking, params, model),prompt(the composed
system text, once per distinct hash per run) andtools(the offered
catalog with its policy chips, once per distinct hash per run). -
RunStart.InstructionsHash(instructions_hashon the wire, an
additive field): sha256 of the run's raw configured instructions,
always set by the loop; mirrored asweft.instructions.hashon the
run_startrecord and theinvoke_agentspan. -
ContentPromptandContentStop, theContentKindsotel's
Redactreceives for a prompt's text and a request's stop sequences. -
weft.Origin(name), a tool option naming the tools record'ssource
(localby default;Subagentsetssubagent,mcp.Toolssets
mcp). -
Model-call timing and the answering model (plan A4, ADR 0016's A4
note).StepFinish.LatencyMSandStepFinish.TTFTMS
(latency_ms,ttft_mson the wire, additive, omitted when 0): the
step's model call timed by the loop as it consumes the stream, whole
milliseconds rounded up;ttft_msis the firstTextDeltaor
ToolArgsDeltaand absent when neither arrived. Thechatspan gains
weft.stream(true),weft.ttft_ms(when a delta arrived) and
gen_ai.response.model; thestep_finishrecord gains
weft.latency_ms,weft.ttft_ms(when measured) and
gen_ai.response.model; a successfulattemptspan gains
gen_ai.response.model. The answering model is the last attempt the
chain reported as a success (mw.Retry,mw.Fallback, a reporting
adapter), else the model the call asked for.
Studio's step card and the panel's step line show it (A4.2): "attempt
4 of 4 · fallback to glm-b" and "1.2 s · first token 180 ms" from the
foldedstep_finishand the request rows (no fetch while collapsed),
the attempt list (model, outcome,retry_after, times) from the step
route on expand under thestepscapability, the same on the trace
view'schatspan, andnot_recordedon a run from before A4. -
The reporting hook (plan A8, ADR 0016).
weft.ReportFromContext(ctx)
returns the model call'sReporter:.Attempt(weft.AttemptInfo{…})
records one provider request as anattemptspan underchat
(provider, model,weft.attempt.index,weft.attempt.retry_after_ms,
error.typeor Ok) and, from the second attempt on, its own
requestrecord;.Raw(weft.RawPair{…})is accepted and dropped
(sizes on a Debug line). The reporter numbers attempts itself; a
layer that reports declaresReportsAttempts() booland an outer
reporting layer stays silent.mw.Retryandmw.Fallbackreport
every attempt. Outside a run's model call the reporter is a no-op. -
obsdbnames the phase-1 readers added above:CompactionRun,
CompactionSession,RecordCompaction,ReasonCompacted;
HoleCause,HoleCauses,CauseDefault;LogSkew,ParseSeverity,
SeverityText;RunDetail.{InstructionsHash,CatalogHash,RequestCount}
besideRunRow's; theRequestBodypartsRequestMessagesRef,
RequestModel,RequestParams,RequestThinking,RequestToolChoice,
RequestTools.
Fixed
Found by the phase-1 review of the request record (2026-10-08); none
changes what the model sees.
- core: a reported attempt that raced the model call's end could
still add arequestrecord; the record is now emitted under the
lockend()takes, so a late report adds none (ADR 0028 §7). A
cancelled resume whose transcript ended at the assistant message
(every call parked) records its rebuilt tool message like a resume
with a held tail does, so the records still concatenate to
RunError.Result.Messages. A further attempt that reports a model
is recorded under the provider it reported, even an empty one — a
fallback to another vendor no longer carries the primary's provider.
The agent's instructions hash is computed once atNew(two
allocations fewer per run). The Debug line for a contained recorder
panic names the panic's type, never its value. - otel:
capToolsencodes the catalog once instead of once per
dropped entry (a 400-tool catalog took 86 ms on the run goroutine).
ARedactpanic onparams.stopdrops the stop sequences only; the
request keepsmessages_ref.indexon a destination that received
the messages records. - obsdb: a run row with request records but no instructions hash
(therun_startbatch lost or not yet landed) reads recorded, and a
prompt or catalog its requests name but the store lacks is agap—
notnot_recordedwith "upgrade weft" (ADR 0028 §10's table gains
the row).weft.attempt.retry_after_msand the ten
weft.override.*fingerprint fields join the contract keys on both
backends (and 0004's tuples), so a non-string value no longer lands
in ClickHouse's run meta and not SQLite's. Amessagesrecord
without its index — growth or view — and an index attribute present
but empty read position -1 on both backends and stay out of the
transcript and the messages count (before, SQLite stored a growth
record at 0 where the input record dropped it and ClickHouse read it
first). A non-stringweft.messages.reasonreads as "not growth" on
SQLite as it does on ClickHouse. Two session markers with distinct
non-hex hashes keep distinct positions on SQLite (a 60-bit FNV
fallback). App-log lines equal in time, span, severity and body sort
by event name and attributes on both backends, so paging by
next_fromneither skips nor repeats a line. - thread: a compaction in a forked session no longer files its
marker under the origin session's run — it is held for the fork's
first run. A mid-run overflow's compaction keeps the earlier turn's
span context and metadata on its marker (the Session remembers the
last four runs it saw). After a reopen the marker carries
weft.turn,weft.public_idand the lineage pairs, not the session
id alone. A held marker that loses a race withCloseis dropped
with the "compaction markers dropped at close" Debug line. - version:
version.Runtime()maps a VCS-stamped pseudo-version
and a+dirtysuffix toVersion, as it does(devel), so a local
go buildreports the same version its runs stamp. - studio (server): a read-scoped token no longer sees tool names
through theinvoke_agentspan'sweft.override.tools,
weft.override.park_on,weft.override.park_all_except(and a named
tool_choice) onruns/{id}/spans,traces/{id}or the json/jsonl
export, nor a compaction view's messages in the export (nullwith
thehiddenbadge);/api/manifest's 403 carries the hidden badge
like every other prompt-bearing refusal; the fixtures route checks
scope before it decodes the body. The step route joins a child to
the step its id names (a call id repeated across steps no longer
borrows another step's child); a running step is no longer badged
not_recorded/gapfor a chat span that has not ended; a running
run's step without a storedstep_startreads running, not ok with
a gap;weft.model.tool_callsis clamped before it sizes a response;
a lone unnumbered request record merges into attempt 1 instead of
counting the call twice; amessages_refnaming a dropped view reads
gaponmessages_in. The run document and the export share one
rule for lost events (gapwhen requests or steps exist with no
spans,derivedwhen spans exist); one unreadable child no longer
fails the parent's document; export children rows carryholes;
the export's top-levelholesalso liststruncated, non-final
max_tokensand per-eventstripped. Every hole's fix comes from
obsdb.HoleNote/HoleNoteFor(two new causes:result_cap,
log_cap). The auth matrix gainsruns?all=1, a child's otlp/jsonl
export,steps/{bad}(403 wins), HEAD on every export format and
the span-attribute check. - studio (web, panel):
linkViewis idempotent (a re-render no
longer links one child to a second call with the same id); call
rows and trace span keys are step-qualified (c:<step>:<call>,
c:resume:<call>), so a repeated call id selects the right call and
React sees no duplicate keys in a resumed step 0 — old?sel=c:<id>
links no longer resolve; the step document is re-read while a step
runs and once the run ends, and its running-time holes are never
shown as final; the requests query re-reads from the first missing
index on the terminal refetch (an out-of-order row no longer leaves a
falsegap);applyTranscriptclones the steps it overlays (a late
delta no longer appends to transcript text); thederivedattempt
badge says what the server means and lists an unnumbered record by
its request index; the header and the panel share one status-hole
rule (statusHoles); "show original" reports a transcript read
error instead of loading forever;findCallcloses the most recent
open call; no bare "1 attempt".logs.test.tsreads the Go goldens
and a drift guard checks every step and logs golden against the TS
types.