langchain-ai/deepagents · error · RuntimeError

QuickJS context is closed

Error message

QuickJS context is closed

What it means

The QuickJS REPL raises this RuntimeError from `_require_ctx` when its internal `Context` handle is None, i.e. the REPL has been closed and its JS context released. Any lifecycle method (installing console/tools, registering the task bridge, snapshot/restore) that needs a live context checks this first and refuses to operate on a dead REPL. It protects against use-after-free of the underlying QuickJS VM.

Source

Thrown at libs/partners/quickjs/langchain_quickjs/_repl.py:450

        self._task_calls: asyncio.Semaphore | None = None
        # Context creation + console install must happen on the worker
        # thread. Block caller here so the REPL is ready to use when
        # __init__ returns.
        worker.run_sync(self._ainit())

    async def _ainit(self) -> None:
        self._ctx = self._runtime.new_context(timeout=self._per_call_timeout)
        if self._capture_console:
            self._install_console()
        if self._subagents_enabled:
            self._task_calls = asyncio.Semaphore(_MAX_TASK_CALLS_PER_THREAD)
            self._register_task_bridge()

    def _require_ctx(self) -> Context:
        """Return the live QuickJS context or raise if this REPL is closed."""
        if self._ctx is None:
            msg = "QuickJS context is closed"
            raise RuntimeError(msg)
        return self._ctx

    def _install_console(self) -> None:
        ctx = self._require_ctx()
        buf = self._console

        @ctx.function(name="__console_log")
        def _log(*args: Any) -> None:
            buf.append("log", args)

        @ctx.function(name="__console_warn")
        def _warn(*args: Any) -> None:
            buf.append("warn", args)

        @ctx.function(name="__console_error")
        def _error(*args: Any) -> None:
            buf.append("error", args)

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Create a fresh REPL instance instead of reusing the closed one
  2. Audit the code path so `install_tools`/snapshot calls only happen inside the REPL's lifetime (inside the `async with` block)
  3. Check `repl._ctx is not None` (or a public is_closed accessor) before calling lifecycle methods

Example fix

// before
repl.install_tools(tools)  # after repl.close() -> RuntimeError
// after
if not repl.is_closed:
    repl.install_tools(tools)
Defensive patterns

Strategy: try-catch

Validate before calling

if repl is None or getattr(repl, '_ctx', None) is None:
    repl = MyQuickJSRepl(...)  # recreate before use

Type guard

def repl_is_live(repl) -> bool:
    return getattr(repl, '_ctx', None) is not None

Try / catch

try:
    repl.install_tools(tools)
except RuntimeError as e:
    if 'context is closed' in str(e):
        repl = recreate_repl(); repl.install_tools(tools)
    else:
        raise

Prevention

When it happens

Trigger: Calling any REPL method after `close()`/aclose was called, or after the context was torn down: `install_tools`, `_install_console`, `_register_task_bridge`, `_register_tool_bridge`, or `_acreate_snapshot`/`_arestore_snapshot`.

Common situations: Using the REPL after an async context-exit (`async with` block ended), reusing a cached REPL object that another code path closed, or a shutdown race where a background task tries to install tools during teardown.

Related errors


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/73ef931168118c60. Report an issue: GitHub.