{"record":{"id":"21a5d5f45687b4f4","repo":"moeru-ai/airi","slug":"stage-failed-to-post-present-reset-for-source","errorCode":null,"errorMessage":"[Stage] Failed to post present reset for ${source} (channel may be closed)","messagePattern":"\\[Stage\\] Failed to post present reset for (.+?) \\(channel may be closed\\)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"info","filePath":"packages/stage-ui/src/components/scenes/Stage.vue","lineNumber":198,"sourceCode":"const live2dLipSyncOptions: Live2DLipSyncOptions = { mouthUpdateIntervalMs: 50, mouthLerpWindowMs: 50 }\n\nfunction resetAssistantSpeechSurface(source: string) {\n  nowSpeaking.value = false\n  mouthOpenSize.value = 0\n  assistantCaption.value = ''\n\n  try {\n    postCaption({ type: 'caption-assistant', text: '' })\n  }\n  catch (error) {\n    console.warn(`[Stage] Failed to post caption reset for ${source} (channel may be closed)`, { error })\n  }\n\n  try {\n    postPresent({ type: 'assistant-reset' })\n  }\n  catch (error) {\n    console.warn(`[Stage] Failed to post present reset for ${source} (channel may be closed)`, { error })\n  }\n}\n\nconst { activeCard } = storeToRefs(useAiriCardStore())\nconst speechStore = useSpeechStore()\nconst { ssmlEnabled, activeSpeechProvider, activeSpeechModel, activeSpeechVoice, pitch } = storeToRefs(speechStore)\nconst activeCardId = computed(() => activeCard.value?.name ?? 'default')\nconst speechRuntimeStore = useSpeechRuntimeStore()\nconst { trackOfficialTtsAutoEnabled } = useAnalytics()\nlet officialAutoTtsTrackedForTurn = false\nconst backgroundStore = useBackgroundStore()\nconst { activeBackgroundUrl } = storeToRefs(backgroundStore)\n\nconst { currentMotion } = storeToRefs(useLive2dParams())\n\nconst emotionsQueue = createQueue<EmotionPayload>({\n  handlers: [\n    async (ctx) => {","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/components/scenes/Stage.vue#L180-L216","documentation":"The same resetAssistantSpeechSurface cleanup in Stage.vue also posts an 'assistant-reset' present event via postPresent. If the present channel is already closed, the synchronous post throws and this warning is logged after the caption reset attempt. As with the caption variant, in-memory state was already reset; only the present-surface notification was dropped.","triggerScenarios":"Assistant reset (turn end, speech abort, surface teardown) firing after the present channel closed: unmount, window close, reload, or double-cleanup races.","commonSituations":"Window closed mid-utterance; HMR re-mounting the Stage; cleanup called from both an error path and unmount in the same tick.","solutions":["Ignore when it only occurs during shutdown — it is expected on closed channels","Order teardown so assistant-reset is posted before the present channel is disposed","Gate postPresent behind a channel-open check if the warning is noisy in logs"],"exampleFix":"// before\ntry {\n  postPresent({ type: 'assistant-reset' })\n}\ncatch (error) {\n  console.warn(`[Stage] Failed to post present reset for ${source} (channel may be closed)`, { error })\n}\n\n// after\nif (presentChannelOpen.value) {\n  try {\n    postPresent({ type: 'assistant-reset' })\n  }\n  catch (error) {\n    presentChannelOpen.value = false\n    console.warn(`[Stage] Failed to post present reset for ${source} (channel may be closed)`, { error })\n  }\n}","handlingStrategy":"validation","validationCode":"let presentChannelClosed = false\npresentChannel.onclose = () => { presentChannelClosed = true }\n\nif (!presentChannelClosed) {\n  postPresent({ type: 'assistant-reset' })\n}","typeGuard":null,"tryCatchPattern":"try {\n  postPresent({ type: 'assistant-reset' })\n}\ncatch (error) {\n  // expected only when the stage is being torn down\n  if (!isTeardown)\n    console.warn('[Stage] present reset failed', { error })\n}","preventionTips":["Close present channels only after assistant-reset was posted","Centralize channel lifecycle in one owner instead of posting from many components","Filter these warnings out of error dashboards when they correlate with session end"],"tags":["broadcast-channel","teardown","stage","lifecycle"],"backgroundTag":"channel-closed-on-write","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"}