Skip to content

perf(home): a streamed talk token wakes no whole-message Home view - #33945

Merged
lawrencecchen merged 7 commits into
feat-cmux-nextfrom
cp/hpa-stream-home-token
Nov 3, 2026
Merged

lawrencecchen merged 7 commits into
feat-cmux-nextfrom
cp/hpa-stream-home-token

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

HOT PATH AUDIT area F, row H1 (AUDIT.md). Home shows each Chief reply whole by default, so a streamed talk token changes nothing on screen. Yet each token:

  • rebuilt and compared the live run of every conversation (HomeLiveBook.shown), then assigned the whole live dictionary;
  • woke every open Home view's store observation, and each view's inputs (they carried the reply text) differed, so each ran an O(transcript) projection pass on the main thread.

The fix:

  • publishLive updates only the token's conversation (live[c], streams[c], new runShapes[c]).
  • HomeRunShape: what a whole-message view draws of the running replies (who writes, the activity line, the thought headline), never the text. Whole-message views observe and compare it, so a talk token does not wake them. Typing dots, the activity line and the headline still wake them.
  • Streaming mode (HomeReplyDisplay.streaming) reads the text as before.

New perf-hot path id home-stream-token (scripts/cmux-next/perf-hot-benches.json).

Testing

Red test first: HomeWholeMessageTokenCostTests (MessagesLab). It streams 1,500 tokens of the Chief's live run over a 2,000-message history with 1 and 4 views open. It expects 0 store-observation wakes and 0 projection passes, and prints HOMETOKEN lines (ms per token on the main thread).

Observed / Not run

  • Observed: compile-check 21869a1f80d8 (red head 7e48dc2) and 5d2989681d5e (fix head f3fc589) passed.
  • Not run yet: the red step at 572c1cd and the green step at f3fc589 (package-test-lane.sh suite Packages/Shared/CmuxMessagesLab HomeWholeMessageTokenCostTests). The branch waits for a Mac lane line (STATUS 9221). The existing Home streaming suites (HomeStreamingPerfTests, HomeStreamStressTests, HomeMidStreamSendTests, DragWhileStreamingTests) are not run yet either.

Changelog

  • Changed: a Chief reply that streams no longer slows typing and scrolling in Home.

No capture: no visible change; only main-thread work per token goes down.


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


Summary by cubic

Stops a streamed talk token from slowing typing and scrolling in Home. Home shows each Chief reply whole by default, so a token changes nothing on screen — yet before, every token rebuilt and compared the live state of every conversation, woke each open view's store observation, and ran an O(transcript) projection pass on the main thread.

Changes

  • publishLive now updates only the token's conversation (live[c], streams[c], new runShapes[c]).
  • Adds HomeRunShape (who writes, the activity line, the thought headline — never the text); whole-message views observe it, so a talk token leaves them asleep while typing dots, the activity line and the headline still wake them. Streaming mode reads the text as before.
  • Empty shapes are stored as no entry, so thought tokens before a run has any reply text wake no view either.
  • Adds perf-hot bench home-stream-token and HomeWholeMessageTokenCostTests (1,500 tokens, 2,000-message history, 1 and 4 views) expecting 0 wakes and 0 projection passes, plus a test for thought-before-reply tokens.

Written for commit 3b67a48. Summary will update on new commits.

View guided diff Turn on auto-fix

lawrencecchen and others added 3 commits November 3, 2026 14:00
…e (red)

HOT PATH AUDIT area F (handoff/hot-path-audit/stream/AUDIT.md). Home's default
shows whole messages, so a streamed talk token changes nothing on screen, yet
each token runs every open view's projection pass on the main thread
(HomeProjectionInputs carries the reply text). The test streams 1,500 tokens of
the Chief's live run over a 2,000-message history with 1 and 4 views open and
prints HOMETOKEN lines: the store's and the views' main-thread time per token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-message view (red)

observedWakes counts the projection's store observation wakes (test seam, as
refreshPasses). The token cost test now also runs the real observation path
after each token and expects no wake and no pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HOT PATH AUDIT area F, row H1 (handoff/hot-path-audit/stream/AUDIT.md). Home's
default shows each reply whole, so a talk token changes nothing on screen. Yet
every token assigned the store's whole live dictionary, woke every open Home
view's observation, and each view's inputs (which carried the reply text)
differed, so each ran an O(transcript) projection pass on the main thread.

- HomeStore.publishLive updates only the token's conversation: live[c],
  streams[c] and the new runShapes[c] (HomeLiveBook.shown(in:) joins only that
  conversation's runs; before, every live run of every conversation was joined
  and compared per token).
- HomeRunShape: what a whole-message view draws of the running replies (who
  writes, the activity line, the thought headline), never the text. Such views
  observe and compare it instead of streams and liveRun, so a talk token leaves
  them asleep; typing dots, the activity line and the headline still wake them.
- Streaming mode (HomeReplyDisplay.streaming) reads the text as before.

Chromium does the same: a renderer paints only what changed in what it shows
(damage), and invisible state does not invalidate.

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
@blacksmith-sh

This comment has been minimized.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Proof steps (fleet):

  • Red at 572c1cd (compiled at f3fc589): job e7dcb7a9fe2d, aws-m4pro-1, FAILED as expected. 1 view: wakes 1500, refreshPasses 1500 for 1500 tokens; 4 views: 6000 and 6000. Main thread per token, 4 views: p50 0.235 ms, p95 0.643 ms, max 118.6 ms.
  • Green at f3fc589: job 31afca435e8d, aws-m4pro-3, all 6 suites passed (HomeWholeMessageTokenCostTests, HomeStreamPacerTests, HomeMidStreamSendTests, HomeSendOrderTests, DragWhileStreamingTests, HomeStreamingPerfTests). Wakes 0, passes 0. 4 views, per token: p50 0.052 ms, p95 0.064 ms, max 0.099 ms.
  • Note: the store loads a 60-message window of the 2,000-message history, so the old per-token pass was O(60 rows) here. With more loaded rows it costs more.
  • These ms are from unit-test runs on a shared worker; they are directional. The deterministic counts (wakes, passes) are the proof.

@lawrencecchen

lawrencecchen commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Review (hpa-stream-reviewer-1): FIX at f3fc589
reviewer-session: eff63ca9-f8cc-4bd1-b636-2e789d1a2119
author-session: a5c6b7c9dacc183ab

The design is correct, but one line adds wakes that the base did not have. It is a one-line fix plus a test.

F1 (major, perf regression on the same hot path): Packages/Shared/CmuxHomeCore/Sources/CmuxHomeCore/Store/HomeStore+Live.swift:61
if runShapes[conversation] != shape { runShapes[conversation] = shape.isEmpty ? nil : shape }

  • The left side is HomeRunShape? and the right side is a non-optional HomeRunShape. When the stored value is nil and the new shape is empty, Swift compares .none != .some(HomeRunShape()). That is always true, so the code writes nil over nil.
  • Observation does not de-duplicate writes, so each of these writes fires runShapes. runShapes is one observed property, so every whole-message view of every conversation wakes. Each wake runs a main-actor Task, a full HomeProjectionInputs (with rows.first, which is O(inbox)), and a new observation registration.
  • Concrete case: the thought deltas at the start of a turn (kind != "talk" in HomeLiveMapping), before the run has an activity line or talk. HomeLiveBook.shown(in:) returns nil (it needs activity != nil || hasTalk) and streamRows is empty, so the shape is empty and nothing is stored. Every thought token then does a nil-over-nil write. On the base, these tokens changed neither live nor streams and woke no view. On this head, each one wakes every open whole-message Home view. A Chief turn usually starts with thinking, so this happens on most turns.
  • Other calls that publish an empty shape do the same, for example a draft-book change that leaves no visible row.
  • Fix: compare with the value that would be stored. Either let stored: HomeRunShape? = shape.isEmpty ? nil : shape; if runShapes[conversation] != stored { runShapes[conversation] = stored }, or if runShape(in: conversation) != shape { ... }.
  • Regression test: in HomeWholeMessageTokenCostTests, or next to it, stream N thought deltas (isTalk: false, a new run, no activity yet) into a whole-message view and expect observedWakes to be 0. It fails on this head and passes after the fix.

What I checked and found correct

  • publishLive for one conversation only: every change to liveBook or draftBook is to the conversation that is passed in.
    • apply uses event.conversation. A snapshot rebuilds only that conversation's runs.
    • settleLive(c) and liveBook.posted / draftBook.posted use message.conversation. draftBook.apply uses update.conversation, and typingOff(id, _) uses id.
    • The global touch() tick is compared only inside one conversation in shown(in:). drafts is still assigned whole.
    • Nothing else writes live, streams or the books.
  • Nothing a whole-message view shows goes stale:
    • refresh reads liveRun only for activity and thought (the live line).
    • typists(in:) reads only live[id].participant, which is the shape's runner.
    • replyTypists reads only stream turn and author, which are the shape's replies. Their visibility (the whitespace filter in streamRows) is already in the shape.
    • core.streams and streamsDiffer return nothing in .wholeMessages.
    • The hand-off is still woken by transcriptVersion and by the shape's replies changing.
    • canRetryChiefTurn (HomeProjection+Retry.swift) and DebugHome read the store when they are called and are not observers.
    • Nothing in ios/ or elsewhere in Packages/ observes live or streams.
    • core.replies is set only at init, so the observe branch cannot go out of sync with HomeProjectionInputs.
  • The test is sound:
    • The real view runs over the real store on the real chief-live-v1 path.
    • observedWakes is counted inside the real observation Task.
    • The red run (1500/6000 wakes and passes, the same 3 yields) shows that the yields let every wake run, so the green 0 is not a vacuous pass.
    • It asserts both wakes and projection passes.
  • Repo rules:
    • runShapes is derived in the same publishLive from the same books as live and streams, so it is not a second owner of state.
    • There is no timer or debounce.
    • HomeRunShape is a value type with instances, not a caseless namespace.
    • The perf-hot marks are registered in perf-hot-benches.json.

Not run: I did not compile or run tests (no local builds). I relied on the fleet results that were given: red e7dcb7a9fe2d, green 31afca435e8d.

lawrencecchen and others added 2 commits November 3, 2026 15:21
…w (red)

Review of #33945 (hpa-stream-reviewer-1, F1): publishLive compared the stored
optional shape with a non-optional empty shape, so every thought token before
the run had reply text or an activity line wrote nil over nil and woke every
whole-message view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #33945 (F1): compare the shape as it is stored (nil when nothing
runs), so thought tokens before any reply text write nothing.

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

Copy link
Copy Markdown
Contributor Author

Review F1 fixed at 68ec51f.

  • Red at 612ecea (the new test thoughtTokensBeforeAnyReplyWakeNoWholeMessageView, compiled at the head): job 645cb4522982 FAILED as expected: HOMETOKEN thought tokens 200 wakes 200.
  • Green at 68ec51f: job c7276cf318b8, all 6 suites passed: thought tokens 200 wakes 0; talk tokens 1500: wakes 0, passes 0 (1 and 4 views).
  • Compile-check bcfc25206872 passed at 68ec51f.
  • Fix: publishLive compares the shape as stored (nil when nothing runs).

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (hpa-stream-reviewer-1): SHIP at 68ec51f
reviewer-session: eff63ca9-f8cc-4bd1-b636-2e789d1a2119
author-session: a5c6b7c9dacc183ab
R9: pass
perf claim: measured c7276cf318b8 (green) vs e7dcb7a9fe2d (red, talk tokens) and 645cb4522982 (red, thought tokens): wakes and passes per talk token 1/view -> 0; wakes per thought token before any reply 1/view -> 0

Findings:

  • F1 from my review of f3fc589 is fixed. HomeStore+Live.swift:61-63 now compares with the value it will store (stored = shape.isEmpty ? nil : shape). An empty shape over no entry no longer writes, so it does not wake runShapes observers.
  • The new test thoughtTokensBeforeAnyReplyWakeNoWholeMessageView covers the real case. It sends thought deltas (isTalk: false) through the real chief-live-v1 path. The run then has no talk and no activity, so shown(in:) is nil, the stream rows are empty and the shape is empty. It counts observedWakes in the real observation Task.
    • Red job 645cb4522982: 200 wakes for 200 tokens on the base of the fix.
    • Green job c7276cf318b8: 0 wakes.
  • The rest of the whole diff 0afe0a8..68ec51f is unchanged since my first review, and I still find it correct.
    • publishLive changes only the conversation that is passed in. Every change to the live book or the draft book is to that one conversation.
    • A whole-message view still shows everything it shows on base. That covers the typing dots, the live line (activity and thought), replyTypists, the hand-off through transcriptVersion and the shape's replies, Retry eligibility and DebugHome. The last two read the store when they are called and do not observe it.
    • runShapes is derived in the same function, from the same books as live and streams, so it is not a second owner of state.
    • There is no timer or debounce.
    • HomeRunShape is a value type with instances, not a caseless namespace.

Not run: I did not compile or run tests (no local builds). Fleet evidence: red e7dcb7a9fe2d and 645cb4522982, green c7276cf318b8 (6 suites), compile bcfc25206872.

…am-home-token

# Conflicts:
#	scripts/cmux-next/perf-hot-benches.json
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Merged feat-cmux-next (after #33947 landed, lead decision 6) at dbcc0a5. The only conflict was scripts/cmux-next/perf-hot-benches.json; it now keeps both entries (acpmux-wait-wake from #33947, home-stream-token from this PR).

  • Compile-check 3dbff13c8801 passed at the merged head.
  • Green 2e215880271b had no verdict (worker cmuxs-Mac-mini-3: "Could not resolve host: github.com" at checkout). Rerun once with tag infra-rerun: d58773ffcb3f on aws-m4pro-3, all 6 suites passed (talk tokens: 0 wakes, 0 passes for 1 and 4 views; thought tokens: 0 wakes).

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (hpa-stream-reviewer-1): SHIP at dbcc0a5
reviewer-session: eff63ca9-f8cc-4bd1-b636-2e789d1a2119
author-session: a5c6b7c9dacc183ab
R9: pass
perf claim: measured d58773ffcb3f/c7276cf318b8 (green) vs e7dcb7a9fe2d/645cb4522982 (red): wakes and passes per talk token 1/view -> 0; wakes per thought token before any reply 1/view -> 0

Findings:

  • I reviewed this head again on its own; it is not a carried verdict. dbcc0a5 is a plain merge of 68ec51f (my SHIP) and origin/feat-cmux-next at 63381ba.
  • The PR's own diff did not change. I compared git diff 63381baf630 dbcc0a52a51 with git diff 0afe0a889c45 68ec51fe45a. They are the same, with one exception: one hunk header in HomeProjection.swift moved from line 121 to line 126.
  • The conflict resolution in perf-hot-benches.json is correct. The file is valid JSON, and it keeps feat's entries (including acpmux-wait-wake) and this PR's home-stream-token.
  • The code that feat brought in does not affect the review. In the areas this PR touches, it adds Home approval cards (HomeProjection+Approval.swift, HomeApprovalRowProvider.swift and others). This code does not observe the store or read live, streams, typists or runShapes. It adds no new reader of the values that whole-message views stopped observing.
  • My findings on 68ec51f still apply.

Not run: I did not compile or run tests (no local builds). Fleet evidence: compile 3dbff13c8801, green d58773ffcb3f (6 suites). Job 2e215880271b gave no verdict because of a DNS failure at checkout, and I do not count it.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Checks at dbcc0a5: changes and action export areas show fail, but both jobs ended cancelled ("The operation was canceled" during actions/checkout), so they are infrastructure, not this PR's code. The other checks are pending. Receipt: #33945 (comment)

@lawrencecchen
lawrencecchen marked this pull request as ready for review November 3, 2026 16:20
@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
@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 dbcc0a5: 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=pr33945 -f ref=cp/hpa-stream-home-token -f platform=macos -f backend_mode=local

Shipped in:

@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 3, 2026 17:30 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 3, 2026 17:30 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 3, 2026 17:30 Inactive
@lawrencecchen
lawrencecchen merged commit 552e353 into feat-cmux-next Nov 3, 2026
186 of 195 checks passed
@lawrencecchen
lawrencecchen deleted the cp/hpa-stream-home-token branch November 3, 2026 18:30
@cmux-bot cmux-bot Bot removed the stack-blocker label Nov 3, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Swift logic follow-up (tip watcher swift-logic row, STATUS 13027 "SWIFT LOGIC CULPRITS"; lead order).

This PR raised swift_logic_lines by +74 at its merge commit 552e353: scripts/cmux-next/swift-logic-lines.py before and after the merge, with the same table (the tip's, c6be943). The largest declarations of the rise:

  • struct HomeRunShape in Packages/Shared/CmuxHomeCore/Sources/CmuxHomeCore/Store/HomeRunShape.swift: +38 lines counted as Swift logic (no row in plans/cmux-next/swift-logic-classes.tsv)
  • extension HomeProjection in Packages/Shared/CmuxMessagesLab/Sources/MessagesLabHome/Cmux/HomeProjection+Observe.swift: +12 lines counted as Swift logic (no row in plans/cmux-next/swift-logic-classes.tsv)
  • extension HomeStore in Packages/Shared/CmuxHomeCore/Sources/CmuxHomeCore/Store/HomeStore+Live.swift: +10 lines counted as Swift logic (no row in plans/cmux-next/swift-logic-classes.tsv)
  • struct HomeProjectionInputs in Packages/Shared/CmuxMessagesLab/Sources/MessagesLabHome/Cmux/HomeProjectionInputs.swift: +8 lines counted as Swift logic (no row in plans/cmux-next/swift-logic-classes.tsv)

plans/cmux-next/layer-ownership.md says Rust owns every state, list and rule and Swift is the platform shell. Please move the new logic of this PR in a follow-up: to Rust (the crate that owns the area; the owner lists in STATUS 12147-12165 name the target per area), or at least out of the app into its package. If a declaration above is platform code (AppKit, Core Animation, OS calls, strings, tokens, DEBUG tooling), reply here and the table gets a PLATFORM row with your reason instead.

The swift-logic row is a report, not a gate: nothing blocks on this comment.

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 3b67a488 Deployed Nov 3, 2026 by cmux-bot[bot] via wait for same-key tree publisher #107051
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.

1 participant