This document records the current runtime design that Hive actually implements. It is intentionally narrower than a full architecture spec. The goal is to pin down the meanings, sources, and intended uses of the runtime fields and active-turn fork routing that already exist in code.
This document covers:
busyinputStateturnPhase- root-message summary/artifact protocol
- active-turn fork routing
This document does not define:
- a semantic global
busy/idletruth model - automatic scheduling
- automatic fork/spawn decisions
- automatic garbage collection
For the raw Claude transcript structures that feed these runtime decisions,
see docs/transcript-signals.md.
Hive now exposes two different runtime layers on purpose:
- Output activity layer (
busy) - Turn phase layer (
turnPhase)
They answer different questions and should not be conflated.
Field:
busy: true | false
Question answered:
- Has this pane produced tmux-visible output in the last 3 seconds, and is that output corroborated by recent transcript jsonl mtime advance?
What it is good for:
- lightweight live activity display
- knowing whether a pane is currently emitting output
What it is not:
- not a semantic "agent is definitely busy"
- not a safe-to-interrupt truth value
Field:
turnPhase: <token>
Question answered:
- What phase of a turn does the receiver's transcript tail currently show?
What it is good for:
- deciding whether to fork the target or direct-send
- explaining why Hive treated that target as it did
What it is not:
- not the same thing as pane output activity
- not the same thing as a universal busy/idle truth model
Source — busy=true when either of two branches holds:
- Output branch — tmux control-mode output stream
(
tmux.ControlModeOutputMonitor) reported visible output within the last3s, AND the agent transcript jsonl mtime advanced within the same window. The mtime check is a phantom-redraw gate that suppresses TUI frame-redraw spikes (Ink / ratatui re-printing on-screen characters during idle). - Active-turn branch — transcript
turnPhase∈ :data:activity.ACTIVE_TURN_PHASES(tool_open/tool_result_pending_reply/user_prompt_pending/input_backlog). This branch catches the streaming-gap case where tmux visible-text payloads space out beyond3smid-tool, and it bypasses the output branch's gates: an agent in mid-turn is busy regardless of monitor activity or transcript mtime.
Combined into sidecar._pane_is_truly_busy.
Native-daemon override: a daemon-backed (born-connected) codex or grok pane
reports busy from its own per-pane daemon instead — both branches above are
still computed but then replaced for that pane. See "Codex Native Runtime
(app-server source)" and "Grok Native Runtime (leader source)".
Fail-open: if the transcript path can't be resolved (non-agent pane, no session yet, stat error), the output branch returns true on monitor activity alone — idle-notify must never silently disappear for panes the gate can't introspect.
Notes:
- the active-turn branch is what makes idle-notify safe under streaming
agents (Claude/Codex tool loops): the public
busyfield tracks "agent in mid-turn", not just "tmux output in the last 3s" - known limitation: a CLI that emits visible output for longer than the
threshold without writing the transcript jsonl AND whose
turnPhasethe probe can't recognise can be gated as a false negative; the threshold is intentionally conservative
Source — live process evidence on the pane's TTY only: the pane's current
command and its TTY process table, parsed by the shared CLI matchers
(agent_cli.detect_cli_process_for_pane). Never the pane title, the
@hive-cli tag, a surviving codex app-server or grok leader daemon, or
transcript/session metadata — all of those outlive the CLI process. Probe
failures fail closed to false.
Meaning — the member's CLI process is actually running. Spawned launches do
not exec over the pane shell, so the pane (and alive) survives its CLI
exiting; the retained shell is not an agent runtime. The three states:
| state | alive |
cliAlive |
inputState |
inputReason |
busy |
|---|---|---|---|---|---|
| pane dead | false | false | offline | pane_dead | false |
| retained shell (CLI exited) | true | false | offline | cli_exited | false |
| live CLI | true | true | per runtime | per runtime | per runtime |
Consumers — delivery refuses a retained shell before any native transport (the send event stays durable on the bus); idle notify, session-snapshot capture, and duo pairing all skip retained shells.
Source:
- transcript gate inspection via
check_input_gate() - codex app-server
status.activeFlagsfor a daemon-backed codex pane (overrides the transcript gate for that pane — see "Codex Native Runtime") - grok leader
session/request_permissionfor a daemon-backed grok pane (see "Grok Native Runtime")
Current values:
readywaiting_userunknownoffline
Meaning:
- whether the agent is currently waiting for a user answer
Important consumer:
- the send gate (
hive sendrefuses while the target iswaiting_user)
Source:
- transcript probe for claude (last observed transcript state)
- codex app-server thread status for a daemon-backed codex pane — codex has no transcript probe (see "Codex Native Runtime")
- grok leader notifications for a daemon-backed grok pane — grok has no transcript probe either (see "Grok Native Runtime")
Current values:
tool_openturn_closedinput_backlogtool_result_pending_replyuser_prompt_pendingassistant_text_idleunknown_evidence
Meaning:
- the phase the receiver's turn is in, as seen in the transcript tail
- consumers pick the subsets they care about (see "Consumer Subsets" below)
These are related, but they are not the same concept.
hard busy is a reasoning concept, not a public field. It means:
- a tool/task open event has happened
- the corresponding close event has not happened yet
Example:
- Claude:
tool_usewithout matchingtool_result
In turnPhase terms, hard busy surfaces as tool_open. input_backlog is a
strategy-level non-open state that also matters to consumers but is not hard
busy.
Hard busy is not currently surfaced as its own public runtime field.
Each row maps a transcript/JCL observation to the emitted turnPhase value.
tool_open—tool_useopeninput_backlog— unresolved queue backlog is the newest decisive evidenceturn_closed—turn_durationorstop_hook_summarywithpreventedContinuation=falsetool_result_pending_reply— tool result arrived but assistant has not clearly continueduser_prompt_pending— real user prompt pendingassistant_text_idle— assistant text without stronger closing/opening evidence
Codex has no transcript/JCL probe. A daemon-backed pane reports natively (see
"Codex Native Runtime" below); an embedded (daemon-less) codex is unsupported
and reads as unknown_evidence.
Grok has no transcript/JCL probe either. A daemon-backed pane reports natively
(see "Grok Native Runtime" below); a grok hive never spawned has no leader
socket and no session record, and reads as unknown_evidence.
A born-connected codex pane — hive-spawned, or launched through hcodex (the
hive shell-init launcher) — runs a per-pane codex app-server
daemon. Hive connects as a second client over that pane's unix socket and reads
busy / inputState / turnPhase natively from the daemon's
notification stream, instead of reverse-engineering them from the transcript.
The emitted payload is tagged _runtimeSource: codex_app_server.
This path is taken only when a live per-pane daemon answers. An embedded
(manually launched, non-daemon) codex has no socket and is deliberately
unsupported as a Hive team member: hive init / hive duo reject it at
team entry. A team-bound hive fork / hive handoff --fork of a codex pane
launches the clone through hive codex fork <sid>, which binds the clone's own
per-pane daemon, so it joins daemon-backed. A standalone embedded
codex still runs, but hive reads no state from it — session id stays
unresolved, turnPhase stays unknown, and there is no transcript fallback.
State is event-sourced from app-server notifications and stays valid until the
next event — there is no time-based staleness gate. The relevant notifications
are thread/status/changed, turn/started, and turn/completed.
Field mapping (notification → runtime field):
busytrue—turn/started, orthread/status/changedwithstatus.type=activefalse—turn/completed, orstatus.type=idle
turnPhasetool_open— anyactiveturn. The native path does not subdivide active phases (notool_result_pending_reply/user_prompt_pendingsplit); it trades transcript-tail granularity for an authoritative busy edge.turn_closed—idle/turn/completedunknown_evidence— before the first event;notLoaded/systemErrorleave the prior phase unchanged
inputStatewaiting_user—activewhosestatus.activeFlagscontainwaitingOnApprovalorwaitingOnUserInput; emitted withinputReason=app_server_active_flagready— any otheractive, oridle
sessionId for a daemon-backed pane resolves from app-server thread metadata
(thread.sessionId via thread/resume), with an lsof-on-daemon-pid fallback.
It stays unresolved until the thread has produced activity.
A born-connected grok pane — hive-spawned, or launched through hgrok (the
hive shell-init launcher) — runs a per-pane grok agent leader daemon. The
TUI attaches to it, and hive attaches as a second client through grok agent --leader stdio, an ACP JSON-RPC subprocess. busy / inputState / turnPhase
are folded from that client's notification stream; the emitted payload is tagged
_runtimeSource: grok-leader.
The leader keeps every session of the cwd, so which one is this pane's is not
discoverable from it: hive mints the session id at spawn time, passes it as
--session-id, and records it beside the socket. The client loads exactly that
session and ignores notifications for any other.
session/load replays the session's past updates before it answers, so
everything received before the load response is discarded — a replayed turn must
never mark the pane busy. This is why spawn asks the sidecar to connect
(connect-grok) once the pane's session exists and its grok is up, rather than
lazily on the next tick.
Field mapping (notification → runtime field):
busy—_x.ai/sessions/changedactivityis the authority:true—activity: working, or anysession/updatechunk/tool eventfalse—activity: idle, or_x.ai/session_notificationturn_completed
turnPhasetool_open—session/updatetool_calltool_result_pending_reply—tool_call_updatewithstatus: completeduser_prompt_pending— an agent/thought/user message chunk with no tool phase openinput_backlog—_x.ai/queue/changedwith non-empty entriesturn_closed—turn_completed, oractivity: idleunknown_evidence— before the first post-load notification
inputStatewaiting_user— the leader askedsession/request_permission; hive answers its copycancelled(the decision belongs to the human at the TUI, which gets its own copy) and emitsinputReason=leader_permission_requestready—turn_completed,activity: idle, or anytool_call_update(the permission it was blocked on has been decided)
Queue semantics: a prompt sent mid-turn is queued FIFO by the leader and runs
when the current turn ends — there is no steering and no bounce, the same as
typing into the TUI. Delivery is therefore accepted at the echo (a queue entry
or user_message_chunk carrying the text), not at the session/prompt
response, which only lands when the whole turn ends.
sessionId for a daemon-backed pane is the spawn-minted id read straight from
the pane's .session file — no probing, and no unresolved window while the
session warms up.
Root sends are every hive send; the command no longer accepts
--reply-to. Continuing an existing thread is done via hive reply
(which always carries a replyTo and is therefore not subject to the
root protocol).
Hive enforces a two-layer protocol for root sends:
body: short summary onlyartifact: detailed content
Current root-body hard failures:
- body longer than
500chars - body with
3+lines - body containing fenced code
- body lines starting with markdown heading/list markers:
#-*
This rule applies to root sends. Replies are not subject to these summary-body limits.
This design was split many times during discussion, but the stable part that actually shipped is small enough to keep in one place:
- output activity
- interrupt safety
- root protocol
- active-turn fork routing
Keeping these together reduces drift between overlapping docs.