Repository navigation
fix(desktop): stop per-event nested commits that crashed the Live Session View - #444
Conversation
…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).
| 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); |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
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
useSyncExternalStoresync commit. Three sources left work pending after each event, so the denser Pi 1.0 event bursts pushed the counter past 50:LiveSessionColumnmarked 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.ChatInlinePagerset state on every parent render for same-page content; it now renders that content straight fromchildren.@astryxdesign/core@0.3.0Text/Headingrebuilt their merged ref every render, souseTruncationset state during each commit. Patched viapatchedDependenciesto a memoized ref (upstream fixed in 0.6.x withuseMergedRefs).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.Ablation: disabling only the read-marker effect removed the crash screen; fixing only the pager did not (prod still 3/5).
Test plan
bun run typecheck/lint/test(169 files / 1983 tests) /build/test:bundled-runtime