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
          ? turnHandlers

View on GitHub (pinned to 677329427f)

Solutions

  1. Subscribe to the 'signal-handler-error' event to capture tokenType and the error for the failing handler
  2. Validate the parsed payload shape at the top of each handler and return early for unexpected types
  3. 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

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


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