Source: R8 fresh-eyes QA round, adversarial cross-tab scope (2026-05-17)
Severity: MED-HIGH (data loss in high-flow sessions, but no panic-level user impact)
Status: Deferred from R8 — design call, not a surgical patch
Symptom
MAX_RECENT_ALERTS = 50 in app/src/store/flowStore.ts:55 caps the global alert
buffer. In high-flow sessions with 10+ active positions × 13 alert kinds, the
buffer can saturate within minutes, evicting older terminal alerts (TargetHit,
StopHit, BigCloseDetected) before the 24h grace window in
synthesizeSymbolAlerts wants them.
User-visible effect: a TargetHit from 30 min ago can disappear from the Action
Summary panel because 50 newer alerts pushed it off the end. Raw Alerts tab
also loses it (it reads from the same buffer).
Reproduction (synthetic)
- Open 10 positions across 5 symbols
- Trigger enough state changes to fire ~60 alerts in <24h (Approaching*, Target,
Stop, BigClose, Wall, IV, Expiry, Roll — easy with realistic poll cadence)
- Observe: the oldest 10 alerts (including any TargetHit/StopHit) are gone
from the buffer even though the positions are still open or in grace
Current code
// flowStore.ts:55
const MAX_RECENT_ALERTS = 50;
// flowStore.ts:697-700 (terminal alerts merged on every position update)
if (terminalAlerts.length > 0) {
const combined = [...terminalAlerts, ...get().recentAlerts]
.slice(0, MAX_RECENT_ALERTS);
set({ recentAlerts: combined });
}
Newest wins. No special preservation of high-severity terminal events.
Fix options
(A) Raise the cap to ~200. Crudest fix. Lets ~1 trading day of alerts
survive at realistic firing rates. Memory cost is negligible (PositionAlert is
small). Risk: hides the underlying issue rather than addressing it.
(B) Severity-aware eviction. Preserve critical/warn over info. When
evicting, prefer dropping ApproachingX / IvDecaying (heads-up tier) before
TargetHit / StopHit / BigCloseDetected (act-now tier). Roughly:
// Sort by (severity desc, fired_at desc) before slicing
const ranked = combined.sort((a, b) => {
const sd = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
if (sd !== 0) return sd;
return b.fired_at - a.fired_at;
});
set({ recentAlerts: ranked.slice(0, MAX_RECENT_ALERTS) });
Caveat: this changes the array's chronological invariant. Callers (AlertsPanel
sort, Action Summary most_recent_fired_at) would need to handle a
non-chronological input or re-sort downstream.
(C) Per-position cap instead of global. Cap at e.g. 5 alerts per position,
unlimited overall. Bounds growth at 5 × |positions| which is naturally
bounded by the position lifecycle. Best fit for the actual data shape.
Recommendation
(C) is the cleanest semantically. (A) is the safest 1-line shim while (C) is
designed. Avoid (B) — the chronological invariant matters more than I initially
thought.
Cross-refs
- R8 scorecard:
H-X4
- Related:
app/src/lib/alertSynthesis.ts ALERT_GRACE_AFTER_CLOSE_MS = 24h
assumes the buffer holds at least 24h of alerts
- Test gap: no test currently exercises buffer saturation; add one in
app/src/store/flowStore.test.ts before shipping any fix
Source: R8 fresh-eyes QA round, adversarial cross-tab scope (2026-05-17)
Severity: MED-HIGH (data loss in high-flow sessions, but no panic-level user impact)
Status: Deferred from R8 — design call, not a surgical patch
Symptom
MAX_RECENT_ALERTS = 50inapp/src/store/flowStore.ts:55caps the global alertbuffer. In high-flow sessions with 10+ active positions × 13 alert kinds, the
buffer can saturate within minutes, evicting older terminal alerts (TargetHit,
StopHit, BigCloseDetected) before the 24h grace window in
synthesizeSymbolAlertswants them.User-visible effect: a TargetHit from 30 min ago can disappear from the Action
Summary panel because 50 newer alerts pushed it off the end. Raw Alerts tab
also loses it (it reads from the same buffer).
Reproduction (synthetic)
Stop, BigClose, Wall, IV, Expiry, Roll — easy with realistic poll cadence)
from the buffer even though the positions are still open or in grace
Current code
Newest wins. No special preservation of high-severity terminal events.
Fix options
(A) Raise the cap to ~200. Crudest fix. Lets ~1 trading day of alerts
survive at realistic firing rates. Memory cost is negligible (PositionAlert is
small). Risk: hides the underlying issue rather than addressing it.
(B) Severity-aware eviction. Preserve
critical/warnoverinfo. Whenevicting, prefer dropping ApproachingX / IvDecaying (heads-up tier) before
TargetHit / StopHit / BigCloseDetected (act-now tier). Roughly:
Caveat: this changes the array's chronological invariant. Callers (AlertsPanel
sort, Action Summary
most_recent_fired_at) would need to handle anon-chronological input or re-sort downstream.
(C) Per-position cap instead of global. Cap at e.g. 5 alerts per position,
unlimited overall. Bounds growth at
5 × |positions|which is naturallybounded by the position lifecycle. Best fit for the actual data shape.
Recommendation
(C) is the cleanest semantically. (A) is the safest 1-line shim while (C) is
designed. Avoid (B) — the chronological invariant matters more than I initially
thought.
Cross-refs
H-X4app/src/lib/alertSynthesis.tsALERT_GRACE_AFTER_CLOSE_MS = 24hassumes the buffer holds at least 24h of alerts
app/src/store/flowStore.test.tsbefore shipping any fix