# How pi-repl works

These are the details behind the [README](../README.md): how sessions start and stop, the shared record, what the agent is told, and the submission display. [Runtime notes](runtimes.md) cover each runtime.

## Starting a session

`repl_start` requires `runtime`: `python`, `ipython`, `julia`, `r`, `ghci`, `clojure`, `ruby`, `java`, `octave`, `matlab`, `gnuplot`, `cpp`, or `lfortran`. Apart from `lfortran`, which is described in the [runtime notes](runtimes.md#lfortran-sessions-experimental), it shares the `/repl` and `/lab` startup implementation: a detached tmux session, launched from Pi's working directory through your normal interactive login shell. It returns `created`/`reused`, `ready`, the requested and recorded runtimes, session status (including record/history paths), and an `attachCommand`. It does not open a terminal or attach a client automatically.

An existing session is reused without resetting variables, changing its working directory, replacing its history logger, or sending input. Python and IPython share `pi-repl-python`: requesting the other interpreter preserves the one already running and reports the difference. As with status inspection, legacy sessions can lazily acquire clean-record metadata; existing metadata is preserved.

Startup waits for a recognised normal prompt on the physical cursor row, not merely a running process, an old prompt in scrollback, or a startup banner. No probe code, Enter, Ctrl-C, or prompt-setting changes are sent. The default wait is 20 seconds; `timeoutMs` accepts 1,000–120,000 milliseconds for prompt polling, in addition to bounded tmux setup/inspection calls. If the prompt cannot be confirmed, the tool returns `ready: false` with status and a warning, leaving the session running. Busy sessions, unfinished direct input, and customised prompts can all produce this result; inspect the pane before sending code. Prompt detection is a snapshot, not a guarantee that another person cannot begin typing afterwards.

Missing tmux, failed creation, early runtime exit, and cancellation are reported as tool errors. Cancellation after creation leaves the session running too; inspect it with `repl_status`. Stopping or restarting remains an explicit user action. `repl_send` does not silently start, restart, or switch a REPL.

Nonzero tmux `base-index` and `pane-base-index` settings are supported. `pi-repl` selects the lowest-indexed pane in the lowest-indexed window, then pins that pane's stable ID for each send so switching active windows cannot redirect its output capture.

## The shared record

pi-repl doesn't depend on Pi Studio or agent-repl. When they use the same tmux session, the clients find one clean record, owned by the session, and see each other's submitted code and captured output.

Compatible clients publish a versioned opaque ID in tmux and store the bounded JSON snapshot in a private per-user temporary directory. The record is tied to the exact tmux session ID and creation time, uses atomic locked updates, and holds a shared send lease from pre-send capture through completion capture. If `repl_send` times out or is aborted after submission, that live client keeps the lease until the code finishes, then captures its output from the original pane and records it; its entry stays `sending` until then. Only a verified end of the session or runtime releases it sooner. The result tells the agent the code is still running and not to send it again, and status says which send holds the lease. Caller cancellation does not stop code already running in the REPL. This serializes compatible Studio and `pi-repl` sends so they do not claim each other's output. Agent sends still run without a lease when the session has no usable record; a host's person sends (`sendAndSettle`) refuse instead.

The record's directory is under the temporary directory (`TMPDIR`), which can differ between terminals of one account, such as an ssh login and a desktop terminal. So each session also records which directory holds its record, as a key in tmux (`@pi_repl_record_root_key`), set when its record is first made. A send from a terminal whose record directory is a different one, or has been replaced (by temporary-file cleanup, say), is refused before anything is sent, and status says why. Use the session from a terminal with the same `TMPDIR`, or restart it, which discards its state. A session that an older client made gets its key at its first send from a terminal whose directory holds its record; until then, status says its record directory is not verified. Status and `/repl export` only read: they never set the key or create a record. Older clients ignore the key, so the check holds only among updated clients.

The clean record does **not** infer semantic boundaries for commands typed directly into an attached tmux pane. Direct interaction remains in the raw pane/history mirror. `/repl export [target]` writes the canonical clean-record Markdown to a new no-clobber file in Pi's current working directory; its metadata identifies entry origin, mode, status, runtime, and timestamp and states the direct-input limitation.

Existing sessions attach lazily. Unsupported versions and invalid or stale session identities are left untouched, with ordinary `pi-repl` behavior and raw history still available. See [`shared/REPL_SESSION_RECORD_PROTOCOL.md`](../shared/REPL_SESSION_RECORD_PROTOCOL.md) for protocol, safety, retention, and compatibility details.

## What the agent is told

### Pane text

The agent is told what it missed in each pane. Each agent keeps its own view of each pane, which only what is delivered to it advances: other clients' sends and your `/repl` commands never hide pane text from it. In Pi, when a run starts (you send a prompt while the agent is idle), a message is added before the model plans, with the pane text the agent has not seen for the sessions it has started or sent to: most recently used first, at most 6,000 characters in all, and checked within 3 seconds in all, naming any pane left out or not checked in time. Pi shows it to you as one line naming each pane and how many lines it showed; Ctrl+O shows the text. It counts as seen once the run starts, and nothing is added when nothing changed. Prompts you send while the agent is working join that run without a new message, so text from then on reaches it with its next send, whose result starts with the pane text the agent has not seen: typing at the prompt, other clients' sends, or late or background output. A session the agent's process creates is seen from its ready prompt on, without the startup banner; for any other session, its first send shows the pane's recent text. The send's loader line, unique to each send, marks where its view ends, so repeated output cannot be mistaken for it; when that line has scrolled out of the 5,000-line capture, or the end is unclear, the report says so and shows recent text instead, or says so alone if there is none. `repl_status` previews the same text without marking it seen. Reports hold at most 40 lines or 4,000 characters, are labelled as untrusted terminal text, are not parsed into code and output, and never enter the clean record.

Because pane text, including anything typed there, then reaches the agent and its model provider, even with a prompt that has nothing to do with the REPL, attach instructions say so. `PI_REPL_PANE_ACTIVITY` in the environment of Pi or the agent's MCP server chooses: `on` (the default) for run starts and sends, `send` for sends only, and `off` for neither, which stops reports while it is set. It does not erase the pane's history: text from that time can still reach the agent in a later report once reports are on again, or in a new agent's first report. A new, resumed, branched or compacted Pi conversation starts without a view, so its first send shows recent pane text.

### Record entries

The agent is also told of other clients' entries in the clean records of the sessions it has started or sent to: your `/repl send` from another conversation, `agent-repl send`, Pi Studio's sends and notes, and other agents' sends. Each says who sent it and how (`Ada · via terminal`, or an agent's label), its status, and its prose, code and output. An entry it was told of that changes, such as one whose output was captured after its sender stopped waiting, is told again as an update. Its own sends aren't told back, and neither is a `/repl send` posted into its own conversation; another agent with the same label is still another client, since entries are told apart by ID.

In Pi, the entries come in a message when a run starts, before the pane text and within the same 6,000 characters and 3 seconds (Pi shows it to you as one line, and Ctrl+O shows the entries), and with each `repl_send` result, before its pane text and its own result, within 8,000 characters. A report tells at most 10 entries, newest change first; those left out are listed by ID, for `repl_history`, and aren't offered again unless they change. What a report tells counts only once it is in the request Pi sends the model, so a run that never starts, or a report another extension removed, tells it again; until then, its entries aren't put in another report. With agent-repl's MCP server, it counts once it is in the result as returned, unless the call was cancelled. A report keeps at most 40 lines of each entry's prose, code and output, its first and last. A new, resumed, branched or compacted Pi conversation, or a new MCP server, is told again, within the same bounds. It's best effort: the record keeps at most 300 entries and can be cleared, so an entry can leave it before anyone is told, and a report says when the record was cleared or discarded entries. Code typed straight into a pane isn't recorded, nor are cells run at LFortran's own prompt, so neither can be told this way; for a pane, the pane text covers it. `repl_history` lists a session's entries, or shows one by its ID, without counting as told.

This sends other clients' code, output, notes and names to the agent and its model provider, so attach instructions and status say so. `PI_REPL_RECORD_ACTIVITY` chooses: `on` for run starts and sends, `send` for sends only, and `off` for neither. When it isn't set, it follows `PI_REPL_PANE_ACTIVITY`, so turning pane text off turns this off too. Reports are labelled as data from the shared session, not instructions, whoever they name.

When Pi Studio is loaded in the same Pi, it can leave the `pi-repl-*` sessions its pane shows and its agent uses to pi-repl, over Pi's event bus, so each entry is told once: pi-repl tells them when a run starts, and Studio's tool results carry pi-repl's report, as `repl_send` results do. Each extension also works alone.

## Submission display and alignment anchors

Pane echo, enabled in Summary mode by default, places one blank line before the compact begin anchor, separating the runtime loader command from the readable block. Submitted code, the plain `── output ──` divider, output and the completion anchor then follow without added internal padding. All runtimes add one display-only blank line after `done` too, separating the block from the returning prompt. Native prompt spacing is left unchanged. Blank lines printed by user code remain in the raw pane; display spacing does not change submitted code or clean captured output. The header reads `── pi-repl · input · 2 lines · id: 40c4561ef0f6 ──`, followed by plain code with its indentation and no added vertical bars. The same labelled ID appears after `done`. This is a stable 12-character hexadecimal hash derived from the Shared REPL Record entry ID: a correlation label for matching raw history to a recorded send, not a counter, timestamp or execution signal. Older marker formats remain readable. `repl_send` removes the exact header, source preview, divider, and footer from captured output and the clean record, while they remain in raw pane history.

Use `/repl echo off|summary|full` to change the default for the current Pi process, or set `PI_REPL_ECHO_MODE` before startup. A per-send `echoMode` overrides that default without changing it. **Summary** is the startup default: it shows short submissions in full, truncates after 20 lines or 2,000 source characters, and puts a plain output divider before runtime output. **Off** disables the optional display and alignment anchors for quiet output, although the REPL can still echo its unavoidable temporary-file control command. **Full** is an explicit opt-in that raises the bounds to 40 lines or 4,000 source characters. Line limits count source lines, not soft-wrapped terminal rows, and affect only the preview: submitted code still executes in full. Terminal, line-separator, and bidirectional control characters are escaped in all visible previews.

GHCi, Ruby, Java, Octave, MATLAB, gnuplot and C++ use a read-only tmux cursor-column query, with a short timeout, to avoid adding an empty row before `done` while still separating it from output that has no trailing newline. If that query is unavailable, they fall back to the safe newline guard. Output streams and interactive echo settings are not replaced.

Both Summary and Full persist the displayed source in raw terminal history. Off suppresses this extra copy, not the submitted code already retained in tool results and the clean record. Explicit `PI_REPL_ECHO_MODE=off` settings are still honoured.

Runtime wrappers use request-unique paths such as `/tmp/pi-rc-<user-key>/<submission-id>-<random-token>.py`. The 12-character prefix matches the `id:` in the input and completion markers. Related source, guard, driver and `.done` files share that prefix; each source-file allocation still has its own independent 16-hex-character random token. The prefix is retained in Off mode too, without adding any display markers. Transient tmux paste-buffer files keep their unprefixed random names.

The per-user root is current-user-owned mode `0700`, source files are created exclusively with mode `0600`, and files are removed after capture or by the timeout/abort watcher once execution settles. Random allocation tokens and exclusive creation—not the short submission ID—prevent overwrites across clients, processes, tmux servers and runtimes. Cleanup also accepts older unprefixed filenames; file names and presence do not establish record identity or execution status.

These anchors are presentation and alignment evidence only. They do not make direct attached-pane input authoritative and never promote inferred raw history into protocol-v1 entries.

## Stopping safely

`/repl stop [target]` and `/lab stop [target]` verify shutdown, rather than assuming that closing tmux also terminates its runtimes. The command snapshots the selected session's pane processes and descendants, checks local owner/start-time/terminal identities, and guards the final tmux operation against session replacement or changed pane topology. Linked windows are refused because another session still uses them.

After closing the selected session, it waits briefly for normal exit, sends TERM to confirmed survivors, and uses KILL only if those same owned processes still survive. Each signal rechecks process identity and protects live panes, Pi itself, and the tmux server. It never kills by runtime name or signals an entire process group. Runtime exit is reported only after verification; uncertain ownership of identified processes, inspection failures or survivors produce an error requiring manual inspection. Untracked-helper and socket-cleanup limits are reported explicitly as warnings, not as fully verified cleanup. Dead/detached panes whose ownership cannot be established are refused before shutdown.

This is an explicit, state-discarding stop: save anything needed first. It sends no runtime exit command, Ctrl-C or other input into the pane, and leaves raw logs and clean records on disk. Other sessions are not stopped. No process cleanup runs automatically on startup, send, Pi reload or Pi exit. The checks require local Unix process inspection (macOS/Linux); missing inspection support fails closed. It is not a general orphan collector: apart from gnuplot's session-marked interpreter/Qt helpers, processes already detached before inspection, or descendants that cannot be safely attributed after their parent exits, may require manual cleanup.

## Raw history

Besides the clean record, pi-repl logs everything the session's pane shows to a raw history file: prompts, echoed input, the display anchors, output, errors and what you type there. It's plain text, not parsed into entries.

Each session gets its own mode-`0600` log in the current-user-owned mode-`0700` directory `<os temporary directory>/pi-repl-history-<uid>/`. Restarting a session or using another tmux server doesn't truncate an earlier log, and logs aren't deleted automatically. Symlinked, foreign-owned or permissive history directories are refused. Sessions started by older versions keep their `/tmp/pi-repl/*.history.log` files.

## Tests

`npm test` runs unit tests plus local Python/tmux integration tests. Integration tests use dedicated tmux servers with empty configuration, temporary homes and private test files. Test sockets live in short, private temporary directories, not the user's tmux socket directory. Teardown verifies that both the servers and their owned runtime processes exit; it does not treat a vanished tmux session as sufficient. Tests do not attach to or send code to your existing REPLs. Tests requiring tmux or Python skip when those executables are unavailable; no CI service is required.

Test launchers record PID/owner/start-time identities before executing a runtime. The test harness tracks server descendants and pane process groups across session replacement, then individually revalidates survivors before TERM/KILL cleanup. It never kills by executable name or signals a whole process group. The teardown hook is registered before launch, runs after setup failures too, verifies no owned runtime is still running, and removes its private socket directory only after successful cleanup. Regression tests cover ignored hangup/TERM, orphaned children, PID reuse, failed setup, and preservation of unrelated sessions. Production `/repl stop` tests independently verify runtime exit before test teardown, across all supported runtimes, including a busy GHCi session, resistant child processes, linked windows and same-name replacement races. Gnuplot fixtures register an additional private-cwd/birth-identity observer before launch: it can rescue detached Qt helpers after failures without relying on the production marker reader. Process names only filter candidates; they never establish ownership by themselves.

To test other installed runtimes, name them, or use `all`:

```bash
PI_REPL_TEST_RUNTIMES=all npm test
PI_REPL_TEST_RUNTIMES=julia,r npm run test:integration
PI_REPL_TEST_RUNTIMES=ruby,java npm run test:integration
PI_REPL_TEST_RUNTIMES=octave,matlab npm run test:integration
PI_REPL_TEST_RUNTIMES=gnuplot npm run test:integration
PI_REPL_TEST_RUNTIMES=cpp npm run test:integration
# With a matching real-double/int32 PETSc installation (never installed by the tests):
PI_REPL_TEST_RUNTIMES=cpp PI_REPL_TEST_PETSC_PREFIX=/your/petsc/prefix npm run test:integration
```

MATLAB tests need a working licence; they isolate preferences and startup files without changing the installed licence. Julia tests use the juliaup-selected binary.

`PI_REPL_CONTROL_ROOT` optionally overrides the private runtime-control directory; it must be current-user-owned mode `0700`. Tests set it to their temporary directory so even stale-file cleanup stays isolated. This does not change the shared-record protocol or Studio's control-file location.
