can1357/oh-my-pi · error · Error

Cannot ${action} on a disposed JS runtime

Error message

Cannot ${action} on a disposed JS runtime

What it means

`#activateGlobals` is the guard run before any operation that needs the runtime's globals activated (setCwd, setRunScope, run). Once the JS runtime is disposed, its global-scope claim has been released, so any further use throws `Cannot <action> on a disposed JS runtime`. It prevents resurrecting global state after teardown and confusing cross-runtime ownership.

Source

Thrown at packages/coding-agent/src/eval/js/shared/runtime.ts:183

 * Shared JS runtime for the eval worker and the browser tab worker. Owns the prelude,
 * helper bag, console bridge, and indirect-eval execution. Emits text/display/tool-call
 * back through `RuntimeHooks` that the embedder supplies — wire format is the embedder's
 * concern.
 */
export class JsRuntime {
	#globalOwner = Symbol("JsRuntime globals");
	#ownedGlobalKeys = new Set<string>();
	#disposed = false;
	#runHookResolver = () => this.#als.getStore()?.hooks;

	#ownGlobal(key: string): void {
		if (this.#ownedGlobalKeys.has(key)) return;
		claimGlobalKey(key, this.#globalOwner);
		this.#ownedGlobalKeys.add(key);
	}

	#activateGlobals(action: string): void {
		if (this.#disposed) throw new Error(`Cannot ${action} on a disposed JS runtime`);
		activateGlobalOwner(this.#globalOwner, this.#ownedGlobalKeys, action);
	}

	readonly helpers: HelperBundle;
	#cwd: string;
	#session: { cwd: string; sessionId: string };
	readonly sessionId: string;
	#env: Map<string, string>;
	#als = new AsyncLocalStorage<RunContext>();
	#moduleLoader: LocalModuleLoader;
	#localRoots: Record<string, string>;

	constructor(opts: RuntimeOptions) {
		this.#cwd = opts.initialCwd;
		this.#session = { cwd: opts.initialCwd, sessionId: opts.sessionId };
		this.sessionId = opts.sessionId;
		this.#env = new Map();
		this.#moduleLoader = new LocalModuleLoader(this.sessionId);

View on GitHub (pinned to 9690622007)

Solutions

  1. Create a fresh runtime (or new eval session) instead of reusing the disposed one.
  2. Check disposal/liveness state before scheduling runs; drop references after dispose().
  3. Serialize teardown: cancel pending runs before disposing, and guard callbacks with a disposed flag.

Example fix

// before
runtime.dispose();
await runtime.run(code); // throws
// after
if (isDisposed(runtime)) runtime = createRuntime();
await runtime.run(code);
Defensive patterns

Strategy: try-catch

Validate before calling

if (runtimeDisposed) throw new Error("runtime already disposed; create a new one");

Type guard

function isUsable(r: { [disposed]: boolean } | null | undefined): boolean {
	return r != null && !r["disposed"];
}

Try / catch

try {
	await runtime.run(code);
} catch (err) {
	if (String(err?.message).includes("on a disposed JS runtime")) {
		runtime = createRuntime(); // recreate and retry once
		await runtime.run(code);
	} else throw err;
}

Prevention

When it happens

Trigger: Calling `runtime.run(...)`, `runtime.setCwd(...)`, or `runtime.setRunScope(...)` after `runtime.dispose()` (or after the worker/session teardown disposed it) — e.g. a queued run completing after termination, or reusing a cached runtime object across cell teardown.

Common situations: Race between an in-flight eval run and session kill; caching a runtime in user code and reusing it in a later cell; worker restart logic invoking setCwd on the old disposed instance.

Related errors


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