> ## Documentation Index
> Fetch the complete documentation index at: https://docs.podflare.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Persistent REPL

> Variables, imports, and in-memory state survive across run_code calls.

## The model

Each Podflare sandbox runs a **long-lived Python process** (Python 3.12).
Every `run_code(lang="python")` you issue sends a block of code to that
process's REPL. Execution is in a shared `globals` dict — so setting a
variable in one call makes it visible to the next.

```python theme={null}
with Sandbox() as s:
    s.run_code("x = 42; import pandas as pd")
    s.run_code("print(type(x), pd.__version__)")
    # both x and pd are in scope
```

<Tip>
  This matches Jupyter kernel / E2B Code Interpreter / Anthropic
  `code_execution_20260120` semantics. Agents that were built against
  those platforms work the same way on Podflare.
</Tip>

## What persists

* **Named values** (`x = 1`, `df = pd.read_csv(...)`)
* **Imported modules** (`import pandas as pd`)
* **Open file handles**, sockets, threads, subprocesses
* **Changes to `sys.path`, environment, CWD**
* **Anything else you'd expect to live in Python process memory**

## What does not persist

* **bash `run_code` calls.** Shell history and `$X=foo` do NOT carry over —
  each bash call is a fresh subprocess. This is intentional; shells are
  not the right abstraction for long-lived state.
* **Child processes you start via `subprocess.run`.** They run, exit,
  gone. Use `subprocess.Popen` if you want a long-lived child.

## Fork inherits REPL state

This is the reason we built REPL persistence. When you `fork(n)`, each
child starts from the parent's exact Python process state.

```python theme={null}
with Sandbox() as parent:
    parent.run_code("""
        import pandas as pd
        import numpy as np
        df = pd.read_csv('/data/1GB.csv')
    """)
    # 1 GB DataFrame loaded into parent's Python process

    children = parent.fork(n=5)
    # Each child has pandas, numpy, and df already in scope.
    # No re-loading the CSV. No re-importing modules.
```

Because fork captures the full guest memory (including the Python
process's heap) and every child shares that memory copy-on-write until
it mutates a page, the DataFrame is effectively **page-level CoW
shared** across siblings. Memory cost: near-zero per child.

## Exception isolation

A raised exception in one `run_code` call doesn't poison the REPL:

```python theme={null}
with Sandbox() as s:
    r1 = s.run_code("1 / 0")
    # r1.exit_code == 1, r1.stderr contains the ZeroDivisionError traceback

    r2 = s.run_code("print('alive')")
    # r2.exit_code == 0, r2.stdout == "alive\n"
```

`SystemExit` is caught too — its code becomes the call's exit code, not
a hostd crash.

## The protocol

The agent and the in-VM Python process communicate over stdin/stdout with
a minimal framed protocol:

```
# Agent -> Python (stdin):
__PODFLARE_BEGIN__
<code>
__PODFLARE_END__

# Python -> Agent (stdout), one JSON per line, streamed:
{"t":"out","d":"hello"}
{"t":"err","d":"traceback line"}
{"t":"exit","c":0}
```

The agent translates these into the NDJSON Events the SDK expects
(`stdout`/`stderr`/`exit`). See
[`agent/repl.py`](https://github.com/PodFlare-ai/podflare/blob/main/agent/repl.py)
for the runner (compiled into the agent via `include_str!`).

## Serialization

Only one `run_code` executes at a time per sandbox. Concurrent calls
serialize via a mutex. If you need parallelism, `fork(n)` — each child
has its own REPL and you can run them in parallel.
