moeru-ai/airi · info

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

Error message

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

What it means

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.

Source

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

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()
const { activeBackgroundUrl } = storeToRefs(backgroundStore)

const { currentMotion } = storeToRefs(useLive2dParams())

const emotionsQueue = createQueue<EmotionPayload>({
  handlers: [
    async (ctx) => {

View on GitHub (pinned to 677329427f)

Solutions

  1. Ignore when it only occurs during shutdown — it is expected on closed channels
  2. Order teardown so assistant-reset is posted before the present channel is disposed
  3. Gate postPresent behind a channel-open check if the warning is noisy in logs

Example fix

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

// after
if (presentChannelOpen.value) {
  try {
    postPresent({ type: 'assistant-reset' })
  }
  catch (error) {
    presentChannelOpen.value = false
    console.warn(`[Stage] Failed to post present reset for ${source} (channel may be closed)`, { error })
  }
}
Defensive patterns

Strategy: validation

Validate before calling

let presentChannelClosed = false
presentChannel.onclose = () => { presentChannelClosed = true }

if (!presentChannelClosed) {
  postPresent({ type: 'assistant-reset' })
}

Try / catch

try {
  postPresent({ type: 'assistant-reset' })
}
catch (error) {
  // expected only when the stage is being torn down
  if (!isTeardown)
    console.warn('[Stage] present reset failed', { error })
}

Prevention

When it happens

Trigger: Assistant reset (turn end, speech abort, surface teardown) firing after the present channel closed: unmount, window close, reload, or double-cleanup races.

Common situations: Window closed mid-utterance; HMR re-mounting the Stage; cleanup called from both an error path and unmount in the same tick.

Related errors


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