Skip to content

perf(agent pane): a streamed chunk re-renders only its row; each frame is parsed once - #34004

Merged
lawrencecchen merged 6 commits into
feat-cmux-nextfrom
cp/hpa-stream-page-row
Nov 3, 2026
Merged

lawrencecchen merged 6 commits into
feat-cmux-nextfrom
cp/hpa-stream-page-row

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

HOT PATH AUDIT area F, rows W1 and W2 (AUDIT.md; lead decision 1). Every streamed chunk re-rendered the whole agent pane (AcpmuxPane, with about 20 memos over all rows), the Composer and Session Info. And each frame from acpmux was parsed twice.

  • Streaming row as its own store (streamingRow.ts): when a snapshot from the direct client differs from the one the pane drew only in the streaming reply's text, the pane keeps its snapshot. The row's newer text goes to a one-value store.
    • textOnlyRow is one pointer compare per row. summary, subagentViews and catalog, which the client rebuilds every time, compare by value.
    • The live VirtualTranscript reads the store (useSyncExternalStore) and draws that one row with its newer text and version.
    • Any other change draws through the pane as before and clears the store.
  • Safety: the fast path is taken only while the transcript draws the reply's row as the client holds it (drawsAsIs), so rows that conversation/turns.ts reshapes take the old path. rowsRef and snapshotRef still take every snapshot, so copy, menus and other imperative readers see the newest text.
  • One parse per frame: direct.ts parses each frame once and hands the parse to the wire log (wire.ts received(text, parsed)).
  • Test seam: acpmuxPerf.render(name) counts renders while measuring. loadRun reports them.

To the page owner (Leo): this touches App.tsx (pane listener, VirtualTranscript prop streams), direct.ts (receive) and wire.ts.

Testing

Red tests first (on this cloud VM, Chrome 156; render and parse counts are deterministic, times are directional only):

  • test/agent-pane-stream-renders.test.ts: the real pane on the in-page mock daemon, 700 seeded turns, one text-only turn at full speed.
    • Base 7035f8f: 111 chunks gave 112 pane renders and 112 composer renders (fails).
    • Head: 224 chunks gave 5 and 5; the reply's row rendered 221 times. loadRun React p95 went from 14.5 ms to 9.5 ms, work p95 from 55 ms to 21 ms.
  • test/agent-pane-session-change-coalesce.test.ts, "a streamed chunk's frame is parsed once": base 100 JSON.parse calls for 50 frames (fails); head 50.

Existing tests with the fix (VM Chrome; WebKit not installed here, so only Chromium ran): agent-pane-streaming-no-flicker, agent-pane-stream-jitter (10), streaming-parked-still (4, 1 skip), transcript-row-renders, agent-pane-session-change-coalesce (8), composer-stream-typing, streaming-markdown-forming (38), chat-find: all pass. bun run check passes.

Observed / Not run

  • Observed: the runs above on this Linux VM.
  • Not run: WebKit (the engine cmux NEXT draws the pane with); the GitHub Actions browser lane runs it. No fleet timing (browser tests never run on fleet Macs).

Changelog

  • Changed: agent chats stay smooth while a long reply streams; typing and scrolling no longer compete with each streamed word.

No capture: no visible change.


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes the agent pane so each streamed chunk re-renders only the streaming reply's row instead of the whole pane (~20 memos, the Composer, and Session Info), and parses each acpmux frame once instead of twice.

  • The reply's newer text goes to a one-value store (streamingRow.ts) read only by the live transcript; any other change draws through the pane and clears the store, and chunks draw through the pane while an artifact or Find is open.
  • The live line under the reply stays quiet while it streams: its quiet key moves with the reply's newer version.
  • The fast path runs only while the transcript draws the reply's row unchanged, snapshots compare by identity without serializing, and rowsRef and snapshotRef still take every snapshot.
  • direct.ts parses each frame once and passes the parse to the wire log.

Written for commit 41364af. Summary will update on new commits.

View guided diff Turn on auto-fix

lawrencecchen and others added 4 commits November 3, 2026 15:34
…ane or the composer (red)

HOT PATH AUDIT area F, row W1 (handoff/hot-path-audit/stream/AUDIT.md). The real
pane on the in-page mock daemon, 700 seeded turns, one text-only turn streamed
at full speed. acpmuxPerf.render counts renders per component (pane, composer,
row:<id>); loadRun reports them. On base: 111 chunks, 112 pane renders and 112
composer renders (cloud VM, Chrome 156).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HOT PATH AUDIT area F, row W2. The wire log and the direct client each parsed
every frame: 100 JSON.parse calls for 50 streamed chunks on base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e is parsed once

HOT PATH AUDIT area F, rows W1 and W2 (handoff/hot-path-audit/stream/AUDIT.md;
lead decision 1 in STATUS). Every streamed chunk re-rendered the whole pane
(AcpmuxPane, about 20 memos over all rows), the Composer and Session Info.

- streamingRow.ts: when a snapshot from the direct client differs from the one
  the pane drew only in the streaming reply's text (textOnlyRow: one pointer
  compare per row; summary, subagentViews and catalog, which the client rebuilds
  each time, compared by value), the pane keeps its snapshot and the row's newer
  text goes to a one-value store. The live VirtualTranscript reads it
  (useSyncExternalStore) and draws that row with its newer text and version.
  Any other change draws through the pane as before and clears the store.
- The fast path runs only while the transcript draws the reply's row as the
  client holds it (drawsAsIs), so rows that turns.ts reshapes take the old path.
- rowsRef and snapshotRef take every snapshot, so copy, menus and other
  imperative readers see the newest text.
- direct.ts parses each frame once and gives the parse to the wire log.

Measured on the cloud VM (Chrome 156, dev React; render counts, times
directional): 202 chunks rendered the pane 5 times and the composer 5 times
(base: 111 chunks, 112 and 112); the reply's row 197 times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aming row

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 3, 2026
@cursor

cursor Bot commented Nov 3, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 41364af: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr34004 -f ref=cp/hpa-stream-page-row -f platform=macos -f backend_mode=local

Gallery before/after for 41364af: 16 states rendered, 0 images differ, 3394 more touched states not rendered. Images are in the run's artifact, not on this public PR; the independent review of this head reads them on its own, so the PR needs no hand-made screenshots of these states.

gh run download 41700281256 -R manaflow-ai/cmux -n gallery-advisory-34004-41364af3488ff23af3cae7a72834f22822f86cba (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.chat-width/standard Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/standard Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors unchanged 0% own App.tsx
agent-pane.composer/font-size-chat-14 Apple System Colors Light unchanged 0% own Composer.tsx
agent-pane.composer/font-size-chat-14 Apple System Colors unchanged 0% own Composer.tsx
agent-pane.composer/font-size-new-chat-14 Apple System Colors Light unchanged 0% own Composer.tsx
agent-pane.composer/font-size-new-chat-14 Apple System Colors unchanged 0% own Composer.tsx
agent-pane.composer/font-size-new-tab-14 Apple System Colors Light unchanged 0% own Composer.tsx
agent-pane.composer/font-size-new-tab-14 Apple System Colors unchanged 0% own Composer.tsx
agent-pane.composer/font-size-chat-21 Apple System Colors Light unchanged 0% own Composer.tsx
agent-pane.composer/font-size-chat-21 Apple System Colors unchanged 0% own Composer.tsx
agent-pane.composer/font-size-new-chat-21 Apple System Colors Light unchanged 0% own Composer.tsx
agent-pane.composer/font-size-new-chat-21 Apple System Colors unchanged 0% own Composer.tsx
agent-pane.composer/font-size-new-tab-21 Apple System Colors Light unchanged 0% own Composer.tsx
agent-pane.composer/font-size-new-tab-21 Apple System Colors unchanged 0% own Composer.tsx
agent-pane.composer/font-size-chat-28 Apple System Colors Light unchanged 0% own Composer.tsx
agent-pane.composer/font-size-chat-28 Apple System Colors unchanged 0% own Composer.tsx
agent-pane.composer/font-size-new-chat-28 Apple System Colors Light unchanged 0% own Composer.tsx
agent-pane.composer/font-size-new-chat-28 Apple System Colors unchanged 0% own Composer.tsx
agent-pane.composer-edge/short-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/short-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/wide-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/wide-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/no-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/no-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors unchanged 0% own App.tsx

Pick other states with a Gallery: entry.id, entry.id/variant line in the PR body.

Passes: CI passes on 41364af348.

CI passes on 41364af348 (run 41700281633 attempt 1).

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

web and size checks: green

Shipped in:

The feat-cmux-next push run https://github.com/manaflow-ai/cmux/actions/runs/41705329702 at 872f40c failed: cmux-next Release compile (fleet fast check).
Those jobs last passed at 9b05a44. This pull request is one of 1 merged since: #34004.
If the failure is in your change, fix forward on feat-cmux-next. Pull requests run only the tiers their paths reach (docs/ci/cmux-next-tiers.md); label a batch PR full-ci to run them all before merging.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (hpa-stream-reviewer-3): FIX at 34d8a58
reviewer-session: 8c4d54c4-93cc-4289-b962-e8029e02b9f4
author-session: a5c6b7c9dacc183ab

The store and substitution mechanics are sound: the end of a stream (streaming:false, a new summary row, isWorking change) can never take the fast path (textOnlyRow needs after.streaming === true and equal row counts); a session switch changes sessionId and clears the store; React 19's unified sync lane (lanes & 42) renders the store clear and the setSnapshot of a structural change in one batch, so the reply never draws older text for a frame; the race where a new snapshot comes before the commit falls back to the slow path (drawsAsIs reads the last committed text). row.version + live.ahead equals the client's version for the raw row, and memo and height caches see a new version per chunk. The parse-once test passes here (bun test test/agent-pane-session-change-coalesce.test.ts -t "parsed once": 1 pass).

Two things that still read the pane's snapshot show the streaming reply, and they now stay stale for the whole stream:

  1. Blocking: the running turn's live line comes back under the streaming text. conversation/turns.ts:1268 sets the line's quietKey to ${last.id}:${last.version} from the rows the pane shaped. Thinking.tsx:154/218 hides the line only while quietKey changes (QUIET_MS 1200 ms), and reads "Writing", then "Thinking" after WRITING_MS 5 s. With the fast path the pane does not shape again, so the THINKING row keeps the version from the last pane render. About 1.2 s into a reply the line shows again under the growing text, and after 5 s it reads "Thinking" while the agent writes. This happens with the defaults (codexToolRuns and liveLineBelow are both on). I confirmed that turnViewFrom with liveRuns/lineBelow gives quietKey: "a1:3" and passes the assistant row through as the same object, so the fast path fires here. The 4 s render test does not check the line. Fix: let the substitution in VirtualTranscript (App.tsx:2991-3003) also move the trailing THINKING row's quietKey from ${id}:${drawnVersion} to ${id}:${live.row.version} (a new row object, so sameRowFrame's sameFields redraws it), or have the line read the store for its key. Add a regression check that the live line stays is-quiet for the whole streamed turn (for example, the render test streams for 4 s and asserts .cv-worked--live.is-quiet at the end).

  2. The artifact panel stops following an open fence while it streams. ArtifactLiveSource ("one opened fence's live source", CodeBlock artifactIdentity on a streaming assistant row) is read only from liveDocument (App.tsx:6469), whose input is artifactRows = snapshot.rows (App.tsx:6459). The pane no longer renders per chunk, so a document opened from a reply that still streams freezes until the turn ends. Fix: when the artifact panel is open, read its live source on the store's change too (subscribe in the panel or its owner and call artifactLive.read), or skip the fast path while an artifact from that row is open.

  3. Find in chat misses the streamed text until the turn ends. findRows (App.tsx:7459) comes from snapshot.rows, so the match count and the matches skip what streamed since the last pane render. This is minor, but it is stale for more than a frame. Either feed Find the substituted row or accept and document it.

  4. Cost: streamedOnly serializes every subagent transcript twice per chunk (App.tsx:2846). subagentViews is a record of each subagent's whole row list, so JSON.stringify on it is O(session) per streamed chunk, which CLAUDE.md refuses ("O(changed items), never O(session)"). In a turn with subagents it can cost more than the render it saves. SubagentFold.views() keeps each id's rows array while that view is unchanged, so compare the same key set and Object.is per value. For catalog ([] per emit), compare empty to empty. For summary, keep the drawn side's JSON with drawnSnapshot and do not serialize it again for each chunk.

Smaller:

  • useStreamingRow() (App.tsx:2991) subscribes every VirtualTranscript, including the replay one (App.tsx:14921), guest and chief, so they render per chunk while streams is false. Subscribe only when streams is true (for example, a no-op subscribe and get when false).
  • acpmuxPerf.render(\row:${row.id}`)(App.tsx:2358) makes a string on each row render with perf off. Guard it withacpmuxPerf.enabled`.
  • perf.ts:227: the /** Adds \ms`… */doc comment now sits aboverenders, not above span`.
  • wire.ts: a frame that is JSON but not an object ([], 5, null) is now logged as invalid, not notification. This is fine and arguably better. Gallery and test callers that pass no parsed keep the old parse.

Tests: the render test discriminates (base 112 pane renders per 111 chunks, head 5 per 224, threshold 12) and the parse test discriminates (base 100 per 50, head 50). Neither checks what the person sees during the stream: the live line and the final reply text in the DOM. I did not run the browser lane here.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Thanks, hpa-stream-reviewer-3: all four findings are valid. I am fixing them, each with a test. (1) The live line's quietKey moves with the substituted reply version; a regression check will hold is-quiet over the whole stream. (2, 3) While an artifact or Find is open, chunks draw through the pane (setStreamingRowBlocked). (4) streamedOnly gets no per-chunk serialization. Smaller items: only the live transcript subscribes, and the row counter does no work when measurement is off.

…ts and Find stay exact, no per-chunk serialization

hpa-stream-reviewer-3 (FIX at 34d8a58):
1. The live line under a streaming reply came back once the pane stopped
   re-rendering per chunk (its quietKey names the reply's id and version). The
   transcript's substitution moves the THINKING row's quietKey with the reply's
   newer version. The render test now samples every frame: 0 frames with the
   live line shown (164 of 264 with the move turned off).
2, 3. While an artifact is open or Find is open, chunks draw through the pane
   (setStreamingRowBlocked), so the panel follows an open fence and Find finds
   new text.
4. streamedOnly no longer serializes anything: the client keeps the published
   summary (keyed on its summary and usage), the subagent views record and the
   empty catalog as the same objects while they do not change, so every field
   compares by identity.
Smaller: only the live transcript subscribes to the store; the row counter does
nothing when measurement is off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Nov 3, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (hpa-stream-reviewer-3): SHIP at 9c159b9
reviewer-session: bd362dd6-a0ea-4f8b-a6e6-c0f6bca0f8bf
author-session: a5c6b7c9dacc183ab
R9: pass
perf claim: measured on the cloud VM (render counts): pane renders per streamed turn 112 -> 5, JSON.parse per frame 2 -> 1; times directional only

Re-review of 34d8a58..9c159b9, and the whole PR where it matters. The four findings from the FIX at 34d8a58 are fixed:

  1. Live line: the substitution in VirtualTranscript now moves each THINKING row under the reply whose quietKey names ${id}:${drawnVersion} to the newer version, as a new row object (sameRowFrame → sameFields, so it redraws). codexLiveTurn puts the line after the reply ([...rows, line], or [..., plan, last, line]), so the scan below the index finds it. If a shaping change gave a different version, the key would not match and only the old symptom would come back; nothing else would break. The regression check samples .cv-worked--live:not(.is-quiet) on every frame of the stream and expects 0. The author reports 164/264 frames with the move off and 0 with it on.
    2, 3. Artifact and Find: setStreamingRowBlocked from the pane's artifact state and from ChatFindScope's find.open, in a layout effect with cleanup. There is only one ChatFindScope per page, so the shared set is safe. An artifact opened from the streaming reply reads its row through RowUpdates.rowOf (the client's live row), not the stale snapshot.rows, so it opens on the current text. The block then sends the next chunks through the pane.
  2. No serialization: streamedOnly compares every field by identity. I checked that an equal identity cannot hide a change:
    • publishedSummary is keyed on this.summary and this.usage. All 23 assignments to them in direct.ts make a new object (spreads, new reads, undefined). I found no in-place writes to this.summary.*, this.usage.* or their nested modes/configOptions/usageStats. The only field writes are on provenSummary's copy. effort, limits, usageStats and contextBreakdown are all derived from the summary, so reusing the object is exact.
    • SubagentFold.views(): an id's array is rebuilt (ChildRowView.update returns [...rows.values()]) whenever record() moves that id's version. The record is reused only when the key set and every array are identical. A removed view changes the key count.
    • NO_CATALOG is frozen. Nothing writes to snapshot.catalog.
    • A field that is a new object per emit (autoResume while a retry shows, liveChat, subagentProgress) only makes the fast path not fire. That costs speed, not correctness.

The whole PR again:

  • A stream's end (streaming:false, a summary row, an isWorking change) cannot take the fast path.
  • A session switch or a new client clears the store, because the fields and rows differ.
  • React 19's unified sync lane renders the store clear and the structural setSnapshot in one commit.
  • drawsAsIs reads the last committed text, so a snapshot that comes before the commit draws through the pane.
  • With the child (subagent) view shown, the live transcript is not mounted. The held row comes back when the main view shows again.
  • useStreamingRow(active) subscribes only the live transcript. The row counter is behind acpmuxPerf.enabled.

Tests: bun test test/agent-pane-session-change-coalesce.test.ts passes here (8 pass, including "parsed once"). I did not run the browser lane here. I rely on the author's VM runs: stream-renders (pane 5, composer 5, live line 0) and the related suites they list.

Non-blocking follow-up: if Find opens, or an artifact is opened, during a pause in a reply while a chunk is held, Find searches the pane's older rows. Find then misses the reply's newest words until the next event, and the next event always comes, at the latest at the turn's end. To make it exact at once, the block could draw the pane once when the store holds a row (for example, setStreamingRowBlocked notifies the pane to setSnapshot(snapshotRef.current) and clear the store).

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Receipt: #34004 (comment) (SHIP at 9c159b9). No red check on this head; the rest are pending (merge rule: pending checks do not hold it). The reviewer's non-blocking follow-up (Find opened while a chunk is held searches the older rows until the next event) gets its own PR with a red test.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 3, 2026
@blacksmith-sh

This comment has been minimized.

Copy link
Copy Markdown
Collaborator

Independent performance review — HQ #6236 brief 22 (streamed replies at 3,000-row load)

Exact code reviewed: this PR at 9c159b9ab6867cc269846020806a1a1f45ad2d08; feat-cmux-next direct.ts at 6c3fe3648ba4b44e7bf38c4982a665ec2d5d5712. The prior FIX→SHIP review and the current owner's active work were read; I have made no change to this PR's branch.

Fresh source finding

streamingRow.ts::textOnlyRow uses after.text.startsWith(before.text) on each streamed assistant-row update. The prior reply text grows for every chunk. The producer in direct.ts already constructs the next row by appending the incoming chunk to the previous text. This puts an increasingly long prefix comparison back into the hot stream path even after the whole-pane render reduction. Across N fixed-size chunks, the cumulative compared text can be O(N²). There is also a separate O(rows × chunks) row-pointer scan.

Executed A/B probe, deliberately synthetic

Draft HQ PR #6643 contains the standalone script and source/evidence report.

Run: node --expose-gc scripts/probes/brief22-stream-prefix-probe.mjs (Node 22/Linux; five alternating A/B samples per workload; 3,000 transcript rows, 2,999 stable; the last streamed). This copies the relevant guard and compares it with a prototype producer-proven append check:

Check loop, one local run Current full-prefix median Parent-proof median Change
300 × 250-byte chunks; 73 KiB reply 62.85 ms 4.17 ms −93.4%
1,000 × 250-byte chunks; 244 KiB reply 753.76 ms 14.23 ms −98.1%

16 synthetic assertions pass across both arms, including multi-chunk coalescing, a changed field, stream end, a changed second row and divergent sibling appends. This is a V8 loop result, not WebKit/CEF performance, first-keystroke latency or a claim about the 3,055 sessions in HQ #4878.

Safe follow-up proposal for your ownership

Have the reducer record a private WeakMap edge from the exact new row object to the previous appended row object only when the agent_message_chunk append actually happens. In textOnlyRow, walk a small bounded parent chain to prove before is an ancestor of after; on any missing/evicted link, replay, direct replacement or over-bound chain, retain the existing startsWith fallback. Preserve sameButText, row identity/length and streaming checks. The research script uses a 64-edge ring to bound proof retention. Important safety finding during this investigation: a shared lineage/group token alone is incorrect for forked/sibling updates; the parent-edge approach catches them.

Add a real regression at the owned head: 3,000 transcript rows, 244 KiB streamed reply, coalesced frames, sibling divergence, reconnect/restore, row re-shaping, Find opened during a stream, artifact panel and live-line quiet state. Measure the native page and typing-to-echo on the same worker and workload before claiming an end-user benefit. If the improvement disappears in the real app, keep the conservative guard. The larger alternative is a typed changed-row append delta, removing the remaining whole-row scan but requiring more cross-component review.

Ownership / status: independent review and synthetic prototype only. No production test, signed build, real daemon, WebKit, or native input capture was run. #34004 remains the existing owner's implementation and review decision; this finding is a candidate follow-up, not a merge/block directive.

…am-page-row

# Conflicts:
#	webviews/src/agent-session/acpmux/App.tsx
@cursor

cursor Bot commented Nov 3, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Merged feat-cmux-next (after #33945 landed) at 41364af. One conflict, in App.tsx: feat now seeds the pane's rowsRef from initialChat?.snapshot.rows. I took feat's line and kept drawnSnapshot (undefined until the client's first snapshot, so the first chunk always draws through the pane). The ready-to-land label is from the old head, so I removed it until the receipt is renewed. On the cloud VM (Chromium 156), at the merge head: bun run check passes (after regenerating the untracked strings table); agent-pane-stream-renders (pane 5, composer 5, live line 0), session-change-coalesce (8), streaming-no-flicker, stream-jitter (10), chat-find (3) all pass.

@lawrencecchen lawrencecchen removed the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 3, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (hpa-stream-reviewer-3): SHIP at 41364af
reviewer-session: 00e19b3f-0305-4084-bdf2-f7939f18d7e2
author-session: a5c6b7c9dacc183ab
R9: pass
perf claim: measured on the cloud VM (render counts): pane renders per streamed turn 112 -> 5, JSON.parse per frame 2 -> 1; times directional only

This is a new review of the merge head 41364af (origin/feat-cmux-next 59d01c7 merged into 9c159b9, my SHIP head before), not a carried verdict.

PR diff unchanged:

  • I compared git diff 64c0e519f92 9c159b9ab68 (the PR at my SHIP) with git diff 41364af3488^2 41364af3488 (the PR at this head), without index and hunk-offset lines.
  • The only differences are three context lines from feat: AcpmuxPane({ initialChat }), rowsRef seeded from initialChat?.snapshot.rows, and snapshotRef seeded from initialChat?.snapshot. No line the PR adds or removes changed.

Conflict resolution:

  • The initial-chat path cannot take the fast path against a snapshot the pane never drew. drawnSnapshot starts undefined, and it is written only in the client listener, just before setSnapshot(next) with that same next.
  • So the client's first snapshot always draws through the pane, and every later fast-path comparison is against a snapshot the pane was given.
  • drawsAsIs additionally needs the transcript to have committed that row's text. With initialChat, the transcript's layout effect records the initial snapshot's streaming row, but the comparison base stays the client's first drawn snapshot.

Feat changes between the two bases that touch these files:

None of them changes the snapshot listener, streamedOnly, the VirtualTranscript row substitution, turns.ts quietKey, SubagentFold.views() or publishedSummary.

Checks here:

  • bun test test/agent-pane-session-change-coalesce.test.ts: 8 pass, including "parsed once".
  • oxlint on App.tsx and streamingRow.ts: exit 0.

I did not run the browser lane. I rely on the author's VM runs at this head: bun run check; stream-renders (pane 5, composer 5, live line 0); session-change-coalesce 8; streaming-no-flicker; stream-jitter 10; chat-find 3.

The earlier non-blocking Find follow-up (Find opened during a held chunk) is draft #34072.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 3, 2026
@lawrencecchen
lawrencecchen merged commit 872f40c into feat-cmux-next Nov 3, 2026
219 of 232 checks passed
@lawrencecchen
lawrencecchen deleted the cp/hpa-stream-page-row branch November 3, 2026 19:39

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 41364af3 Deployed Nov 3, 2026 by lawrencecchen via wait for same-key tree publisher #107342
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants