Four demo-safety fixes on top of #43, none touching demo-pod data:
- De-dup at source: the suppression path returned before activeConflicts.add(),
so the same unresolved dismissed collision wrote a SuppressionDoc every frame
(~194 spam rows observed on prod). Now mark the conflict handled first, so it
records ONCE per recurrence and re-arms via the onScreenFrame resolution sweep.
- Await the write: recordSuppression is the visible learning proof, so await it
(like recordCollision/recordIntervention) instead of void ...catch().
- Display de-dup: collapse suppressed beats to one per file (most recent) with a
stable per-file id, so any pre-fix duplicate rows never render as spam.
- Live-graph gate: suppression beats now satisfy the "has activity" check in
materializePodGraph, so a clean pod with preserved suppressions (but no
collision/file nodes) no longer falls back to the demo graph and hides the proof.
- Ops: /api/memory/stats counts `suppressions`; docs/mongodb.md documents the
collection and flags it "preserve in DB cleanup" (visible learning evidence).
Verified end-to-end on a throwaway pod (not demo-pod): suppression-only pod
materializes; 2 dupe rows render as 1 beat. backend+frontend typecheck + eslint pass.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>