moeru-ai/airi · error · Error

Computer use has stopped.

Error message

Computer use has stopped.

What it means

setupComputerUse lazily creates the computer-use runtime on first use. getRuntime throws this error when the host previously called dispose() and the service is marked stopped, so any later caller cannot resurrect the runtime. It exists to fail fast instead of spawning a daemon after the feature was shut down.

Solutions

  1. Re-invoke setupComputerUse to create a fresh runtime before any further calls
  2. Remove or cancel queued tool calls when the service is disposed
  3. Check the stopped state (or track dispose in caller state) before dispatching computer-use commands

Example fix

// before
await computerUse.run({ argv: ['invoke', 'screen.capture'] }) // after stop
// after
if (!computerUse.isStopped()) {
  await computerUse.setupComputerUse(context)
  await computerUse.run({ argv: ['invoke', 'screen.capture'] })
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Track service state in the caller
let computerUseDisposed = false
function canUseComputerUse() { return !computerUseDisposed }

Type guard

function isRuntimeAvailable(rt: ReturnType<typeof createComputerUseRuntime> | undefined): rt is ReturnType<typeof createComputerUseRuntime> { return rt !== undefined }

Try / catch

try {
  await computerUse.run(input)
} catch (e) {
  if (e instanceof Error && e.message === 'Computer use has stopped.') {
    return { status: 'cancelled' } // do not retry; re-setup is required
  }
  throw e
}

Prevention

When it happens

Trigger: Calling any computer-use operation (via run or readImage through getRuntime) after setupComputerUse's runtime was disposed with dispose(), which sets stopped = true.

Common situations: App quit/shutdown ordering where a pending AI tool call arrives after dispose; hot-reload of the main process leaving a stale handler; a window requesting a screenshot after the user disabled computer use.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17). Data as JSON: /api/errors/989f9437d20c2023. Report an issue: GitHub.

Appendix: source

Thrown at apps/stage-tamagotchi/src/main/services/airi/computer-use/index.ts:19

import type { createContext } from '@moeru/eventa/adapters/electron/main'

import { join, sep } from 'node:path'

import { binaryPath } from '@auv-js/cli/binary'
import { defineInvokeHandler } from '@moeru/eventa'
import { app } from 'electron'

import { computerUseReadImage, computerUseRun } from '../../../../shared/eventa/computer-use'
import { onAppBeforeQuit } from '../../../libs/bootkit/lifecycle'
import { createComputerUseRuntime } from './runtime'

/** Registers host-owned computer use. The executable is resolved only on the first call. */
export function setupComputerUse(context: ReturnType<typeof createContext>['context']) {
  let runtime: ReturnType<typeof createComputerUseRuntime> | undefined
  let stopped = false
  function getRuntime() {
    if (stopped)
      throw new Error('Computer use has stopped.')
    if (!runtime) {
      const executable = binaryPath()
      runtime = createComputerUseRuntime({
        // Native executables cannot run inside Electron's archive.
        binaryPath: app.isPackaged ? executable.replace(`${sep}app.asar${sep}`, `${sep}app.asar.unpacked${sep}`) : executable,
        storeRoot: join(app.getPath('userData'), 'computer-use'),
      })
    }
    return runtime
  }
  defineInvokeHandler(context, computerUseRun, input => getRuntime().run(input))
  defineInvokeHandler(context, computerUseReadImage, input => getRuntime().readImage(input))
  onAppBeforeQuit(async () => {
    stopped = true
    await runtime?.dispose()
  })
}

View on GitHub (pinned to 438a067dde)