moeru-ai/airi · info

[Stage] Failed to post caption reset for ${source} (channel

Error message

[Stage] Failed to post caption reset for ${source} (channel may be closed)

What it means

Stage.vue's resetAssistantSpeechSurface resets speech state (nowSpeaking, mouthOpenSize, assistantCaption) and then posts an empty 'caption-assistant' event over the stage channel (postCaption). If the underlying channel is already closed when the post runs, it throws synchronously and this warning is logged. The local state reset happens first, so the UI is still cleared; only the cross-surface caption broadcast was lost.

Source

Thrown at packages/stage-ui/src/components/scenes/Stage.vue:191

  }, 100)
}))

const audioAnalyser = ref<AnalyserNode>()
const lipSyncStarted = ref(false)
const lipSyncLoopId = ref<number>()
const live2dLipSync = ref<Live2DLipSync>()
const live2dLipSyncOptions: Live2DLipSyncOptions = { mouthUpdateIntervalMs: 50, mouthLerpWindowMs: 50 }

function resetAssistantSpeechSurface(source: string) {
  nowSpeaking.value = false
  mouthOpenSize.value = 0
  assistantCaption.value = ''

  try {
    postCaption({ type: 'caption-assistant', text: '' })
  }
  catch (error) {
    console.warn(`[Stage] Failed to post caption reset for ${source} (channel may be closed)`, { error })
  }

  try {
    postPresent({ type: 'assistant-reset' })
  }
  catch (error) {
    console.warn(`[Stage] Failed to post present reset for ${source} (channel may be closed)`, { error })
  }
}

const { activeCard } = storeToRefs(useAiriCardStore())
const speechStore = useSpeechStore()
const { ssmlEnabled, activeSpeechProvider, activeSpeechModel, activeSpeechVoice, pitch } = storeToRefs(speechStore)
const activeCardId = computed(() => activeCard.value?.name ?? 'default')
const speechRuntimeStore = useSpeechRuntimeStore()
const { trackOfficialTtsAutoEnabled } = useAnalytics()
let officialAutoTtsTrackedForTurn = false
const backgroundStore = useBackgroundStore()

View on GitHub (pinned to 677329427f)

Solutions

  1. Treat as benign when it only appears during teardown or reload — state reset already succeeded
  2. Stop speech and post the caption reset before closing/disposing the stage channel (order teardown: speech stop -> caption reset -> channel close)
  3. Track channel closed state and skip posting when already closed to silence the warning

Example fix

// before
try {
  postCaption({ type: 'caption-assistant', text: '' })
}
catch (error) {
  console.warn(`[Stage] Failed to post caption reset for ${source} (channel may be closed)`, { error })
}

// after
if (!captionChannelClosed.value) {
  try {
    postCaption({ type: 'caption-assistant', text: '' })
  }
  catch (error) {
    captionChannelClosed.value = true
    console.warn(`[Stage] Failed to post caption reset for ${source} (channel may be closed)`, { error })
  }
}
Defensive patterns

Strategy: validation

Validate before calling

// track channel liveness once
let captionChannelClosed = false
captionChannel.onclose = () => { captionChannelClosed = true }

// before posting
if (!captionChannelClosed) {
  postCaption({ type: 'caption-assistant', text: '' })
}

Try / catch

try {
  postCaption({ type: 'caption-assistant', text: '' })
}
catch (error) {
  // channel closed during teardown: state already reset, safe to ignore
  if (!isTeardown)
    console.warn('[Stage] caption reset failed', { error })
}

Prevention

When it happens

Trigger: Calling resetAssistantSpeechSurface (speech end, abort, or teardown paths) after the caption channel closed: component unmount, window teardown, hot reload, or a second reset racing the channel's close.

Common situations: Closing the desktop window mid-utterance; Vue HMR re-running setup; duplicated cleanup invocations (error handler plus onUnmounted both firing).

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/02da5bac2d16f4ec. Report an issue: GitHub.