pa-daemon #
Session supervision and wire serving.
Scope #
ACP stdio transport (acp): the JSON-RPC serve surface for Agent
Client Protocol clients - a thin transport over the pa-core session engine
(initialize, session/new, session/prompt, session/close, session/cancel,
and the outgoing session/update notification with namespaced _meta
correlation), owned by this crate because wire-protocol serving is its area;
the in-process transport hosts the automatic compaction arms at its turn
boundaries (acp/compaction_arms.rs: the TS _checkCompaction /
_runPreTurnCompaction / _consumePendingRequestedRefine flows — the
overflow compact-and-retry, the model-requested arm, and the threshold
arm, publishing the ACP compaction_end mapping and aborting an in-flight
run on session/cancel+close; the daemon-attached transport runs the
worker turn loop's arms instead), because the TS arms live in the session
turn loop every transport shares and the in-process ACP path drives the
pa-core engine directly. The compact-trigger auto-refine scheduling owns
its surface here (compact_autorefine.rs for the daemon worker: every
compaction arm plus the manual compact command arm the pa-core
trigger, and the quiescent turn boundaries / the command path consume
the gated review - TS _scheduleAutoRefineAfterCompaction plus the
background _maybeAutoRefine("compact"); acp/autorefine.rs for ACP:
the serialized checkpoint consumes the armed trigger after the
requested refine.run and session close drains what no turn serviced -
TS's serialized scheduling for the acp app mode). The direct-ACP goal
continuation boundary owns its surface here too (acp/goal_continuation.rs:
the TS session's continuation hook _getContinuationMessages runs inside
the one session/prompt request on the in-process path — the goal arm with
exclusive priority over the autonomous arm, the budget-limit wrap-up steer,
the natural continuation mint per settled boundary, the terminal-error
goal failure, the threshold-arm goal queue before its compaction (with
the cancel rollback), and the compact-with-active-goal continue; the
daemon-attached transport rides the worker's #244 loop instead).
Supervisor process (one worker process per active session), restart/backoff
supervision, session registry/roster + worker self-registration (session
identity survives supervisor restarts: workers re-register with backoff and
the roster rebuilds), parent-death child cleanup (supervisor_parent_death.rs:
an unexpectedly exited worker's resident RLM children close with it - the
same plain-stop semantics as the worker-side #246 close, which SIGKILL
bypasses - via the durable create's parentActiveSessionId join), append-only session store ownership (including the
compaction-entry fold for compacted message reads), client attach/detach
(full-snapshot and chunked session_snapshot_begin/chunk/end
streaming), direct-attach transport (supervisor-issued single-use tickets
with a 10s TTL, worker-side peer grants burned on first use, session-plane
command gating on peer links), kernel agent_message/agent_observe bridge (agent_messaging.rs: the worker-side controllers over the supervisor link; the family view joins the supervisor roster with the session's own RLM children registry - the same registry rlm.list_subagents reads - so registry children are Child members addressable by name, RLM child id, and persisted session id, the spawning session is the Parent member for a subagent worker, and everything else stays a sibling; delivery tries the direct peer transport then the supervisor-routed send_message), worker-to-worker peer messaging (stage 3:
worker-purpose single-use grants minted by get_worker_peer_transport
for a source worker's kernel agent_message.send, direct
worker_deliver_message on the target worker's socket with the supervisor
routed send_message as the never-retried fallback), session archiving
(session_archive.rs: the sessions directory must not grow forever — a
supervisor boot sweep plus a periodic re-sweep at the TS idle-eviction
cadence retire sessions by age (settings sessionArchiveMaxAgeDays,
default 30) and count (settings sessionArchiveMaxSessions, default 200;
each rule independently off-able) by MOVING them into
<agent-dir>/sessions-archive; resident-worker sessions and sessions with
active scheduled jobs are never archived; an archived session stays
resumable — the catalog resolve falls back to the archive, restores the
file into the sessions dir, and the wake spawns over the live path),
saved-session wake
for non-resident send_message targets (session_catalog.rs: catalog
resolve by session-id prefix or exact name, cwd-scoped first, archived
fallback with restore; a resident
worker hosting the file is reused, otherwise one spawns over it; RLM
children fall back to the spawn ledger's live child edges when the catalog
misses - children persist outside the sessions dir, under the parent's
session-artifacts tree - and wake over their own session file), wire protocol serve/negotiation (including the
compact/abort_compaction/set_auto_compaction commands and their
compaction_start/compaction_end events, and the live
set_model/set_thinking_level commands — model_switch.rs: registry
resolution, the durable model_change/thinking_level_change rows, the
settings defaults, and the engine's live agent/provider-target/level
switch), session export commands (session_export.rs: the worker-side
export_html/export_jsonl handlers; the HTML data carries the engine's
tools section and custom-tool pre-render through the
SessionEngine::export_tools/export_rendered_tools seams, built
lazily when an export precedes the first turn - the TS state exists from
create), the get_model_catalog command (the TS refreshModelCatalog:
registry refresh — live Prime Inference catalog fetch, disk cache at
<agent-dir>/prime-inference-models-cache.json, bundled fallback — then
the full catalog minus unauthorized private models plus the configured
providers; the worker create path fires the same refresh at startup), the
agent-roster arms
(roster_subscribe/roster_unsubscribe with the full snapshot, live
roster_update pushes keyed by the TS roster agentId = session id, and
authenticated worker_roster_delta self-reports so live status reaches
subscribers without polling) plus the live-roster ledger seed (TS
seedRosterLedger, supervisor_roster.rs: passivated ledger descendants
of resident workers seed into the live roster - roots are the resident
session files, descent is membership at any parent-walk step, and
present rows by agent id or session file are never overwritten - at
subscribe, spawn-admission, and worker-stop moments, so subscribers see
the full family, not just resident rows), cloud sandbox attach, session
leases (core/session-lease.ts port). Daemon-owned RLM spawn ledger (rlm_ledger.rs):
one append-only JSONL per sessions dir (spawn/rename/delete admissions with
the TS rlm-ledger.ts record grammar, bounds, stat-guarded replay, and
legacy-registry seeding) plus the per-child display files; family topology
is read from the ledger, never re-derived from session files. Passive-RLM
roster walk (rlm_roster.rs): list --all and the saved-session catalog
merge non-resident ledger children (walk roots = saved + resident session
files), so passivated children stay roster-visible (TS
walkPassiveRlmSubagents / withPassiveRlmDescendantInfos). The saved
session scan (session_store.rs) folds each file once into the durable
catalog row, including the agents-view search corpus the TS scan builds:
the capped allMessagesText transcript text (64 KiB). Archived sessions live in
<agent-dir>/sessions-archive and never reach the catalog scan, so search
covers live sessions only.
Supervisor-backed RLM child sessions
(rlm_children.rs, the daemon side of the pa-core RlmSubagentHost seam):
rlm.spawn/rlm.create_session create real daemon sessions through the
worker's supervisor link - one supervised worker process per child - and the
parent-side registry serves rlm.list_subagents/rlm.collect/
rlm.delete_subagent with TS-parity selector errors; child model resolution
and thinking-level validation live in rlm_child_model.rs; the create
command carries the RLM recursion identity (rlmDepth/rlmMaxDepth/
parentSessionPath/thinking) so respawned children keep their depth. Per-session model binding: the
create-config provider/model/apiKey are authoritative for worker model
resolution (explicit CLI flags reach the worker; env remains the no-flag
fallback). Worker session files carry the TS creation prefix
(model_change/thinking_level_change/service_tier_change), and queue
snapshots persist to the worker recovery journal, not the session file.
Queue-lane command surface (queue_commands.rs): the full TS
DAEMON_COMMAND_TYPES accept list with exhaustive router tables in
protocol.rs, plus the worker's mutate_queued_message/resume_queue arms
(AgentSession.mutateQueuedMessage/resumeQueuedWork: preview-addressed
delete/move/replace over the two lanes with the TS status vocabulary, and
the empty-queue resume refusal). The sessionActions queue projection
(SessionActionSnapshot) carries one Rust-native typed rider the TS wire
has no counterpart for: rlmChildStatus, the parked RLM child status
notices' lane indices, derived per item from the injected custom row (the
rlm_child_terminal_notice/rlm_child_failure kinds) at projection time —
so journal recovery re-derives it, the lane strings stay the TS
queuedAgentMessagePreview projection verbatim, and the condensed queue
strip folds exactly these rows (a user-typed lookalike never flags); the
rider serializes only when a notice is parked. The reserved kinds are
daemon provenance, never client data (child_status_notices.rs): the
prompt/steer/follow-up parse and the restore_actions validation answer a
caller-supplied custom row claiming one loudly — it never parks, never
folds — and the daemon's own notice injection rides the follow-up route
with a one-shot capability minted in the worker process (the wire's
optional rlmNoticeNonce), so the queue's classification only ever sees
daemon-authentic rows.
Prompt attachments: the prompt/steer/follow_up wire images array
(base64 payload + mime type) rides the queue item into the session engine
as multimodal user content (images on a queued prompt do not survive a
worker respawn - the recovery journal keeps the text lanes only, the TS
command-recovery shape).
Eager turn abort (TS requestAbort's closing agent.abort()): every
worker abort surface — abort/abort_and_clear_queue, the compaction and
branch-navigation interrupt-and-settle waits, shutdown, kill, and the
cancel_prompt_admission cancel-owned arm — funnels through
SessionEngine::abort_in_flight_turn (the engine's build-time agent
mirror), so an abort landing mid-provider-wait cancels the in-flight fetch
immediately and the aborted turn settles on its zero-usage aborted message
(the usage-accounting parity of the #238 adjacent gap 3 fix; the transport
half is pa-core's provider-adapter cancellation token).
Queued-input suspension (TS _sessionInputPumpSuspended, the #227/#233
ruling): abort/abort_and_clear_queue/manual
compact suspend queued-input admission indefinitely; while suspended a
plain prompt/prompt_and_wait is rejected with the TS admission error
and the lanes park. Resume sites: a prompt carrying streamingBehavior,
steer/follow_up, resume_queue (even on the empty queue), an applied
mutate_queued_message, a cron/heartbeat fire, and a successful compact
with an active goal. Agent-message delivery is rejected while suspended
and idle (TS acceptAgentMessagePrompt runs with resumeIfIdle: false).
The compact-with-active-goal resume is also the TS post-compaction
continue (agent-session.ts compact()'s didCompact branch): with no
queued work parked, the worker mints the owed goal continuation through
the engine (SessionEngine::mint_post_compaction_goal_continuation, the
_maybeResumeGoalContinuationAfterRlmWork mint — the follow-up lane item
with the durable goal-context row), emits the mint's goal_update, then
resumes: the parked continuation crosses the suspension gate through the
resume site and the turn runner drives it (the _schedulePostCompactionContinue
schedule). The compact-trigger auto-refine defers behind that continuation
(TS _compactAutoRefinePending), servicing at its turn boundary.
Thread-goal durability (TS one-store parity): every goal_update
announcement mirrors the new state as a thread_goal_state custom row in
the worker session file (the turn emit closure; the compact mint persists
its row at the mint site, outside a turn), and the engine's session-build
adoption rehydrates the fresh goal driver from the moved branch's or the
session file's latest valid thread_goal_state entry
(goal_state_persist.rs) — status, objective, usage counters, and
continuation counts continue across a worker-recovery rebuild, and the
seeded published baseline keeps the rehydrated state from announcing
itself (TS loads at construction without emitting). Recovery branch
rebuild (TS one-store parity): the owned-session worker respawns with
--resume <sessionFile> (createRpcRecoveryArgs), so the rebuilt
session's branch is the durable history — while the Rust worker owns the
file writes and the engine keeps an in-memory manager, the engine's
session-build adoption rebuilds the branch from the worker session file
(the pending_branch seam's recovery twin), so a post-recovery compaction
walk (and the live loop context) reads the full durable history instead
of a fresh engine's empty branch skipping "Session is too short to
compact". Platform wall
(platform): per-OS endpoint naming and socket identity; the transport itself
is the shared trait in pa_types::platform::transport, and the private-frame
codec plus command planes are the shared wire contract in
pa_types::daemon (clients in pa-tui/pa-cli speak them).
Live token-stream rendering
(streaming): turn events forward from the hosted engine to the worker's
emit path as they arrive (one message_update per provider delta, the
full partial message per frame - never buffered until turn settle); the
worker coalesces them for broadcast in a single-slot coalescer with a
50ms flusher, while message_start/message_end/tool frames flush the
parked update first and go out directly, so wire order and event-sequence
order match uncoalesced streaming. The supervisor stays payload-free:
deltas ride the worker -> client session-event stream (direct-attach or
supervisor-routed).
Autonomous
continuation driving in the worker's engine: per-message usage accounting
runs in the agent-loop subscription, and the in-run continuation hook
(autonomous_continuation.rs) consults the pa-core AutonomousDriver
policy at the agent loop's natural turn end (product default: shell
quality gates in the session cwd; deterministic drivers injectable for
eval harnesses) — continuations mint as durable user rows churned
inside the one prompt wait (the TS getContinuationMessages shape), a
threshold compaction holds its minted continuation for the worker's
queued followUp admission, and gate pass/fail or limit stops surface no
row (the headless status request and the print exit contract carry
them).
Live MCP product-path verifier
(tests/mcp_product_path_e2e.rs): a settings-declared stdio MCP server
round-trips through a real worker session and kernel - the kernel cell's
mcp.list_tools/mcp.call_tool resolve mcp.config through the
session's host handlers, spawn the fixture
(tests/fixtures/mcp_echo_server.py), and echo back.
Update-prepare transaction (update_prepare.rs): the supervisor-side FSM
Draining -> Fenced -> Snapshotted -> Prepared -> Stopping with the
mutation-drain latch + admission gate (mutating commands refused with
"Daemon is preparing an update restart" while active; reads/attach and the
TS abort-family drain commands stay served), the hard prepare deadline
(90 s default) and the durable 45 s marker self-expiry as watchdogs — both
re-checked on a timer and on any later command — idempotent
prepare_update_restart on updateId (a different id is a typed refusal
the coordinator maps to Join), and Aborted -> Serving with prepared-dir
cleanup as the failure default. The prepare RPC drives accept
through the mutation drain to Fenced, snapshots every resident worker
(update_snapshot, worker.rs), assembles the roster (update_roster.rs:
sessions/workers/subagents from the ledger + displays, heartbeats as a
read-only scheduled-jobs.json projection), writes
prepared/<id>/{roster,marker}.json durably, and acks Prepared — all
inside the prepare budget. commit_update_restart consumes the
transaction (update_stop.rs): the acked worker shutdown (its handler is
the flush barrier) with the 30s + 30s budgets, abandon-on-refusal (the
supervisor resumes Serving, refused sessions untouched, stopped workers
relaunched — never a kill), or the update exit when all workers stopped
(descriptors survive for the new supervisor's create-or-adopt restore).
The new supervisor's boot owns the update's restore side
(update_restore.rs, spec §6): it consumes PRIME_AGENT_UPDATE_ROSTER
from its spawn env before the unconditional scratch-dir sweep (this
socket's update-restarts/ subtree plus the TS-era legacy names — no
liveness checks, no exceptions), then restores the roster rows bottom-up
(create-or-adopt; per-session failure records, restore never fails the
boot), re-arms scheduled work (a boot scan of scheduled-jobs.json;
due active jobs of sessionless files are woken once, never archived), and
reports the pass over update_restore_status while hello.update_resume
carries the settle state; client attaches to a not-yet-restored roster row
queue behind the pass instead of failing. The heartbeat-catalog change
broadcast owns its surface here too (the worker's cron-store
on_heartbeat_change listener emits a heartbeats_changed outbound frame
— TS daemon-mode's broadcastGlobal — and the supervisor re-broadcasts it
to every client, so any heartbeat mutation anywhere in the daemon reaches
attached UIs daemon-wide).
Non-goals #
No agent behavior inside workers beyond hosting a pa-core engine; no UI.
Public API #
Supervisor entrypoint, worker entrypoint, mcp_login::{WorkerMcpLoginUi, wire_worker_mcp_login} (the worker's browser+callback login behind mcp.begin_login; wired by the agent engine before sessions register host handlers), client connection API for pa-tui/pa-cli, acp::{run_acp_mode, AcpOptions} (pa-cli dispatches --mode acp through it), agent_messaging::LinkAgentMessageController + rlm_children::{SupervisorChildSessions, ParentIdentity, RlmChildIdentity} (e2e verifiers construct the worker-side family controller and the children registry; the engine wires the same types), agent_engine::AgentSessionEngine::dispose_kernel (the session-end kernel teardown the worker invokes at kill/shutdown/orphan exit — the engine outlives the session, so the pa-core engine-drop teardown cannot run there), the worker's get_mcp_connections command (mcp_connections.rs: the /mcp view's roster from the session's MCP manager overlaid with the session kernel's per-server tool listing, plus the api-key credential rows the same response serves — Rust-native session-plane extension; the TS daemon has no counterpart). Supervision internals pub(crate).
Depends on #
pa-types, pa-core (one-way).
Experimental editable live context (CLM) #
Opt-in worker hosting delegates context authority to pa-core. The existing
get_session_context request accepts includeModelContext: true to fetch
modelContext on demand. Default/off responses stay unchanged. Attach snapshots
carry optional modelContextStatus; clm_context_update events carry status
only, never the document. Historical navigation/fork and side questions reject
CLM before source mutation or provider calls. Current worker resume uses the
core current-session restore facade, not the historical rebuild facade.
No new dependencies or public model tools; worker-engine accessors default off.