moeru-ai/airi · error · Error

[${errorCode}] Unexpected output action: ${output.action}

Error message

[${errorCode}] Unexpected output action: ${output.action}

What it means

Thrown in generate() when the inference worker returns an output object whose `action` field is not 'generate'. The adapter only knows how to handle generate-action outputs (PCM samples); any other action means the worker and adapter have diverged on the inference-result protocol. The message is prefixed with a classifyError code for telemetry routing.

Source

Thrown at packages/stage-ui/src/libs/inference/adapters/kokoro.ts:461

      )

      worker.postMessage({
        type: 'run-inference',
        requestId,
        input: { action: 'generate', text, voice },
      })

      const response = await resultPromise
      const output = response.output

      if (output.action === 'generate') {
        state = 'ready'
        onSuccess()
        return encodeWav(output.samples as Float32Array, output.samplingRate as number)
      }

      const errorCode = classifyError(new Error('Unexpected output action'))
      throw new Error(`[${errorCode}] Unexpected output action: ${output.action}`)
    }), { text: text.slice(0, 50), voice }).catch((error) => {
      if (error === notReadyError)
        throw error

      // Cancellation is a caller-controlled lifecycle outcome, not a worker
      // failure. Keep the loaded model available and avoid restarting the
      // worker after waitForWorkerMessage has already posted `cancel`.
      if ((error as Error)?.name === 'AbortError') {
        if (state === 'running')
          state = 'ready'
        throw error
      }

      handleWorkerError(error instanceof Error ? error : new Error(String(error)))
      throw error
    })
  }

View on GitHub (pinned to 27111382b4)

Solutions

  1. Ensure the Kokoro worker bundle and adapter are from the same version (protocol contracts must match).
  2. Inspect the actual output.action value (it is in the message) to identify which worker path produced it.
  3. If a new legitimate action was added, extend the adapter's if/else to handle it before this throw.
  4. Reload the model and retry once to rule out a transient worker state corruption.
Defensive patterns

Strategy: try-catch

Validate before calling

// before generate(), ensure the model is ready
if (!worker || state !== 'ready') throw notReadyError
// the action check is inherent to the worker protocol; keep worker and adapter versions aligned

Type guard

function isGenerateOutput(output: unknown): output is { action: 'generate', samples: Float32Array, samplingRate: number } {
  return !!output && typeof output === 'object'
    && (output as { action?: string }).action === 'generate'
}

Try / catch

try {
  const wav = await adapter.generate(text, voice, { signal })
}
catch (err) {
  if (err instanceof Error && err.message.includes('Unexpected output action')) {
    // worker/adapter protocol mismatch — reload model, realign versions; do not retry unchanged
  }
  else if ((err as Error)?.name === 'AbortError') {
    // caller cancellation; do not treat as failure
  }
  else throw err
}

Prevention

When it happens

Trigger: waitForWorkerMessage for 'inference-result' resolves with response.output.action !== 'generate'. Caused by a worker that emits a different action (e.g. a partial/progress result, an error-shaped result, or a new protocol action the adapter does not understand).

Common situations: Worker bundle updated to a protocol version that adds new action types the adapter does not handle. A worker bug emitting an unexpected action on success. Model/quantization producing a result shape the adapter did not anticipate.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/2f05578c41a11610. Report an issue: GitHub.