pa-daemon #
Session supervision and wire serving.
Scope #
ACP stdio transport (acp): the JSON-RPC serve surface for Agent
Client Protocol clients, served over a daemon session (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 worker turn loop owns compaction
arms and goal continuation.
Explicit model refresh (supervisor/model_refresh.rs): a coalesced, bounded
ready-resident leader sweep with local snapshot fan-out; never launches saved
or disconnected workers. model_refresh_worker.rs owns the session commands;
model selection stays pinned and metadata adoption waits for a turn boundary.
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
execution locks (lease.rs): OS file locks exclude duplicate resident workers,
separate from the session log's per-append lock. Holder metadata is courtesy
information only; process exit releases ownership without stale-owner recovery.
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; while the hold stands over parked
work 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. A suspension with nothing parked (both lanes empty, no
compaction window, worker idle) protects nothing and is released
automatically at the admission checks, the turn boundary, and every
compaction outcome, so an idle session is never left deaf to later input
(issue #33). Agent-message delivery follows that same rule: it releases a
stale gate, and under a hold that has parked work it parks behind the hold
and answers queued with queuedReason: "input_admission_suspended" rather
than the admission error (divergence from TS acceptAgentMessagePrompt's
resumeIfIdle: false, operator ruling 2026-10-29). A family broadcast runs
the same per-member send_agent_message path, so it inherits these semantics
exactly; per-member failures stay receipt-level entries.
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).
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.
For historical TypeScript update manifests, the restore pass recreates parents
before their children, remaps active parent IDs, and restores pending context
and action queues explicitly because those manifests predate Rust's journals.
Attaches remain gated until that replay completes.
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 #
AgentSessionEngine::set_plugin_job_sink wires the host observer onto current and
future sessions. The worker persists terminal generic plugin-job outcomes and
publishes them through the existing custom-message event transport, fenced by
the durable session identity. Successful jobs may supply result.message with
customType (nonempty string), content (string), display (boolean), and optional
object details. The host publishes that existing custom-message payload without
plugin-specific rendering rules. Absent projections use the generic job outcome;
malformed projections keep the committed result and show a generic outcome with
an explicit projection diagnostic. Failed jobs always show the generic error.
Failed outcome persistence is visible without re-running the completed job.
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::daemon::{run_daemon_attached_acp_mode, DaemonAcpOptions} (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).
The composition root initializes the packaged product version with
protocol::configure_app_version before entering daemon or worker mode.
protocol::app_version supplies that identity to both hello frames and update
checkpoints; library embedders fall back to the compiled version. Release
manifest restamping therefore remains consistent with CLI --version.