can1357/oh-my-pi · error · Error

Cannot ${action} while another same-realm JS runtime is runn

Error message

Cannot ${action} while another same-realm JS runtime is running

What it means

The JS eval runtime supports only one active global-owning runtime per JS realm. `assertCanUseGlobalOwner` checks the module-level `activeGlobalRunOwner` symbol and throws when a runtime different from the currently active one attempts to install/activate globals or enter a global run. This prevents two same-realm runtimes (e.g. two concurrent eval sessions sharing the vm context) from silently clobbering each other's injected globals.

Source

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

	if (!wasTop) return;
	const next = stack.entries.at(-1);
	if (next) {
		(globalThis as Record<string, unknown>)[key] = next.value;
		return;
	}
	restoreGlobal(key, stack.base);
	GLOBAL_STACKS.delete(key);
}

// Plain globalThis cannot safely serve two different runtimes at the same instant:
// helpers dereference reserved globals on every call. Sequential cmux tab revisits
// re-activate their owner stack; overlapping cross-runtime runs fail explicitly.
let activeGlobalRunOwner: symbol | null = null;
let activeGlobalRunDepth = 0;

function assertCanUseGlobalOwner(owner: symbol, action: string): void {
	if (activeGlobalRunOwner === null || activeGlobalRunOwner === owner) return;
	throw new Error(`Cannot ${action} while another same-realm JS runtime is running`);
}

function activateGlobalOwner(owner: symbol, keys: Iterable<string>, action: string): void {
	assertCanUseGlobalOwner(owner, action);
	for (const key of keys) {
		const stack = GLOBAL_STACKS.get(key);
		const index = stack?.entries.findIndex(entry => entry.owner === owner) ?? -1;
		if (!stack || index === -1) throw new Error(`Cannot ${action} on a disposed JS runtime`);
		const entry = stack.entries[index];
		stack.entries.splice(index, 1);
		stack.entries.push(entry);
		(globalThis as Record<string, unknown>)[key] = entry.value;
	}
}

function enterGlobalRun(owner: symbol, action: string): () => void {
	assertCanUseGlobalOwner(owner, action);
	activeGlobalRunOwner = owner;

View on GitHub (pinned to 9690622007)

Solutions

  1. Serialize the overlapping runs: await the first global run before starting the second
  2. Use a separate runtime instance/realm for concurrent work so each has its own owner symbol
  3. Ensure the first runtime properly exits its run so `activeGlobalRunOwner` returns to null (check for leaked runs on error paths)

Example fix

// before: two concurrent runs
const [a, b] = await Promise.all([runA(), runB()]);
// after: serialize
const a = await runA();
const b = await runB();
Defensive patterns

Strategy: try-catch

Validate before calling

// track an app-level mutex around global eval runs
let runChain = Promise.resolve();
function withExclusiveRun<T>(fn: () => Promise<T>): Promise<T> {
  const next = runChain.then(fn, fn);
  runChain = next.catch(() => undefined);
  return next;
}

Try / catch

try {
  await withExclusiveRun(() => runtime.run(code));
} catch (err) {
  if (err instanceof Error && /another same-realm JS runtime is running/.test(err.message)) {
    // queue the run until the active owner finishes
  }
}

Prevention

When it happens

Trigger: Calling `#install`, `activateGlobalOwner`, or `enterGlobalRun` on a second JS runtime while `activeGlobalRunOwner` is set to a different symbol — i.e. two overlapping cross-runtime eval runs in the same process realm.

Common situations: Running two eval/kernel sessions concurrently (e.g. a JS kernel and a second JS runtime instance), or background tasks overlapping with an interactive eval run that both try to install globals on the shared global stack.

Related errors


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