slopus/happy · error · Error

Backend has been disposed

Error message

Backend has been disposed

What it means

AcpBackend.startSession() checks the `disposed` flag before creating a new ACP session. Once `dispose()` has been called (e.g. process exit, CLI shutdown, or manual teardown), the backend is permanently unusable and any attempt to start a session throws this error instead of spawning the agent process. It exists to prevent use-after-free on killed child processes and closed connections.

Source

Thrown at packages/happy-cli/src/agent/acp/AcpBackend.ts:376

    if (index !== -1) {
      this.listeners.splice(index, 1);
    }
  }

  private emit(msg: AgentMessage): void {
    if (this.disposed) return;
    for (const listener of this.listeners) {
      try {
        listener(msg);
      } catch (error) {
        logger.warn('[AcpBackend] Error in message handler:', error);
      }
    }
  }

  async startSession(initialPrompt?: string): Promise<StartSessionResult> {
    if (this.disposed) {
      throw new Error('Backend has been disposed');
    }

    const sessionId = randomUUID();
    this.emit({ type: 'status', status: 'starting' });
    let startupStatusErrorEmitted = false;

    try {
      logger.debug(`[AcpBackend] Starting session: ${sessionId}`);
      // Spawn the ACP agent process
      const args = this.options.args || [];
      
      // On Windows, spawn via cmd.exe to handle .cmd files and PATH resolution
      // This ensures proper stdio piping without shell buffering
      if (process.platform === 'win32') {
        const fullCommand = [this.options.command, ...args].join(' ');
        this.process = spawn('cmd.exe', ['/c', fullCommand], {
          cwd: this.options.cwd,
          env: { ...process.env, ...this.options.env },

View on GitHub (pinned to b824cd0a46)

Solutions

  1. Check `backend.disposed` (or track your own disposed flag) before calling startSession and create a fresh AcpBackend instead.
  2. Replace the disposed backend with a new instance and retry once, rather than retrying on the same instance.
  3. Audit code paths that call dispose() (signal handlers, process exit) to ensure no in-flight startSession callers remain.

Example fix

// before
await cachedBackend.startSession(prompt);
// after
if (cachedBackend.disposed) {
  cachedBackend = new AcpBackend(config);
}
await cachedBackend.startSession(prompt);
Defensive patterns

Strategy: try-catch

Validate before calling

if (backend.disposed) {
  backend = new AcpBackend(config);
}
await backend.startSession(prompt);

Type guard

function isUsable(b: AcpBackend): b is AcpBackend & { disposed: false } {
  return !b.disposed;
}

Try / catch

try {
  await backend.startSession(prompt);
} catch (err) {
  if (err instanceof Error && err.message === 'Backend has been disposed') {
    backend = new AcpBackend(config);
    await backend.startSession(prompt);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling startSession() after dispose() was invoked on the AcpBackend instance; reusing a cached backend across sessions after shutdown; a race where an async caller starts a session while the CLI is tearing the backend down.

Common situations: Reusing a stored AcpBackend reference after the CLI terminated an agent; retry logic that retries startSession after a failed/disposed session; long-lived wrappers that keep the backend in a map and a concurrent shutdown disposes it.

Related errors


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/7cc8c6d58d5606ab. Report an issue: GitHub.