Skip to content

feat(codex): live agent status from the app-server event stream (A3) - #236

Merged
aterrylu merged 1 commit into
mainfrom
terry/codex-iac-a3-status
Jun 20, 2026
Merged

feat(codex): live agent status from the app-server event stream (A3)#236
aterrylu merged 1 commit into
mainfrom
terry/codex-iac-a3-status

Conversation

@aterrylu

Copy link
Copy Markdown
Owner

Problem

Codex agents were status-blind — no hook relay, so the dashboard showed a flat "running" regardless of whether the agent was working, idle, or done. CC/Gemini show live busy/idle via the hook→deriveStatus path; Codex had nothing.

Solution

Source Codex working-status from the app-server daemon's event stream (which A1/A2 already connect to), and feed it into the same in-memory status map CC/Gemini use — so no dashboard changes were needed.

flowchart LR
  D["codex app-server daemon"] -->|"thread/status/changed (active/idle)<br/>+ periodic thread/read reconcile"| C["codexControl status watcher<br/>(eager from spawn)"]
  C -->|"setAgentStatus(working/idle)"| M["in-memory agentStates map"]
  M -->|"GET /api/hooks (polled, unchanged)"| DASH["dashboard status icon"]
Loading
  • The per-agent control client connects eagerly at spawn (startCodexStatusWatch) and runs a status loop: thread/status/changed pushes for instant updates + a periodic thread/read reconcile as a safety net (a missed push can't leave the dashboard stale). The inbound idle-gate's thread/read also emits status, for free.
  • setAgentStatus() sets the status map directly — the daemon gives ground truth, so no deriveStatus event-machine / fake-event synthesis is needed.
  • Wired in gateway/index.ts; started at spawn in runtime.ts (guarded — cosmetic status can never break a spawn).

Hardened from /polish (3-agent review)

  • connect() wedge — its promise settled only in onopen, so a mid-life daemon death left the status loop (and the pre-existing drain path) parked forever. Now settles on close/error + a connect timeout → the loop retries.
  • Silent dead-daemonstatusLoop now counts failures and raises a dashboard SystemWarning when a daemon is persistently unreachable.
  • Unrecognized status types are logged (not silently dropped); respawn TOCTOU fixed; the as AgentStatus cast removed via a CodexStatus union (checked end-to-end).

Testing

  • Real-spawn E2E: a Codex agent transitions idle → working → idle in the live status map (observed sequence asserted).
  • Unit test for setAgentStatus (set/transition/dedupe/clear). Full suite green, biome + tsc clean.

Note

This needs no dashboard changes — so a provider-distinct status icon (Claude vs Codex, using the existing agent.provider) is a clean, separate follow-up.

🤖 Generated with Claude Code

Codex agents were status-blind (flat "running") — no hook relay. A3 sources their
working-status (busy/idle) from the app-server daemon instead, reusing A2's
per-agent control client:

- codexControl.ts: the client now connects EAGERLY at spawn (startCodexStatusWatch)
  and runs a status loop — thread/status/changed pushes for instant updates plus a
  periodic thread/read reconcile (STATUS_POLL_MS) as the safety net so a missed
  push can't leave the dashboard stale. queryIdle (the inbound idle-gate) also
  emits status, for free.
- routes/hooks.ts: setAgentStatus() sets the in-memory status map directly (the
  daemon gives ground truth, so no deriveStatus event-machine needed). The
  dashboard polls this map exactly as it does for CC/Gemini — zero dashboard change.
- gateway/index.ts: wires the status sink to setAgentStatus.
- runtime.ts: starts the watch at spawn for sidecar (Codex) agents; guarded so
  cosmetic status can never break a spawn.

Hardened from /polish:
- connect() now settles on socket close/error + a connect timeout — previously its
  promise resolved/rejected only in onopen, so a daemon that died mid-life left the
  status loop (and the pre-existing drain path) parked forever. This is the exact
  staleness the reconciler exists to prevent.
- statusLoop counts consecutive failures and raises a dashboard SystemWarning when a
  daemon is persistently unreachable, instead of silently never reconciling.
- unrecognized Codex status types are logged once (not silently dropped); respawn
  TOCTOU fixed (getOrCreate treats a disposed controller as absent); the
  string->AgentStatus cast is gone (CodexStatus union, checked end-to-end).

Verified with a real-spawn E2E: a Codex agent transitions idle -> working -> idle in
the live status map. Full suite green, biome + tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qfhGLXBWPQM8a2ixVrnZY
@aterrylu
aterrylu enabled auto-merge (squash) June 20, 2026 21:31

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — clean, well-defended A3 wiring: eager status loop with periodic reconciler, connect promise now settles on close/error/timeout so a dead daemon can't park awaiters, respawn-safe getOrCreate, unmapped status types logged-once instead of silently dropped, and threshold-bounded SystemWarning on persistent feed failure. setAgentStatus is a focused minimal surface on hooks.ts and feeds the same dashboard map as CC/Gemini with no cast (closed CodexStatus union). Test coverage is on the right contract (set/transition/dedupe/clear-tool). No blocking issues.

@aterrylu
aterrylu merged commit 1369058 into main Jun 20, 2026
10 checks passed
@aterrylu
aterrylu deleted the terry/codex-iac-a3-status branch June 20, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants