Skip to content

perf(client): streamed text no longer republishes thread queue and checkpoints - #27425

Merged
juliusmarminge merged 6 commits into
mainfrom
fix/issue-27325
Nov 11, 2026
Merged

juliusmarminge merged 6 commits into
mainfrom
fix/issue-27325

Conversation

@juliusmarminge

Copy link
Copy Markdown
Member

Problem

While a turn streams, providers that send message.updated on every tick (ACP agents including the dev Replay provider, Cursor, OpenCode) give the client's thread projection a new messages array each tick. Two selectors in packages/client-runtime/src/state/threadDetail.ts, the thread queue workflow and the checkpoint summaries, re-derived and published a new value with identical contents on every tick. Every subscriber that reads only these selectors (the queue sheet on mobile, the diff panel and chat view on web) re-rendered on every streamed update for nothing. The issue measured streaming on iPad with the Replay provider and names this per-update thread publication as one of the causes.

Change

Both selectors now keep the value they last published when the new value is equal, and publish a new one only when something they derive actually changes.

  • Queue workflow: compares activeRun, the four flags, and each queued entry's run and message by identity. A queued entry's text, attachments and context all come from those two.
  • Checkpoint summaries: if the checkpoints array is the same, the summaries are the same, except for which assistant message each one points at. So a new value is published only when the checkpoints change or a summary's assistantMessageId does.

Scope and approval

Fixes #27325

Triaged via the automated issue flow. This PR covers the per-update republication of these two selectors. The issue's other items are left for follow-up: the work-row shimmer, full markdown re-parse on each update, Liquid Glass compositing, the route-level thread state subscription, and idle display links.

Verification

  • packages/client-runtime/src/state/threadDetail.test.ts, derived thread state through a streamed message: the tests apply streamed message.updated text ticks and check that the queue workflow and checkpoint summaries keep their identity. They also check that a queued message edit, a new assistant message for a checkpointed run, and a new checkpoint each still publish a new value. Removing the assistantMessageId comparison makes the checkpoint test fail.
    • Before (tests on fa4e4677c0, fix not applied):
      FAIL … > keeps the queue workflow through streamed text and follows a queued message edit
      AssertionError: expected { activeRun: null, …(5) } to be { activeRun: null, …(5) } // Object.is equality
      FAIL … > keeps checkpoint summaries through streamed text and follows a new checkpoint
      AssertionError: expected [ { …(9) } ] to be [ { …(9) } ] // Object.is equality
      Tests  2 failed | 14 passed (16)
      
    • After (this branch, threadDetail.test.ts and threadWorkflows.test.ts): Tests 38 passed (38).
  • vp run check:changed passes.
  • No visible surface changes, so there are no captures. These tests show the selectors no longer republish. They do not measure power. On mobile, the composer hook still subscribes to the whole thread, so it still re-renders on each update.
  • cannot-verify ios-device: the issue's power measurement needs a physical iPad with battery telemetry. The mechanism is shared client-runtime logic and is covered by the tests above.

Claude Opus 5.5 (Claude Code) and GPT-6.1 Sol (Codex), via the T3 Code issue flow.

🤖 Generated with Claude Code

juliusmarminge and others added 4 commits October 30, 2026 06:45
…checkpoint state

These tests reproduce #27325. A streamed assistant
message.updated that only grows the message text makes queueWorkflowAtom
and checkpointSummariesAtom publish a new value. Neither derived value
changed, but the composer and queue subscribers wake on every tick.

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

Providers that stream assistant text through message.updated replace the
messages array on every tick. The queue workflow and checkpoint summary
selectors re-derived a fresh value each time, waking the composer and
queue control. Both now keep their previous value when what they derive
is unchanged.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 30, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 093810b

Macroscope's review found this PR approvable — This change avoids redundant queue and checkpoint subscriber updates while assistant text streams. Its effects are confined to client-side derived-state publication, with no changes to product defaults or persisted data.

No code changes detected at dd1e609. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

Queue workflow and checkpoint summary atoms retain cached values when their derived state is unchanged. Tests cover identity retention during streamed updates and updates after queued-message edits, new assistant messages, and captured checkpoints.

Changes: Thread state identity

Layer / File(s) Summary
Retain unchanged derived state
packages/client-runtime/src/state/threadDetail.ts, packages/client-runtime/src/state/threadDetail.test.ts
The queue workflow atom compares active runs, workflow flags, and queued run and message identities before replacing its cached value. The checkpoint summary atom retains its array when checkpoint references and assistant message IDs remain unchanged. Tests cover identity retention and state updates.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: t3dotgg

Merge Risk: Minimal · up to dd1e6

The selector change is mergeable after normal checks. Its effect on iPad power remains unmeasured, but no correctness regression is established.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title clearly and concisely describes the main performance change: streamed text no longer republishes thread queue and checkpoint selector values.
Description check Passed The description includes all required sections. It explains the problem, the implementation, scope and issue reference, focused verification results, limitations, and follow-up work.
Linked Issues check Passed Direct issue [#27325] identifies per-update thread-state publication as a streaming cost and supports focused fixes. This PR retains the queue workflow when activeRun, flags, and queued run/message …
Out of Scope Changes check Passed The diff changes only the queue workflow and checkpoint-summary selectors in packages/client-runtime/src/state/threadDetail.ts and adds focused tests in `packages/client-runtime/src/state/threadDeta…
Full details: Linked Issues check

Explanation

Direct issue [#27325] identifies per-update thread-state publication as a streaming cost and supports focused fixes. This PR retains the queue workflow when activeRun, flags, and queued run/message identities are unchanged. It retains checkpoint summaries when checkpoints and derived assistantMessageId values are unchanged. Tests cover streamed text, queued message edits, new assistant messages, and new checkpoints. The issue does not require this PR to implement its other contributors or provide physical-device power measurements.

Full details: Out of Scope Changes check

Explanation

The diff changes only the queue workflow and checkpoint-summary selectors in packages/client-runtime/src/state/threadDetail.ts and adds focused tests in packages/client-runtime/src/state/threadDetail.test.ts. The changes support [#27325]. The diff does not change the work-row shimmer, markdown parsing, Liquid Glass compositing, route-level subscriptions, or display-link behavior.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Usage-based review receipt

Note

This review was completed with usage-based billing: files reviewed beyond your plan's included limits are billed at $0.25/file. View usage-based billing.

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 093810b and dd1e609.

Files selected for processing: 2

  • packages/client-runtime/src/state/threadDetail.test.ts
  • packages/client-runtime/src/state/threadDetail.ts

Limit details: You’ve used all 10 included reviews currently available.

@dbxe

dbxe commented Oct 30, 2026 •

Copy link
Copy Markdown

Note

🤖 Claude Opus 5.5 responding on behalf of Daniel Briskin

This covers one slice of #27325 (the queue and checkpoint selectors), so could the body say Refs #27325 instead of Fixes? The larger items are still open: the work-row shimmer, re-parsing the whole message on every update, and the route-level thread subscription. I'm running this branch through the same iPad bench tonight and will post the number here.

@dbxe

dbxe commented Oct 30, 2026

Copy link
Copy Markdown

Note

🤖 Claude Opus 5.5 responding on behalf of Daniel Briskin

Bench result: no measurable power change while a turn streams, which is what I expected for two selectors.

  • Setup: the issue's iPad bench, streaming phase only, 2 ABBA/BAAB blocks. The PR's threadDetail.ts diff was applied onto the issue's baseline dac8b11ddc, because the bench backend is pinned there.
  • Result: −5 / −11 mW per block against the baseline, about 1059 mW. That reads same: the bench resolves about 25 mW.

That doesn't argue against merging. It just means the streaming cost lives in the other items. I'm measuring candidates for the markdown re-parse and the route and composer subscriptions next.

Copy link
Copy Markdown
Member Author

The automated pipeline stopped on this pull request and will not merge it:

someone else pushed to fix/issue-27325 (head is now 093810b); the pipeline does not build on commits it did not review

It stays open for a person to finish or close.

@juliusmarminge

Copy link
Copy Markdown
Member Author

Evidence

Proven in the real web app using Replay (dev), Chromium, dark theme, and a 1440×1000 viewport. Both revisions started with fresh synthetic data.

Steps: open a completed checkpoint and the diff panel; start another streaming turn; queue and edit a follow-up; approve the Replay action; observe streamed text; finish the turn and expand the new checkpoint diff. The scenario applies an identical synthetic file change on both sides because Replay describes edits without executing them.

Before After
Commit c90b67859a dd1e609b95
Redundant notifications during streamed text Queue: 4; checkpoint summaries: 4 Queue: 0; checkpoint summaries: 0
Streaming profile Before: live streaming profile After: live streaming profile
Queue edit Before: edited queued follow-up After: edited queued follow-up
Completed checkpoint Before: new checkpoint and file diff After: new checkpoint and file diff
Recording Before interaction After interaction

The live selector subscriptions counted new identities with unchanged serialized contents when assistant text changed. Queued-text edits still published, and checkpoint summaries increased from one to two on both revisions. The React commit observer recorded 14 DiffPanel commits during the measured streaming phase on base and 8 on head; those totals include legitimate lifecycle and tool updates and are not a timing benchmark.

Before versus after: redundant queue and checkpoint publications

Scenario source · Before profiling data · After profiling data · Sanitized runner records and original recordings

Recordings remove startup footage and unused canvas padding; interaction speed is unchanged. The profiling overlay is observational instrumentation. No visual redesign, CPU improvement, or battery improvement is claimed.

AI-assisted; verified with the tests and tools listed above.

Not shown:

  • The native mobile queue sheet was not profiled; this comparison exercised the shared queue selector through the real web client.
  • Queue attachment and attribution edits were not exercised; the meaningful queue-update control edited queued message text.
  • Reassociation of an already completed checkpoint with a later assistant message was not exercised; Replay produced a new checkpoint and its assistant-message association.
  • CPU, battery, and real-provider timing effects were not measured.

@juliusmarminge
juliusmarminge merged commit ad0b2a9 into main Nov 11, 2026
34 checks passed
@juliusmarminge
juliusmarminge deleted the fix/issue-27325 branch November 11, 2026 13:14
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Nov 11, 2026
**v0.1.10-nightly.20261111.4529** · 54 changes from 29 contributors

- **Find in browser.** Search pages in the desktop integrated browser with Cmd+F or Ctrl+F.
- **GitLab overview threads.** Resolve merge request overview threads from Summary and Timeline.
- **Faster worktree updates.** Checkpoint captures and status refreshes avoid rereading every file in newly created worktrees.
- **Imported session history.** Imported agent sessions now show their existing history instead of opening as empty threads.
- **SSH reconnect prompts.** Expired SSH verification prompts no longer stack up and force an app restart.
- **Large reasoning traces.** Opening a thread with a large collapsed reasoning trace no longer freezes the app.
- **Terminal scrollback.** Terminal history retains substantially more output instead of silently discarding older lines.

### Plus 45 more bug fixes and performance improvements.

<details>
<summary>Full changelog</summary>

## What's Changed
* fix(server): new worktrees refresh their index once, so captures and status stop re-hashing every file by @josephv123 in pingdotgg/t3code#21341
* fix(usage): Grok and Cursor usage limits show pace and the even-spend line by @wookiist in pingdotgg/t3code#21897
* fix(web): retain diff selection across navigation by @aleo113 in pingdotgg/t3code#22347
* fix(mobile): subscription widget no longer expires on a window that already reset by @juliusmarminge in pingdotgg/t3code#22993
* fix(opencode): verify CLI updates when loopback server is still running by @intrepid37 in pingdotgg/t3code#22839
* fix(usage): Claude subagent output is no longer undercounted by @rfrost-xyz in pingdotgg/t3code#20276
* fix(server): a remote update points the service unit at the runtime it committed by @Marve10s in pingdotgg/t3code#19652
* fix(server): a limited provider's queue stays held after another provider's turn succeeds by @Marve10s in pingdotgg/t3code#22425
* fix(shared): keep spreadsheet shorthand as inline code by @juliusmarminge in pingdotgg/t3code#23203
* fix(web): offer retry while an environment reconnects by @juliusmarminge in pingdotgg/t3code#23194
* fix(server): custom Codex providers no longer report native quota by @fixfon in pingdotgg/t3code#18622
* fix(desktop): Cmd+F now searches the page in the integrated browser by @armanarutiunov in pingdotgg/t3code#22960
* fix(desktop): an SSH password prompt that ended leaves the dialog instead of stacking by @Umais-Adeed in pingdotgg/t3code#34192
* fix(web): render Claude insight blocks as a callout by @macodev00 in pingdotgg/t3code#22239
* fix(scheduler): show short server timezone on fixed-time labels by @shawk08033 in pingdotgg/t3code#23111
* fix(web): focus the primary action when confirmation dialogs open by @rgrvlsk in pingdotgg/t3code#18187
* fix(web): Changes row matches the diff comparison by @whoisaldo in pingdotgg/t3code#19553
* fix(server): imported ACP sessions show their native history by @only21mil in pingdotgg/t3code#22155
* fix(preview): always return evaluated results by value by @juliusmarminge in pingdotgg/t3code#22948
* fix(provider-core): a model reply of "title" no longer replaces the thread title by @juliusmarminge in pingdotgg/t3code#25743
* fix(desktop,web): Open in runs locally for a background service sharing the desktop's T3 home by @KinanLak in pingdotgg/t3code#21661
* fix(web): question answers submit while normal sends are blocked by @t3dotgg in pingdotgg/t3code#22874
* fix(server): cancelling a background-only delegated task reports it interrupted by @only21mil in pingdotgg/t3code#21676
* fix(web): fall back to a readable server for shared repositories by @sanjaibalajee in pingdotgg/t3code#21530
* fix(web): composer follows an optionless thread instead of the settings default by @f4llenz in pingdotgg/t3code#16546
* fix(web): don't save retention age until committed by @macodev00 in pingdotgg/t3code#22208
* fix(server): one dropped probe no longer lists a live linked peer as unreachable by @juliusmarminge in pingdotgg/t3code#34208
* fix(server): preview automation reports ambiguous selectors instead of calling them invalid by @juliusmarminge in pingdotgg/t3code#34205
* fix(opencode): a context overflow that OpenCode compacts no longer fails the turn by @juliusmarminge in pingdotgg/t3code#26743
* fix(web): Ctrl + "+" zooms on keyboard layouts with a dedicated plus key by @juliusmarminge in pingdotgg/t3code#26408
* fix(ci): release version bump push retries when main moves by @t3dotgg in pingdotgg/t3code#34326
* fix(server): CLI commands no longer migrate the database of a running server by @juliusmarminge in pingdotgg/t3code#26763
* fix(web): keep saved default model when changing effort by @macodev00 in pingdotgg/t3code#22193
* fix(web): reveal expanded work rows hidden behind the composer by @ln-bodicherla in pingdotgg/t3code#21968
* fix(composer): steer instead of queueing during automatic compaction by @NikitaMGrimm in pingdotgg/t3code#24138
* feat(gitlab): resolve overview threads from Summary and Timeline by @TonybynMp4 in pingdotgg/t3code#30575
* fix(web): pull request search keeps partial-word matches after GitHub answers by @juliusmarminge in pingdotgg/t3code#26992
* fix(server): Claude subagents no longer show the parent's effort by @juansuaya17 in pingdotgg/t3code#17042
* fix(server): runs cancelled by a server restart say why in the thread by @juliusmarminge in pingdotgg/t3code#27192
* fix(client-runtime): server updates stop waiting 32 minutes when the host returns without this update's outcome by @juliusmarminge in pingdotgg/t3code#27275
* fix(web): Stop shows a pending state and repeat clicks no longer resume by @voida6 in pingdotgg/t3code#20809
* fix(web): collapsed reasoning previews no longer process the whole block by @juliusmarminge in pingdotgg/t3code#23001
* fix(server): storage cleanup skips stale worktree paths under a regular file by @juliusmarminge in pingdotgg/t3code#26594
* fix(terminal): use a byte budget for Ghostty scrollback by @zero-two-rafaeltab in pingdotgg/t3code#8766
* fix(server): storage cleanup keeps worktrees whose file listing exceeds the inspection limit by @juliusmarminge in pingdotgg/t3code#26593
* fix(server): delete temporary ACP health probe sessions by @juliusmarminge in pingdotgg/t3code#25397
* perf(client): streamed text no longer republishes thread queue and checkpoints by @juliusmarminge in pingdotgg/t3code#27425
* fix(web): convert composer list markers completed later by @juliusmarminge in pingdotgg/t3code#22997
* test: remove tests that repeat another case or restate the source by @shivamhwp in pingdotgg/t3code#34170
* fix(server): drop stale queued pull-request watch wakes by @Sinan53x in pingdotgg/t3code#21239
* fix(web): preview recordings stay decodable after a viewport resize by @MansTomb in pingdotgg/t3code#10495
* fix(server): only offer claude update for native Claude installs by @AI091 in pingdotgg/t3code#14170
* fix(server): tell outside agents which MCP tools need a thread and that t3_thread_list takes allProjects by @juliusmarminge in pingdotgg/t3code#26525

## New Contributors
* @wookiist made their first contribution in pingdotgg/t3code#21897
* @aleo113 made their first contribution in pingdotgg/t3code#22347
* @intrepid37 made their first contribution in pingdotgg/t3code#22839
* @rfrost-xyz made their first contribution in pingdotgg/t3code#20276
* @armanarutiunov made their first contribution in pingdotgg/t3code#22960
* @KinanLak made their first contribution in pingdotgg/t3code#21661
* @sanjaibalajee made their first contribution in pingdotgg/t3code#21530
* @ln-bodicherla made their first contribution in pingdotgg/t3code#21968
* @juansuaya17 made their first contribution in pingdotgg/t3code#17042
* @zero-two-rafaeltab made their first contribution in pingdotgg/t3code#8766
* @MansTomb made their first contribution in pingdotgg/t3code#10495
* @AI091 made their first contribution in pingdotgg/t3code#14170

**Full Changelog**: pingdotgg/t3code@v0.1.9-nightly.20261111.4515...v0.1.10-nightly.20261111.4529

</details>


Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.1.10-nightly.20261111.4529
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop pr:gate-failed A land gate failed size:M 30-99 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: iOS app draws ~2.8x the power of a minimal Flutter client while a turn streams

2 participants