Symptom
A real user's `status.js` reported `activeStreams: 333` in `~/.config/devclocked/cursor-hook-state`. Stream-state files accumulate without bound.
Root cause
`removeStreamState` is only called on `sessionEnd` (`hooks/ship.js`), and it only removes the session-root stream. Two leak paths:
- If Cursor never delivers `sessionEnd` (which the 333 count strongly implies), the root state is never removed.
- Sidechain/primary streams that get their own throttle-state file (`ship.js` saves `last_tick_at` under `throttleId`, which can be `primaryStreamId`) are never removed — `sessionEnd` only deletes the root id.
There is no time-based sweep of `STATE_DIR` anywhere.
Fix
Add a TTL-based `pruneStaleStreamState()` sweep (run once per ship pass, under the shipper lock) that deletes `stream_*.json` whose `last_tick_at || started_at` is older than a TTL (proposed 6h), plus orphans with no timestamp. Robust regardless of why `sessionEnd` is missing.
Impact
State-dir clutter; not data loss, but a reliability/hygiene bug and a signal that `sessionEnd` delivery is unreliable.
Symptom
A real user's `status.js` reported `activeStreams: 333` in `~/.config/devclocked/cursor-hook-state`. Stream-state files accumulate without bound.
Root cause
`removeStreamState` is only called on `sessionEnd` (`hooks/ship.js`), and it only removes the session-root stream. Two leak paths:
There is no time-based sweep of `STATE_DIR` anywhere.
Fix
Add a TTL-based `pruneStaleStreamState()` sweep (run once per ship pass, under the shipper lock) that deletes `stream_*.json` whose `last_tick_at || started_at` is older than a TTL (proposed 6h), plus orphans with no timestamp. Robust regardless of why `sessionEnd` is missing.
Impact
State-dir clutter; not data loss, but a reliability/hygiene bug and a signal that `sessionEnd` delivery is unreliable.