{"record":{"id":"e0c0da86d6d3f66b","repo":"moeru-ai/airi","slug":"speech-pipeline-provider-voice-model-changed-mid","errorCode":null,"errorMessage":"[Speech Pipeline] provider/voice/model changed mid-session, tearing down","messagePattern":"\\[Speech Pipeline\\] provider/voice/model changed mid-session, tearing down","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/stage-ui/src/components/scenes/Stage.vue","lineNumber":899,"sourceCode":"  // })\n\n  // await db.value?.execute(`INSERT INTO memory_test (vec) VALUES (${JSON.stringify(res.embedding)});`)\n}))\n\n// Mid-session provider / voice / model swaps would otherwise keep feeding\n// tokens to the OLD adapter (segmenter for the new provider, or stale ws\n// for the streaming provider). Cancel the active session so the next LLM\n// token after the swap falls through `currentSession?.` cleanly (silent\n// drop is acceptable — we don't try to fork-replay text into a new\n// adapter with potentially different voice/model).\nwatch(\n  [activeSpeechProvider, () => activeSpeechVoice.value?.id, activeSpeechModel],\n  ([provider, voiceId, model], [prevProvider, prevVoiceId, prevModel]) => {\n    if (!currentSession)\n      return\n    if (provider === prevProvider && voiceId === prevVoiceId && model === prevModel)\n      return\n    console.warn('[Speech Pipeline] provider/voice/model changed mid-session, tearing down', {\n      provider,\n      prevProvider,\n      voiceId,\n      prevVoiceId,\n      model,\n      prevModel,\n    })\n    currentSession.cancel('provider-or-voice-changed')\n    currentSession = null\n  },\n)\n\n// Resume audio context on first user interaction (browser requirement)\nlet audioContextResumed = false\nfunction resumeAudioContextOnInteraction() {\n  if (audioContextResumed || !audioContext)\n    return\n  audioContextResumed = true","sourceCodeStart":881,"sourceCodeEnd":917,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/components/scenes/Stage.vue#L881-L917","documentation":"A watcher on [activeSpeechProvider, activeSpeechVoice.id, activeSpeechModel] cancels the current speech session whenever any of the three changes mid-session. The session is torn down via currentSession.cancel('provider-or-voice-changed') and in-flight LLM tokens for that turn are silently dropped rather than fork-replayed into the new provider (whose voice/model may differ).","triggerScenarios":"Any change to speech provider, selected voice id, or speech model while a TTS session is active: the user edits speech settings mid-conversation, or a store hydration/sync rewrites these refs after speech already started.","commonSituations":"Changing voice while the character is speaking; switching provider mid-turn; settings hydration firing after session open, producing a spurious teardown with identical values replaced by new references.","solutions":["Avoid mutating speech provider/voice/model while a reply is being voiced; apply changes between turns","If teardown fires right after startup with no user action, make settings hydration write identical values without churn so the watcher does not trigger spuriously","Re-send the message to hear it with the new provider/voice","For smoother UX, defer applying speech settings until the current session finishes"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"// Freeze speech identity while a session is active\nconst speechIdentityFrozen = computed(() => currentSession !== null)\n// In settings UI: disable voice/model/provider pickers while speechIdentityFrozen","typeGuard":"function speechIdentityChanged(\n  cur: [string, string | undefined, string],\n  prev: [string, string | undefined, string],\n): boolean {\n  return cur[0] !== prev[0] || cur[1] !== prev[1] || cur[2] !== prev[2]\n}","tryCatchPattern":null,"preventionTips":["Defer speech settings changes until the current session finishes instead of cancelling mid-turn","Ensure settings hydration writes identical values without object/string identity churn so the watcher does not fire spuriously","After a teardown, re-send the message if the user needs to hear it with the new settings"],"tags":["speech","session-lifecycle","vue-watch","teardown","settings"],"backgroundTag":"config-changed-mid-session","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}