fix(cli): render tool results when cards settle, not at turn end - #3526
fix(cli): render tool results when cards settle, not at turn end#3526Astro-Han wants to merge 2 commits into
Conversation
c35b556 to
a789d4f
Compare
Live tool_start/tool_result broadcast frames carried no args or result content, so a transcript card could only settle into a '(no output)' placeholder and wait for the turn-terminal durable backfill. Terminal scrollback cannot be rewritten, so any card scrolled out of the viewport before the backfill kept the placeholder forever (#3521). The runtime already holds the full result when it emits the event (persist-then-broadcast). Project it into the live frame, bounded so the frame stays under the subscription frame cap: args up to 16 KB, result content up to 48 KB; an oversized result degrades to its serialized byte size (contentBytes) so the row stays truthful. The wire keeps rejecting raw provider error text and thinking signatures. Generated-by: Maka (Kimi K3)
With content now riding live tool_result frames, a result that exceeds the 48 KB live budget arrives as an empty placeholder plus contentBytes. Render the truthful byte count in the compact row (instead of the '(no output)' lie) and a dim note in the expanded card; the terminal reconcile still heals the full body from the durable transcript at turn end, clearing the placeholder. Generated-by: Maka (Kimi K3)
a789d4f to
7aa64d2
Compare
jackwener
left a comment
There was a problem hiding this comment.
Independent review of 7aa64d298. Did not treat Astro-Han authorship as evidence.
-
Problem. Live
tool_start/tool_resultframes had no args or result. Cards settled to(no output)and terminal scrollback froze that lie before the durable backfill. -
Solution. Persist-then-broadcast already has the result; project it onto the live frame. Args ≤16 KB, content ≤48 KB (frame cap 64 KB−1). Oversized args omitted; oversized content becomes
contentBytesso the row showsN bytesinstead ofno output. Durable reconcile still replaces the placeholder at turn end. Error text and thinking signatures stay rejected. -
Timing. Events are keyed by
toolUseId; later tools do not overwrite earlier cards.contentandcontentBytesare mutually exclusive on the wire. Reconcile clearsresultByteswhen real text lands.
Privacy: live frames now carry the same args/results the durable transcript already exposed; tool_output_delta already streamed output. Size bounds keep the existing cap.
Approve. CI test was still running on this head; merge stays with the author.
Third independent line found P1: decodeLiveToolResultContent() only checks kind then casts. Malformed or unknown kinds can pass the wire. Dismissing this APPROVE; head 7aa64d2 is not merge-ready.
Summary
Fixes #3521 — TUI tool cards settled into
(no output)and, once scrolled out of the viewport, stayed that way forever; Ctrl+O/Ctrl+T could not touch them.Root cause: the live
tool_start/tool_resultbroadcast frames carried no args and no result content. A card therefore settled into the empty placeholder, and the only content path was the turn-terminal durable backfill — which arrives after terminal scrollback has already frozen the card's lines (#1139), and the viewport-anchored toggles (#1130) then lock the stale render in place.The fix makes live events self-sufficient, which is the design the immutable-scrollback medium requires: the card must be renderable at settle time, before it can scroll away. The runtime already holds the full result when it emits the event (persist-then-broadcast in
tool-runtime.ts), so the Host now projects it into the live frame instead of dropping it:tool_startframes carryargs(bounded to 16 KB serialized; oversized args are omitted, matching today's mid-turn placeholder for that rare case).tool_resultframes carry the settledcontent(bounded to 48 KB serialized, keeping the whole frame under the 64 KB subscription frame cap); an oversized result degrades to its serialized byte size (contentBytes) so the row stays truthful instead of claimingno output.{ kind: 'text', text: '' }substitution and passes both fields through; the TUI settles cards with the real result, so the freeze now preserves a correct render. Oversized results render asN byteswith a dim note in the expanded card, and the existing turn-terminal reconcile (kept as the backstop for shed/oversize edges) heals the full body at turn end.What deliberately did NOT change: raw provider error text and thinking signatures remain rejected on the wire; the desktop merge paths are untouched (live content simply reaches its projection earlier); the viewport-anchored expansion toggles stay as designed — with content correct at settle time, the "earlier cards show nothing" half of the issue disappears, and the remaining "cannot retoggle scrollback" half is the documented #1097/#1130 tradeoff.
Verification
Same scripted driver, same four mid-turn cards, captured through
runMakaPiTui+FakeTerminal.Before (main) — cards settle to
(no output)/ duration-only, with no input target:After (this PR) — settle-time facts: invocation target + outcome, including a truthful oversized-result size instead of a
no outputlie:tool_resultbroadcasts content,tool_startbroadcasts args, oversized args/content degrade to omission/contentBytes(frame re-decodes under the subscription cap), the adapter projector passes them through, and the TUI renders100000 bytesrather thanno outputuntil the terminal reconcile heals the body.args/contentare now accepted (bounded), while raw providererrortext and thinkingsignatureframes remain rejected.@maka/runtime-hostworkspace suite: 1075/1075 pass.@maka/cli: 374/374.@maka/ui: 201/201.tscbuilds andbiome format:checkare clean.Security
This relaxes a deliberate boundary: live subscription frames were privacy-normalized to exclude tool args/results. The same data was always available to the same clients through the durable transcript read path, and
tool_output_deltaalready streams raw output live; this change removes the asymmetry. Per the maintainer's call on #3521, that normalization is not a priority for this channel. Bounded sizes keep frames under the existing cap.AI use
Tool(s) and scope: Maka (Kimi K3) diagnosed the root cause, designed the wire shape, and wrote the implementation and tests. Commits carry
Generated-bytrailers.Checklist
Does this PR entail a change in behavior?