From c4ac0d2722c047e86793c04eaf56e796aed2b00e Mon Sep 17 00:00:00 2001 From: Juno Date: Tue, 27 Jan 2026 14:42:37 +0000 Subject: [PATCH] fix(voice-call): skip stale calls on load to prevent stuck concurrent limit When the gateway restarts, loadActiveCalls() reloads non-terminal calls from calls.jsonl. However, these calls may have already ended (e.g., Twilio timed them out) and are now stale. This causes the concurrent call limit to be reached with phantom calls that can never be cleared. Fix: Skip loading calls that are older than maxDurationSeconds, as they cannot possibly still be active. Fixes calls getting stuck in 'initiated' state after gateway restarts. --- extensions/voice-call/src/manager.ts | 36 +++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/extensions/voice-call/src/manager.ts b/extensions/voice-call/src/manager.ts index 2e2e4661b..bc9c63e98 100644 --- a/extensions/voice-call/src/manager.ts +++ b/extensions/voice-call/src/manager.ts @@ -830,6 +830,10 @@ export class CallManager { const logPath = path.join(this.storePath, "calls.jsonl"); if (!fs.existsSync(logPath)) return; + // Calls older than maxDurationSeconds are definitely stale + const maxAgeMs = this.config.maxDurationSeconds * 1000; + const now = Date.now(); + // Read file synchronously and parse lines const content = fs.readFileSync(logPath, "utf-8"); const lines = content.split("\n"); @@ -847,18 +851,28 @@ export class CallManager { } } - // Only keep non-terminal calls + // Only keep non-terminal calls that aren't stale for (const [callId, call] of callMap) { - if (!TerminalStates.has(call.state)) { - this.activeCalls.set(callId, call); - // Populate providerCallId mapping for lookups - if (call.providerCallId) { - this.providerCallIdMap.set(call.providerCallId, callId); - } - // Populate processed event IDs - for (const eventId of call.processedEventIds) { - this.processedEventIds.add(eventId); - } + // Skip terminal states + if (TerminalStates.has(call.state)) continue; + + // Skip stale calls (older than max duration) + const callAge = now - call.startedAt; + if (callAge > maxAgeMs) { + console.log( + `[voice-call] Skipping stale call ${callId} (age: ${Math.round(callAge / 1000)}s, max: ${this.config.maxDurationSeconds}s)`, + ); + continue; + } + + this.activeCalls.set(callId, call); + // Populate providerCallId mapping for lookups + if (call.providerCallId) { + this.providerCallIdMap.set(call.providerCallId, callId); + } + // Populate processed event IDs + for (const eventId of call.processedEventIds) { + this.processedEventIds.add(eventId); } } }