can1357/oh-my-pi · warning · AggregateError

Failed to dispose one or more eval kernels

Error message

Failed to dispose one or more eval kernels

What it means

During EvalRunner disposal, disposeKernels() tears down Python, Ruby, Julia kernel sessions and VM contexts in parallel via Promise.allSettled. If any teardown promise rejects, all failure reasons are collected into an AggregateError with this message, so one bad kernel does not hide the others' failures.

Source

Thrown at packages/coding-agent/src/session/eval-runner.ts:189

	beginDispose(): void {
		this.#disposing = true;
	}

	/** Waits for active work and disposes every retained eval kernel owned by the session. */
	async disposeKernels(): Promise<void> {
		const settled = await this.#prepareExecutionsForDispose();
		if (!settled) {
			logger.warn("Detaching retained eval-kernel ownership during dispose while eval execution is still active");
		}
		const results = await Promise.allSettled([
			disposeKernelSessionsByOwner(this.#kernelOwnerId),
			disposeRubyKernelSessionsByOwner(this.#kernelOwnerId),
			disposeJuliaKernelSessionsByOwner(this.#kernelOwnerId),
			disposeVmContextsByOwner(this.#kernelOwnerId),
		]);
		const errors: unknown[] = [];
		for (const result of results) if (result.status === "rejected") errors.push(result.reason);
		if (errors.length > 0) throw new AggregateError(errors, "Failed to dispose one or more eval kernels");
	}

	async #waitForExecutionsToSettle(timeoutMs: number): Promise<boolean> {
		const deadline = Date.now() + timeoutMs;
		while (this.#activeExecutions.size > 0) {
			const remainingMs = deadline - Date.now();
			if (remainingMs <= 0) return false;
			const settled = await Promise.race([
				Promise.allSettled(Array.from(this.#activeExecutions)).then(() => true),
				Bun.sleep(remainingMs).then(() => false),
			]);
			if (!settled && this.#activeExecutions.size > 0) return false;
		}
		return true;
	}

	async #prepareExecutionsForDispose(): Promise<boolean> {
		if (!(await this.#waitForExecutionsToSettle(3_000))) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Iterate error.errors on the AggregateError to see which kernel(s) failed and why
  2. Retry dispose() once — stale kernels are often already gone and a second pass succeeds
  3. Kill leftover kernel processes by the owner id manually if cleanup IPC is stuck
  4. Log and continue: this happens during teardown, so partial failure usually means resources were already being reclaimed

Example fix

// before
try { await evalRunner.dispose(); } catch (e) { throw e; }
// after
try { await evalRunner.dispose(); } catch (e) {
  if (e instanceof AggregateError) {
    for (const inner of e.errors) logger.warn('kernel dispose failed', { inner: String(inner) });
  } // teardown errors are usually non-fatal
}
Defensive patterns

Strategy: try-catch

Type guard

function isKernelDisposeFailure(err: unknown): err is AggregateError {
  return err instanceof AggregateError && err.message === 'Failed to dispose one or more eval kernels';
}

Try / catch

try {
  await evalRunner.dispose();
} catch (err) {
  if (err instanceof AggregateError && err.message.includes('eval kernels')) {
    for (const inner of err.errors) logger.warn('kernel dispose failed', { inner: String(inner) });
    return; // teardown best-effort; do not mask shutdown
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling dispose()/disposeKernels() when at least one of disposePythonKernelSessionsByOwner, disposeRubyKernelSessionsByOwner, disposeJuliaKernelSessionsByOwner, or disposeVmContextsByOwner rejects — e.g. the kernel process already died and its cleanup IPC fails.

Common situations: Kernel process crashed earlier (OOM, killed by OS) leaving stale registration that cleanup cannot remove; shutdown timeout racing kernel shutdown; platform quirk in one language's kernel teardown; repeated disposal attempts.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/672e13623fe5bd5a. Report an issue: GitHub.