fix(desktop): recover root boundary from transient render races - #41787
fix(desktop): recover root boundary from transient render races#41787izumi0uu wants to merge 1 commit into
Conversation
|
Thanks for tackling this — confirming this is option A from #41693 (boundary-level mitigation). LGTM as a UX fix: users will no longer be latched on the "Reload window" screen. One concern about scope: the underlying Suggestion: when this lands, please leave #41693 open (or split out a follow-up issue) tracking the lower layer — store throw semantics + teardown ordering — so the root cause doesn't get marked resolved by a symptom-only fix. Otherwise the race may resurface as a different symptom (frozen UI, stale message lists, etc.) under a different reproduction path. |
|
We are hitting the same crash locally, so this mitigation would be useful. Representative Desktop log: The root-boundary recovery in this PR should prevent the Desktop from getting stuck on the reload screen, which matches the user-visible failure we see. One related debugging note: in our case the stack points through |
|
Thanks for this @izumi0uu — the boundary-level recovery is the right UX fix, and your recoverable-error pattern set ( I've reimplemented it in #52704 with your approach as the core, addressing the one concern raised on this thread (symptom-only / unbounded): auto-recovery is now bounded — at most 3 attempts per 5s window — so a genuinely persistent error can't spin the boundary in a You're credited via |
…aces A stale-index render race in assistant-ui (a just-shrunk thread rendered at an old message index during a session switch / teardown) throws errors like "tapClientLookup: Index N out of bounds", "Cannot read properties of undefined (reading 'type')", or "Tried to unmount a fiber that is already unmounted". These bubble to the root ErrorBoundary and latch the WHOLE desktop app on the "Reload window" fallback even though the next render against fresh state would be fine. Teach the root boundary to treat that small set of known-transient renderer errors as recoverable: log them and schedule a next-tick reset() so React re-renders against current state instead of stranding the user on the fallback. Auto-recovery is BOUNDED -- at most MAX_RECOVERIES (3) attempts within a 5s window -- so a genuinely persistent error can't spin the boundary in a reset -> throw -> reset loop; after the budget is spent the fallback is left up for the user. Manual retry (the button) resets the budget. Only the root boundary auto-recovers; scoped boundaries keep their own fallbacks, and unrecognized errors are never swallowed. Tests: transient race recovers (fallback never sticks), a persistent recoverable error stops at the cap and surfaces the fallback (proving the loop is bounded), and neither a non-root boundary nor an unrecognized root error auto-recovers. Closes #41693. Supersedes #41787 by @izumi0uu, reimplemented with a bounded recovery budget so a non-transient error can't loop forever. Co-authored-by: izumi0uu <izumi0uu@gmail.com>
…recover fix(desktop): recover root error boundary from transient render races (salvage #41787)
…aces A stale-index render race in assistant-ui (a just-shrunk thread rendered at an old message index during a session switch / teardown) throws errors like "tapClientLookup: Index N out of bounds", "Cannot read properties of undefined (reading 'type')", or "Tried to unmount a fiber that is already unmounted". These bubble to the root ErrorBoundary and latch the WHOLE desktop app on the "Reload window" fallback even though the next render against fresh state would be fine. Teach the root boundary to treat that small set of known-transient renderer errors as recoverable: log them and schedule a next-tick reset() so React re-renders against current state instead of stranding the user on the fallback. Auto-recovery is BOUNDED -- at most MAX_RECOVERIES (3) attempts within a 5s window -- so a genuinely persistent error can't spin the boundary in a reset -> throw -> reset loop; after the budget is spent the fallback is left up for the user. Manual retry (the button) resets the budget. Only the root boundary auto-recovers; scoped boundaries keep their own fallbacks, and unrecognized errors are never swallowed. Tests: transient race recovers (fallback never sticks), a persistent recoverable error stops at the cap and surfaces the fallback (proving the loop is bounded), and neither a non-root boundary nor an unrecognized root error auto-recovers. Closes NousResearch#41693. Supersedes NousResearch#41787 by @izumi0uu, reimplemented with a bounded recovery budget so a non-transient error can't loop forever. Co-authored-by: izumi0uu <izumi0uu@gmail.com>
…aces A stale-index render race in assistant-ui (a just-shrunk thread rendered at an old message index during a session switch / teardown) throws errors like "tapClientLookup: Index N out of bounds", "Cannot read properties of undefined (reading 'type')", or "Tried to unmount a fiber that is already unmounted". These bubble to the root ErrorBoundary and latch the WHOLE desktop app on the "Reload window" fallback even though the next render against fresh state would be fine. Teach the root boundary to treat that small set of known-transient renderer errors as recoverable: log them and schedule a next-tick reset() so React re-renders against current state instead of stranding the user on the fallback. Auto-recovery is BOUNDED -- at most MAX_RECOVERIES (3) attempts within a 5s window -- so a genuinely persistent error can't spin the boundary in a reset -> throw -> reset loop; after the budget is spent the fallback is left up for the user. Manual retry (the button) resets the budget. Only the root boundary auto-recovers; scoped boundaries keep their own fallbacks, and unrecognized errors are never swallowed. Tests: transient race recovers (fallback never sticks), a persistent recoverable error stops at the cap and surfaces the fallback (proving the loop is bounded), and neither a non-root boundary nor an unrecognized root error auto-recovers. Closes NousResearch#41693. Supersedes NousResearch#41787 by @izumi0uu, reimplemented with a bounded recovery budget so a non-transient error can't loop forever. Co-authored-by: izumi0uu <izumi0uu@gmail.com> (cherry picked from commit 2e3efce)
…aces A stale-index render race in assistant-ui (a just-shrunk thread rendered at an old message index during a session switch / teardown) throws errors like "tapClientLookup: Index N out of bounds", "Cannot read properties of undefined (reading 'type')", or "Tried to unmount a fiber that is already unmounted". These bubble to the root ErrorBoundary and latch the WHOLE desktop app on the "Reload window" fallback even though the next render against fresh state would be fine. Teach the root boundary to treat that small set of known-transient renderer errors as recoverable: log them and schedule a next-tick reset() so React re-renders against current state instead of stranding the user on the fallback. Auto-recovery is BOUNDED -- at most MAX_RECOVERIES (3) attempts within a 5s window -- so a genuinely persistent error can't spin the boundary in a reset -> throw -> reset loop; after the budget is spent the fallback is left up for the user. Manual retry (the button) resets the budget. Only the root boundary auto-recovers; scoped boundaries keep their own fallbacks, and unrecognized errors are never swallowed. Tests: transient race recovers (fallback never sticks), a persistent recoverable error stops at the cap and surfaces the fallback (proving the loop is bounded), and neither a non-root boundary nor an unrecognized root error auto-recovers. Closes NousResearch#41693. Supersedes NousResearch#41787 by @izumi0uu, reimplemented with a bounded recovery budget so a non-transient error can't loop forever. Co-authored-by: izumi0uu <izumi0uu@gmail.com>
…ot-boundary-recover fix(desktop): recover root error boundary from transient render races (salvage NousResearch#41787)
…aces A stale-index render race in assistant-ui (a just-shrunk thread rendered at an old message index during a session switch / teardown) throws errors like "tapClientLookup: Index N out of bounds", "Cannot read properties of undefined (reading 'type')", or "Tried to unmount a fiber that is already unmounted". These bubble to the root ErrorBoundary and latch the WHOLE desktop app on the "Reload window" fallback even though the next render against fresh state would be fine. Teach the root boundary to treat that small set of known-transient renderer errors as recoverable: log them and schedule a next-tick reset() so React re-renders against current state instead of stranding the user on the fallback. Auto-recovery is BOUNDED -- at most MAX_RECOVERIES (3) attempts within a 5s window -- so a genuinely persistent error can't spin the boundary in a reset -> throw -> reset loop; after the budget is spent the fallback is left up for the user. Manual retry (the button) resets the budget. Only the root boundary auto-recovers; scoped boundaries keep their own fallbacks, and unrecognized errors are never swallowed. Tests: transient race recovers (fallback never sticks), a persistent recoverable error stops at the cap and surfaces the fallback (proving the loop is bounded), and neither a non-root boundary nor an unrecognized root error auto-recovers. Closes NousResearch#41693. Supersedes NousResearch#41787 by @izumi0uu, reimplemented with a bounded recovery budget so a non-transient error can't loop forever. Co-authored-by: izumi0uu <izumi0uu@gmail.com>
…ot-boundary-recover fix(desktop): recover root error boundary from transient render races (salvage NousResearch#41787)
…aces A stale-index render race in assistant-ui (a just-shrunk thread rendered at an old message index during a session switch / teardown) throws errors like "tapClientLookup: Index N out of bounds", "Cannot read properties of undefined (reading 'type')", or "Tried to unmount a fiber that is already unmounted". These bubble to the root ErrorBoundary and latch the WHOLE desktop app on the "Reload window" fallback even though the next render against fresh state would be fine. Teach the root boundary to treat that small set of known-transient renderer errors as recoverable: log them and schedule a next-tick reset() so React re-renders against current state instead of stranding the user on the fallback. Auto-recovery is BOUNDED -- at most MAX_RECOVERIES (3) attempts within a 5s window -- so a genuinely persistent error can't spin the boundary in a reset -> throw -> reset loop; after the budget is spent the fallback is left up for the user. Manual retry (the button) resets the budget. Only the root boundary auto-recovers; scoped boundaries keep their own fallbacks, and unrecognized errors are never swallowed. Tests: transient race recovers (fallback never sticks), a persistent recoverable error stops at the cap and surfaces the fallback (proving the loop is bounded), and neither a non-root boundary nor an unrecognized root error auto-recovers. Closes NousResearch#41693. Supersedes NousResearch#41787 by @izumi0uu, reimplemented with a bounded recovery budget so a non-transient error can't loop forever. Co-authored-by: izumi0uu <izumi0uu@gmail.com>
…ot-boundary-recover fix(desktop): recover root error boundary from transient render races (salvage NousResearch#41787)
…aces A stale-index render race in assistant-ui (a just-shrunk thread rendered at an old message index during a session switch / teardown) throws errors like "tapClientLookup: Index N out of bounds", "Cannot read properties of undefined (reading 'type')", or "Tried to unmount a fiber that is already unmounted". These bubble to the root ErrorBoundary and latch the WHOLE desktop app on the "Reload window" fallback even though the next render against fresh state would be fine. Teach the root boundary to treat that small set of known-transient renderer errors as recoverable: log them and schedule a next-tick reset() so React re-renders against current state instead of stranding the user on the fallback. Auto-recovery is BOUNDED -- at most MAX_RECOVERIES (3) attempts within a 5s window -- so a genuinely persistent error can't spin the boundary in a reset -> throw -> reset loop; after the budget is spent the fallback is left up for the user. Manual retry (the button) resets the budget. Only the root boundary auto-recovers; scoped boundaries keep their own fallbacks, and unrecognized errors are never swallowed. Tests: transient race recovers (fallback never sticks), a persistent recoverable error stops at the cap and surfaces the fallback (proving the loop is bounded), and neither a non-root boundary nor an unrecognized root error auto-recovers. Closes NousResearch#41693. Supersedes NousResearch#41787 by @izumi0uu, reimplemented with a bounded recovery budget so a non-transient error can't loop forever. Co-authored-by: izumi0uu <izumi0uu@gmail.com>
…ot-boundary-recover fix(desktop): recover root error boundary from transient render races (salvage NousResearch#41787)
…aces A stale-index render race in assistant-ui (a just-shrunk thread rendered at an old message index during a session switch / teardown) throws errors like "tapClientLookup: Index N out of bounds", "Cannot read properties of undefined (reading 'type')", or "Tried to unmount a fiber that is already unmounted". These bubble to the root ErrorBoundary and latch the WHOLE desktop app on the "Reload window" fallback even though the next render against fresh state would be fine. Teach the root boundary to treat that small set of known-transient renderer errors as recoverable: log them and schedule a next-tick reset() so React re-renders against current state instead of stranding the user on the fallback. Auto-recovery is BOUNDED -- at most MAX_RECOVERIES (3) attempts within a 5s window -- so a genuinely persistent error can't spin the boundary in a reset -> throw -> reset loop; after the budget is spent the fallback is left up for the user. Manual retry (the button) resets the budget. Only the root boundary auto-recovers; scoped boundaries keep their own fallbacks, and unrecognized errors are never swallowed. Tests: transient race recovers (fallback never sticks), a persistent recoverable error stops at the cap and surfaces the fallback (proving the loop is bounded), and neither a non-root boundary nor an unrecognized root error auto-recovers. Closes NousResearch#41693. Supersedes NousResearch#41787 by @izumi0uu, reimplemented with a bounded recovery budget so a non-transient error can't loop forever. Co-authored-by: izumi0uu <izumi0uu@gmail.com>
…ot-boundary-recover fix(desktop): recover root error boundary from transient render races (salvage NousResearch#41787)
What does this PR do?
Prevents Hermes Desktop from getting stuck on the root "Reload window" screen when assistant-ui hits a transient stale-index render race.
The root
ErrorBoundarynow treats a small set of known renderer errors as recoverable, logs them, and resets itself on the next tick so React can re-render against fresh state instead of leaving the whole app latched on the fallback screen.This intentionally implements the smallest mitigation proposed in #41693 (option A). It does not yet try to reorder the underlying session / message teardown path.
Related Issue
Fixes #41693
Type of Change
Changes Made
apps/desktop/src/components/error-boundary.tsx: add root-only recoverable error matching fortapClientLookup: Index ... out of bounds,Cannot read properties of undefined (reading 'type'), andTried to unmount a fiber that is already unmounted; schedule a next-tickreset(); clear pending recovery timers on reset/unmount.apps/desktop/src/components/error-boundary.test.tsx: add a component test that reproduces atapClientLookupstale-index race throughAssistantRuntimeProvider+ThreadPrimitive.MessageByIndexand verifies the boundary recovers instead of leaving theReload windowfallback on screen.How to Test
cd apps/desktopnpm run type-checknpm run test:ui -- --run src/components/error-boundary.test.tsxnpm run test:ui -- --run src/components/assistant-ui/streaming.test.tsx -t "renders assistant text incrementally before completion"Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A