fix(fluxor): a dismissed error stays dismissed — the sink remembers every exception reported, not only the one it shows #34
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/error-sink-late-report"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The bug. Fluxor raises an effect's exception to every
StoreInitializeron the page, and each reports it to theCircuitErrorSinkwhen its ownInvokeAsyncruns. The sink ignored a report only of the exception it was showing. So:The fix. The sink now remembers every exception reported, not only the one it shows. It keeps them in a
ConditionalWeakTable, so none outlives its reports. A dismissed error stays dismissed, and a late report never replaces a later error. The public API is unchanged.How it was found. Tsumugi's CI: page tests that press Continue on the containment's modal timed out waiting for it to go (
HistoryStoreFailureTestson Tsumugi's PR #92, andDocumentsStoreFailureTestslocally). A temporary probe inStoreInitializerlogged three failing runs under CPU load. Each time:Tests. Two new
CircuitErrorSinkrows, both failing on the previous sink:Kagaku.UI.Fluxor.Tests15 → 17;Kagaku.UI.Tests578, unchanged.Tsumugi's pointer bump follows in its own PR once this merges.
🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagaku.UI - 96.1%
Kagaku.UI.Fluxor - 97.7%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A race condition hunted down with a 125-run failure table and a logging probe under CPU load, then fixed by swapping one guard for a
ConditionalWeakTable? That is how you earn a review, fufu~ ♡ I read every line of the sink, the initializer, and both test files — and then I did something the smile doesn't show: I tried to break it.Verdict: ✅ Looks good to me~
The mutation probe. I reverted
!_reported.TryAdd(exception, Seen)back to the oldCurrent-only guard in my sandbox and ranCircuitErrorSinkTests:A_dismissed_exception_reported_again_stays_dismissed— failed (modal came back, exactly the Tsumugi timeout)A_late_report_of_an_earlier_exception_leaves_the_later_one_shown— failed ("earlier"stole the screen from"later")Both regression rows killed the mutant dead. These aren't tests that merely compile — they guard the fix. 8/10 passed, the two that should scream screamed. That's the only kind of test I trust~ ♡
The semantics. Keyed on exception identity,
TryAddas a first-wins gate: an exception ever reported never shows again, whether still on screen, dismissed, or replaced. The onlyReportcaller isStoreInitializerpassing Fluxor'sUnhandledExceptionEventArgs.Exception— always a live instance, non-null, and fresh per failure, so distinct failures stay distinct (the pre-existingA_different_exception_replaces_the_current_onerow pins that). And holding the entries weakly means the memory can't outlive the exception — no unbounded growth on a long-lived circuit. Thestatic Seensentinel shared across sink instances is a nice touch: one allocation, ever.Also verified: the dedupe-while-shown path still raises
Changedexactly once (multicast, one log line — the original contract, untouched), the guard lives insidelock (_sync)so theCurrent-and-table composite stays atomic, and the public API is unchanged, so Tsumugi's pointer bump will be a pure recompile. Surgical is the right word.💡 Little ideas (non-blocking)~
✅ What I liked~
raised == 2— pinning that a suppressed report raises no event, not just "doesn't set Current". Sneaky-thorough. I noticed. I noticed very hard. ♡Automated review by Jibril · 2026-10-07
CI: green for head SHA
bc1dfca· Mutation probes: 1 run, 0 survived · Local suite: skipped (CI green)