#9760: fix(voice-call): discard stale calls on plugin restart
channel: voice-call
stale
Cluster:
Voice Call Enhancements and Fixes
Fixes #9707
## Problem
When the voice-call plugin restarts (e.g. after a crash or gateway restart), it reloads active calls from `calls.jsonl`. However, calls that were active before the crash but have since expired (exceeded `maxDurationSeconds`) are still restored as active. These stale calls count toward `maxConcurrentCalls`, blocking new calls from being placed until the stale entries are manually cleared.
## Solution
- Added optional `maxAgeMs` parameter to `loadActiveCallsFromStore()` so it can skip calls older than a given threshold during recovery
- In `CallManager.loadActiveCalls()`, use `config.maxDurationSeconds` to compute the TTL and discard any call whose `startedAt` exceeds that window
- Both the manager-level inline filter and the store-level parameter work together for defense-in-depth
## Changes
- `extensions/voice-call/src/manager.ts` - skip stale calls in `loadActiveCalls()` using `maxDurationSeconds * 1000` as the age threshold
- `extensions/voice-call/src/manager/store.ts` - accept `opts.maxAgeMs` in `loadActiveCallsFromStore()` and filter accordingly
- `extensions/voice-call/src/manager/store.test.ts` - 5 new tests covering: empty store, non-terminal load, terminal skip, stale discard with maxAgeMs, and retention without maxAgeMs
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR updates voice-call crash recovery to avoid restoring stale active calls from `calls.jsonl`. `CallManager.loadActiveCalls()` now discards non-terminal calls whose `startedAt` is older than `config.maxDurationSeconds`, and the store helper `loadActiveCallsFromStore()` adds an optional `maxAgeMs` filter to enforce the same TTL during load. New vitest coverage exercises loading behavior and the max-age filter in the store loader.
<h3>Confidence Score: 4/5</h3>
- Mostly safe to merge, but TTL bypass on future timestamps should be fixed.
- Change is localized and test-covered, but both recovery paths use `now - startedAt` without handling future timestamps; a single bad persisted record could still be restored and block concurrency, undermining the PR goal.
- extensions/voice-call/src/manager.ts, extensions/voice-call/src/manager/store.ts
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Also fixes #6721
lobster-biscuit
Most Similar PRs
#4325: fix(voice-call): verify call status with provider before loading st...
by garnetlyx · 2026-01-30
83.7%
#18852: fix: Voice-call state persistence is fire-and-forget, causing silen...
by coygeek · 2026-02-17
80.4%
#6702: fix(voice-call): mark calls as ended when media stream disconnects
by johngnip · 2026-02-01
75.0%
#19103: fix(voice-call): replace console.log with runtime logging
by Clawborn · 2026-02-17
73.3%
#19073: feat(voice-call): streaming TTS, barge-in, silence filler, hangup, ...
by odrobnik · 2026-02-17
72.7%
#7535: fix(voice-call): queue realtime transcript waiters
by hclsys · 2026-02-02
71.4%
#7652: fix(voice-call): fix Telnyx transcription (STT) not working
by tturnerdev · 2026-02-03
71.3%
#6677: fix(tts): always load fresh config for voice selection
by Jinqiao · 2026-02-01
70.9%
#8251: fix(voice-call): remove redundant transcript from extraSystemPrompt
by geodeterra · 2026-02-03
70.9%
#21566: feat(voice-call): bridge call transcripts to main agent session
by MegaPhoenix92 · 2026-02-20
69.9%