Watch process memory and emit baseline + threshold crossings
Prowl users report that after running for hours the app drifts from a
500–600 MB working set to tens of gigabytes. The observability we have
up to now (crashes, events, super properties) doesn't surface that
trajectory — a hang or crash might fire eventually, but the growth
between a healthy baseline and the explosion is invisible.
Add MemoryProbe + MemoryWatchdog so every session contributes exactly
enough signal to reconstruct that trajectory without drowning the free
tier:
- MemoryProbe reads phys_footprint via task_info(TASK_VM_INFO). That's
the same number Activity Monitor shows and what Apple recommends for
"this app's contribution to RAM pressure" (folds in compressed pages).
Marked nonisolated so it can be consumed from any actor.
- MemoryWatchdog ticks every 5 minutes on a weak-ref Task. Fires:
* app_memory_baseline once, at 3 min uptime — the clean working set
after first-time-setup settles.
* memory_threshold_2048mb / 4096mb / 8192mb — each at most once per
session when phys_footprint first crosses that floor. 4GB+ also
routes through SentrySDK.capture(message:) so Sentry dashboards
pair the spike with action breadcrumbs + device context.
Re-crossing a threshold after dropping does NOT re-fire — intentional,
because we want the monotonic envelope of the session, not noise.
- Every event carries repository_count / opened_worktree_count /
terminal_tab_count in addition to resident_mb + growth_ratio, so
PostHog can answer "does the leak track worktree count?" via a single
query.
The watchdog is constructed regardless of build config but only start()s
in Release — the analytics path is #if !DEBUG gated downstream anyway.
Context provider captures appStore + terminalManager so the watchdog
itself stays decoupled from TCA.
6 unit tests cover: no baseline before delay, baseline once-only,
threshold once-each including Sentry routing at 4GB+, re-cross is a
no-op, thresholds stay silent without a baseline, property payload
content.