Skip to content

fix(desktop): stop per-event nested commits that crashed the Live Session View - #444

Merged
bubbleptr merged 2 commits into
mainfrom
fix/inline-pager-nested-updates
Oct 7, 2026
Merged

bubbleptr merged 2 commits into
mainfrom
fix/inline-pager-nested-updates

Conversation

@bubbleptr

@bubbleptr bubbleptr commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

Pace 0.2.0 crashed the whole window with React error #185 (Maximum update depth exceeded) when sending a prompt from Home (reproducible every time on the reporter's machine).

Root cause. React 19.2 counts a commit as nested when Sync/Default work is still pending right after it; every projection-store event is a useSyncExternalStore sync commit. Three sources left work pending after each event, so the denser Pi 1.0 event bursts pushed the counter past 50:

  1. LiveSessionColumn marked the on-screen Session read from a passive effect, adding a second store commit per assistant event (the main driver). The store now tracks viewed Sessions (store.view / useViewedSession) and folds the read marker into the event's own write.
  2. ChatInlinePager set state on every parent render for same-page content; it now renders that content straight from children.
  3. @astryxdesign/core@0.3.0 Text/Heading rebuilt their merged ref every render, so useTruncation set state during each commit. Patched via patchedDependencies to a memoized ref (upstream fixed in 0.6.x with useMergedRefs). AppFrame's expand effect is keyed on the selected project instead of the whole projections array.

Evidence

Repro harness: a copy of the reporter's ~/.pace + ~/.pi/agent, Playwright-driven send from Home.

dev send prod send+follow, 16x CPU throttle max nested count
before 2/6 crash 3/5 crash up to 51
after 0/9 0/5 5 or less

Ablation: disabling only the read-marker effect removed the crash screen; fixing only the pager did not (prod still 3/5).

Test plan

  • New tests, each shown red before the fix: viewed Session commits once per event (old pattern 10 vs 5), pager same-page update costs no extra commit (6 vs 3), Astryx Text/Heading keep a callback ref attached (7 calls vs 1), viewed/unviewed unread semantics
  • bun run typecheck / lint / test (169 files / 1983 tests) / build / test:bundled-runtime
  • Viewed Session shows no unread dot after the reply in the repro

Devin Review

…sion View

Sending a prompt crashed the window with React error #185 (maximum update
depth). React 19 counts a commit as nested when updates are still pending
right after it, and every projection-store event is a sync commit. Three
things left work pending after each event, so dense Pi 1.0 event bursts
pushed the counter past 50:

- LiveSessionColumn marked the on-screen Session read from a passive
  effect, adding a second store commit per assistant event. The store now
  tracks viewed Sessions (store.view / useViewedSession) and folds the read
  marker into the event's own write.
- ChatInlinePager set state on every parent render for same-page content;
  it now renders that content straight from children.
- @astryxdesign/core 0.3.0 Text/Heading rebuilt their merged ref every
  render, so useTruncation set state during each commit. Patched to a
  memoized ref (upstream fixed in 0.6.x). AppFrame's expand effect is now
  keyed on the selected project instead of the whole projections array.

The renderer's projection writes now cost one commit per event; nested
counts in the repro stay at 5 or below (was up to 51).

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Devin Review

Comment on lines +268 to +276
view(sessionId) {
viewed.set(sessionId, (viewed.get(sessionId) ?? 0) + 1);
// An already-unread Session is caught up now: one notify, only on view
// start — nothing writes "read" back while events land afterwards.
const current = get(sessionId);
if (current) {
const next = seen(current);
if (next !== current) {
replace(next);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Late session arrival stays unread

If view() starts before an unread Session is inserted, it cannot mark that Session read. insert bypasses seen, so its unread badge remains while the Session is on screen.

Learn more

A view can start while its Session is absent from the store. view() records the id but only clears unread on the projection available at that moment. A later insert commits its projection unchanged. useViewedSession depends on the id and store, not on the projection, so no second view call clears the badge. The previous read effect depended on liveProjection?.unreadResult, so it handled a projection that appeared after the view mounted.

Example: A view starts for Session s before its projection exists. An unread projection for s is then inserted. Its unread indicator remains visible even though the Session is open.

Recommended fix: Apply seen() to projections entering through insert() while their id is viewed. Also check replacement paths such as rehydrate() for projections that can enter the store unread after a view starts.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, thanks. Fixed in 8358f65: commit() is now the single place that applies the read marker, so insert() and rehydrate() are covered too. Added red-first tests for a Session inserted after its view started and one rehydrated unread.

insert() and rehydrate() committed projections without the read marker, so a Session that entered the store unread after its view started kept its unread badge while on screen. commit() now applies it on every write.
@bubbleptr
bubbleptr merged commit 294c69c into main Oct 7, 2026
1 check passed
@bubbleptr
bubbleptr deleted the fix/inline-pager-nested-updates branch October 7, 2026 08:16
@bubbleptr bubbleptr mentioned this pull request Oct 7, 2026
3 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant