Decisions/ADR-003-lite-mode-accumulation.md
ADR-003: Lite Mode Uses USER_TALKING_MESSAGE Accumulation Pattern
Decision
The Lite mode conversation loop (attachLiteLoop) accumulates text from USER_TALKING_MESSAGE events into pendingTranscript. On USER_END_MESSAGE, it drains pendingTranscript as the final user utterance. It does NOT read event.message on USER_END_MESSAGE.
Context
The implementation plan originally assumed USER_END_MESSAGE carried the final transcript in an event.message field. Reading the actual SDK type definitions revealed:
// UserTalkingEndEvent
interface UserTalkingEndEvent {
type: string
task_id: string
// NO message field
}
USER_END_MESSAGE only signals "user stopped speaking." The transcript lives in USER_TALKING_MESSAGE events (which fire incrementally with partial/full transcript chunks).
Implementation
let pendingTranscript = ''
function handleUserTalking(event: { message: string }) {
const text = event.message?.trim()
if (text) pendingTranscript = text
}
async function handleUserEnd() {
const userText = pendingTranscript.trim()
pendingTranscript = ''
if (!userText) return
// → call LLM → speak
}
Consequence
The last USER_TALKING_MESSAGE before USER_END_MESSAGE becomes the final transcript. If HeyGen ever adds a message field to UserTalkingEndEvent, this pattern still works correctly (we just ignore the new field).