Skip to content

JupyterCodeExecutor leaks temporary output directory on stop — inconsistent with other executors #7217

@YLChen-007

Description

@YLChen-007

Summary

JupyterCodeExecutor creates a temp directory via tempfile.mkdtemp() when no output_dir is provided, but never cleans it up in stop(). This is inconsistent with every other executor in the codebase (LocalCommandLineCodeExecutor, DockerCommandLineCodeExecutor, AzureContainerCodeExecutor) which all properly clean up their temp directories. Over time, leaked directories and their contents (generated images, HTML files) accumulate on disk.

Details

When a user creates a JupyterCodeExecutor without specifying output_dir, the constructor creates a temp directory using the low-level tempfile.mkdtemp():

# _jupyter_code_executor.py, line 148
self._output_dir: Path = Path(tempfile.mkdtemp()) if output_dir is None else Path(output_dir)

The class even declares self._temp_dir: Optional[tempfile.TemporaryDirectory[str]] = None on line 151, suggesting the intention was to use tempfile.TemporaryDirectory() (which auto-cleans), but it's never actually used.

When stop() is called, only the kernel context is cleaned up — the output directory is left behind:

# _jupyter_code_executor.py, lines 297-309
async def stop(self) -> None:
    if not self._started:
        return
    if self.kernel_context is not None:
        await self.kernel_context.__aexit__(None, None, None)
        self.kernel_context = None
    self._client = None
    self._started = False
    # ← No cleanup of self._output_dir

Compare this with how other executors handle it:

LocalCommandLineCodeExecutor (lines 494-503):

async def stop(self) -> None:
    if self._temp_dir is not None:
        self._temp_dir.cleanup()  # ← Properly cleans up
        self._temp_dir = None
    self._started = False

AzureContainerCodeExecutor (line 520):

self._temp_dir.cleanup()  # ← Properly cleans up

DockerCommandLineCodeExecutor (line 443):

self._temp_dir.cleanup()  # ← Properly cleans up

Each code execution that produces visual output (matplotlib charts, HTML reports) writes files into the leaked directory. These files persist indefinitely, and over many executor lifecycles, they accumulate.

PoC

import asyncio
from autogen_ext.code_executors.jupyter import JupyterCodeExecutor
from autogen_core.code_executor import CodeBlock
from autogen_core import CancellationToken

async def main():
    leaked_dirs = []

    for i in range(3):
        executor = JupyterCodeExecutor()
        leaked_dirs.append(str(executor.output_dir))
        await executor.start()

        result = await executor.execute_code_blocks(
            [CodeBlock(code=f"print('run {i}')", language="python")],
            CancellationToken()
        )

        await executor.stop()

    # Verify: all directories still exist after stop()
    import os
    for d in leaked_dirs:
        exists = os.path.exists(d)
        print(f"{d} exists after stop(): {exists}")
        # Expected: True (BUG - should be cleaned up)

asyncio.run(main())

Output:

/tmp/tmpr1ax38gw exists after stop(): True
/tmp/tmp2i11247w exists after stop(): True
/tmp/tmpvt43odlp exists after stop(): True

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions