State Transitions and Operations Guide¶
This guide documents how houmao-server state values change over time and what operations are appropriate in each state. It is diagram-first: read the statecharts and sequence diagram to understand the state model visually, then consult the prose sections for operational details.
For the definition of each individual state value, see the State Reference Guide. For the internal pipeline architecture, see State Tracking.
Source of truth: Tracker-owned transition logic is implemented in
src/houmao/shared_tui_tracking/session.pytogether with the shared detector/profile contract insrc/houmao/shared_tui_tracking/detectors.pyand app-owned implementations such assrc/houmao/shared_tui_tracking/apps/codex_tui/. The live server tracker insrc/houmao/server/tui/tracking.pyfeeds that shared session and then merges the result with server-owned diagnostics and lifecycle metadata.
Diagnostics Availability¶
All five diagnostics.availability values and their transitions. The entry state is unknown. Only available enables meaningful surface and turn tracking — all other states degrade surface and turn values.
stateDiagram-v2
[*] --> unknown : initial state
unknown --> available : tmux up +<br/>TUI process running +<br/>parse success +<br/>surface present
unknown --> unavailable : tmux session<br/>not found
unknown --> tui_down : tmux up but<br/>TUI process absent
unknown --> error : probe failed or<br/>parse failed
available --> tui_down : TUI process exits<br/>while tmux stays up
available --> unavailable : tmux session<br/>disappears
available --> error : probe failure or<br/>parse failure<br/>on current cycle
tui_down --> available : TUI process restarts +<br/>parse success
tui_down --> unavailable : tmux session<br/>disappears
tui_down --> error : probe failure<br/>on current cycle
unavailable --> unknown : tmux session<br/>reappears
unavailable --> error : probe failure<br/>on current cycle
error --> available : next cycle succeeds<br/>fully
error --> unavailable : tmux gone on<br/>next cycle
error --> tui_down : tmux up but<br/>TUI gone on next cycle
error --> unknown : partial recovery<br/>on next cycle
note right of available : Gateway state —<br/>only here are surface/turn<br/>values meaningful
Turn Phase¶
All four turn.phase values and their transitions. The entry state is unknown. The blocked edges are reachable only through the opt-in hook observation channel; all other edges describe TUI-only tracking.
stateDiagram-v2
[*] --> unknown : initial state
unknown --> ready : diagnostics available +<br/>ready_posture=yes +<br/>no active evidence +<br/>no ambiguous surface
unknown --> active : anchor armed or<br/>active evidence detected
ready --> active : explicit input anchor armed<br/>or active evidence detected
ready --> unknown : diagnostics degraded<br/>(error / tui_down / unavailable)<br/>or ambiguous interactive surface
active --> ready : turn completed —<br/>success settled +<br/>ready_posture=yes<br/>OR interrupted/failure +<br/>ready surface<br/>OR host active recovery
active --> unknown : diagnostics degraded<br/>(error / tui_down / unavailable)
active --> blocked : hook permission_requested<br/>(hook channel active)
blocked --> active : hook permission_resolved<br/>OR TUI modal dismissal /<br/>User rejected line
blocked --> ready : hook terminal event
blocked --> unknown : diagnostics degraded<br/>(error / tui_down / unavailable)
note right of unknown : Not an error —<br/>means the system<br/>cannot confidently classify<br/>the current posture
Hook Observation Merge¶
When the opt-in hook observation channel is active, hook facts merge into the tracker at its existing projection layer. surface.* stays TUI-owned. The merged transitions are:
- Hook
prompt_submittedentersactivefrom any non-terminal posture, (re)arms the turn, and clears the previous terminal result. A prompt submitted while another prompt is armed enqueues behind it; terminal events consume armed prompts in arrival order. For Kimi,UserPromptSubmitarms an epoch and the followingTurnStarted(origin_kind=user)binds the integerturn_id;UserPromptQueuedrecords queue evidence without arming a second epoch. - Only hook
permission_requestedentersblocked, and it records the blocked tool name when the payload carries one. For Codex it also records the paired tool call from the precedingPreToolUse; for Kimi it records the pairedtool_call_id. Kimi emits permission events only under an ask permission policy, so never-ask sessions never enterblocked. TUI parsing never entersblocked. - For Claude,
blockedexits toactiveon hookpermission_resolvedor TUI modal-dismissal evidence, including a positiveUser rejectedline; the turn remains armed. For Codex,blockedexits on the pairedPostToolUse(allow) or onInterrupt(deny, which settles the turn interrupted); TUI evidence does not settle a Codex blocked wait. For Kimi,blockedexits at the pairedPermissionResultfor both decisions (approvedorrejected); the turn returns toactive, a rejected call produces a non-terminalPostToolUseFailure, and the turn continues to a normalStop. TUI evidence does not settle a Kimi blocked wait. - A hook terminal event (
Stop,StopFailure, or Codex or KimiInterrupt) movesactiveorblockedtoreadyimmediately, without waiting for the TUI repaint or settle window. The repaint window remains visible throughsurface.*and is reported ashook_terminal_tui_active. - Hook
activeorblockedholdsturn.phaseagainst a ready-looking TUI surface, reported ashook_active_tui_readyorhook_blocked_tui_ready. maintenance_startedmarks a maintenance window without invalidating the previous terminal outcome.maintenance_endedrestoresactivewhen a turn is still armed, otherwise the pre-maintenance posture. Maintenance posture does not close admission by itself; TUI surface facts govern admission during compaction. For Codex and Kimi, the window holds posture acrossPreCompact/PostCompact, suppresses TUI idle heuristics, and the post-compact turn starts a newturn_idepoch. Kimi emits noSessionStart(source=compact), so the pair is the whole compaction signal.- TUI stale-active and final stable-active recovery rules are unchanged and may return
activetoready. - A hook terminal event with no armed prompt is an orphan diagnostic (
hook_orphan_terminal) and never manufactures alast_turn. Duplicate terminal deliveries for the same prompt are idempotent; the first terminal result wins. - Events from an older channel generation are dropped before correlation (
hook_orphan_generation). - For Codex and Kimi, stream end without
Stop,Interrupt, orSessionEndfor an open turn, orSessionEndwith an open turn, publishes presence loss,turn.phase=unknown, andhook_terminal_gap; it never records success. A Kimi headless run that exits cleanly after a latchedStopbut withoutSessionEndis terminal-settled with the diagnosticheadless_session_end_gap.
Kimi Merge Notes¶
- Turn correlation is two-phase and ordering-based.
UserPromptSubmitarms;TurnStarted(origin_kind=user)binds the integer, session-scopedturn_id, which continues across--continueresume.turn_idis absent onUserPromptSubmit,Stop, and all tool events, so those attribute to the armed epoch by stream order. Interrupt(reason=cancelled)is authoritative forinterrupted(the shared Codex carve-out). Esc produces it within about 50 ms; the TUIInterrupted by usernotice is corroboration, not authority, and noStopfollows the aborted turn.- Permission denial is non-terminal.
PermissionResult(rejected)settlesblocked,PostToolUseFailurerecords the denied call, and the turn continues toStop. Kimi has no deny-aborts-turn semantics. - Queued or steered input surfaces as
UserPromptQueuedonly. Steered input produces no secondUserPromptSubmit, and the running turn completes once. - Subagent activity opens nested scopes (
SubagentStart/SubagentStopand nestedTurnStarted(origin_kind=system_trigger)). Subagentturn_idvalues collide with the parent's, so scope, not id, disambiguates them. SessionHeartbeatis presence and liveness evidence only, at a 60-second cadence. Theworkaxis staysunknown, and hooks alone never producesettled.SessionStartis awaited at session creation, but a fresh TUI creates its session lazily on the first message; startup event silence is judged after the first prompt. A resumed session deliversSessionStart(resume)before the first prompt.- A strict read-back violation at install time, or any collector failure, degrades the channel to
unavailable; tracking then reverts to TUI-only, exactly as the Claude lane specifies.
Last-Turn Settlement With Hooks¶
- A hook
Stopsetslast_turn.result=successand a hookStopFailuresetsknown_failurewith the provider error class inlast_turn.error_class. For Codex and Kimi,Interruptsetslast_turn.result=interrupted. Kimi'sStopFailuremapping is defensive because the campaign never observed that event. Settlement is immediate. last_turn.sourceishook_observationfor hook-settled turns, orexplicit_inputwhen a gateway submission record correlates with the same turn.- Once a hook terminal event latches a turn, the pending TUI settle for that turn is suppressed. For Claude, later TUI evidence is discarded except that TUI temporal interrupt evidence for the same turn epoch, observed outside a maintenance window, may still flip the result to
interrupted, reported ashook_success_tui_interrupted. For Codex and Kimi, the latched outcome is final for its epoch and no TUI heuristic rewrites it. Compaction repaints during the same epoch do not count as interrupt evidence. - TUI settlement rules run unchanged when no hook terminal event exists for the turn.
- Hook events settle
interruptedonly through the Codex or KimiInterruptevent; Claude has no interrupt hook and keeps the outcome TUI-derived. - If the channel goes stale after a hook terminal event latched the turn, the latched
last_turnpersists through the degraded window withwork=unknownandsettled=false. On recovery, agreement with a TUI settlement keeps the TUI source. Disagreement applies the hook result as an explicit correction (hook_correction_after_staleness), except that on Claude a TUIinterruptedsettlement keepsinterruptedand on Codex and Kimi the hook latch stands.
Work And Settled¶
work starts each channel generation as unknown. A Claude turn_completed payload replaces it: listed background tasks set pending, none set clear. A turn_failed event makes no background-work statement and leaves the axis unchanged. The Codex and Kimi adapters publish work=unknown for every sample because neither payload carries a background-task mirror. Whenever the channel is not active, work is unknown.
settled is derived, not stored independently: turn.phase=ready and work=clear. It is false for a degraded channel. work=pending never blocks admission in this release.
Channel Health¶
| State | Effect on tracked state |
|---|---|
active |
Hook facts have lifecycle authority; work and settled are published from hook evidence. |
unavailable |
Startup state before any event; after the 15-second startup grace with no event, tracking is TUI-only. |
stale |
No event for more than 30 seconds while the TUI keeps changing; TUI regains lifecycle authority for new evidence, including new settlements. A hook-latched last_turn persists. |
disabled_by_policy |
Terminal policy state from channel metadata; TUI-only. |
untrusted_workspace |
Terminal policy state from channel metadata; TUI-only. |
error |
Event store read failure; a healthy read restores active or unavailable. |
stopped |
Collector or session stopped; terminal. A same-generation collector.json record with status=stopped moves the channel to stopped on the next poll cycle. |
Every state other than active is degraded: the channel contributes no new lifecycle authority, admission is byte-equivalent to TUI-only behavior, work is unknown, and settled is false. A hook-latched last_turn is the one persisted hook lifecycle fact during a stale window.
Codex dispatches SessionStart lazily at the first turn start, so an idle session can remain unavailable past the 15-second startup grace until its first prompt; the first event still moves the channel to active. A provider upgrade that changes the trust hash algorithm makes Codex skip the untrusted handlers silently, which the channel observes as startup event silence with a live collector and reports as unavailable (degraded). A channel that was already active and then loses events follows the ordinary staleness rule to stale.
Kimi awaits SessionStart at session creation, but a fresh TUI session is created lazily on the first message, so the startup grace is judged after the first prompt; a resumed session delivers SessionStart(resume) at launch. If the installer's strict read-back rejects the merged [[hooks]] file, the channel starts as unavailable rather than launching a channel the provider would silently void. Kimi loads its hook table once per process, so there is no hot-reload recovery path: a config edit mid-session has no effect until restart.
Last-Turn Result¶
All four last_turn.result values and their transitions. The entry state is none. The last_turn value is sticky — it only changes when a tracked turn reaches a terminal outcome, not on every poll cycle.
stateDiagram-v2
[*] --> none : initial state
none --> success : settle timer fires —<br/>success candidate held<br/>through settle window
none --> interrupted : interruption signal<br/>detected by detector
none --> known_failure : failure signal<br/>detected by detector
success --> success : new turn also<br/>succeeds
success --> interrupted : new turn<br/>interrupted
success --> known_failure : new turn<br/>failed
success --> none : premature success retracted —<br/>surface changed after settle
interrupted --> success : new turn<br/>succeeds
interrupted --> interrupted : new turn also<br/>interrupted
interrupted --> known_failure : new turn<br/>failed
known_failure --> success : new turn<br/>succeeds
known_failure --> interrupted : new turn<br/>interrupted
known_failure --> known_failure : new turn also<br/>failed
note right of success : Sticky — persists<br/>across poll cycles<br/>until next terminal outcome
note left of none : Returns here only<br/>if premature success<br/>is retracted
Turn Lifecycle Sequence¶
A complete turn lifecycle from the consumer's perspective, showing both the successful and interrupted paths.
sequenceDiagram
participant C as Consumer
participant S as Server API<br/>/houmao/terminals/
participant T as LiveSession<br/>Tracker
participant D as Detector
C->>S: POST .../input<br/>(send prompt)
S->>T: note_prompt_submission()
T->>T: arm explicit_input anchor
Note over T: turn.phase → active
loop Poll cycles
T->>D: detect(output_text,<br/>parsed_surface)
D-->>T: DetectedTurnSignals
T->>D: derive_temporal_hints(recent_frames)
D-->>T: TemporalHintSignals
end
alt Successful turn
D-->>T: active_evidence=true
Note over T: turn.phase stays active
D-->>T: success_candidate=true
T->>T: arm settle timer
Note over T: waiting for<br/>settle window...
T->>T: settle timer fires
Note over T: last_turn.result → success<br/>last_turn.source → explicit_input<br/>turn.phase → ready
else Interrupted turn
D-->>T: interrupted=true
Note over T: cancel settle timer<br/>last_turn.result → interrupted<br/>last_turn.source → explicit_input<br/>turn.phase → ready
end
C->>S: GET .../state
S-->>C: HoumaoTerminal<br/>StateResponse
State Composition¶
How low-level observations compose upward through the pipeline into public state groups. This reflects the shared module extraction — detectors and mapping helpers live in shared_tui_tracking/, not inside the server module.
flowchart TD
Probe["tmux probe<br/><i>pane capture + liveness</i>"]
Proc["process inspection<br/><i>PaneProcessInspector</i>"]
Parse["official parser<br/><i>OfficialTuiParserAdapter</i>"]
Surf["HoumaoParsedSurface"]
Detect["tracked-TUI profile<br/><i>shared_tui_tracking/apps/<app_id>/</i>"]
Shared["TuiTrackerSession<br/><i>shared_tui_tracking/session.py</i>"]
MapDiag["diagnostics_availability()<br/><i>shared_tui_tracking/public_state.py</i>"]
Anchor{"turn anchor<br/>lifecycle"}
Settle["settle timer<br/><i>ReactiveX kernel</i>"]
PubDiag["diagnostics<br/>.availability"]
PubSurf["surface<br/>.accepting_input<br/>.editing_input<br/>.ready_posture"]
PubTurn["turn<br/>.phase"]
PubLast["last_turn<br/>.result / .source"]
Stable["stability<br/>.stable_for_seconds"]
Hist["recent_transitions"]
Probe --> Proc --> Parse --> Surf
Probe --> MapDiag
Proc --> MapDiag
Parse --> MapDiag
Probe --> Detect
Detect --> Shared
Shared --> PubSurf
Shared --> PubTurn
Shared --> PubLast
MapDiag --> PubDiag
Surf --> Anchor
Anchor --> Settle
PubDiag --> Stable
PubSurf --> Stable
PubTurn --> Stable
PubLast --> Stable
Stable --> Hist
Operation Acceptability¶
What operations are acceptable for major state combinations, using Houmao-native routes.
diagnostics.availability |
turn.phase |
Send input (POST /houmao/terminals/{id}/input) |
Poll state (GET /houmao/terminals/{id}/state) |
Expect meaningful results |
|---|---|---|---|---|
available |
ready |
Yes — terminal is idle and ready | Yes | Yes — all state groups are meaningful |
available |
active |
No — agent is working | Yes — poll for turn completion | Yes — watch for last_turn.result to change |
available |
blocked |
No — provider is waiting for a permission decision (hook-only phase) | Yes — poll for resolution | Yes — observation_channels[].state and conflict reasons explain the block |
available |
unknown |
Wait — posture is ambiguous | Yes — may resolve shortly | Partial — surface values may be approximate |
unavailable |
any | No — no terminal to receive input | Yes — monitor for recovery | No — values are stale or default |
tui_down |
any | No — TUI process is not running | Yes — monitor for TUI restart | No — values reflect last observation before exit |
error |
any | No — current sample is untrustworthy | Yes — may be transient | No — wait for recovery |
unknown |
any | Wait — state is not classifiable | Yes — may resolve shortly | No — insufficient data |
Stability and Timing¶
stable_for_seconds¶
The stability.stable_for_seconds field indicates how long the current public state has remained unchanged. Consumers should use this to avoid acting on transient states.
Practical guidance:
- A last_turn.result=success with stable_for_seconds above the settle window threshold indicates a truly settled outcome
- Acting on an unstable success (low stable_for_seconds) risks seeing the result retracted — the settle window may not have elapsed yet
- For turn.phase=ready, stability confirms the TUI has been idle, not just momentarily between outputs
Premature Success Retraction¶
The reducer records the surface signature when a success settles. If a subsequent observation under the same anchor shows a different surface but the detector still reports success_candidate, the previous success is retracted (last_turn.result reverts to none) before the new candidate is evaluated. This prevents a premature success from persisting when the TUI was actually still producing output.
Settle Window¶
The settle window is a configurable duration in the standalone TuiTrackerSession (TrackerConfig.settle_seconds) during which a success_candidate surface must remain stable before being promoted to success. If any of the following occur during the window, the pending success is cancelled:
- Diagnostics degrade (availability leaves
available) - An
interruptedsignal is detected - A
known_failuresignal is detected active_evidencereappears- The surface signature changes (surface is still evolving)
Turn Anchor Effects¶
explicit_input vs surface_inference¶
The two anchor sources have different timing characteristics:
explicit_input — armed immediately when the server receives POST /houmao/terminals/{terminal_id}/input and calls note_prompt_submission():
- The exact submission timestamp is known
- turn.phase transitions to active immediately
- Settle timing starts from a precise anchor point
- This is the preferred path for automation
surface_inference — armed by the tracker when it detects that a direct tmux interaction has produced a new turn:
- The tracker uses raw tmux snapshots plus tool/version detector rules; parser-derived surface growth is not part of this authority path
- The submission moment is estimated from surface changes, not known precisely
- Settle times may be slightly longer due to the estimation uncertainty
- This path keeps repaint churn, cursor motion, and small prompt edits from manufacturing turn semantics
Reducer Transition Rules¶
The standalone TuiTrackerSession in shared_tui_tracking/session.py applies a strict priority chain when processing each raw snapshot. The compatibility StreamStateReducer wrapper and the live server host adapter both reuse that same core behavior. Server-owned diagnostics, parser sidecars, and lifecycle enrichment are published alongside this reducer output; they do not become reducer inputs. The hook observation merge described above runs at the host projection layer after this chain and never feeds hook facts into the shared tracker, so the blocked phase does not appear here.
Priority Chain¶
Each observation is processed through these checks in order. The first matching condition determines the output:
-
Interrupted signal — If the detector reports
interrupted=true, emitturn.phase=readyandlast_turn.result=interrupted. Cancel any pending success timer. Clear the armed turn source. -
Known failure signal — If the detector reports
known_failure=true, emitturn.phase=readyandlast_turn.result=known_failure. Cancel any pending success timer. Clear the armed turn source. -
Active evidence — If the detector reports
active_evidence=true, emitturn.phase=active. Cancel any pending success timer. If no turn source is armed yet, armsurface_inference. -
Success candidate — If the detector reports
success_candidate=true, emitturn.phase=readyand arm or retain the settle timer. If a previous settled success exists with a different surface signature, retract it first. -
Default — No strong signal matched. Cancel any pending success timer. Emit
turn.phase=readyifready_posture=yes, otherwiseturn.phase=unknown.
Host-Side Stale-Active Recovery¶
After the shared tracker publishes its surface / turn / last_turn fields, the live host adapter may still apply one corrective step for unanchored sessions.
That recovery exists to handle stale active evidence that survived in tmux history after the visible surface has already become submit-ready again. The current Codex tracker is the main case that benefits from it.
The host adapter recovers turn.phase=active to turn.phase=ready only when all of the following remain true through the configured recovery window:
- diagnostics remain
available - parsed surface is submit-ready (
availability=supported,business_state=idle,input_mode=freeform) - public surface remains
accepting_input=yesandediting_input=no - the session is unanchored (
lifecycle_authority.turn_anchor_state=absent) - remaining shared-tracker active reasons are limited to stale-like
status_rowevidence
This correction does not set last_turn.result=success. It only fixes the public readiness posture.
Success Timer¶
When a success_candidate is detected, the reducer arms a settle timer for the configured settle window duration. The timer fires only if:
- The latest signals still report success_candidate
- No current_error_present
- The surface signature has not changed since arming
On fire, the reducer emits last_turn.result=success with the appropriate turn source and clears the armed tracker source.
Cancellation¶
The success timer is cancelled whenever the priority chain reaches a step before the success_candidate check — i.e., diagnostics degradation, interrupted, known_failure, or active_evidence all cancel a pending success.