docs(adr): Agent Lifecycle State Machine - #1
Merged
Conversation
Studio's first ADR: a runtime-independent 5-state machine (Starting / Running / Unhealthy / Stopping / Stopped) for classifying an agent instance, with config (identity + version + state) as the through-line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fold the 8-axis refute-pass findings (Mira/Jellyfish/Falcon): - Paused promoted to a 6th state, defined by (desiredStatus,accepting_work, health); restores single-field dispatch predicate state==Running. - Unhealthy is fault-only; version-skew becomes a Running 'superseded' attribute (drives drain->replace), not Unhealthy. - reclaim split into planned (compressed Stopping) vs hard-loss (->Stopped); diagram gains Starting->Stopped, Unhealthy->Stopped, Paused edges. - identity: name trust root (runtime injection), per-instance credential at Starting, CP-signed instance-bound lease + monotonic fencing epoch, revocation + re-prove on recovery. - projection: desiredStatus discriminator (ECS STOPPED / k8s deletionTimestamp / compose stop-requested), ACTIVATING->Starting, DEACTIVATING conditional, compose requires healthcheck + restart:no, k8s Unknown->Unhealthy. - config model: Instance = Desired Spec (identity+version) + Observed State (state is observed, not desired config). - prior art: add k8s/Nomad/Temporal/OTP/EC2/systemd/Ray rows; death cause enum. - restructure to MADR + Y-statement; add docs/review-runbook.md (8-axis rubric). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final 8-axis re-review fixes (Jellyfish F1/F2 + Falcon MCP + Mira): - discriminator 3->4 axes: add latching identity_verified (separates Starting from Unhealthy, whose 3-axis tuples collided). CP-observable per runtime. - accepting_work authority pinned to CP/director, never agent self-report. - superseded => cordon to Paused (accepting_work=false) -> Stopping/replace, so a superseded agent is never left dispatchable in Running. - Principle 6 in-flight set = Running U Paused U Stopping(within deadline). - compose: docker pause (SIGSTOP) -> healthcheck stall -> Unhealthy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Jellyfish R1 + Mira (ECS): the v3 'superseded => Paused -> Stopping/replace' sentence prescribed fleet-level rollout ordering (make-before-break, cordon sequencing = deployment controller's job), which is this instance-level ADR's declared non-goal. Keep the instance-level fact (superseded => accepting_work =false => Paused, never dispatchable) and defer drain/replace ordering to a future rollout / RuntimeDriver ADR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brettchien
added a commit
that referenced
this pull request
Aug 14, 2026
…relay, clearer errors (review #1–4) Addresses the four findings from the Part B(1) backend review: 1〔med〕 In-flight guard for `session/prompt`. A second prompt while a turn was pending unconditionally overwrote `pending_prompt`, orphaning the first turn's result id so its `turn_end` never fired and the panel spinner hung. The single-shot loop now rejects (and logs) a prompt while one is in flight instead of clobbering the pending id. Part C's turn management still gates this, but the backend no longer depends on the UI for correctness. 2〔low〕 Non-blocking sidecar relay. `handle_inner` (`tools/list`/`tools/call`) awaited inline in the select! read arm, stalling queued prompts/cancels and chat chunks behind a slow round-trip. The relay now runs in a spawned task and returns its reply through a new `reply_tx` channel drained by the loop's single writer — responsiveness restored, wire frames still serialized. Each reply carries its own id, so out-of-order completion is fine for MCP. 3〔low〕 Clearer error when the outbound channel's receiver is gone. A send into a torn-down connection reported a bare "connection closed"; it now matches the not-connected message so the teardown window reads correctly. 4〔info〕 out_rx/reply_rx arms use Some(..) patterns, disabling the arm on a closed channel so a None can never busy-spin the loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brettchien
added a commit
that referenced
this pull request
Aug 14, 2026
…p socket (#48) * feat(chat): Part B(1) backend — session/prompt pipe over the live /acp socket First implementation slice of ADR agent-chat-panel (Part C). Adds the bidirectional chat pipe on the existing reverse-MCP session; no UI yet (Part C) and no cross-reconnect session/resume yet (Part B2). acp-tunnel: - Session::prompt(text) — builds session/prompt {sessionId, prompt:[text]} - Session::cancel() — the one-way session/cancel notification - Inbound::AgentChunk classified from session/update/agent_message_chunk (other update kinds — thoughts, tool_call — openab doesn't emit yet) - unit tests for all three (23 pass) remote.rs: - RemoteState grows an mpsc write-handle (prompt_tx), published only once the session is active, retracted by run_reconnecting/disconnect - run_once's read loop becomes a tokio::select! over inbound frames AND outbound OutMsg (Prompt/Cancel); write never leaves the task - prompt results are id-correlated (pending_prompt), not phase-driven — a method-less frame in SessionActive ends the turn only if its id matches the in-flight prompt; emits agent-update {kind:"turn_end"} - agent_message_chunk → emit agent-update {kind:"chunk", text} lib.rs: - agent_prompt / agent_cancel commands (delegate to Remote), registered Verified: acp-tunnel unit tests. studio-desktop compilation left to CI (separate GUI workspace). Follow-ups: B2 session/resume + persistence, then Part C the panel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(remote): backend chat robustness — in-flight guard, non-blocking relay, clearer errors (review #1–4) Addresses the four findings from the Part B(1) backend review: 1〔med〕 In-flight guard for `session/prompt`. A second prompt while a turn was pending unconditionally overwrote `pending_prompt`, orphaning the first turn's result id so its `turn_end` never fired and the panel spinner hung. The single-shot loop now rejects (and logs) a prompt while one is in flight instead of clobbering the pending id. Part C's turn management still gates this, but the backend no longer depends on the UI for correctness. 2〔low〕 Non-blocking sidecar relay. `handle_inner` (`tools/list`/`tools/call`) awaited inline in the select! read arm, stalling queued prompts/cancels and chat chunks behind a slow round-trip. The relay now runs in a spawned task and returns its reply through a new `reply_tx` channel drained by the loop's single writer — responsiveness restored, wire frames still serialized. Each reply carries its own id, so out-of-order completion is fine for MCP. 3〔low〕 Clearer error when the outbound channel's receiver is gone. A send into a torn-down connection reported a bare "connection closed"; it now matches the not-connected message so the teardown window reads correctly. 4〔info〕 out_rx/reply_rx arms use Some(..) patterns, disabling the arm on a closed channel so a None can never busy-spin the loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What problem does this solve?
Studio needs a single, runtime-independent way to say "what state is this
agent in" — the vocabulary the director's console displays and the control
plane observes. Today state is expressed differently per runtime (ECS task
states, k8s pod phases, compose status) with no glanceable, agreed set.
This is Studio's first ADR: a canonical 5-state agent lifecycle.
What's in this ADR
docs/adr/agent-lifecycle.mddefines 5 states — Starting / Running /Unhealthy / Stopping / Stopped (mutually exclusive, exhaustive) — with:
native → 5-stateprojection table for ECS / k8s / GKE / docker-compose;Runningdoes work; onlyStoppedis terminal;identity is default-deny and control-plane-issued;
reclaim(spot/preemption)can jump from any live state straight to
Stopped.Prior Art
machine; HMAC-signed lifecycle events. Identity =
HERMES_HOMEpath +process-name matching — the weak self-report we reject.
pi-agent-core):idle ⇄ turnphases withflushPendingWritesbetween turns — validates "checkpoint while Running." One layer below instance
lifecycle.
pi --mode rpcCLI; clean shell/runtimesplit — validates a thin director front-end over a core.
Alternatives Considered
with runtime state; no health/identity distinction.
Unhealthy— loses the "alive but fenced-off" distinction.Validation
Docs-only ADR. Mermaid renders on GitHub. No code changes.
Review Contract
as the vocabulary Studio displays and the control plane observes.
RuntimeDriververb contract (separate ADR); fleet-levellifecycle; any implementation.
prior art documented.
RuntimeDrivercontract ADR; fleet lifecycle ADR.