Retired Houmao Server State Tracking¶
This page is internal reference for retained old-server TUI modules and shared tracking models. houmao-server is no longer a packaged executable or maintained public server API. Current API-based observation is exposed through houmao-passive-server, while this page explains retained implementation pieces that maintained code may still import.
The retained tracking model still distinguishes two live-tracking execution modes behind the same public models:
- Direct fallback: retained old-server modules can run the tracker locally for eligible managed TUI sessions.
- Gateway-owned tracking: an attached healthy per-agent gateway runs the live tracker for that one session, and maintained API surfaces can project the gateway state back through
/houmao/agents/*routes.
This keeps one public state contract while enforcing single-owner tracking authority during attach, detach, and gateway health transitions.
The public models live in src/houmao/server/models.py, which re-exports core types from src/houmao/shared_tui_tracking/models.py. The standalone tracker engine now lives in src/houmao/shared_tui_tracking/session.py, while src/houmao/server/tui/tracking.py acts as the live host adapter that merges shared tracker state with server-owned diagnostics and lifecycle metadata. Timed readiness and completion behavior still reuse the shared ReactiveX lifecycle kernel in src/houmao/lifecycle/rx_lifecycle_kernel.py. For the file-by-file package layout of the server watch plane, see internals/tui_tracking_module.md.
Public Contract¶
For the full definition of each state value (intuitive meaning, technical derivation, operational implications), see the State Reference Guide. For state transition diagrams and operation acceptability, see the State Transitions Guide.
The public state is intentionally small:
| Field | Values | Meaning |
|---|---|---|
diagnostics.availability |
available, unavailable, tui_down, error, unknown |
Whether the current sample is usable for normal tracked-state interpretation |
surface.accepting_input |
yes, no, unknown |
Whether typed input would currently land in the prompt area |
surface.editing_input |
yes, no, unknown |
Whether prompt-area input is actively being edited now |
surface.ready_posture |
yes, no, unknown |
Whether the visible surface looks ready for immediate submit |
turn.phase |
ready, active, blocked, unknown |
Current turn posture; blocked is hook-only |
last_turn.result |
success, interrupted, known_failure, none |
Most recent completed terminal outcome |
last_turn.source |
explicit_input, surface_inference, hook_observation, none |
Where that recorded terminal turn came from |
work |
pending, clear, unknown |
Session-scoped background work from Claude hook payloads; unknown for Codex and Kimi and whenever hook evidence is absent or degraded |
settled |
boolean | Derived convenience: turn.phase=ready and work=clear |
observation_channels |
optional list of HoumaoObservationChannel |
Additive hook channel state; null when no channel is configured |
chat_context |
current, degraded, unknown |
Whether the current chat context is healthy, recoverably degraded, or unknown |
stability |
signature, stable, stable_for_seconds, stable_since_utc |
Generic visible-state stability for the published response |
recent_transitions |
bounded list of HoumaoRecentTransition |
Recent visible state changes kept in memory for diagnostics |
Low-level observation detail still remains available alongside the simplified model:
transport_stateprocess_stateparse_status- optional
probe_error - optional
parse_error - nullable
parsed_surface - optional
probe_snapshot - tracked-session metadata such as
tracked_session.observed_tool_version - optional hook diagnostics:
diagnostics.hook_channel_state,diagnostics.hook_counters, anddiagnostics.hook_conflict_reasons - optional
last_turn.error_class, populated for a hook-settledknown_failure
Server-owned lifecycle sidecars still remain available for consumers that need timing or authority detail:
operator_statelifecycle_timinglifecycle_authority
lifecycle_timing now includes the configured stale-active and final stable-active recovery windows in addition to readiness unknown and anchored completion timing.
Those lower-level fields are diagnostic evidence, not the primary consumer-facing lifecycle vocabulary.
End-To-End Flow¶
A more detailed state composition flowchart reflecting the
shared_tui_tracking/module extraction is maintained in state-transitions.md § State Composition.
One tracking cycle moves through these layers:
flowchart TD
Probe["tmux probe<br/>pane capture"]
Proc["process inspection"]
Parse["official parser"]
Surf["HoumaoParsedSurface"]
Detect["tool/version<br/>raw-text detector profile"]
Shared["standalone tracker session<br/>surface / turn / last_turn"]
Ready["readiness pipeline"]
Anchor{"turn anchor?"}
Anch["anchored settle pipeline"]
Public["diagnostics + shared tracker state<br/>+ server sidecars"]
Stable["stability"]
Hist["recent_transitions"]
Probe --> Proc --> Parse --> Surf
Probe --> Detect
Surf --> Ready
Surf --> Anchor
Anchor -->|active| Anch
Surf --> Public
Detect --> Shared
Ready --> Public
Anch --> Public
Shared --> Public --> Stable --> Hist
LiveSessionTracker.record_cycle() keeps the internal timing and anchor bookkeeping, but it now maps those internals into the simplified public contract rather than exposing readiness/completion/authority terms directly. The standalone tracker session may combine single-snapshot detector output with profile-owned temporal hints over a recent sliding window before those public fields are published.
Mapping Rules¶
Diagnostics¶
diagnostics.availability is derived from the low-level observation outcome:
error: probe or parse failed for this sampleunavailable: tracked tmux target is gonetui_down: tmux is reachable but the supported TUI process is not runningavailable: parser produced a supported parsed surfaceunknown: the server is still watching, but the sample is not classifiable confidently enough for normal interpretation
Foundational Surface Observables¶
surface.accepting_input, surface.editing_input, and surface.ready_posture are built from raw tmux pane text through the shared tracker’s tool/version detector profile. Parsed surface data is published as server-owned sidecar evidence and does not feed tracker authority.
Important consequences:
- progress indicators are supporting evidence only; they are not required for
turn.phase=active - ambiguous interactive UI such as menus, selection boxes, permission prompts, or slash-command pickers degrades to
unknownunless stronger active or terminal evidence exists - unexplained churn may still change diagnostics, surface observables, stability, or transitions without creating a turn
- full tmux scrollback may still be captured for parser and history use, but current activity cues do not have to trust arbitrary historical rows
For Codex, the tracker intentionally splits the surface into two scopes:
- the prompt-scoped latest-turn region for interruption, success context, and temporal transcript growth
- the live-edge tail for current status-row and tool-cell activity
That split prevents stale historical • Working (... esc to interrupt) rows far above the prompt from pinning turn.phase=active when the current surface is already prompt-ready.
Prompt-adjacent Codex compact/server error cells are recoverable degraded-context evidence. They set current-error diagnostics and block success candidacy for that surface, but they do not force prompt readiness away from ready when the prompt/composer facts are otherwise ready. Historical compact/server error cells in long scrollback do not affect current chat_context.
Turn Phase¶
turn.phase is intentionally narrower than the internal reducer graph:
ready: the surface looks ready for another turn nowactive: there is enough evidence that a turn is currently in flightblocked: the provider is asking the operator for an action and will not proceed until it is settled; only hook observation enters this phaseunknown: the server cannot safely classify the posture asreadyoractive
Explicit server-owned input acceptance is enough to arm an active turn immediately. Direct interactive prompting can still become active through shared-tracker raw-snapshot evidence without any parser-derived bridge. TUI parsing never emits blocked; a TUI-detected permission dialog keeps the existing turn.phase=active plus operator_state=waiting_user_answer presentation. Hook PermissionRequest enters blocked. For Claude, hook permission resolution, a hook terminal event, or TUI modal-dismissal evidence leaves it; for Codex, the paired PostToolUse (allow path) or Interrupt (deny path, which settles the turn interrupted) leaves it; for Kimi, the paired PermissionResult settles it for both decisions, and TUI evidence does not settle a Kimi blocked wait.
There are two host-owned corrections on top of the shared tracker. The narrow stale-active correction clears an unanchored session after it stays submit-ready for the configured stale-active recovery window while the shared tracker is still stuck on stale status_row evidence. The final stable-active correction uses a longer default 20-second window and clears a stable false-active posture when turn.phase=active and the rendered-surface signature derived from the tmux capture remains unchanged for that window. Unlike the narrow stale-active path, final stable-active recovery is parser-independent; it exists as the backstop for detector false positives, so parser-derived fields cannot veto its timer or participate in its recovery key. These recoveries:
- require parsed submit-ready posture plus
surface.accepting_input=yesandsurface.editing_input=noonly for the narrow stale-active path - keep the 5-second narrow recovery limited to unanchored stale
status_rowevidence - allow the final 20-second recovery to correct
surface.ready_posture=nowithout consulting parser readiness, active reasons, or tracker stability fields - stop anchored completion monitoring when final recovery expires a stale active anchor
- do not manufacture
last_turn.result=success - preserve recoverable error diagnostics such as
chat_context=degradedwhen recovery corrects a false active state
Last Turn¶
last_turn is sticky and only changes when a tracked turn reaches a terminal outcome:
success: the latest answer-bearing ready surface passed the settle window and has no current interrupt, failure, or advisory blockerinterrupted: the detector saw a supported current interruption signature, or a Codex hookInterruptsettled the turnknown_failure: the detector saw a specifically supported failure signaturenone: no terminal result has been recorded yet
Two clarifications matter:
- success does not require a
Worked for <duration>-style marker on every turn - a premature success may be retracted if a later observation proves the same turn surface was still evolving; the tracker keeps the success anchor alive briefly to allow that correction before expiry
Turn Sources And Server Anchors¶
The public last_turn.source field and the server-owned lifecycle anchor state are related, but they are not the same thing.
Tracked state records three turn sources:
| Source | How it is armed | Public source |
|---|---|---|
terminal_input |
POST /houmao/terminals/{terminal_id}/input succeeds and the service calls note_prompt_submission() |
explicit_input |
surface_inference |
raw tmux snapshots show enough active/success evidence for the shared tracker to infer a direct in-terminal turn | surface_inference |
Hook terminal event (Stop, StopFailure, or Codex Interrupt) |
latches the turn; a gateway submission record correlated with the same turn keeps explicit_input |
hook_observation |
Separately, the server still owns explicit-input lifecycle anchors for parser-fed operator_state, lifecycle_timing, and lifecycle_authority.
explicit_inputcan arm both the shared tracker and the server-owned lifecycle anchor.surface_inferenceis tracker-owned only. It can produce publiclast_turn.source=surface_inferencewhile server-ownedlifecycle_authority.turn_anchor_stateremainsabsent.hook_observationis hook-owned. A hook terminal event settles the turn immediately, without waiting for the TUI settle window, and records the providererror_classforknown_failure. On Claude, TUI temporal interrupt evidence for the same turn epoch, observed outside a maintenance window, may still override a hook success withinterrupted. On Codex and Kimi, the hookInterruptevent supplies the interrupted outcome, and a latched outcome is final for its epoch.- Parser-derived surface churn does not arm tracker authority. Parsed surface remains server-owned evidence and lifecycle input only.
- Observed tool version for tracker profile selection comes from tracked-session metadata, primarily manifest launch-policy provenance with graceful fallback when unavailable.
Hook Observation Channel¶
Managed local_interactive sessions can opt into a hook observation channel that feeds provider-emitted lifecycle events into tracked state. The channel is off by default and opt-in per adapter: set HOUMAO_HOOK_OBSERVATION_CLAUDE=1 for Claude Code 2.1.296 or later, HOUMAO_HOOK_OBSERVATION_CODEX=1 for Codex 0.162.1 or later, or HOUMAO_HOOK_OBSERVATION_KIMI=1 for Kimi Code 2.1.1 or later. Only the exact value 1 enables an adapter, and any other value leaves behavior unchanged. The supported pairs are ("claude", "local_interactive"), ("codex", "local_interactive"), and ("kimi", "local_interactive"). claude_headless, codex_headless, kimi_headless, joined sessions, and unmanaged launches remain TUI-only.
A session with the channel configured has one collector process bound to 127.0.0.1 on an ephemeral port. Deliveries authenticate with a per-session bearer token, and the collector appends normalized events to <session_root>/hook-observation/<adapter>/events.ndjson with a secret-free channel.json alongside it; <adapter> is claude, codex, or kimi. Every tracking consumer tails that file with its own offset and folds new observations at its next read or poll cycle, so the gateway-owned tracker, the retained houmao-server tracker, and the passive server can all track the same session independently.
The channel is additive. When no channel is configured, every field described here keeps its previous value or default, and tracked behavior is byte-identical to TUI-only tracking.
Adapter Coverage¶
All three adapters reuse the collector, store, health monitor, and reader. They differ in injection, event vocabulary, and posture rules.
| Adapter | Enablement | Observer injection | Subscribed events |
|---|---|---|---|
| Claude Code | HOUMAO_HOOK_OBSERVATION_CLAUDE=1, floor 2.1.296 |
Marker merge into the managed home settings.json, with a session-scoped --settings fallback |
Nine events |
| Codex | HOUMAO_HOOK_OBSERVATION_CODEX=1, floor 0.162.1 |
Observer hooks.json in the runtime-owned CODEX_HOME plus a pre-trust block in the runtime-home config.toml; no bypass flag and no operator file |
Fourteen events, ten dispatched at the supported floor |
| Kimi Code | HOUMAO_HOOK_OBSERVATION_KIMI=1, floor 2.1.1 |
Marker merge of a 20-entry [[hooks]] observer block into the launch-owned runtime-home config.toml; no trust step and no operator file |
Twenty events |
Codex subscribes PreModelRequest, PostModelRequest, PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, SessionStart, SessionEnd, UserPromptSubmit, SubagentStart, SubagentStop, Stop, and Interrupt. At the supported floor, PreModelRequest, PostModelRequest, SubagentStart, and SubagentStop never fire; the reducer counts them as unavailable event families in diagnostics rather than failures and keeps the other ten families as the posture evidence. SessionStart is lazy: Codex dispatches it at the first turn start, not at process launch. Model-request events and Stop carry no token usage, and no Codex payload exposes a background-work mirror, so the Codex adapter publishes work=unknown and never claims settled from hook evidence alone.
Codex trust is automatic on the managed path. The launch writes the observer hooks.json into the runtime home and pre-trusts every handler through Houmao-owned [hooks.state."<absolute hooks.json path>:<snake_case event label>:<group index>:<handler index>"] entries with trusted_hash values in the same home's config.toml. The marker-delimited block is generation-scoped, rewritten at every provider start, and removed on cleanup; operator-authored hooks.state entries and every other config.toml line are preserved. The managed path never adds --dangerously-bypass-hook-trust, never writes the operator's Codex home or profile, and requires no operator action. See the Codex Signal Reference for the event and payload details.
Kimi subscribes PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionResult, UserPromptSubmit, UserPromptQueued, TurnStarted, Stop, StopFailure, Interrupt, SessionStart, SessionEnd, SessionHeartbeat, SubagentStart, SubagentStop, TaskStarted, PreCompact, PostCompact, and Notification, with one synchronous entry each, an omitted matcher, and a 5-second timeout. StopFailure, TaskStarted, Notification, and the cancelled and error permission decisions were never exercised live at 2.2.0; they stay subscribed as silent or defensive families, and the normalizer counts unseen families in diagnostics rather than failing. Kimi correlation is two-phase: UserPromptSubmit arms a turn epoch and the following TurnStarted(origin_kind=user) binds the integer, session-scoped turn_id; tool events and Stop carry no turn_id and attribute to the armed epoch by stream order. Permission events exist only under an ask permission policy, so a never-ask session legitimately produces none. Stop carries no background-task payload, SessionHeartbeat is liveness evidence only, and the adapter publishes work=unknown for every sample; hooks alone never produce settled.
Kimi needs no trust step. The launch merges the marker-delimited observer block into the launch-owned KIMI_CODE_HOME/config.toml after launch-policy synthesis, rewrites it at every provider start, and removes it on cleanup while preserving every byte outside the block. The provider validates the hooks section against its own schema and silently voids the whole section when any entry is malformed; the TUI shows no warning, and only print-mode stderr carries the diagnostic. The installer therefore validates the merged file by strict tomllib read-back before launch: every key must be in {event, matcher, command, timeout}, every event must be in the pinned 20-name enum, and every timeout must be an integer in 1..600. Any violation restores the pre-install text and degrades the channel to unavailable instead of launching a dead channel. Kimi loads its hook table once per process, so install-before-start ordering is sufficient and mid-session edits have no effect. See the Kimi Signal Reference for the event, payload, and correlation details.
Three Axes¶
| Axis | Values | Authority |
|---|---|---|
surface.* |
accepting_input, editing_input, ready_posture, pending_input, stability |
TUI only |
turn.phase |
ready, active, blocked, unknown |
Hook primary for lifecycle; TUI supplies surface-derived entry into active and unknown plus recovery |
work |
pending, clear, unknown |
Hook primary on Claude; always unknown for Codex and Kimi; unknown whenever hook evidence is absent or the channel is degraded |
settled |
boolean | Derived: turn.phase=ready and work=clear |
No axis silently impersonates another. surface.* never borrows hook facts, and a hook terminal event does not rewrite TUI surface fields. blocked is hook-only: a TUI-detected permission dialog keeps the existing turn.phase=active presentation. settled recovers the pre-background-work meaning of "you can assign the next task" without treating prompt readiness as task completion; work=pending never closes gateway admission by itself.
Channel Health¶
| State | Meaning |
|---|---|
active |
Current-generation hook events are arriving. Only this state grants hook lifecycle authority. |
unavailable |
Startup state before any event. After the 15-second startup grace with no event, or when the channel could not be installed, tracking is TUI-only. |
stale |
An active channel delivered no event for more than 30 seconds while the visible TUI kept changing. TUI regains lifecycle authority for new evidence, including new settlements. A terminal result already latched from hook evidence persists through the stale window; work becomes unknown and settled becomes false. |
disabled_by_policy |
Terminal policy state recognized from channel metadata. TUI-only. |
untrusted_workspace |
Terminal policy state recognized from channel metadata. TUI-only. |
error |
The event store could not be read. A read-failure error clears on the next healthy read; an error state published in channel metadata is terminal. |
stopped |
The collector stopped, or the session stopped. Terminal. A collector.json record with status=stopped for the same generation moves the channel to stopped on the next poll cycle. |
Every state other than active is degraded: the channel contributes no new lifecycle authority, gateway admission matches TUI-only behavior, work is unknown, and settled is false. A hook-latched last_turn remains the one persisted hook lifecycle fact during a stale window. A non-terminal channel moves to active on the first current-generation event and back to active from stale on the next event.
Codex adds one startup nuance. Because SessionStart is dispatched lazily at the first turn start, a fresh session that sits idle at the composer can legitimately report unavailable past the 15-second grace; the first turn's event still moves the channel to active. A provider upgrade that changes the trust hash algorithm or event names makes Codex skip the untrusted handlers silently. The channel detects that as startup event silence with a live collector, reports unavailable (degraded), and keeps tracking TUI-only; a channel that was already active and then loses events follows the ordinary staleness rule to stale. The trust-hash fixture tests pin Codex's published vectors, so an upstream algorithm change fails CI before a managed launch silently loses hooks.
Kimi has two startup nuances of its own. The provider awaits SessionStart at session creation, but a fresh TUI creates its session lazily on the first message, so the first event arrives after the first prompt; a resumed session restores at launch and delivers SessionStart(resume) before the first prompt. Judge startup event silence after the first prompt, never at launch. Separately, a provider upgrade that renames or removes a hook event would make Kimi void the whole [[hooks]] section silently, so the install-time read-back guard is the deterministic tripwire and any violation starts the channel as unavailable. Kimi loads its hook table once per process, so there is no reload race after install and no hot-reload recovery path.
Authority And Conflict Diagnostics¶
When the channel is active, hook facts win where they speak:
| Situation | Resolution | Conflict reason |
|---|---|---|
Hook active, TUI ready |
Hook wins for lifecycle | hook_active_tui_ready |
Hook blocked, TUI ready |
Hook wins | hook_blocked_tui_ready |
| Hook terminal, TUI still active | Terminal latches; surface fields stay TUI-owned | hook_terminal_tui_active |
| Hook success, TUI interrupt evidence (Claude) | interrupted wins for that turn |
hook_success_tui_interrupted |
| Hook failure, TUI success | Hook failure wins for the current prompt | hook_failure_tui_success |
Codex or Kimi stream ends with an open turn, or SessionEnd arrives with an open turn |
Presence lost, turn unknown, no success recorded |
hook_terminal_gap |
| Stale channel, TUI still changing | TUI authoritative for new evidence; a hook-latched last_turn persists |
hook_stale_tui_authoritative |
| Recovery disagrees with a TUI settlement | Hook corrects with an audit trail | hook_correction_after_staleness |
| Older-generation event | Dropped before correlation | hook_orphan_generation |
| Unknown or malformed event | Skipped and counted | hook_unknown_event, hook_malformed_event |
| Terminal without an armed prompt, or duplicate delivery | Counted, no last_turn manufactured |
hook_orphan_terminal, hook_duplicate_event |
Conflict reasons are bounded, deduplicated, secret-free, and carried in observation_channels[].conflict_reasons and diagnostics.hook_conflict_reasons. Posture diagnostics such as prompt_id_missing, permission_without_armed_prompt, and hook_stop_hook_active remain local reducer diagnostics; they are not part of the public conflict list.
Additive Public Fields¶
The tracked-state response adds these optional fields. Existing consumers remain valid because every field is optional or has a backward-compatible default.
| Field | Type | Notes |
|---|---|---|
turn.phase |
TurnPhase |
Gains blocked |
last_turn.source |
TrackedLastTurnSource |
Gains hook_observation |
last_turn.error_class |
str or null |
Provider error class for a hook-settled known_failure |
work |
HookWorkState |
pending, clear, or unknown; defaults to unknown |
settled |
boolean | Defaults to false |
observation_channels |
list of HoumaoObservationChannel or null |
Null when no channel is configured |
diagnostics.hook_channel_state |
HookChannelState or null |
Mirrors the first channel state |
diagnostics.hook_counters |
HoumaoHookObservationCounters or null |
Bounded counters |
diagnostics.hook_conflict_reasons |
list of strings | Empty when no conflicts were observed |
HoumaoObservationChannel carries exactly channel, provider, state, detail, generation, last_event_name, last_event_at_utc, last_sequence, counters, and conflict_reasons. channel is "hook" and provider is the active adapter ("claude", "codex", or "kimi"). HoumaoHookObservationCounters carries events_seen, normalized_events, unknown_events, malformed_events, orphan_terminals, orphan_generations, duplicate_events, steer_events, turn_epochs, compaction_boundaries, allow_settles, deny_settles, and terminal_gaps.
The gateway publishes the same channel payload as GatewayStatusV1.observation_channels (GatewayObservationChannelV1, field-for-field identical) and records hook facts with prompt decisions. The passive server exposes observation_channels on AgentTuiStateResponse and AgentTuiDetailResponse; when the session store is not visible from the passive server host, the channel is reported as state="unavailable" with detail="remote observation".
Stability And Recent Transitions¶
stability is generic visible-state stability, not public completion vocabulary.
The stability signature includes:
- diagnostics
- parsed surface
- foundational surface observables
- public
turn - public
last_turn
Any change resets stable_since_utc and stable_for_seconds.
recent_transitions is a bounded in-memory change log. It now records public transition facts such as:
diagnostics_availabilitysurface_accepting_inputsurface_editing_inputsurface_ready_postureturn_phaselast_turn_resultlast_turn_source
The low-level transport/process/parse fields remain attached for debugging and coarse managed-agent availability projection.
Because stale-active and final stable-active recovery change the published surface / turn fields, they also reset visible-state stability just like any other public-state change.
Maintainer Validation¶
Maintainer-facing validation is replay-grade and content-first:
live tmux pane + runtime liveness
|
v
content-first groundtruth
^
| compare
ReactiveX replay reducer
Use the scripts/explore/claude-code-state-tracking/ workflow to compare groundtruth against replayed observations. When detector behavior is refined, capture the stable matcher change in openspec/changes/.../tui-signals/ rather than leaving it implicit in code alone.