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
- Check `backend.disposed` (or track your own disposed flag) before calling startSession and create a fresh AcpBackend instead.
- Replace the disposed backend with a new instance and retry once, rather than retrying on the same instance.
- 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
- Always check the disposed flag before reusing a cached backend.
- Centralize backend lifecycle in one owner that invalidates references on dispose.
- Never retry startSession on the same instance; recreate instead.
- Cancel prompt producers before calling dispose().
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
- Session not started
- No active thread. Call startThread first.
- Gemini backend or session not initialized
- Failed to create stdio pipes
- ACP session is not started
AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31).
Data as JSON: /api/errors/7cc8c6d58d5606ab.
Report an issue: GitHub.