0103 - Agent session resource and producer-fed streams
AgentSession is the second resource kind: a provider, an opaque native id, a derived state, and a Terminal parent.
Full source summary
AgentSession is the second resource kind: a provider, an opaque native id, a derived state, and a Terminal parent. Its output stream is producer-fed: the harness shim appends AgentEventsJsonlV1 records through APPEND_RESOURCE_OUTPUT, and the server stamps sequence and time, retains a bounded ring, replays it as the bootstrap, and derives working, blocked, and done from record types. Screen scraping and hook reports become fallbacks below the stream.
Status: Accepted Date: 2026-09-09
Context
The server learns what an agent is doing by reading its screen
(ADR-0046) and, since
ADR-0085, by accepting one state word
per hook. Both recover from the outside facts the harness states plainly: a
prompt was submitted, a tool started, a permission is pending, the turn ended.
The detector is some 6,500 lines of manifests, fixtures, and hysteresis that
guess those facts back, and a restyled prompt box silently breaks it. Neither
path leaves a log: a late observer gets the current phux.agent/v1 record
(ADR-0040) and nothing before it.
ADR-0102 makes the server serve kinds with an ordered opaque stream each. An agent session is the obvious second kind, with one difference: no process writes its stream. Something has to append.
Decision
- The facet.
AgentSessioncarriesprovider: String(for exampleclaude),native_id: Option<String>(an opaque provider session id under ADR-0068’s native-id bounds), and a derivedstate. It always names a Terminal parent (ADR-0104). InSPAWN_RESOURCEit iskind = 1with field 12parent, field 13provider, and field 14native_id; the decoder requires 12 and 13 and forbids the Terminal-only fields 3 to 6 and 10.SpawnErrorgainsUnsupportedKind,ParentNotFound, andParentKindMismatch. - The codec is
AgentEventsJsonlV1, a newBootstrapCodectag (0 and 2 are never reused). One UTF-8 JSON object per line, at most 16 KiB:{"seq":u64,"ts_ms":u64,"type":<str>,"data":{...}}.seqandts_msare assigned by the server on append; a producer-supplied value is ignored. The v1typeset is closed:session_start,prompt,tool_start,tool_end,notification,ask,stop,session_end,state,provider_raw. An unknowntypeisRECORD_INVALID. - The stream is producer-fed. New command
APPEND_RESOURCE_OUTPUT { resource_id, bytes }, tag0x1a, at most 64 KiB per call, answersCOMMAND_RESULTOk or Error withWRONG_RESOURCE_KIND,NOT_PRODUCER,RECORD_INVALID, orOVERFLOW. A Terminal refuses it. The producer holds the ADR-0098Inputverb on the resource (every owner-socket client does).OVERFLOWis a call over its bound or a full outbound queue; retention eviction is never an error.ServerFeature::RESOURCE_KINDS = 0x4000advertises kind and command. - Retention and replay. The server keeps a bounded record ring per
session, ceiling
defaults.agent-log-bytes, default 4 MiB, in the ADR-0094 pattern. AnAgentSessionstream always selects the raw profile with this codec:BOOTSTRAP_BEGIN.codecnames it, chunks carry the retained records, liveRESOURCE_OUTPUT.bytescarries whole records,FRAME_ACKis forbidden. - The server derives state from the stream.
promptandtool_startmean working;ask, andnotificationwhose kind ispermissionorelicitation, mean blocked;stopmeans done;session_endretracts. The evidence enters the existing arbiter at a new rank,Stream, aboveHook; precedence is Stream, Hook, Process, Screen. While a live child exists, screen derivation runs only for idle and departure;idlestays detector-owned, as ADR-0085 decided. REPORT_AGENT_STATEstays as the fallback. With a live child its handler appends a synthesizedstaterecord; otherwise ADR-0085 applies.- The Claude shim is the first producer. Every
--phux-hookarm reads the hook’s stdin JSON.SessionStartrunsphux agent session open @$PHUX_TERMINAL_ID --provider claude --native-id <session_id>and emitssession_start;UserPromptSubmitemitsprompt;PreToolUseandPostToolUse(new registrations) emittool_startandtool_endwithtool_nameonly, nevertool_input;PermissionRequestandNotificationemitaskandnotificationand still runphux ask;Stopemitsstop;SessionEndemitssession_endand closes the session. Apromptcarries its length only;provider_rawneedsPHUX_AGENT_EMIT_RAW=1. WithoutRESOURCE_KINDSon the server the shim keeps itsreport-stateandagent set/clearcalls. - Consumers.
phux agent session open|close,phux agent emit TARGET --type T [--data JSON|-], andphux agent log TARGET [--follow] [--json] [--tail N];agent showandagent listaddagent_session; MCP gainsphux_agent_logandphux_agent_emit.%name(ADR-0075) resolvesAgentSessionresources, which gives that ADR its production caller.
This amends ADR-0040 (when a session exists, phux.agent/v1 is a projection
of its stream), ADR-0046 and ADR-0085 (the detector is a fallback and the hook
path is a producer). It does not touch
ADR-0092: the stream is live
and bounded, not durable evidence.
Why
Recover nothing the harness states. Every state the detector derives from pixels is a fact the harness emits at a known hook. Reading the hook’s own JSON costs a few lines per arm; recovering it from the screen cost a subsystem that fails silently when a vendor restyles. The detector remains for agents no shim covers, and idle, which no hook can prove, stays with it.
A stream, not a state word. ADR-0085 sends one word per edge and a late
observer learns nothing. An ordered, replayable log gives phux agent log an
event transcript, agent wait a sequence to resume from, and the TUI a state
with evidence behind it. The ADR-0070 bootstrap shape already fits: cut at a
sequence, replay, go live.
Opaque records under a codec name, not a frame family. ADR-0030’s rule is
that the wire carries bytes under a negotiated codec and consumers project.
JSONL records with a closed type set are that: the server parses them only
to validate and derive state, and a new type is a codec revision, not a
frame. The server owns seq and ts_ms because producers are short-lived
hook processes that can race; server assignment keeps the log ordered.
Privacy defaults closed. Prompt text and tool input are the user’s work, not lifecycle; the stream carries lengths and names by default.
Tradeoffs
- The Cockpit projection lags the stream. Cockpit does not yet show an AgentSession row under its parent Terminal; tracked as phux-am9y.25.
- The server parses one codec it serves to validate and derive state, an opacity exception confined to this codec as ADR-0046 confined its to a key.
- The stream is only as good as its producer. A harness with no shim gets the detector only; a shim that dies mid-turn leaves the last derived state until screen evidence for idle or departure corrects it.
agent logis a bounded window, not a transcript; new types need a v2 codec. Both are deliberate.
Alternatives
Keep screen scraping primary. Rejected: 6,500 lines to recover, unreliably, what the harness states; kept as the fallback.
Make the agent session the coordinator’s WorkSession. Rejected: that is
durable identity with its own endpoint (ADR-0092); this is a live L1 stream.
A typed frame family for agent events. Rejected on ADR-0030’s principle: structured taxonomies on the wire drift and tax every consumer.
Record full transcripts. Rejected for v1 on privacy and size;
provider_raw is the opt-in.