Skip to content

Agent-state monitor over the experimental control-mode engine#692

Open
tony wants to merge 56 commits into
engine-opsfrom
engine-ops-supatui
Open

Agent-state monitor over the experimental control-mode engine#692
tony wants to merge 56 commits into
engine-opsfrom
engine-ops-supatui

Conversation

@tony

@tony tony commented Jun 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Add libtmux.experimental.agents — a headless, self-healing monitor that reports, per pane, which coding agent is RUNNING, AWAITING_INPUT, IDLE, EXITED, or UNKNOWN, without polling or scraping pane output. Answers the orchestration question "which of my parallel agents needs me right now?"
  • Ingest two cooperative signal channels — a local tmux option subscription (@agent_state%subscription-changed) and a remote OSC 3008 escape carried in control-mode %output — reconciled by a lock-free last-writer-wins merge so the two channels can race freely.
  • Extend AsyncControlModeEngine with a supervised reconnect loop, a death-sentinel that closes subscribers cleanly, and per-subscriber broadcast queues, so a tmux restart or socket blip self-heals and concurrent consumers never steal each other's events.
  • Add typed refresh-client -B/-C to the RefreshClient operation — the substrate the monitor subscribes through (debounced, server-side change detection) instead of raw %output.
  • Expose the monitor over MCPlist_agents, watch_agents, and install_agent_hooks tools, plus a lifespan that starts/stops the monitor and gates the tools behind an engine capability check.
  • Ship non-clobbering shell hooks for Claude Code (~/.claude/settings.json) and Codex (~/.codex/config.toml), installable into a running session at any time.

Stacks on #690. Everything is additive under libtmux.experimentalno existing public API is touched, and the package is mypy-strict clean.

Changes by area

src/libtmux/experimental/agents/ (new package)

  • state.py: the AgentState enum (with from_signal() that maps unknown values to UNKNOWN rather than raising) and the frozen Agent record (name, state, since, source, pid, alive, plus is_running/is_awaiting).
  • merge.py: Stamp(counter, writer) ordering and latest() — the convergent, idempotent, out-of-order-tolerant merge rule. The clock is a pluggable callable (MonotonicCounter now, HLC later).
  • store.py: the durable value tier — a frozen AgentStore, the pure apply(store, event, *, now) reducer, Observed/Vanished events, a Storage protocol, and JsonFile (atomic temp-write + rename). Only apply() mutates state.
  • signals.py: OptionSignal (local) and OscSignal (remote) — the latter reassembles the byte-fragmented OSC 3008 stream tmux delivers in %output.
  • health.py: is_alive(pid) via os.kill(pid, 0); None (a PID-less remote pane) is treated as alive so a remote agent is never falsely expired.
  • tree.py: panes_of() / diff_panes() derived from models snapshots — the live session→window→pane projection.
  • monitor.py: AgentMonitor — the supervised start/stop/reconcile/status/agents contract, the reducer pipeline, and the reconcile sweep that self-attaches (excluding its own tmux -C session) and self-heals across reconnects.
  • hooks/: the AgentHook protocol (detect/install/uninstall/status), the shared emit() (local set-option, else remote OSC 3008/dev/tty), ClaudeCodeHook, CodexHook, and a registry().

src/libtmux/experimental/engines/async_control_mode.py

  • Supervised reconnect (_supervisor, deterministic jittered _backoff, backoff reset on a healthy connect), desired-state replay (add_subscription/set_attach_targets_replay_subscriptions/_replay_attach), a _STREAM_END death-sentinel broadcast to per-subscriber queues, and a subscribe() that no longer hangs when called after aclose().

src/libtmux/experimental/ops/_ops/refresh_client.py

  • Typed subscribe (-B) and size (-C) parameters, version-gated and serializable like every other operation.

src/libtmux/experimental/mcp/

  • vocabulary/agents.py: register_agents() registers list_agents / watch_agents / install_agent_hooks, behind a supports_monitor() capability gate.
  • _lifespan.py, events.py, fastmcp_adapter.py: start/stop the monitor in the server lifespan and skip the agent tools when the lifespan won't bring a monitor up.

CHANGES

  • An Agent-state monitor entry under ### What's new.

Design decisions

  • Source of truth is split by kind. tmux is authoritative for the observed tree (derive it, never store it); the monitor is authoritative for intent/run-state (agent identity + state), which tmux cannot hold and never persists. Deltas drive the fast path; a periodic full list-* snapshot diff is the correctness backstop, because tmux's change feed has blind spots (pane-died, window-resized, and title changes emit no notification).
  • Both signal channels, by necessity. The option path is lossless and re-queryable (show-options -p -v self-heals a dropped notification) but slow (~1 s debounce); the OSC path is instant and the only one that survives SSH (a remote set-option can't reach the local socket) but rides the lossy %output stream. Neither alone is sufficient.
Channel Transport Latency Loss model Reach
OptionSignal @agent_state%subscription-changed ~1 s lossless, re-queryable local socket
OscSignal OSC 3008%output (byte-fragmented) instant lossy (stream) survives SSH
  • Never infer death from silence. A missing notification never marks an agent EXITED. Local panes expire only on a failed PID probe; PID-less remote panes stay at their last-known state.
  • Lock-free convergence. The (counter, writer) latest() guard runs before the coalescing value write, so the two channels merge deterministically without locks regardless of arrival order.
  • Decoupled from the classic ORM. The package depends only on the AsyncTmuxEngine protocol, the models snapshots, and a Storage sink — not on Server/Session/Window/Pane.
MCP tool Purpose
list_agents snapshot of every known pane's agent + state
watch_agents live stream of agent-state transitions
install_agent_hooks install Claude Code / Codex hooks into the running session

Verification

The package is decoupled from the classic ORM (depends only on the engine protocol + models):

$ rg -n "from libtmux\.(server|session|window|pane|common) import|import libtmux\b" src/libtmux/experimental/agents

The remote signal is written to the pane tty, not stdout (which agent hooks capture):

$ rg -n "/dev/tty" src/libtmux/experimental/agents/hooks/emit.py

Async tests follow the repo convention (asyncio.run, no pytest-asyncio):

$ rg -n "pytest_asyncio|pytest\.mark\.asyncio" tests/experimental/agents tests/experimental/engines/test_async_control_mode_supervisor.py

MCP agent tools are gated behind an engine capability check:

$ rg -n "supports_monitor" src/libtmux/experimental/mcp

Test plan

  • Pure tier, no tmuxstate / merge / store / signals / tree / health unit tests + doctests (reducer idempotence, byte-fragmented OSC reassembly, last-writer-wins).
  • Engine resiliencetest_async_control_mode_supervisor.py and test_async_control_mode_sentinel.py cover supervised reconnect, attach replay on reconnect, broadcast to concurrent subscribers, and subscribe-after-close.
  • Live monitortest_live_monitor.py validates self-attach, reconcile, and survives-reconnect against a real tmux server via the libtmux fixtures.
  • Hooks — non-clobbering install/uninstall/status for Claude Code and Codex, including the legacy-Codex notify fallback.
  • MCPtest_agents_tools.py / test_attach_reset.py cover tool registration and the capability gate.
  • Full repo gateruff check, ruff format --check, mypy src tests, pytest, and just build-docs (the CHANGES entry + catalog directive) all green.

Refs #688, #689. Builds on #690.

@codecov

codecov Bot commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.36530% with 279 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.31%. Comparing base (e88e87b) to head (ca13dfd).

Files with missing lines Patch % Lines
src/libtmux/agents/cli.py 70.06% 76 Missing and 15 partials ⚠️
src/libtmux/experimental/agents/monitor.py 81.22% 35 Missing and 20 partials ⚠️
src/libtmux/experimental/agents/hooks/codex.py 78.37% 10 Missing and 6 partials ⚠️
src/libtmux/experimental/agents/wait.py 87.50% 7 Missing and 8 partials ⚠️
tests/experimental/agents/test_monitor.py 86.91% 9 Missing and 5 partials ⚠️
tests/experimental/agents/test_cli.py 90.74% 3 Missing and 7 partials ⚠️
src/libtmux/experimental/agents/processes.py 88.00% 5 Missing and 4 partials ⚠️
tests/experimental/mcp/test_agents_tools.py 90.78% 5 Missing and 2 partials ⚠️
src/libtmux/experimental/agents/drive.py 93.02% 1 Missing and 5 partials ⚠️
src/libtmux/experimental/agents/hooks/claude.py 90.90% 5 Missing and 1 partial ⚠️
... and 18 more
Additional details and impacted files
@@              Coverage Diff               @@
##           engine-ops     #692      +/-   ##
==============================================
+ Coverage       75.23%   77.31%   +2.08%     
==============================================
  Files             225      267      +42     
  Lines           13638    16002    +2364     
  Branches         1784     2037     +253     
==============================================
+ Hits            10260    12372    +2112     
- Misses           2690     2859     +169     
- Partials          688      771      +83     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony tony force-pushed the engine-ops-supatui branch from cd305cf to 68e6a55 Compare June 27, 2026 22:17
tony added a commit that referenced this pull request Jun 28, 2026
why: A PR does not own its release version; the monitor entry named a
concrete version that also went stale after rebasing onto newer master.

what:
- Open the entry with the package as subject, not "libtmux X.Y.Z ships"
- Add the (#692) PR ref to the deliverable heading
@tony tony force-pushed the engine-ops-supatui branch from 9bc1f82 to 6a0a06a Compare June 28, 2026 23:52
@tony tony force-pushed the engine-ops-supatui branch from 4226ae7 to e619b32 Compare July 4, 2026 13:40
tony added a commit that referenced this pull request Jul 4, 2026
why: A PR does not own its release version; the monitor entry named a
concrete version that also went stale after rebasing onto newer master.

what:
- Open the entry with the package as subject, not "libtmux X.Y.Z ships"
- Add the (#692) PR ref to the deliverable heading
tony added a commit that referenced this pull request Jul 4, 2026
why: A PR does not own its release version; the monitor entry named a
concrete version that also went stale after rebasing onto newer master.

what:
- Open the entry with the package as subject, not "libtmux X.Y.Z ships"
- Add the (#692) PR ref to the deliverable heading
@tony tony force-pushed the engine-ops-supatui branch from e619b32 to cae944c Compare July 4, 2026 14:39
tony added a commit that referenced this pull request Jul 4, 2026
why: A PR does not own its release version; the monitor entry named a
concrete version that also went stale after rebasing onto newer master.

what:
- Open the entry with the package as subject, not "libtmux X.Y.Z ships"
- Add the (#692) PR ref to the deliverable heading
@tony tony force-pushed the engine-ops-supatui branch from cae944c to 18ea685 Compare July 4, 2026 20:50
tony added a commit that referenced this pull request Jul 5, 2026
why: A PR does not own its release version; the monitor entry named a
concrete version that also went stale after rebasing onto newer master.

what:
- Open the entry with the package as subject, not "libtmux X.Y.Z ships"
- Add the (#692) PR ref to the deliverable heading
@tony tony force-pushed the engine-ops-supatui branch from 18ea685 to 246c591 Compare July 5, 2026 12:42
tony added a commit that referenced this pull request Jul 5, 2026
why: A PR does not own its release version; the monitor entry named a
concrete version that also went stale after rebasing onto newer master.

what:
- Open the entry with the package as subject, not "libtmux X.Y.Z ships"
- Add the (#692) PR ref to the deliverable heading
@tony tony force-pushed the engine-ops-supatui branch from 246c591 to 2f87282 Compare July 5, 2026 14:31
tony added a commit that referenced this pull request Jul 5, 2026
why: A PR does not own its release version; the monitor entry named a
concrete version that also went stale after rebasing onto newer master.

what:
- Open the entry with the package as subject, not "libtmux X.Y.Z ships"
- Add the (#692) PR ref to the deliverable heading
@tony tony force-pushed the engine-ops-supatui branch from 2f87282 to ec03d60 Compare July 5, 2026 19:43
tony added a commit that referenced this pull request Jul 5, 2026
why: A PR does not own its release version; the monitor entry named a
concrete version that also went stale after rebasing onto newer master.

what:
- Open the entry with the package as subject, not "libtmux X.Y.Z ships"
- Add the (#692) PR ref to the deliverable heading
@tony tony force-pushed the engine-ops-supatui branch from ec03d60 to 0f57964 Compare July 5, 2026 20:31
tony added a commit that referenced this pull request Jul 5, 2026
why: A PR does not own its release version; the monitor entry named a
concrete version that also went stale after rebasing onto newer master.

what:
- Open the entry with the package as subject, not "libtmux X.Y.Z ships"
- Add the (#692) PR ref to the deliverable heading
@tony tony force-pushed the engine-ops-supatui branch from 0f57964 to 6332ab0 Compare July 5, 2026 22:53
tony added 4 commits July 5, 2026 18:17
why: The shared vocabulary every agents module reads/writes.

what:
- AgentState (running/awaiting_input/idle/exited/unknown) with from_signal
- frozen Agent record + is_running/is_awaiting helpers
why: Out-of-order/replayed agent-state updates must converge to newest.

what:
- Stamp(counter, writer) with deterministic tie-break
- latest() guard + pluggable Clock + MonotonicCounter default
tony added 29 commits July 5, 2026 18:17
why: When the display-message own-id probe fails, own is None, so the
`sid != own` guard is always true and _primary_session_id falls through
to return ids[0] — tmux's phantom `tmux -C` session (it sorts first),
which holds no agent panes. Attaching there leaves the option channel
effectively silent.

what:
- Return None from _primary_session_id when the own-session probe fails,
  so start() skips attach instead of binding to the phantom session
- Cover the case with a fake engine whose display-message probe raises
why: tmux reports a failed attach (e.g. stale session id) as a non-zero
returncode, not an exception, so the monitor recorded _attached_session
even when the attach failed -- silencing the option channel.

what:
- monitor.start() now records the sticky attach only when attach-session
  returns returncode 0; logs the stderr on failure
- document _replay_attach's optimistic fire-and-forget attach and that a
  failed re-attach self-corrects on the monitor's next reconcile
why: The agent monitor needs to tell a floating overlay (e.g. a status
HUD) apart from a real agent pane; the snapshot had no floating flag and
the monitor's pane format did not request it.

what:
- Add PaneSnapshot.floating, parsed from #{pane_floating_flag} (tmux
  3.7+; renders empty -> False on older tmux, so no version gate)
- Request pane_floating_flag in the monitor's PANE_FORMAT
- Cover the snapshot floating flag and the format request

Foundation for the floating HUD; the renderer + monitor self-exclusion
land next.
why: The agent monitor was observation-only; on tmux 3.7 a floating
overlay can surface live agent state in the session itself, with no
external UI.

what:
- Add HudRenderer: a pure AgentStore -> text frame plus the typed
  RespawnPane paint op (the frame is shell-quoted and held open with
  `tail -f /dev/null` so it persists between repaints)
- AgentMonitor gains opt-in `hud=True`: start() creates one floating
  NewPane over the primary session and captures its id; the supervised
  drain repaints on every store change (dirty flag set in _observe and
  reconcile); stop() kills the HUD pane
- Exclude the HUD's own pane from _reconcile_once so it never enters the
  diff or the health sweep (tracking is signal-driven, so this is the
  only exclusion needed); best-effort throughout (no session, engine
  error, or tmux < 3.7 silently skips the HUD)
- Cover the renderer, the repaint op, HUD create/teardown, and the
  reconcile self-exclusion
why: watch_agents opened its own engine.subscribe() and re-ingested the
fan-out stream while the monitor's drain already ingests it, so every
event was processed twice -- drifting the MonotonicCounter and `since`
stamps. It was also tagged readOnlyHint=True despite mutating the store.

what:
- Drop the redundant subscribe()+ingest loop; observe the monitor's live
  store over the window (the monitor's drain is the sole ingester), so
  readOnlyHint=True is now accurate
- Cover that watch_agents never calls engine.subscribe()
why: _ensure_hud ran only in start(). After a full tmux restart the HUD
pane id is dead; _repaint_hud ignored the arun result (which reports a
tmux-side failure as data, not a raise), so _hud_pane_id was never
cleared and the HUD stayed dark for the rest of the session.

what:
- _repaint_hud now checks the result: a failed/errored repaint drops
  _hud_pane_id (leaving it dirty) so it can be recreated; the dirty flag
  is cleared only on a successful paint
- _run recreates the HUD (_ensure_hud) after a reconcile when it is
  enabled but has no pane id -- covering both a restart and an initial
  create that had no session yet
- Cover ok-keeps-pane vs fail-drops-pane (parametrized)
why: The module docstring said the HUD "repaints on every store change",
but _hud_dirty is set unconditionally on each notification and reconcile,
so it repaints on those events rather than only on an actual mutation.

what:
- Reword to "repaints after each notification and reconcile"
why: The AgentHook.install/uninstall doctests ran a bare
ClaudeCodeHook(), which defaults settings_path to the real
~/.claude/settings.json and rewrites it on every doctest run. The
"no-op on stub" comment was wrong -- these methods always write.

what:
- Redirect each doctest into a tempfile.TemporaryDirectory(), mirroring
  the already-isolated claude.py/codex.py doctests
why: The agent-monitor entry described the liveness/reconcile sweep as
"a periodic health probe" running "every few seconds". It is actually
event/reconnect-driven: _run reconciles at startup and again each time
the subscribe stream ends (a disconnect/reconnect). The only sleep is a
retry backoff on the failure path, not a timer.

what:
- "refreshed by a periodic health probe" -> "refreshed on each
  reconciliation"
- "runs every few seconds" -> "runs at startup and on each engine
  reconnect"
why: Two monitor test fakes failed strict mypy: a stream-engine fake
missing the _StreamEngine Protocol's _attached_session member, and a
capturing FastMCP stand-in passed where FastMCP is expected.

what:
- Add _attached_session to FakeStreamEngine/_BlockingStreamEngine
- Cast the capturing MCP fake to FastMCP at the register_agents call
why: from_dict raised ValueError on a state string absent from the
current enum (e.g. a store written by a newer version), crashing the
monitor on startup when it loads the store.

what:
- Deserialize state via AgentState.from_signal (unknown -> UNKNOWN),
  mirroring signal ingestion
- Add parametrized tests for known/unknown/garbage states
why: `emit ... --name` with the flag as the final CLI arg raised
IndexError instead of a clean exit, surfacing a traceback to the
agent's hook runner.

what:
- Fall back to name=None when --name has no following value
- Add parametrized tests for main's --name parsing
why: install_agent_hooks awaited blocking file I/O (read, fsync,
atomic replace) directly on the event loop, stalling concurrent
MCP tools during an install.

what:
- Run hook install/status via asyncio.to_thread
- Add parametrized tests for the install tool (known/unknown agent)
why: A PR does not own its release version; the monitor entry named a
concrete version that also went stale after rebasing onto newer master.

what:
- Open the entry with the package as subject, not "libtmux X.Y.Z ships"
- Add the (#692) PR ref to the deliverable heading
why: The monitor knew agent state but gave no way to act on it — no
blocking wait for a state, no safe driver, and concurrent sends to one
pane interleaved keystrokes. These are the fan-in/fan-out verbs an
orchestrator needs, plus the correctness primitives single-operator
tools never provide.

what:
- agents/wait.py: wait_for_agent_state + fleet wait_for_agents over a
  pure WaiterRegistry woken from the monitor's single _observe edge;
  outcomes as data (AgentWait/WaitReason); zero tmux calls (level check
  + future, never a poll).
- agents/drive.py: send_to_agent/send_to_agents fold every multi-step
  and fleet send into ONE dispatch via LazyPlan + FoldingPlanner; a
  process-wide per-pane pane_lock chokepoint serializes all async
  keystroke injection; DedupLedger makes a keyed retry a no-op.
- agents/state.py: AgentTransition value.
- agents/monitor.py: host the registry/ledger; diff before->after in
  _observe/_apply_health/reconcile to wake waiters, emit a structured
  agent_* INFO log, and fan AgentTransition to observers; stop()
  resolves parked waits (STOPPED) so none hang.
- mcp: wait_for_agent + send_to_agent tools; asend_input now takes the
  same pane_lock (the chokepoint retrofit).
- Design spec + unit/live tests (folding asserted at one dispatch).
why: "facade" is generic jargon. The replacement is grounded in real
Python package-naming convention rather than invented: "wrappers" is the
word Flask (wrappers.py: Request/Response over base classes), PyTorch
(torch/_prims_common/wrappers.py), and the stdlib (io.TextIOWrapper)
use for "an object that wraps a lower thing and adds ergonomics" — which
is exactly what these engine-bound, mode-in-the-type classes are, and
what their own docstrings already called them.

Rejected alternatives: "handles" collides with asyncio.Handle (the very
runtime an AsyncPane lives in), file handles, and logging Handlers;
"orm" collides with describing the whole library as a typed ORM;
"proxies" has only class-level pedigree (no studied lib names a *package*
proxies) and mis-signals (these build and run typed Operations, they do
not transparently forward to a referent) — and types.MappingProxyType
already means "read-only view" one directory over in ops/.

what:
- git mv experimental/facade -> experimental/wrappers (+ the test dir
  and the two scope-named test files).
- Rewrite imports experimental.facade -> experimental.wrappers and
  reword the layer's prose (facade/handle -> wrapper) in docstrings and
  docs/experimental.
- Add the "typed proxy over the Core spine" lineage (the SQLAlchemy
  AsyncSession parallel) to the package docstring, where the insight
  belongs rather than in the directory name.
- Class names (Eager*/Lazy*/Async* x Server/Session/Window/Pane/Client)
  are unchanged.

Naming chosen by a 10-agent study of SQLAlchemy/Django/CPython/tmux and
the broader ecosystem under ~/study, with adversarial review.
why: an orchestrator wants the fleet's state visible at a glance, and the
references' most common surface is the tmux status line. We can render it as
an "instantly rendering UI" -- read every agent's state from the in-process
store (zero tmux calls) and paint the whole fleet with a SINGLE set-option.

what:
- Add experimental/agents/statusline.py: pure render_status_line(agents) ->
  compact per-state tally in attention order (labels overridable);
  status_line_op() building the lone set-option; and async
  paint_status_line(engine, source) that reads agents from a monitor (0 calls)
  or a pure sequence and dispatches exactly one set-option for status-right.
- A tmux-native render surface parallel to the floating HudRenderer; owns the
  mechanism (read at zero cost -> one dispatch), not the format.
- Export from the agents package. Tests: pure tally + empty + custom labels +
  op shape + a recorder proving exactly one write + a live read-back of
  status-right against real tmux.
why: the sync ControlModeEngine has the same throwaway-session leak as the
async one -- a bare `tmux -C` connect implies new-session, littering the target
server. Fix both engines so the defect is gone everywhere, not just the async
path the MCP happens to use.

what:
- Add ControlModeEngine._reap_own_session(): after connect (still on the
  phantom, before any attach), set `destroy-unattached on` on the current
  session via the low-level _write/_read_blocks inside _ensure_started's locked
  section, reading and discarding its result block so it can't desync the next
  command (a re-entrant self.run() would deadlock on _lock).
- Live tests mirroring the async ones: the phantom carries destroy-unattached
  and is reaped on close.
why: the settle monitor is deliberately needle-free, but "wait until the pane
prints READY / the sentinel line appears" is a distinct, common await -- a server
coming up, a build banner, a prompt. Resolving the instant it appears returns
sooner than waiting for the idle gap (the "instantly" north star).

what:
- accumulate_until_settle gains an optional `needle` (a str matched literally, or
  a compiled re.Pattern searched) and a new `matched` SettleReason; it short-
  circuits the moment the accumulated output matches, before the idle/byte/time
  caps. None keeps the pure needle-free settle unchanged.
- Thread `needle` through the MCP `wait_for_output` tool.
- Tests: literal, regex, cross-chunk match, and no-needle-still-settles; doctest.
why: The experimental user-facing domain layer should use Pythonic
object naming and avoid facade or handle terminology. The branch has not
shipped this import surface, so the clean package name can land without a
compatibility alias.

what:
- Rename libtmux.experimental.wrappers to libtmux.experimental.objects
- Update imports, tests, and package prose for the object surface
- Add an import guard for the experimental objects package roots
why: Agents need a one-call path for running shell commands in panes and
observing the live result. Composing send and wait behind the pane drive
lock avoids extra backend calls and prevents interleaved input.

what:
- Add run_in_pane over the existing SendKeys operation and output monitor
- Extract the wait_for_output monitor body for reuse
- Register and document the new MCP tool in prompts, instructions, and docs
why: Agent orchestration needs a first-class state for completed
turns that need review, distinct from idle shells and approval waits.

what:
- Add AgentState.DONE to parsing, rollups, status-line rendering, and waits
- Emit done from Claude/Codex turn-complete hooks and keep done send-ready
- Cover store, MCP, hook, wait, drive, and query handling for done
why: Agents can now submit and build a family of declarative workspaces in
one backend call while keeping the chainable engine and planner in charge of
dispatch cost.

what:
- Add WorkspaceSet compile/build APIs with SlotRef and host-step rebasing
- Add workspace_status projections over snapshots and agent state
- Expose build_workspaces through MCP and document the new workspace-set flow
why: Raw tmux %output notifications are silent until the control-mode client
attaches to a session, so output-source event tools could appear healthy while
returning empty streams.

what:
- Add target-aware attachment for watch_events and poll_events in output mode
- Report a visible tmux://events error before an output-source pull stream is attached
- Cover push, pull, resource, and live raw-output event paths
why: Agent monitoring already has live state, hooks, and synchronization
primitives, but users need a tmux-like command that boots or reattaches the
agent interface without manual setup.

what:
- Add top-level libtmux.agents module and libtmux-agents console script
- Build the managed console session through the declarative workspace engine
- Add status, hooks, wait, and send CLI verbs backed by existing agent state
- Document the console workflow and cover detached boot/status behavior
why: The console dashboard could stay on "(no agents)" because the CLI
opened the control-mode connection before registering the monitor
subscription, and reconcile did not seed pane options that were already
present.

what:
- Register CLI monitor subscriptions before the first control-mode connection
- Include durable @agent_state and @agent_name values in pane reconciliation
- Add live and reducer coverage for startup and after-start state changes
- Harden the live MCP output test against tmux chunk boundaries
why: The console can start before Claude or Codex lifecycle hooks emit
@agent_state, leaving visible agent panes absent from the dashboard.

what:
- Seed weak process-discovered agent records from pane commands and local descendants
- Let explicit option or OSC state override process-discovered records
- Mark process-discovered agents exited when the agent command disappears
- Add reducer, process-table, and missing-process-table coverage
why: rebasing the agent-monitor branch onto the fluent engine-ops merged
two divergent versions of query.py and the control engines. The mechanical
rebase resolution kept engine-ops's split-type pane handles and supatui's
fuller async engine; this restores what each side dropped.

what:
- Re-graft the agents() query (AgentQuery/agents()/ATTENTION/_query_agents)
  onto the split-type query.py
- Re-add tmux_version() to both control engines (kept supatui's engine,
  which lacked it)
- Export workspace_status from the workspace package
why: The base engine-ops branch renamed the in-memory engine to
MockEngine (from the former name); the agent monitor, statusline, and
drive examples plus their tests still constructed the old async class.

what:
- Update the async in-memory engine references to AsyncMockEngine in
  agents doctests (monitor, statusline, drive) and their tests
@tony tony force-pushed the engine-ops-supatui branch from 6332ab0 to ca13dfd Compare July 5, 2026 23:23
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.

1 participant