diff --git a/assistant/src/calls/call-orchestrator.ts b/assistant/src/calls/call-orchestrator.ts index 2a795731729..24d466e7f1b 100644 --- a/assistant/src/calls/call-orchestrator.ts +++ b/assistant/src/calls/call-orchestrator.ts @@ -51,6 +51,13 @@ export class CallOrchestrator { registerCallOrchestrator(callSessionId, this); } + /** + * Returns the current orchestrator state. + */ + getState(): OrchestratorState { + return this.state; + } + /** * Handle a final caller utterance from the ConversationRelay. */ diff --git a/assistant/src/calls/twilio-routes.ts b/assistant/src/calls/twilio-routes.ts index 998ae124e6f..aacbfeb36e7 100644 --- a/assistant/src/calls/twilio-routes.ts +++ b/assistant/src/calls/twilio-routes.ts @@ -218,7 +218,15 @@ export async function handleCallAnswer(req: Request, callSessionId: string): Pro return Response.json({ error: 'No active orchestrator for this call' }, { status: 409 }); } - // Mark question as answered + if (orchestrator.getState() !== 'waiting_on_user') { + log.warn( + { callSessionId, state: orchestrator.getState() }, + 'handleCallAnswer: orchestrator is not in waiting_on_user state', + ); + return Response.json({ error: 'Orchestrator is not waiting for an answer' }, { status: 409 }); + } + + // Mark question as answered — only after confirming the orchestrator is ready answerPendingQuestion(question.id, body.answer); // Route answer to the orchestrator diff --git a/assistant/src/tools/calls/call-end.ts b/assistant/src/tools/calls/call-end.ts index e040725394b..091ca8e5e2e 100644 --- a/assistant/src/tools/calls/call-end.ts +++ b/assistant/src/tools/calls/call-end.ts @@ -5,6 +5,7 @@ import { registerTool } from '../registry.js'; import { getCallSession, updateCallSession } from '../../calls/call-store.js'; import { getCallOrchestrator, unregisterCallOrchestrator } from '../../calls/call-state.js'; import { activeRelayConnections } from '../../calls/relay-server.js'; +import { TwilioConversationRelayProvider } from '../../calls/twilio-provider.js'; import { getLogger } from '../../util/logger.js'; const log = getLogger('call-end'); @@ -61,6 +62,17 @@ class CallEndTool implements Tool { log.info({ callSessionId, reason }, 'Ending call'); + // Terminate the call via the provider API so Twilio hangs up, + // even if the relay WebSocket is not connected. + if (session.providerCallSid) { + try { + const provider = new TwilioConversationRelayProvider(); + await provider.endCall(session.providerCallSid); + } catch (endErr) { + log.warn({ err: endErr, callSessionId, callSid: session.providerCallSid }, 'Failed to terminate call via provider API — proceeding with cleanup'); + } + } + // End the relay connection if active const relayConnection = activeRelayConnections.get(callSessionId); if (relayConnection) { diff --git a/assistant/src/tools/calls/call-start.ts b/assistant/src/tools/calls/call-start.ts index c69030f134b..f031bd48d25 100644 --- a/assistant/src/tools/calls/call-start.ts +++ b/assistant/src/tools/calls/call-start.ts @@ -70,6 +70,10 @@ class CallStartTool implements Tool { const callContext = input.context as string | undefined; + // Create session outside the try block so it's available in the catch block + // for marking as failed if the provider call fails. + let sessionId: string | null = null; + try { const config = getTwilioConfig(); const provider = new TwilioConversationRelayProvider(); @@ -81,6 +85,7 @@ class CallStartTool implements Tool { toNumber: phoneNumber, task: callContext ? `${task}\n\nContext: ${callContext}` : task, }); + sessionId = session.id; log.info({ callSessionId: session.id, to: phoneNumber, task }, 'Initiating outbound call'); @@ -111,6 +116,16 @@ class CallStartTool implements Tool { } catch (err) { const msg = err instanceof Error ? err.message : String(err); log.error({ err, phoneNumber }, 'Failed to initiate call'); + + // Mark the session as failed so it doesn't stay in 'initiated' state + if (sessionId) { + updateCallSession(sessionId, { + status: 'failed', + endedAt: Date.now(), + lastError: msg, + }); + } + return { content: `Error initiating call: ${msg}`, isError: true }; } }