fix(console): the turn timeline waited on every body in the turn (v0.7.3) - #195
Merged
Conversation
added 2 commits
August 17, 2026 17:02
Opening an Agent Turn felt slow because the panel would not paint until the calls list arrived body-bearing. Nothing above the call cards reads a body: the timeline, the stat cards, the agent breakdown and every collapsed card are built from scalars. Measured against a production store, that list is 2-20 MB and 0.4-4.8 s where the same list with `?lite=1` is 16-60 KB and ~10 ms, and the browser then parsed those megabytes on the main thread. Paint off the small shape; fetch bodies afterwards, as a background upgrade for the three views that do derive from them (timeline call-type icons, StatsCards type counts, tool index). Over the 50-call threshold it was worse than slow, it was waste. Whether to ask for bodies came from `call_count`, which arrives from a different request — so on mount the answer defaulted to yes, the body-bearing fetch went out for every turn, and it was abandoned a few ms later when the count came back over the threshold. `apiFetch` passed no AbortSignal, so abandoned still meant downloaded, parsed and cached: a 102-call turn pulled 20.6 MB it could never render. Confirmed with a real browser against a live instance, before and after — 20.61 MB of spans traffic to open that turn, now 0.06 MB. The span and body endpoints now pass the query's signal, so a panel closed mid-download stops the download. That matters more now that bodies are fetched in the background, not less: clicking through turns would otherwise leave one abandoned multi-MB fetch running per turn.
There was a problem hiding this comment.
I've completed my investigation. Here's my review.
Summary
Two commits: (1) a console perf fix that splits the turn-calls list fetch into a cheap ?lite=1 paint path plus a background body-bearing upgrade, and threads the TanStack Query AbortSignal through apiFetch so abandoned multi-MB fetches actually abort; (2) a version bump to v0.7.3. The change is console-only, well-documented, and the version bump is consistent across VERSION / Cargo.toml / package.json / CHANGELOG. The core logic is sound and the perf/behaviour claims hold up against the surrounding code. I found no blocking defects; a few non-blocking observations.
Blocking
None.
Suggestions
- console/src/pages/agent-turn-detail-panel.tsx:235 — The body-bearing upgrade query runs even when the caller has already opened a full-body per-call detail:
bodiedCallsanduseLlmCallDetail(fired byCallCardon expand, gated separately per card) fetch the same body data in parallel, andcalls.findat :251 references the array before the upgrade lands. Not a bug (the upgrade purely improves the tool index/type counts), but with a<50-call turn where the reader clicks into a card immediately, the browser holds two near-identical body fetches. Consider keying the upgrade off whether any card detail is open, or letting it share an entry withuseLlmCallDetail. - console/src/hooks/use-agent-turns.ts:86 / console/src/lib/api.ts:36 —
fetchaborts onAbortSignalbut TanStack Query REST-query culture often usesretry; retries of an aborted fetch could re-thrash. TanStack Query handles abort as terminal (no retry) here, so this is only worth a second look if you later add.retryto these queries. - server/h-api/src/routes/traces.rs:317 — unrelated to this diff, but worth a glance: the lite/full split decides bodies on the per-item NULL-ing path (
lite==0). Since the panel now always paints off?lite=1even when it later fetches bodies, the "bodies reachable per-card" promise held byuseLlmCallDetailand?liteonly holds if/api/spans/{id}bypasses the lite path — confirmed elsewhere in this file (include_bodies).
Questions
- The
enabledgate for the bodied query at :235 isturn != null && !liteMode. SinceliteModederives fromturn.call_count, this still races the lite response against the detail response — is firing the body fetch beforecall_countarrives (then cancelling via signal) the intended behaviour, or shouldenabledalso wait on the lite list being present?
Verified
useAgentTurnCallscallers: onlyagent-turn-detail-panel.tsx:233,235— both new calls match the new(id, lite, enabled)signature; no other consumers to break.- No remaining
apiFetchcall sites pass a different arity — the third-argoptsis optional and backward-compatible; the two hook call sites (use-agent-turns.ts:86,use-llm-call-detail.ts:10) anddownloadFileall remain valid. - Query keys:
["agent-turn-calls", id, lite]correctly includeslite, so the lite vs. full shapes live in distinct cache entries and don't clobber each other. - Version bump consistent:
VERSION,server/Cargo.toml:11,console/package.json, and CHANGELOG header0.7.3all agree; the two commits are cleanly separated (console fix commit touches only console + CHANGELOG; bump commit only the four version files). - No sensitive-content risk: the only changed repo file touching URLs/config is console TS; no new IPs, hostnames, key material, or paths were added in the diff.
- Relevance check for the "window-width" and "body-scan" gotchas: no SQL / body-window aggregation was touched in this PR (console-only).
Recommendation: APPROVE.
🤖 Reviewed by the review bot • workflow run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Opening an Agent Turn made the detail panel wait on every request and
response body in the turn. Nothing the reader is looking at reads a body: the
timeline, the stat cards, the agent breakdown and every collapsed call card are
built from scalars.
Measured against a production store, over a LAN, from a browser:
/traces/{id}/spans?lite=1/spans(with bodies)The panel waited for the right-hand column, then parsed those megabytes on the
main thread. The storage backend is not implicated — it answers the small shape
in 6–16 ms, and still does while a body-bearing request for the same turn is in
flight.
The second defect: over the threshold it fetched the bodies anyway
CALLS_LITE_THRESHOLDexists precisely so a large turn does not fetch bodies.But whether to ask for them was derived from
turn.call_count, which arrivesfrom a different request — so on mount the answer defaulted to "yes" and the
body-bearing fetch went out for every turn, to be abandoned a few milliseconds
later when the count came back over the threshold.
apiFetchpassed noAbortSignal, so abandoned meant the browser downloadedit, parsed it, and held it in the query cache for the gc window. The threshold
was read after the fetch it was meant to prevent.
Fix
Two fetches of the same list; the panel paints off the cheap one.
?lite=1isunconditional. The body-bearing shape is requested only once
call_countisknown and under the threshold, and then only as a background upgrade for the
three views that do derive from bodies — the timeline's call-type icons,
StatsCards' tool/text/final counts, and the tool index. Above the threshold it
never lands and those degrade, exactly as they already did.
The span and body endpoints now pass the query's
AbortSignal. That mattersmore after this change than before it: bodies are now fetched in the background,
so clicking through turns would otherwise leave one abandoned multi-MB download
running per turn.
Verification
A real browser against a live instance, before and after, recording what the
panel actually requests:
tsc -band the production build pass;bun test136/136; lint is unchangedfrom baseline.
Not fixed here
The three body-derived views still degrade above the threshold. The proper fix
is to classify server-side at write time, the way
tokens_estimatedandapp_hintalready are — that is a change across all storage backends, not aconsole one.