moeru-ai/airi · warning
[llm-streaming-control] signal handler failed
Error message
[llm-streaming-control] signal handler failed
What it means
The llm-streaming-control controller parses control tokens ('signals') embedded in the LLM output stream and awaits each registered signal handler in turn, each inside its own try/catch. When one handler rejects, the error is emitted as a 'signal-handler-error' event (with tokenType) and logged here; iteration continues with the remaining handlers, so a single bad handler degrades only its own side effect.
Source
Thrown at packages/pipelines-audio/src/llm-streaming-control/controller.ts:314
const signalContext: LlmStreamingControlSignalContext = {
...dispatchContext,
createdAt: Date.now(),
}
// snapshot prevents mutation during iteration
for (const handler of [...signalHandlers]) {
try {
await handler(parsed, signalContext)
}
catch (error) {
emit(context, {
type: 'signal-handler-error',
tokenType: parsed.type,
error,
})
console.warn(
'[llm-streaming-control] signal handler failed',
error,
)
}
}
if (parsed.type !== 'call')
return true
const turnState = dispatchContext.turnId
? turns.get(dispatchContext.turnId)
: undefined
const turnHandlers = turnState?.handlers.get(parsed.name)
const activeHandlers
= turnHandlers?.size && turnState
? turnHandlersView on GitHub (pinned to 677329427f)
Solutions
- Subscribe to the 'signal-handler-error' event to capture tokenType and the error for the failing handler
- Validate the parsed payload shape at the top of each handler and return early for unexpected types
- Wrap fallible side effects (audio, network, DOM) inside the handler with their own try/catch so the handler itself never rejects
Example fix
// before
controller.onSignal(async (parsed) => {
await scheduleAudioCue(parsed.cue) // throws when parsed has no cue field
})
// after
controller.onSignal(async (parsed) => {
if (parsed.type !== 'cue')
return
try {
await scheduleAudioCue(parsed.cue)
}
catch (error) {
console.warn('audio cue skipped', error)
}
}) Defensive patterns
Strategy: try-catch
Type guard
function isCueSignal(parsed: unknown): parsed is { type: 'cue', cue: string } {
return typeof parsed === 'object' && parsed !== null && 'cue' in parsed && parsed.type === 'cue'
} Try / catch
// inside your signal handler
try {
await applySignalSideEffect(parsed)
}
catch (error) {
// report without letting the controller see a rejection
console.warn('signal side effect failed for', parsed?.type, error)
} Prevention
- Subscribe to the 'signal-handler-error' event to observe handler failures with tokenType
- Narrow the parsed token shape before acting on it
- Never let side effects (audio, network, DOM) reject the handler itself
When it happens
Trigger: An onSignal-registered handler throwing on a specific parsed token shape: assuming a field exists on a signal type that doesn't have it, or a side effect (UI update, audio scheduling) raising for certain payloads.
Common situations: A handler written against one token schema while the model emits another variant; state accessed before initialization inside the handler; async side effects failing transiently.
Related errors
- [llm-streaming-control] handler failed
- [parseActEmotion] Failed to parse ACT payload JSON: "${paylo
- streaming models upstream ${res.status}: ${await res.text().
- streaming models upstream missing models[]
- streaming voices upstream ${res.status}: ${await res.text().
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/0ab25edf67f4a72c.
Report an issue: GitHub.