Summary
initialize() has no generation counter or cancellation flag, and the mount-effect cleanup only destroys sphereRef.current (still null while an init is in flight). Two overlapping initialize() calls both pass if (sphereRef.current) await destroy(), both build fresh provider bundles (each with its own Nostr transport + IndexedDB connections), both run Sphere.init to completion. The loser of the setSphere race is never destroyed — a zombie wallet stays subscribed to relays with the real identity, keeps processing transfer:incoming/message:dm, writes to the same IndexedDB, and (wallet-api mode) holds a second session under the same deviceId. The app runs under <StrictMode>, so in dev this happens on every load; ConnectPage/IframeAgent guard StrictMode but SphereProvider does not.
Where
src/sdk/SphereProvider.tsx:479. Verified NOT addressed by PR #443.
Fix
Add a monotonic initGenRef (same pattern already used for subscription keys in this file): capture const gen = ++initGenRef.current at the top; after each await check gen !== initGenRef.current and, if stale, destroy the just-created instance and return. Bump the generation in unmount cleanup so a post-unmount init resolution self-destroys.
Found by multi-agent audit @ main b477d4d; adversarially verified. Fix is app-side.
Summary
initialize()has no generation counter or cancellation flag, and the mount-effect cleanup only destroyssphereRef.current(still null while an init is in flight). Two overlappinginitialize()calls both passif (sphereRef.current) await destroy(), both build fresh provider bundles (each with its own Nostr transport + IndexedDB connections), both runSphere.initto completion. The loser of thesetSphererace is never destroyed — a zombie wallet stays subscribed to relays with the real identity, keeps processingtransfer:incoming/message:dm, writes to the same IndexedDB, and (wallet-api mode) holds a second session under the same deviceId. The app runs under<StrictMode>, so in dev this happens on every load;ConnectPage/IframeAgentguard StrictMode butSphereProviderdoes not.Where
src/sdk/SphereProvider.tsx:479. Verified NOT addressed by PR #443.Fix
Add a monotonic
initGenRef(same pattern already used for subscription keys in this file): captureconst gen = ++initGenRef.currentat the top; after eachawaitcheckgen !== initGenRef.currentand, if stale, destroy the just-created instance and return. Bump the generation in unmount cleanup so a post-unmount init resolution self-destroys.Found by multi-agent audit @ main b477d4d; adversarially verified. Fix is app-side.