| # Trace Processor Warm Sessions |
| |
| **Authors:** @LalitMaganti |
| |
| **Status:** Draft |
| |
| ## Problem |
| |
| A caller that runs many queries against one trace re-parses it on every |
| `tp query <trace>` invocation. Large traces take seconds to minutes to parse, so |
| the common AI-agent pattern of repeated one-shot queries is slow. |
| |
| `tp server http` and `tp server stdio` already hold a trace warm, but two things |
| are missing for an agent driving the CLI: |
| |
| 1. No self-managing lifetime. A server runs until killed, so an agent that forgets |
| to stop one, or whose session ends, leaves it resident. |
| 2. No name-based discovery. http needs the client to know a port; stdio needs the |
| client to hold the pipe for the whole session. |
| |
| This RFC adds both. Single trace per server; multi-trace is a separate API. |
| |
| ## Decision |
| |
| Three additions: |
| |
| 1. A self-managing lifecycle (idle-timeout plus owner-aware reaping), wired into |
| the `http` and `unix` server modes. |
| 2. A `unix` transport addressed by name, plus a `--remote` flag on every |
| trace-consuming subcommand (`query`, `metrics`, `summarize`, `interactive`), so |
| an agent can start a warm session in the background and run any of them against |
| it by name. |
| 3. A `tp ctl` management command, starting with `tp ctl kill-server |
| <name|host:port>` to stop a running server by address. |
| |
| Transport and lifecycle are independent: the lifecycle flags apply to `http` as |
| well as `unix`. `unix` is added because it makes name-based discovery clean: a name |
| maps to a fixed socket path, whereas a TCP port cannot be derived from a name. |
| |
| ## Design |
| |
| ### Lifecycle (transport-agnostic) |
| |
| Applies to the `http` and `unix` modes (stdio is bound to its pipe). A server |
| starts only when explicitly created (no auto-spawn) and is a single process: the |
| query engine exits with the server, with no detached child that can outlive it. |
| Two flags, both defaulting to `auto`: |
| |
| * `--idle-timeout auto|<dur>`: idle duration before the server exits. `auto` is 30 |
| minutes. `--daemonize` uses the same default; an explicit value, including |
| disabling the timeout, is honored, with no enforced minimum. |
| * `--idle-start auto|orphaned|last-query`: when the idle clock applies. With `auto` |
| the server does not idle-reap while it has a live owning parent, since the parent |
| is expected to reap it. Once orphaned (the parent exits, or it was started with |
| `--daemonize`) it arms the clock. An owned foreground server is therefore not |
| reaped during a long pause, and an orphaned one, including one backgrounded with |
| `nohup` or `&`, still exits when idle. |
| |
| ### `unix` transport and naming |
| |
| ``` |
| tp server unix <trace> [--name N | --path PATH] [--daemonize] [--idle-timeout ...] [--idle-start ...] # new |
| tp server http <trace> [--port P] [--idle-timeout ...] [--idle-start ...] # flags now apply here too |
| tp query --remote <addr> 'SELECT ...' # --remote (new) goes on every trace-consuming |
| tp metrics --remote <addr> <metric...> # subcommand: query, metrics, summarize, interactive. |
| tp summarize --remote <addr> ... # <addr> = name | socket-path | host:port. |
| tp query <trace> 'SELECT ...' # existing one-shot (no --remote) unchanged |
| tp ctl kill-server <name|socket-path|host:port> # new: stop a running server by address |
| ``` |
| |
| A session is a running server addressed by name. There is no registry; the name |
| maps to a socket path by convention: |
| |
| ``` |
| <name> -> $XDG_RUNTIME_DIR/perfetto/<name>.sock (Linux) |
| %LOCALAPPDATA%\perfetto\<name>.sock (Windows) |
| ``` |
| |
| If `--name` is omitted the server generates a three-word name such as |
| `calm-blue-otter`. Liveness is whether the socket accepts a connection. A stale |
| socket from a crashed server is removed on the next bind: try to connect, and |
| unlink if the connection fails. There are no descriptor files and no garbage |
| collector. |
| |
| `--path PATH` binds an explicit socket path instead of the convention path: for |
| sandboxed environments whose writable directory is elsewhere, or when the |
| convention path would exceed the `AF_UNIX` limit. The server validates the |
| assembled path and, if it would overflow, errors and asks for an explicit |
| `--path`. A client reaches such a server with `tp query --remote <path>`. |
| |
| On startup the server prints a `key=value` record on stdout and human-readable |
| guidance on stderr: |
| |
| ``` |
| pid=1234 session=calm-blue-otter socket-path=.../calm-blue-otter.sock idle-timeout=300s |
| ``` |
| |
| ### Agent guidance |
| |
| The skill doc tells the agent to run `tp server unix <trace>` in the background and |
| then query it, and that the session cleans itself up. It does not mention |
| `--daemonize`. Testing showed that exposing the foreground-versus-daemonize choice |
| causes agents to start the server several times before settling. With the choice |
| removed, each agent backgrounds the server with whatever mechanism it has, and the |
| server adapts to being owned or orphaned on its own. |
| |
| ### Stopping a server (`tp ctl`) |
| |
| `tp ctl kill-server <target>` resolves `<target>` the same way as `--remote` (a |
| bare name to the convention socket path, an absolute path to a socket, anything |
| else to `host:port`), connects to the server, and sends a shutdown over the RPC. |
| This works for both `unix` and `http` servers and needs no pid or signal, so it |
| behaves the same on every OS. Idle-timeout is still the normal way a session ends; |
| `tp ctl kill-server` is for stopping one early. `ctl` is the home for later |
| management verbs (e.g. listing live servers). |
| |
| ### Cross-platform |
| |
| `AF_UNIX` is available on all supported platforms, including Windows since Windows |
| 10 1803, so the naming, liveness check, and stale-socket cleanup are identical |
| everywhere. Only the lifecycle primitives differ per OS: |
| |
| | concern | Linux | Windows | |
| |---|---|---| |
| | ownership / orphan detection | `getppid()` | open a handle to the parent and wait on it | |
| | child exits with server | `PR_SET_PDEATHSIG` | Job Object with `KILL_ON_JOB_CLOSE` | |
| | detach for `--daemonize` | `setsid` + fork | `CreateProcess(DETACHED_PROCESS)` | |
| |
| ### Output |
| |
| Query results are JSON. SQL can be passed inline, on stdin (`-`), or from a file |
| (`--file`, already supported). Results stream over the existing RPC, which uses |
| `is_last_batch` to mark the end. |
| |
| ## The `--remote` flag |
| |
| `--remote <addr>` makes a trace-consuming subcommand run against a running server |
| instead of loading a trace itself. It is added to `query`, `metrics`, `summarize`, |
| and `interactive`. `export` and `convert` are file-to-file operations and keep |
| taking a trace path; `server` starts a server rather than consuming one. The point |
| of a warm session is that all of these reuse it, not just `query`. |
| |
| `--remote` and a positional trace are mutually exclusive: a command either loads a |
| trace or talks to a server. |
| |
| ### Address resolution |
| |
| One `<addr>` syntax covers local sockets and TCP, auto-detected in order: |
| |
| 1. Contains `://` or a trailing `:port` -> HTTP (`host:port`, `http://host:port`). |
| 2. Absolute path, or ends in `.sock` -> a unix socket at that path. |
| 3. Otherwise (matches the session-name charset) -> a unix socket at the convention |
| path for that name. |
| |
| The forms are mostly disjoint because a session name cannot contain `:`, `/`, or |
| `.`. The remaining ambiguity is a bare single-label host such as `localhost`: it |
| has no `:` and looks like a name, so it resolves as a session, not a host. Use a |
| port (`localhost:9001`) for HTTP. |
| |
| ### Missing target |
| |
| If the resolved address has no live server (idle-timed-out, never started, or wrong |
| name), the subcommand fails fast with a message naming the address and how to start |
| one. It does not hang, and does not auto-spawn a server. Agents recover by |
| re-creating the session and retrying; this was reliable across the tested agents. |
| |
| ### Naming |
| |
| "Remote" is used loosely: the target is a separate server process, usually on the |
| same host over a unix socket. The flag is named for the client/server split, not |
| for being on another machine. `--server`, `--session`, and `--attach` were |
| considered; `--remote` reads best across `query`, `metrics`, and `summarize`. |
| |
| ## Validation |
| |
| A prototype over the existing RPC was tested against five coding agents: claude, |
| codex, pi, agy, and opencode. All reused one warm session across queries, restarted |
| a reaped session by name when a query reported it gone, and left no process behind |
| under `--idle-start auto`. Agents with a background-task API (claude, agy) ran the |
| server in the foreground under their own management. Those without it, pi via `&` |
| and opencode via a detached spawn, left the server orphaned, where the idle backstop |
| collected it. |
| |
| ## Alternatives considered |
| |
| ### Name-addressed sessions over http instead of a `unix` transport |
| |
| Pro: no new transport. |
| |
| Con: a TCP port cannot be derived from a name, so the client must be told the port |
| out of band or via a registry, which is the discovery problem this is meant to |
| remove. A unix socket path is derivable from the name. |
| |
| ### Automatic content-addressed sessions |
| |
| Auto-spawn a daemon per trace and reap it on a TTL. |
| |
| Con: implicit, easy to leave large daemons resident, and the lifecycle is not |
| visible to the user. |
| |
| ### MCP server as the primary interface |
| |
| Con: serves only LLM agents, not scripts, CI, or humans at a terminal. An MCP |
| server can instead wrap a warm session as a child it owns. |
| |
| ### Harness-tracked foreground with no server-side backstop |
| |
| Con: pi and opencode have no background-process API, and a foreground server blocks |
| the turn, so they must launch it detached. A detached server escapes harness |
| cleanup, so a server-side idle backstop is still required. |
| |
| ## Open questions |
| |
| * Whether `--remote` should accept explicit `unix:` / `tcp:` scheme prefixes to |
| remove the bare-hostname ambiguity. |
| * Multi-trace and shared-cache sessions, which are out of scope here. |