Verbs

agentproto sessions

agentproto sessions                                one-shot table dump
agentproto sessions --watch [--simple] [--no-color]
agentproto sessions --attach <id-or-name> [--no-color]
agentproto sessions --json                         JSON dump
agentproto sessions --stats[=full] [--verbose] [--json] [--no-color]
                                                   per-session RAM / CPU / process counts
agentproto sessions board    [--json] [--watch] [--all]
agentproto sessions start    <adapter> [--cwd <dir>] [--workspace <slug>]
                                        [--model <id>] [--base-url <url>]
                                        [--auth subscription|api-key]
                                        [--auth-token <token>]
                                        [--options-json <json|@file>]
                                        [--access-profile <ref>]
                                        [--worktree | --no-worktree]
                                        [--mode <id>] [--effort <level>]
                                        [--prompt <text>] [--label <text>]
                                        [--title <text>]
                                        [--orchestrator | --orchestrator-json <json>]
                                        [--mcp-servers-json <json|@file>]
                                        [--sandbox <provider-or-json|@file>]
                                        [--hold-permissions]
                                        [--max-cost-usd <n>]
                                        [--cost-budget <spec>]
                                        [--attach] [--json] [--no-color]
agentproto sessions terminal [--preset <name>] [-- <argv...>]
                                           [--cwd <dir>] [--workspace <slug>]
                                           [--name <slug>] [--label <text>]
                                           [--cols <n>] [--rows <n>]
                                           [--attach] [--json] [--no-color]
agentproto sessions restart  <id-or-name> [--attach] [--json] [--no-color]
agentproto sessions prompt   <id-or-name> --prompt <text> [--wait] [--interrupt]
                                           [--force] [--json]
agentproto sessions pin      <id-or-name> [--json]
agentproto sessions unpin    <id-or-name> [--json]
agentproto sessions mirror   <id-or-name> [--no-color]
agentproto sessions story    <id-or-name> [--json] [--no-color]
                                           [--source auto|native|daemon]
agentproto sessions export   <id-or-name> [--json] [-o <file>]
                                           [--source auto|native|daemon]
                                           [--adapter <slug>] [--cwd <dir>]
agentproto sessions stop     <id-or-name> [--json]
agentproto sessions wait     <id-or-name> [--until <event>] [--timeout <duration>]
                                           [--policy <policyId>] [--json]
agentproto sessions gc       [--older-than-days <n>] [--forget] [--json]
agentproto sessions continue-interrupted [--send] [--id <id-or-name>...]
                                           [--prompt <text>] [--json]
agentproto sessions queue    <id-or-name> [--force <n>] [--deliver <n>]
                                           [--drop <n>] [--json]
agentproto sessions inbox    <id-or-name> [--ack <msgId,...|all>] [--json]
agentproto sessions message  <id-or-name> "<text>" [--kind <kind>]
                                           [--urgency <tier>] [--json]
agentproto sessions checkpoint <id-or-name> [--note "<text>"] [--json]
agentproto sessions handoff  <id-or-name> --to <harness> [--model <id>]
                                           [--profile <ref>] [--note "<text>"]
                                           [--dry-run] [--json]

Browse and control the daemon's live sessions — terminals, agent CLIs, generic commands — from any shell. Requires a running daemon (serve.md or daemon.md).

Discovery

Sessions discovers the daemon by trying candidates in this order — the first live one wins:

  1. AGENTPROTO_DAEMON_URL env var (token from AGENTPROTO_DAEMON_TOKEN, or looked up from a matching runtime.json if unset):
  2. ~/.agentproto/runtime.json, only if its pid is still alive;
  3. the central registry ~/.agentproto/daemons/<port>.json, for the port declared in config.json (falling back to any other live entry);
  4. each configured workspace's own <workspace>/.agentproto/runtime.json (written by serve at boot).

A descriptor whose pid is dead is ignored, never trusted. The token from whichever candidate wins is sent as Bearer on mutating routes:

AGENTPROTO_DAEMON_URL=http://127.0.0.1:18790 \
AGENTPROTO_DAEMON_TOKEN=<token> \
  agentproto sessions

When no daemon is found, the verb lists any stale runtime.json files (PID dead) so you can clean them up:

agentproto sessions: no daemon found.
  Start one with `agentproto serve` or set AGENTPROTO_DAEMON_URL.

  found 1 stale runtime.json file(s) (PID dead):
    /Users/me/code/proj/.agentproto/runtime.json  (pid=12345 · 2d old)

  these confuse discovery — delete them and re-run:
    rm /Users/me/code/proj/.agentproto/runtime.json

Subverbs

One-shot list

agentproto sessions
agentproto sessions --json

Prints a table:

PIN ID         KIND       WORKSPACE  STATUS    AGE       COMMAND
●   ses_abc12  agent-cli  my-proj    running   3m        claude --print --output-format=json
    ses_def34  pty        my-proj    running   1m        bash
    ses_ghi56  agent-cli  my-proj    exited    1h        claude --print …

Pinned sessions sort to the top and are marked with ● in the PIN column. Pinning is list-visibility only — it does not affect keep-alive, the idle reaper, or notifications.

When any session was spawned inside a git worktree, a WORKTREE column is inserted between WORKSPACE and STATUS showing the worktree's leaf directory name. The full path and the worktree id are shown in the --watch detail pane and in --json.

A session whose worktree is still being provisioned reports starting plus what it is doing: starting queued #2 while it waits for a provisioning slot (see worktree), or starting deps / starting setup etc. once it holds one. sessions show <id> prints a provision: line, and --json carries the full provisioning: { state, position, phase, startedAt } object.

board

agentproto sessions board
agentproto sessions board --json
agentproto sessions board --watch
agentproto sessions board --all

At-a-glance status board: every session is assigned exactly one Badge sorted by attention-worthiness, with a one-line summary header.

BadgeCondition
AWAITINGawaitingInput or awaitingPermission — needs a human
BLOCKEDblockedOn is set — wedged on a subagent, command, or inbox
STALERunning, not busy, no keepAlive, idle past the 15-min reap-risk age — or interrupted: true (will never wake on its own)
ACTIVERunning and busy (a turn is in flight)
IDLERunning, not busy, not stale (young or keepAlive: true)
COMMANDkind: "command" execution-log row (only visible with --all)
ENDEDTerminal status (exited / killed / error)

Sort order (most-attention-worthy first): AWAITING > BLOCKED > STALE > ACTIVE > IDLE > COMMAND > ENDED.

FlagDescription
--allInclude kind: "command" execution-log rows (same rows command_list returns).
--jsonEmit the classified rows as JSON — includes the Badge plus every evidence field the rules read (keepAlive, interrupted, continuedFrom, blockedOn, idleMs, costUsd when usage exists). Cannot be combined with --watch.
--watchRe-render every 2 s on a TTY (q to quit). On non-TTY stdout falls back to a single snapshot.

--stats[=full] (who is eating the host)

agentproto sessions --stats
agentproto sessions --stats=full        # same as --stats --verbose
agentproto sessions --stats --json

Samples the host process table once (ps on macOS and Linux, /proc when Linux has no ps) and attributes every process to a session by descending from the adapter pid the daemon already knows. No env scraping is needed for a session whose pid is known. Rows are sorted by RAM:

host: load 3.25 2.50 1.75 (12 cpus) | memory free 20.0 GB of 64.0 GB

ID          LABEL                               KIND       STATUS      RAM    CPU  PROCS  TOP
sess_big    gate-run                            agent-cli  running  2.0 GB  84.2%      9  vitest x4 1.5 GB, claude 400 MB
sess_small  docs                                agent-cli  running  300 MB   0.5%      2  claude 280 MB
sess_idle   -                                   terminal   ended         -      -      -
----------------------------------------------------------------------------------------------------
-           (daemon pid 100)                    daemon              150 MB   1.2%      1  node 150 MB
-           (worktree provisioning)             provision              0 B   0.0%      0
TOTAL       (sessions + daemon + provisioning)                      2.4 GB  85.9%     12

orphans: 1 agentproto-looking process group(s) with no live session (reported only, never killed)
PID  COMMAND     RAM   CPU  PROCS  AGE  SESSION    WHY
777  vitest   512 MB  0.0%      3   1d  sess_dead  adapter-config marker in command line
  • TOP names the biggest command groups by RSS with normalized names (pnpm install, vitest, tsc, tsup, esbuild, git, claude, node, ...) instead of raw node /long/path/... command lines.
  • (daemon) is the daemon process plus its own non-session children.
  • (worktree provisioning) is work not yet attached to a session: daemon children (pnpm install, builds from worktree.setup) that started after an in-flight worktree provision began. In-flight provisions are listed beneath the row. This is a heuristic on start time; a daemon child that happens to start during a provision window is counted here too.
  • orphans are processes owned by the same user that look agentproto-owned but belong to no live session: their command line (or, best effort, their environment) names a ~/.agentproto/adapter-config/<id> path or AGENTPROTO_SESSION_ID, or references a killed session's adapter-config directory or worktree. Only reparented roots have their environment read (Linux /proc/<pid>/environ; macOS ps -E, which some setups blank out). They are reported only, never killed, and are not part of TOTAL. A marked process whose session is still live is folded into that session and shown as (detached) in the full view. Orphans are judged per daemon: on a host running several daemons, another daemon's sessions show up here because this daemon does not know them.
  • CPU is the %CPU that ps reports: a kernel-averaged figure that lags a sudden spike, the same number you read off ps/top. It can exceed 100 for multi-threaded work.
  • RAM is resident set size, summed over the tree. Shared pages are counted once per process, so a tree of forked workers can over-report.
  • The sample is cached for about 3 seconds and shared across the CLI, HTTP and MCP surfaces, so repeated calls cost one ps.

--stats=full (or --stats --verbose) adds one line per process under each row: pid, command, RSS, CPU and elapsed time (capped at 12 per row). --json prints the daemon's report as-is (see below). --stats cannot be combined with --watch. A daemon started before this feature has no /sessions/stats route; restart it to pick the route up.

MCP and HTTP expose the same data:

SurfaceCall
session_list / agent_sessions_list{stats: true | "full"} adds a stats object (rssBytes, cpuPercent, procCount, topCommands, pid, and processes for "full") to each live row that has a pid.
session_stats{detail?: "summary" | "full", fresh?: boolean} returns the whole report: host (load average, free/total memory), labelled sessions sorted by RSS, daemon, provisioning (with inFlight), orphans, totals.
HTTPGET /sessions/stats?detail=summary|full&fresh=true

A caller scoped to its own subtree sees only its own sessions; the host-wide daemon, provisioning and orphans buckets are withheld.

--watch (3-pane dashboard, default)

agentproto sessions --watch

Alt-screen TUI: sessions list (left), detail pane with preview (right), recent events strip, footer with keys. Polls /sessions every 2s and subscribes to /events for live updates.

Keys:

KeyAction
↑ / ↓ / j / kMove selection
EnterAttach to selected (PTY-aware via runAttach)
mMirror (read-only attach; Ctrl-C exits cleanly)
RRestart selected from history (works on exited/killed too)
KKill selected (POST /sessions/:id/kill)
dForget selected (DELETE /sessions/:id; exited/killed/error only)
sShow the selected session's Story / conversation
rRefresh now
q / Ctrl-CQuit

Non-TTY stdin degrades to a one-shot table dump.

--watch --simple

agentproto sessions --watch --simple

The original flat-table picker — same keys minus the detail pane. Smaller terminals, piping into a pager, or scripted screen-recording.

--attach <id-or-name>

agentproto sessions --attach ses_abc12
agentproto sessions --attach claude-tui    # by name

Attaches to a session. PTY sessions get full bidirectional I/O; non-PTY sessions get the SSE event stream (read-only). While attached:

  • Ctrl-] q — detach. Session keeps running on the daemon.
  • Ctrl-C — PTY mode sends it to the child; SSE mode detaches.

start <adapter>

agentproto sessions start claude-code --workspace my-proj --attach
agentproto sessions start claude-code --cwd ~/code --prompt "review the diff"
agentproto sessions start hermes --label "investigation" --json

POSTs /sessions/agent. Spawns a persistent agent-CLI session managed by the daemon — survives the spawning shell and can be reattached later.

FlagPurpose
--cwd <dir>Adapter working dir (absolute resolved).
--workspace <slug>Registered workspace to bind to (see workspace.md).
--model <id>Adapter model option.
--base-url <url>Manifest base_url option (claude-code/claude-sdk) — injected as ANTHROPIC_BASE_URL.
--auth-token <token>Manifest auth_token option — injected as ANTHROPIC_AUTH_TOKEN.
--auth subscription|api-keyDeterministic billing-auth mode + inline credential for adapters that declare a subscription login (authSubscription) and/or an API-key provider in their manifest; any other adapter fails the spawn with unsupported_auth_mode. This is the inline billing selector; the first-class config axis is a named auth profile (access.profileRef) — see Config axes.
--options-json <json|@file>Object form of manifest-declared AIP-45 options; merged with --base-url/--auth-token/--auth/--model/--effort (discrete flags win on collision).
--prompt <text>, -pInitial user turn.
--label <text>UI label for this session.
--title <text>Display title (auto-derived fallback) for this session.
--orchestratorMake this child a scoped orchestrator — the daemon mounts a scoped sub-gateway into the session so it can spawn + supervise its own sub-agents.
--orchestrator-json <json>Object form of the above: {"tools":[…],"maxDepth":N,"maxChildren":N}. Wins over --orchestrator when both are passed.
--mcp-servers-json <json|@file>Inject MCP servers (AcpMcpServer[]) into the session — inline JSON array, or @path to read it from a file.
--sandbox <provider-or-json|@file>Spawn inside an isolated sandbox box instead of the local host. Pass a provider slug (e.g. e2b or box, configured via setup_sandbox_provider) or an inline AIP-36 SandboxDefinition JSON object (optionally with {"reuse":"<sandboxId>"} for reconnect). @file reads the slug or JSON from a file. Mirrors agent_start.sandbox.
--access-profile <ref>Bill this spawn through a named auth profile (CLI twin of agent_start's access.profileRef — pin endpoint + credential, never silently the default). Overrides the daemon's default profile. See Config axes.
--worktreeIsolate this spawn in its own git worktree (auto-minted slug/branch on origin/main) regardless of the daemon's worktrees.isolation policy. Mirrors agent_start.worktree=true.
--no-worktreeSpawn in cwd directly, overriding an isolation policy that would otherwise isolate. Mirrors agent_start.worktree=false.
--mode <id>Manifest-declared posture mode id applied at spawn (e.g. claude-code plan, codex read-only). Mirrors agent_start.mode.
--effort <level>Reasoning effort — low|medium|high|xhigh|max|ultracode, calibrated per model. Mirrors agent_start.effort.
--hold-permissionsStart in permission-hold mode: every tool-permission request the agent raises is parked in the cross-session inbox instead of auto-answered. Approve/deny with permissions.md.
--max-cost-usd <n>HARD spend ceiling in USD for this session: the daemon kills the session at the next turn-end once cumulative cost exceeds n. A positive number; rejected client-side if non-numeric or ≤ 0. Mirrors agent_start.maxCostUsd.
--cost-budget <spec>Windowed governance cap that never kills the session — raises a notification when the window's cumulative cost exceeds the cap, leaving the decision to an orchestrator or human. Two spellings: compact <usd>:<window>[:<scope>] (e.g. 20:5h:profile, 15:7d) or full JSON {"maxCostUsd":20,"window":"5h","scope":"session"}. scope is session (default) or profile. Mirrors agent_start.costBudget.
--attachAttach immediately after spawn.
--jsonEmit the session descriptor as JSON instead of a friendly line.

There is no --role / --prompt-append flag on this verb today — spawn-time role gating (whether this child may itself delegate, and to whom) is MCP/HTTP-only: the agent_start MCP tool's role / promptAppend fields, or the same fields on the POST /sessions/agent body. See concepts/roles.md.

Session identity env

Every process the daemon spawns on a session's behalf — an agent-CLI adapter (this verb), a terminal -- <argv...> PTY, or a command_execute / cron kind:"command" shell command — gets these identity env vars set into its own process environment. The first two are always present; the third is added only for agent-CLI children that resolved a parentSessionId:

VarValue
AGENTPROTO_SESSION_IDThe spawned session's own id (sess_…) — the same id session_list/agent_sessions_list show for it.
AGENTPROTO_WORKSPACE_SLUGThe workspace slug the session resolved to ("default" when none).
AGENTPROTO_PARENT_SESSION_IDThe id of the session that spawned this one. Present only for nested agent-CLI children; lets a child report back via the message_parent MCP tool without a registry round-trip (see Messages between sessions).

A hook, script, or tool a session shells out to can read these to report back, tag telemetry, or spawn a further child with parentSessionId set to its own id — closing the loop for e.g. a git push hook that spawns a reviewer session and wants it nested under the session that triggered it.

These are set last, after any other env the spawn composes (manifest defaults, billing-auth, a caller-supplied env on POST /sessions or POST /sessions/terminal) — a caller can never override or forge them, and a session never inherits a value from the daemon's own process env. Every spawn — including a resumed/restarted one — gets its own freshly minted id; session_restart mints a new id (see resumedFrom on the descriptor for lineage back to the prior one), while the daemon's own crash/restart-time lazy resume revives the same descriptor row and so keeps the same id. label/name are deliberately not carried into env — they're optional, mutable, and absent on most sessions; look one up via AGENTPROTO_SESSION_ID + session_list instead.

Implicit spawn deduplication

agent_start deduplicates spawns by default. When a spawn has a label and no explicit idempotencyKey, the daemon derives an implicit key from the label plus a hash of the initial prompt; a repeat with the same adapter, cwd, and key within ~2 minutes returns the existing session instead of forking a second one. Unlabelled spawns are never deduped, so deliberate parallel fan-out into one cwd is still safe.

Control the policy with the spawn.dedupe config field or the AGENTPROTO_SPAWN_DEDUPE env var:

  • "always" (default) — derive an implicit key whenever a label is present.
  • "on-request" — only an explicit idempotencyKey dedupes (pre-default behaviour).

Over MCP/HTTP, pass dedupe: false on a single spawn to opt out regardless of the policy. session_restart always mints a new session id and is unaffected.

Orchestrator & mcpServers

--orchestrator and --mcp-servers-json reach the same spawn capability as the agent_start MCP tool: the CLI, the HTTP route (POST /sessions/agent), and MCP all delegate to one shared spawn path, so any surface can start an orchestrator-enabled or mcpServers-injected session.

# Scoped orchestrator — the child can spawn + supervise its own sub-agents
agentproto sessions start claude-code --orchestrator --workspace my-app --attach

# Bound it: at most 2 levels deep, 3 concurrent children
agentproto sessions start claude-code \
  --orchestrator-json '{"maxDepth":2,"maxChildren":3}'

# Inject MCP servers (here: mount the daemon's own gateway into hermes)
agentproto sessions start hermes \
  --mcp-servers-json '[{"name":"agentproto","transport":"http","ref":"http://127.0.0.1:18790/mcp"}]'

Both are parsed and validated client-side before the daemon round-trip: malformed JSON, a non-array --mcp-servers-json, or an unreadable @file fail fast with exit 2. --orchestrator requires a daemon started with the scoped orchestrator sub-gateway wired (the default for agentproto serve); otherwise the route returns 501.

Permission hold mode (--hold-permissions)

By default a spawned agent's tool-permission requests (Write, Bash, …) are auto-answered in the driver so the turn never blocks. With --hold-permissions each request is instead surfaced and parked in the daemon's cross-session inbox — the agent's turn blocks until a human or orchestrator approves or denies it:

agentproto sessions start claude-code --workspace my-app --hold-permissions
# … the agent tries to Write a file …
agentproto permissions ls                 # see what's held, across every session
agentproto permissions approve <id>       # unblock (allow-once)
agentproto permissions approve <id> --always   # allow-always, if offered
agentproto permissions deny <id>          # reject

Same capability over MCP (agent_start { permissionHold: true } + permissions_list / permissions_respond) and HTTP (POST /sessions/agent { permissionHold: true }, GET /permissions, POST /permissions/:id). A held session renders with a ! badge in --watch. See permissions.md for the full inbox verb. ACP adapters only (e.g. claude-code); adapters with no permission surface ignore the flag.

Sandbox

agent_start (and the --sandbox CLI flag above) boot the session inside an isolated cloud sandbox instead of the local machine, via a pluggable SandboxProvider (e2b's Firecracker microVMs ship today — @agentproto/sandbox-e2b). The daemon boots the box, starts its own sub-daemon inside it, and proxies the session's turns back over that box's MCP endpoint (SandboxAgentSessionProxy) — from the outside it behaves like any other session. Closing the session pauses the box by default (AIP-36 lifecycle) — it stays reattachable via agentproto sandbox attach or sandbox.reuse. An explicit lifecycle.destroy_on declaration kills the box instead.

Reachable via the --sandbox flag (CLI), the MCP agent_start tool's sandbox field, or POST /sessions/agent. Companion MCP tools: list_sandbox_providers (see what's configured) and setup_sandbox_provider (register credentials for one).

commandSandbox (MCP/HTTP only — no CLI flag yet) — NOT the same thing as sandbox

Do not confuse this with sandbox above. sandbox boots a whole SEPARATE machine/box and runs the session there. commandSandbox is a completely different, much smaller mechanism: it wraps the adapter's OWN spawned process on THIS host — the same argv agent_start would have run anyway — through an OS-level confinement backend (macOS Seatbelt / Linux bubblewrap, @agentproto/command-sandbox, the same backends command_execute already uses). It denies the adapter's process tree filesystem access outside the session's cwd, confinement an ACP permission seam can never give you since it only sees tool calls the adapter chooses to report, not what an in-process Bash actually touches. The two fields are independent — set either, both, or neither; commandSandbox is ignored for a sandbox spawn (the box's own daemon would need to apply it itself).

Values: "off" (default — unconfined, unchanged behaviour), "workspace" (deny reads/writes to $HOME outside the workspace — protects ~/.ssh, ~/.aws, credentials, …; network stays allowed), "strict" ("workspace"

  • deny all network). "workspace"/"strict" FAIL the spawn outright if no backend is installed for the platform (macOS needs sandbox-exec, Linux needs bwrap) — it never silently falls back to running unconfined.

A workspace can set this persistently instead of passing it on every agent_start call, via the adapterSpawn key of .agentproto/ command-sandbox.json:

{
  "mode": "off",
  "adapterSpawn": {
    "mode": "workspace",
    "extraReadPaths": [],
    "extraWritePaths": [],
    "network": "allow"
  }
}

Note the top-level mode (read by command_execute) and adapterSpawn.mode (read for the adapter-spawn axis above) are DELIBERATELY separate keys in the same file, not one shared setting — a misconfigured command_execute jail breaks one shell command; a misconfigured adapter-spawn jail breaks the WHOLE session for as long as the adapter runs, a strictly bigger blast radius that needs its own explicit opt-in. An explicit agent_start. commandSandbox call always overrides the file. AGENTPROTO_ADAPTER_COMMAND_SANDBOX_MODE is the env-var escape hatch for the adapter axis (mirroring AGENTPROTO_COMMAND_SANDBOX_MODE for command_execute — the two vars are also separate, on purpose).

Config axes (MCP/HTTP)

A session's behaviour is configured along a fixed set of axes — the unified surface that replaces the older overloaded mode concept. Each is set at spawn (agent_start / POST /sessions/agent) and, where it can apply live, switched mid-session:

AxisWhat it controlsValues
modelroute-identity ref[route:]vendor/product[:pin][@route]
effortreasoning/compute budgetlow|medium|high|xhigh|max|ultracode
accessa named auth profile (access.profileRef), not an inline tokenprofile ref eligible for the resolved (adapter × route)
routeendpoint/gateway railanthropic|openrouter|requesty|…
posturewhat the agent may dodefault|plan|accept-edits|bypass|read-only (or a raw harness mode id)
contextProfilewhat enters contextfull|lean|…

Live switches — best-effort, mid-session, no restart. Each returns {applied:false, reason} (rather than throwing) when the running adapter can't apply it live:

  • agent_set_model { sessionId, model }
  • agent_set_effort { sessionId, effort }
  • agent_set_posture { sessionId, posture }

An axis that can't switch live (e.g. requires-restart) can be re-applied through restart-with-override. posture supersedes the legacy mode/--auth-only framing for "what the agent may do" and "which wallet pays"; use catalog_models (see models.md) to discover which (model, route) pairs are actually runnable given the configured auth profiles.

terminal -- <argv...>

agentproto sessions terminal --name claude-tui --attach -- claude
agentproto sessions terminal -- bash
agentproto sessions terminal --cols 120 --rows 30 -- htop

POSTs /sessions/terminal. Spawns a PTY-backed session running the literal <argv>. The -- separator is canonical — everything after it is forwarded verbatim to the spawn, including flags that would otherwise be eaten by the verb's parser.

FlagPurpose
--cwd <dir>Spawn cwd.
--workspace <slug>Registered workspace to bind to.
--name <slug>Stable session name (alphanumeric + -); used as an alias for attach/stop/restart.
--label <text>UI label.
--cols <n> / --rows <n>Initial PTY dimensions. Default: current terminal size, fallback 80x24.
--attach / --json / --no-colorAs above.

node-pty must be installed for PTY routes to work; without it, the daemon returns 501 and this verb fails.

restart <id-or-name>

agentproto sessions restart claude-tui
agentproto sessions restart ses_abc12 --attach

Looks up the (possibly historical) descriptor and spawns a new session of the same shape. For agent-CLI sessions, attempts to resume the conversation via the prior adapter session id; falls back to a fresh shape when the adapter reports the id is unknown ("session killed too early to persist"). The banner reports which path was taken: (resumed via claude --resume from ses_abc12) or (fresh — resume not available).

Restart-with-override (MCP/HTTP). The session_restart MCP tool (and the POST /sessions/:id/restart route) accept per-axis overrides — model, effort, posture, route, access.profileRef, and contextProfile (plus a legacy mode). An omitted axis is carried forward from the prior session; an axis set here wins. A restart carrying any override is treated as a config change: it re-resolves auth and takes the forced agent-resume path (bypassing the PTY-native claude --resume branch, which can't re-resolve billing or apply an axis). This is the way to apply an axis that a live switch reported as requires-restart. Only agent-CLI sessions have axes to override — a PTY/command restart with overrides is rejected 400.

prompt <id-or-name>

agentproto sessions prompt claude-tui --prompt "check the PR comments"
agentproto sessions prompt claude-tui --prompt "stop and fix this" --interrupt
agentproto sessions prompt claude-tui --prompt "one more thing" --wait

POSTs /sessions/:id/prompt to send a follow-up message into an already-running session. Default is fire-and-forget and queued behind any in-flight turn — the reply is not printed; read it back with story or export.

FlagPurpose
--prompt <text>, -pMessage to send (required).
--waitBlock until the turn this prompt starts has drained.
--interruptCancel the in-flight turn and dispatch immediately instead of queuing.
--forceJump the prompt queue (only meaningful without --wait).
--jsonEmit the raw server response as JSON.

pin <id-or-name> / unpin <id-or-name>

agentproto sessions pin claude-tui
agentproto sessions unpin ses_abc12

POSTs /sessions/:id/pin. A pinned session sorts to the top of the sessions table and the VS Code "Pinned" group, marked with a PIN indicator. Pinning is list-visibility only — it has no effect on keep-alive, the idle reaper, or notifications.

mirror <id-or-name>

agentproto sessions mirror claude-tui

Read-only attach. For PTY sessions: bytes flow daemon → stdout only; stdin stays in your shell's normal state and Ctrl-C cleanly exits this Node process without touching the underlying PTY. For non-PTY sessions: same as --attach.

Dead sessions (exited/killed/error) print a hint pointing at restart; the WS upgrade would only return a confusing close 1011 mid-stream.

story <id-or-name>

agentproto sessions story ses_abc12
agentproto sessions story claude-tui --json
agentproto sessions story ses_abc12 --source daemon

CLI parity for the agentproto_session_story MCP App: parses the session's transcript and renders it as chapters/steps (a human-readable narrative of what the agent did) instead of raw events. Takes the same --source auto|native|daemon backend selection as export above. --json emits the structured { sessionId, adapter, chapters, steps } shape instead of the rendered terminal view.

export <id-or-name>

agentproto sessions export ses_abc12
agentproto sessions export claude-tui --json -o transcript.json
agentproto sessions export ses_abc12 --source daemon

GETs /sessions/:id/export — renders a clean transcript from the session's structured history (see concepts/session-transcripts.md for what's captured and where). Works on stopped sessions as well as running ones.

FlagDefaultPurpose
--jsonmarkdownEmit the raw ExportedSession JSON instead of rendered markdown.
--output <file>, -ostdoutWrite to a file instead of stdout.
--source <auto|native|daemon>autoWhich backend to read. auto prefers the adapter's own native store (claude-code JSONL, hermes SQLite) and falls back to agentproto's events.jsonl capture when there isn't one or it can't be read; native/daemon force one and surface its own error instead of falling back.
--adapter <slug>from registryOverride the adapter slug — required with --source native when exporting a raw adapter-native id that isn't in the registry.
--cwd <dir>from registryOverride the working directory — required for a claude-code native export when the session isn't in the registry (used to locate the JSONL file).

The /sessions/:id/export route accepts the same format (markdown|json), source, adapter, and cwd as query params. On failure it responds {error: "export_failed", message, sessionId, adapter} — 404 when the session/adapter/store couldn't be found at all, 422 for any other export error (e.g. a native store that failed to parse). --source values other than auto/native/daemon are rejected client-side by the CLI (exit 2) before any request is made.

stop <id-or-name>

agentproto sessions stop ses_abc12
agentproto sessions stop claude-tui --json

POSTs /sessions/:id/kill — sends SIGTERM to the child. Idempotent on already-dead sessions (reports "not running"; exit 1).

wait <id-or-name>

agentproto sessions wait ses_abc12
agentproto sessions wait ses_abc12 --until turn-end --timeout 5m
agentproto sessions wait --policy pol_abc12 --timeout 2m

Blocking long-poll: blocks the caller until the session fires a lifecycle event or the timeout expires. Chains calls across the daemon's ~55s per-call ceiling so the CLI-side timeout can be arbitrarily long.

This is the scriptable equivalent of the session_monitor MCP tool, but without the 49s MCP constraint — prefer it when you have shell access.

FlagDefaultDescription
--until <event>anyWhich lifecycle event to wait on: turn-end, awaiting-input, exited, any.
--timeout <duration>60s / 15mTotal wait budget. Duration string: 500ms, 30s, 5m, 2h. Bare integers under 1000 are rejected as ambiguous (3000 → "did you mean 3000ms or 3s?"). Default is 60s without --until, 15m with --until (agent turns commonly run 5-20 minutes).
--policy <policyId>—Wait on a completion policy instead of a session event. Long-polls GET /policies/:id/wait until the policy leaves watching/gating/queued/nudging/acting. When set, the positional <id-or-name> is ignored.
--jsonfalseMachine-readable output. Suppresses the up-front "waiting up to…" banner; the matched result includes timeoutMs and timeout fields.

Before blocking, the CLI prints its interpreted budget to stderr (waiting up to 5m (300000ms) for turn-end on ses_abc12…) so a units mistake is caught immediately — the incident this module exists for was a --timeout 3000 that was meant as 3000 seconds, not 3000ms.

Exit codes

CodeMeaning
0Condition met (session event matched, or policy reached done/awaiting-ack).
2Timeout expired, usage error, or policy blocked/cancelled.
3Session/policy not found, or daemon unreachable.
4The matched turn-end was a silent no-op — empty: true (zero assistant output, zero tool calls) — or ended with reason: "error" (the adapter reported a failed turn). The wait DID resolve; the turn it resolved on produced nothing. Commonly a bad auth/model config.

status vs wait

agentproto sessions (or --json) is a non-blocking snapshot — it shows current state. wait blocks until a state transition occurs. Use wait in scripts and supervisors; use the snapshot for dashboards and humans.

When to use wait vs session_monitor (MCP)

SurfaceMax timeoutMulti-sessionUse when
sessions wait (CLI)Unlimited (chains calls)No (one session)You have a shell and want to script a gate or supervisor loop.
session_monitor (MCP)49s (MCP constraint)Yes (up to 20)You're an agent with MCP tools and need to fan-in across children.

Both hit the same daemon endpoint (GET /sessions/:id/wait); the CLI chains across the ~55s per-call ceiling, while session_monitor multiplexes across sessions but is capped by MCP's own timeout.

gc

agentproto sessions gc                         # print plan, archive eligible
agentproto sessions gc --apply                 # actually archive
agentproto sessions gc --forget                # drop descriptors instead of archiving
agentproto sessions gc --older-than-days 7 --apply

Bulk garbage-collects terminal-status sessions (exited/killed/error). By default it archives them (hidden from the default view, still readable/importable) via POST /sessions/gc. Pass --forget to drop the descriptors instead (the native conversation on disk survives). --older-than-days keeps anything more recent. Live sessions are never touched.

continue-interrupted

agentproto sessions continue-interrupted
agentproto sessions continue-interrupted --send
agentproto sessions continue-interrupted --send --id ses_abc12 --id ses_def34
agentproto sessions continue-interrupted --send --prompt "continue from where you left off"

CLI parity for the session_continue_interrupted MCP verb. Lists the sessions the last daemon restart cut off mid-turn — i.e., sessions that were still in a running turn when the daemon process exited and whose turn therefore never received a proper turn-end. Without --send it's a dry run that prints the eligible sessions and exits without touching anything. Pass --send to send each eligible session a one-shot continue prompt (resuming it in place first if needed). Hits POST /sessions/continue-interrupted.

The opt-in daemon.continueInterruptedOnBoot config flag runs this pass automatically whenever the daemon boots (with --send behaviour), so a crash and restart can self-heal without manual intervention.

FlagDefaultDescription
--sendfalseActually send the continue prompt to each eligible session. Without this the command is a dry run — it prints what it would do, then exits.
--id <id-or-name>(all eligible)Restrict to this session only. Repeatable.
--prompt <text>(default continue message)Custom text to send as the continue prompt.
--jsonfalseEmit {dryRun, sessions, eligible, sent, skipped, failed} instead of the human summary. Exits 1 if any session failed.

queue <id-or-name>

agentproto sessions queue ses_abc12
agentproto sessions queue claude-tui --json
agentproto sessions queue claude-tui --deliver 2
agentproto sessions queue claude-tui --drop 3
agentproto sessions queue claude-tui --force 2

Inspects — and optionally manipulates — the session's prompt FIFO. With no action flag, lists what's queued: each item's position (1 = next to dispatch), origin (user/agent/child), preview, and queuedAt.

FlagEffect
--force <n>Jump position n to the front of the queue without touching the in-flight turn.
--deliver <n>Interrupt whatever is running and dispatch position n now.
--drop <n>Remove the item at position n without delivering it.

Positions are 1-indexed, matching sessions prompt output. After any action the queue is re-listed to show the result.

inbox <id-or-name>

agentproto sessions inbox ses_abc12
agentproto sessions inbox ses_abc12 --ack msg_3f2a91c0,msg_77e00000
agentproto sessions inbox ses_abc12 --ack all --json

Lists the session's inbox — typed messages from other sessions (or from you, see message below) that it hasn't consumed yet: sender (relation + session), kind, urgency, how it was delivered, and a text preview. A message leaves the inbox when it's delivered into a turn, when the session receives it through inbox_wait, or when it's acked. --ack removes messages by id (or all); an acked message that's still waiting in the prompt queue is dropped from the queue too. The full history stays in the transcript either way. See Messages between sessions.

message <id-or-name> "<text>"

agentproto sessions message ses_abc12 "status?" --kind question
agentproto sessions message ses_abc12 "FYI: main was force-pushed" --urgency fyi
agentproto sessions message ses_abc12 "stop, the spec changed" --urgency interrupt

Sends a typed message to the session as the human operator. It's recorded as a session-message from human (never a user-prompt), rendered to the agent inside a daemon-attested <agentproto-message> tag, and routed by --urgency like any other message (see the tier table below). A human sender keeps interrupt. --kind is one of report (default), question, blocker, done, notice.

checkpoint <id-or-name>

agentproto sessions checkpoint ses_abc12
agentproto sessions checkpoint ses_abc12 --note "decision: keep the zod schema, drop yup"
agentproto sessions checkpoint ses_abc12 --json

Writes a structured context-continuity checkpoint for an agent-cli session and prints the path of the JSON file:

agentproto sessions checkpoint: ckpt_ses_abc12_1788371280000
  /Users/you/.agentproto/sessions/ses_abc12/checkpoints/ckpt_ses_abc12_1788371280000.json

The session keeps running and its transcript is untouched; the checkpoint is a bounded summary saved next to its events.jsonl. --note records operator notes (decisions, constraints) in the checkpoint. --json prints the daemon's full response, { checkpointId, path, checkpoint }. The field-by-field layout is in the handoff guide.

HTTP twin: POST /sessions/:id/checkpoint with body { notes? } (bearer token required, like every mutating /sessions route). 404 for an unknown session, 400 when it isn't an agent-cli session.

handoff <id-or-name> --to <harness>

agentproto sessions handoff ses_abc12 --to codex --dry-run
agentproto sessions handoff ses_abc12 --to codex
agentproto sessions handoff ses_abc12 --to opencode --model anthropic/claude-sonnet-5 \
  --profile work-anthropic --note "tests in packages/api are the bar"

Hands a session's work to another harness: writes a checkpoint (as above), then starts a NEW session on --to whose first prompt is that checkpoint. The source session is left untouched except for a continuedTo link; the new session carries continuedFrom, the checkpointId, and handoff: { fromHarness, toHarness, at }.

FlagMeaning
--to <harness>Target harness slug (codex, opencode, claude-code, …). Required.
--model <id>Model for the new session. Default: carried from the source.
--profile <ref>Auth profile (billing wallet) for the new session. Rejected 400 if it isn't eligible for the resolved harness × route.
--note "<text>"Operator notes carried in the checkpoint and the resume prompt.
--dry-runPrint the checkpoint prompt that would be handed over. Nothing is written, nothing is spawned, the source session is not modified.
--jsonPrint the daemon's full response.

Normal output: the new session id, its harness, and the checkpoint path.

agentproto sessions handoff: ses_abc12 (claude-code) → ses_def34 (codex)
  checkpoint: /Users/you/.agentproto/sessions/ses_abc12/checkpoints/ckpt_ses_abc12_1788371280000.json

Handoff is something you trigger — it is not automatic by default (see contextContinuity mode). Step-by-step walkthrough: Hand off from Claude Code to Codex.

HTTP twin: POST /sessions/:id/handoff with body { to, model?, access?: { profileRef }, notes?, dryRun? } → 201 { continuedFrom, continuedTo, checkpointId, path, handoff, session }, or 200 { dryRun: true, checkpoint, prompt } for a dry run. 400 to_required when to is missing, 404 for an unknown session or unresolvable harness.

Messages between sessions

Sessions in the same tree talk through typed messages, separate from prompts. A prompt is an instruction from whoever drives a session; a message is a report between sessions whose sender the daemon attests: the from block (session id, relation, label) is computed from the caller's verified identity and the session tree, and no send request can set it. The recipient's agent sees each message as its own <agentproto-message id=… from="child" session=… kind=…><body>…</body></agentproto-message> block. It's never glued onto another prompt, and its body is escaped so it can't forge a header. The recipient's transcript records a session-message record, never a user-prompt.

Who may message whom: child → parent, parent → direct child, human/daemon → anyone. Siblings only with defaults.messaging.allowSiblings: true.

Tools (MCP)

ToolInputsUse
message_parent{message, kind?, urgency?, interrupt?, replyTo?}Report up to the session that spawned you.
message_send{to: "parent" | sessionId, text, kind?, urgency?, replyTo?, correlationId?, data?}Message a tree neighbour.
message_reply{replyTo, text, kind?, urgency?}Answer a message you received — routed to its sender, same thread.
inbox_list{from?, kind?, correlationId?, limit?}Read your un-consumed messages.
inbox_ack{ids | "all"}Acknowledge (remove) them.
inbox_wait{from?: ids | "children", kind?, correlationId?, timeoutMs? (1–49 s), ack?}Block until a matching message arrives; returns {messages, timedOut, pendingChildren}.

HTTP: POST /sessions/:id/messages (human sender, or a session via the trusted ?callerSessionId=), GET /sessions/:id/inbox, POST /sessions/:id/inbox/ack. Bus/SSE: session:message (at send and again at delivery).

Delivery tiers

urgencyrecipient in inbox_waitidlebusy (its prompted turn)busy (autonomous turn)
fyireturned by the waitinbox only — no wake; a one-line digest opens its next turninbox onlyinbox only
next-turnreturned by the waitits own turn nowqueued; delivered when the turn endsqueued
steerreturned by the waitits own turn nowinjected into the running turn if the agent supports ACP steering (steering: true in session_list), at most once per 10 s — else next-turnnext-turn (never steered)
interruptreturned by the waitits own turn nowcancels the turn and delivers — only when granted (humans always; sessions only with defaults.messaging.agentInterrupt: "allow"), otherwise delivered as steersame

Default urgency by kind: blocker/question → steer, the rest → next-turn. Every send result reports the tier actually used (urgencyApplied), so a sender is never told a message interrupted when it didn't.

Supervising without ending your turn

A supervisor that fans out children should not end its turn to wait for them — nothing wakes an idle parent on a timer. Loop on inbox_wait instead; it returns as soon as a child reports, and pendingChildren tells you when to stop:

agent_start {…child A…}   agent_start {…child B…}
loop:
  inbox_wait { from: "children", kind: ["done", "blocker"], timeoutMs: 45000 }
  → handle each returned message (message_reply to answer a blocker)
  until both are done, or pendingChildren is empty

Children report with message_parent (or message_send {to: "parent"}) using kind: "done" when finished and kind: "blocker" when stuck.

Interrupting a live session

Use agentproto sessions prompt <id> --prompt "..." --interrupt, or call the same capability through the MCP agent_prompt tool and the HTTP prompt route:

CLI:  agentproto sessions prompt <id-or-name> --prompt "..." --interrupt
MCP:  agent_prompt { sessionId, prompt, interrupt: true }
HTTP: POST /sessions/:id/prompt?wait=false  { "prompt": "...", "interrupt": true }

By default, sending a prompt to a session that's still mid-turn queues it (FIFO) — the prompt is dispatched automatically when the current turn ends, so fan-in bursts are delivered in order instead of dropped. Passing interrupt: true changes that: the daemon cancels the in-flight turn (the adapter's own soft Ctrl-C — ACP session/cancel, or an adapter-specific SIGINT), waits for it to actually settle, then delivers the new prompt on the same live session — same process, same conversation history, no re-spawn. interrupt is a no-op when the session is already idle.

A target whose adapter advertises mid-turn steering (capabilities.steering, e.g. claude-code over ACP) does not wait for turn end: a prompt sent without interrupt is steered into the running turn at its next safe point (FIFO, turn undisturbed). agent_prompt / the prompt route say exactly what happened:

deliverypendingMeaning
deliveredfalseTarget was idle (or interrupt cut the turn) — the prompt is running now.
steeredfalseInjected into the running turn. deliveredAt is set.
queued-mid-turntrueNot delivered yet — parked until the turn ends. queueId identifies it.

Pass steer: false to force plain queueing. A prompt that stays queued shows up as pendingPrompts (with ageMs, stale past defaults.messaging.pendingPromptStaleMinutes, default 5) in session_list and session_recap, and its sender gets a notice message once it goes stale. deliverWithin: <seconds> (with deliverWithinVia: auto | steer | interrupt, default auto = steer, else interrupt) makes the daemon force delivery if the prompt is still queued after that long.

This is deliberately narrower than restart or stop:

ActionEffect
interrupt: true on agent_prompt / prompt routeCancels the current turn only; session and context survive; next prompt continues the same conversation.
stopKills the process outright (SIGTERM). Conversation ends unless you restart.
restartRe-spawns from history, attempting to resume via the adapter's own session id — a new process, not a redirect of a live one.

A few edge cases worth knowing:

  • If the adapter's session handle doesn't support cancellation, the call fails with a clear error rather than silently dropping the new prompt.
  • The daemon waits up to 30s for the cancelled turn to settle (busy flipping back to false) before giving up — a safety net for an adapter that never delivers a turn-end for the turn it just cancelled, not the normal path.
  • On the HTTP route, interrupt only takes effect with ?wait=false (the fire-and-forget arm, same one MCP agent_prompt always uses) — the default blocking wait=true call has no interrupt semantics of its own since it just waits on sendPrompt.

Raw events (HTTP)

GET /sessions/:id/events?since=<seq>&limit=<n>

No CLI subverb wraps this — it's an HTTP-only route for a frontend that wants the raw, per-kind records (tool calls, plans, usage updates, …) instead of the collapsed markdown/JSON /export gives. It reads the same events.jsonl agentproto's daemon-events export strategy reads (see concepts/session-transcripts.md).

Query paramDefaultNotes
since0Only return records with seq greater than this cursor. Must be a non-negative integer or the route 400s (invalid_since).
limit500Max records per call, clamped to [1, 2000].

Response: {sessionId, events, nextSeq, complete} — events is the raw parsed JSONL objects (seq > since, capped at limit); nextSeq is the last returned event's seq (or since unchanged if nothing matched); complete is false when more events exist beyond limit — poll again with since=nextSeq to keep draining. 404 (no_transcript) when the session never wrote an events.jsonl (a PTY/command session, or an agent-cli session that predates this feature).

Examples

# Start a persistent Claude Code session and attach
agentproto sessions start claude-code --workspace my-app --attach

# Start a scoped orchestrator that can spawn + supervise sub-agents
agentproto sessions start claude-code --orchestrator --attach

# Spawn a PTY-backed REPL with a friendly name
agentproto sessions terminal --name claude-tui --attach -- claude

# List, peek, detach
agentproto sessions
agentproto sessions --attach claude-tui    # then Ctrl-] q

# Watch the dashboard, drive interactively
agentproto sessions --watch

# Stop everything you can find
agentproto sessions --json | jq -r '.[].id' | xargs -n1 agentproto sessions stop

# Export a transcript once the session is done
agentproto sessions export ses_abc12 -o transcript.md

See also

  • Session transcripts — what's captured in events.jsonl, event kinds, native vs daemon export sources, the PTY exception
  • Hand off from Claude Code to Codex — checkpoint / handoff end to end, the checkpoint file, --dry-run
  • chat.md — sending follow-up prompts to a live session, incl. what happens when the target is dead or mid-turn
  • Roles — the spawn-time delegation gate behind agent_start's role field, the privilege lattice, and the role_list introspection tool