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
- 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
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
- 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
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
- [Stage] Failed to post caption reset for ${source} (channel
- MediaPipe backend not initialized (call init() first)
- gameletKit requires a host gamelet orchestration runtime.
- Extension module `${input.id}` is already registered for ses
- Unknown extension session: ${sessionId}
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/21a5d5f45687b4f4.
Report an issue: GitHub.