# Pi Session Control — 0.2.2

Authority: the locally installed `@earendil-works/pi-coding-agent` **0.85.1**, its `docs/sdk.md`, `dist/core/{session-manager,agent-session,agent-session-runtime,agent-session-services}.d.ts` and corresponding implementation. No session JSONL parser was added.

## Verified SDK surface

- `SessionManager.list(cwd, sessionDir?, onProgress?)` returns `SessionInfo[]`: `id`, `path`, `cwd`, `name`, `created`, `modified`, `firstMessage` and message counts. The project catalog uses exact canonical cwd matching.
- `SessionManager.listAll(sessionDir?, onProgress?)` also exists. We deliberately use `list(cwd)` only for explicitly registered projects.
- `SessionManager.open(path, sessionDir?, cwdOverride?)` preserves the stored session ID. `buildSessionContext().model` resolves the model on the selected branch. Opening/cataloguing does not create an AgentSession or send input.
- Default persistence is Pi's `~/.pi/agent/sessions/` per-cwd directory. Pi itself owns file naming, migration and metadata reading.
- `createAgentSessionServices` + `createAgentSessionFromServices` + `createAgentSessionRuntime` construct a full SDK runtime. `bindExtensions({mode:'rpc'})` starts the pager extension.
- `AgentSessionRuntime.switchSession(path, options?)` exists and tears down/recreates cwd-bound services. This implementation instead keeps one runtime per selected session; it opens the saved manager directly, avoiding unnecessary replacement of another session.
- `session.prompt(text, {streamingBehavior:'followUp', expandPromptTemplates:false, preflightResult})` validates/queues input and invokes `preflightResult` before model completion. HTTP waits for this acknowledgment. Invalid preflight does not return success. Later inference failures arrive through activity/lifecycle.
- `runtime.dispose()` aborts/settles execution, emits `session_shutdown`, then disposes the session.

## Boundaries and ownership

`ProjectRegistry` owns explicit project configuration; `PiAdapter` owns catalog and input; `PiSessionRegistry` remains the live event registry. Static catalog rows without a live endpoint/event are `DORMANT`, including rows whose last event was shutdown. No process listing, terminal, tmux, ANSI, or session-file scraping participates in discovery.

Python cannot load a TypeScript SDK in-process. Gateway therefore supervises **one Node SDK host**, not `pi --resume` or a TUI. JSON request/response is its internal control protocol; stdout is not scraped. SDK sessions run concurrently in that host. Input setup/preflight is serialized, while HTTP never waits for model completion. Gateway shutdown closes stdin and disposes owned runtimes; bounded terminate/kill is a last resort. The user systemd service also kills its process group. Do not run multiple Uvicorn workers.

Each runtime has a private filesystem Unix socket under `~/.config/esp32-device-gateway/runtime/` (directory 0700, socket 0600). A Linux abstract socket is an atomic ownership lease carrying no commands; the kernel releases it on exit, so stale socket files can be reclaimed safely. Concurrent cooperative resumes cannot create two owners. This transport is local to the VPS and uses the existing Unix-user trust boundary.

The pager extension offers the same endpoint to already running Pi sessions. For these external runtimes, input is submitted through official `pi.sendUserMessage(..., {deliverAs:'followUp'})`, which dispatches to AgentSession input. Its void Extension API acknowledges dispatch, not model/auth preflight; the stricter preflight acknowledgment applies to Gateway-owned SDK runtimes. A known live session without a control endpoint is refused instead of spawning a second runtime. Existing interactive sessions need one `/reload` to load the updated extension; the acceptance smoke uses no TUI at any point.

A 15-second metadata heartbeat keeps external runtime presence current. Live hints expire after 45 seconds; socket probes remain authoritative for cooperative runtimes. Uninstrumented/old Pi runtimes have no global ownership API: do not open the same stored session concurrently in an uninstrumented client. The Gateway cannot prove ownership of such a client after restart and does not claim to.

Model and auth come from Pi's normal SDK settings. A missing configured model/auth returns an error; there is no hidden model picker or override. Metadata failures produce a visible catalog error, never an empty success. Inputs are not automatically retried after an ambiguous timeout/disconnect.

## API

- `GET/POST /api/projects`; `DELETE /api/projects/{id}`. Atomic repo-external JSON, stable IDs, existing absolute directory required on add. Delete unregisters a project; it does not delete sessions, files, or abort work.
- `GET /api/agents`
- `GET /api/projects/{id}/sessions` → `{sessions, errors}`; a failed adapter does not hide the other catalog.
- `GET /api/agents/{agent}/sessions/{id}`
- `POST /api/agents/{agent}/sessions/{id}/resume`
- `POST /api/agents/{agent}/sessions/{id}/input` with `{text}` → HTTP 202 after resume/preflight/submit; `{accepted, agent, session_id, resumed}`.

Only registered project sessions are controllable. These are Human Admin APIs under existing Cloudflare Access `/api*`. Device and Pi event authentication stay unchanged. Device target `pi:<id>` is unchanged; firmware 0.2.1 receives its existing OFFLINE placeholder for a dormant target until live events arrive. Admin uses DORMANT.
