diff --git a/assistant/src/calls/call-orchestrator.ts b/assistant/src/calls/call-orchestrator.ts index d8e459c6daf..953842ba84d 100644 --- a/assistant/src/calls/call-orchestrator.ts +++ b/assistant/src/calls/call-orchestrator.ts @@ -22,6 +22,7 @@ import type { RelayConnection } from './relay-server.js'; import { registerCallOrchestrator, unregisterCallOrchestrator, fireCallQuestionNotifier, fireCallCompletionNotifier, fireCallTranscriptNotifier } from './call-state.js'; import type { PromptSpeakerContext } from './speaker-identification.js'; import { addPointerMessage, formatDuration } from './call-pointer-messages.js'; +import * as conversationStore from '../memory/conversation-store.js'; import { dispatchGuardianQuestion } from './guardian-dispatch.js'; import type { ServerMessage } from '../daemon/ipc-contract.js'; @@ -452,6 +453,13 @@ export class CallOrchestrator { if (spokenText.length > 0) { const session = getCallSession(this.callSessionId); if (session) { + // Persist assistant transcript to the voice conversation so it + // survives even when no live daemon Session is listening. + conversationStore.addMessage( + session.conversationId, + 'assistant', + JSON.stringify([{ type: 'text', text: spokenText }]), + ); fireCallTranscriptNotifier(session.conversationId, this.callSessionId, 'assistant', spokenText); } } diff --git a/assistant/src/calls/relay-server.ts b/assistant/src/calls/relay-server.ts index 7708c3991ff..3cd8046daa3 100644 --- a/assistant/src/calls/relay-server.ts +++ b/assistant/src/calls/relay-server.ts @@ -454,6 +454,13 @@ export class RelayConnection { const session = getCallSession(this.callSessionId); if (session) { + // Persist caller transcript to the voice conversation so it survives + // even when no live daemon Session is listening. + conversationStore.addMessage( + session.conversationId, + 'user', + JSON.stringify([{ type: 'text', text: msg.voicePrompt }]), + ); fireCallTranscriptNotifier(session.conversationId, this.callSessionId, 'caller', msg.voicePrompt); } diff --git a/assistant/src/config/bundled-skills/phone-calls/SKILL.md b/assistant/src/config/bundled-skills/phone-calls/SKILL.md index a1193fbb153..a85206e2414 100644 --- a/assistant/src/config/bundled-skills/phone-calls/SKILL.md +++ b/assistant/src/config/bundled-skills/phone-calls/SKILL.md @@ -198,7 +198,7 @@ An optional verification step where the callee must enter a numeric code via the | Setting | Description | Default | |---|---|---| | `calls.verification.enabled` | Enable DTMF callee verification | `false` | -| `calls.verification.codeLength` | Number of digits in the verification code | `4` | +| `calls.verification.codeLength` | Number of digits in the verification code | `6` | ## Optional: Higher Quality Voice with ElevenLabs diff --git a/assistant/src/daemon/session-process.ts b/assistant/src/daemon/session-process.ts index 1afea07a075..4878fece246 100644 --- a/assistant/src/daemon/session-process.ts +++ b/assistant/src/daemon/session-process.ts @@ -236,7 +236,6 @@ export async function processMessage( if (guardianDelivery) { const guardianRequest = getGuardianActionRequest(guardianDelivery.requestId); if (guardianRequest && guardianRequest.status === 'pending') { - const resolved = resolveGuardianActionRequest(guardianRequest.id, content, 'mac'); const userMsg = createUserMessage(content, attachments); const persisted = conversationStore.addMessage( session.conversationId, @@ -245,25 +244,36 @@ export async function processMessage( ); session.messages.push(userMsg); - if (resolved) { - void answerCall({ callSessionId: guardianRequest.callSessionId, answer: content }); - const confirmMsg = createAssistantMessage('Your answer has been relayed to the call.'); + // Attempt to deliver the answer to the call first. Only resolve + // the guardian action request if answerCall succeeds, so that a + // failed delivery leaves the request pending for retry from + // another channel. + const answerResult = await answerCall({ callSessionId: guardianRequest.callSessionId, answer: content }); + + if ('ok' in answerResult && answerResult.ok) { + const resolved = resolveGuardianActionRequest(guardianRequest.id, content, 'mac'); + const replyText = resolved + ? 'Your answer has been relayed to the call.' + : 'This question has already been answered from another channel.'; + const replyMsg = createAssistantMessage(replyText); conversationStore.addMessage( session.conversationId, 'assistant', - JSON.stringify(confirmMsg.content), + JSON.stringify(replyMsg.content), ); - session.messages.push(confirmMsg); - onEvent({ type: 'assistant_text_delta', text: 'Your answer has been relayed to the call.' }); + session.messages.push(replyMsg); + onEvent({ type: 'assistant_text_delta', text: replyText }); } else { - const staleMsg = createAssistantMessage('This question has already been answered from another channel.'); + const errorDetail = 'error' in answerResult ? answerResult.error : 'Unknown error'; + log.warn({ callSessionId: guardianRequest.callSessionId, error: errorDetail }, 'answerCall failed for mac guardian answer'); + const failMsg = createAssistantMessage('Failed to deliver your answer to the call. Please try again.'); conversationStore.addMessage( session.conversationId, 'assistant', - JSON.stringify(staleMsg.content), + JSON.stringify(failMsg.content), ); - session.messages.push(staleMsg); - onEvent({ type: 'assistant_text_delta', text: 'This question has already been answered from another channel.' }); + session.messages.push(failMsg); + onEvent({ type: 'assistant_text_delta', text: 'Failed to deliver your answer to the call. Please try again.' }); } onEvent({ type: 'message_complete', sessionId: session.conversationId }); return persisted.id;