| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Codecov Report❌ Patch coverage is 74.00706% with 1178 lines in your changes missing coverage. Please review. @@ Coverage Diff @@
## engine-ops #692 +/- ##
===============================================
+ Coverage 52.44% 78.18% +25.74%
===============================================
Files 26 275 +249
Lines 3726 16175 +12449
Branches 747 2019 +1272
===============================================
+ Hits 1954 12646 +10692
- Misses 1468 2798 +1330
- Partials 304 731 +427 ☔ View full report in Codecov by Harness.
|
Sorry, something went wrong.
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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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
why: tmux does not forward pane output verbatim through control mode -- it writes every non-printable byte, and the backslash itself, as a backslash plus three octal digits. An agent's OSC 3008 state escape therefore reached the monitor as the four characters \,0,3,3 rather than ESC. OscSignal's regex matches a raw ESC byte, which real %output never contains, so the remote/SSH agent-state channel never fired against a live tmux server. Every existing test passed because they hand-feed raw ESC bytes that tmux would never deliver. what: - Feed AgentMonitor.ingest's %output payload through engines.base.unescape_control_output (the inbound counterpart to render_control_line) before OscSignal, so the decoder sees the bytes the pane actually wrote. - Add tests/experimental/agents/test_wire_protocol.py: the first test that crosses both ends of the protocol, every payload produced by the real encoder and consumed by the real decoder, including an end-to-end case that drives emit()'s bytes through a real pane pty, tmux, and control mode into the monitor. - Pin four remaining encoder/decoder desyncs as strict xfail: an unescaped ';', BEL, or ESC in a name corrupts the reading, and a name can forge its own state (last-wins parse of an unescaped payload).
why: the agent-state protocol was specified twice, independently. The encoder spelled the OSC number, the delimiters and the terminator in hooks/emit.py; the decoder spelled them again in signals.py; and the @agent_state / @agent_name option keys were repeated a third and fourth time in monitor.py and tree.py. Nothing tied the two ends together, so a one-sided edit could not go red. what: - Add agents/protocol.py: one Payload plus encode/decode for each channel, built from a single set of constants. The decoder's regex is compiled from the very literals the encoder writes, and the refresh-client subscription spec is derived from the option name it subscribes to, so there is nowhere left to put a one-sided change. - Reduce hooks/emit.py to a channel choice over the codec, signals.py to buffering policy over decode_osc, and have monitor.py and tree.py take the option keys from the codec rather than restating them. - Keep the decoder fragment-aware: decode_osc returns the unconsumed tail, so buffering policy stays with the caller and the grammar stays in one place.
why: the OSC payload is a ';'-delimited, '='-paired body and the decoder takes the last value for a repeated key, but values went onto the wire raw. An agent name is attacker-influenced, so a name could inject a second state= pair and overwrite the state it was reported with: emit(state="running", name="x;state=idle") decoded as IDLE. Any agent able to influence its own name could forge its own state. The same hole truncated a name at a ';', and a name carrying BEL or ESC -- both OSC terminators -- cut the escape short or dropped the reading entirely, losing the state and not merely the name. what: - Percent-encode payload values in encode_payload and decode them in decode_payload, so no character of a value can be structural. Benign values are unreserved, so the common wire form is unchanged and a payload written by an older agent still reads. - Escape only the OSC payload: the option channel carries its values as argv and format fields, which have no delimiter to smuggle, so both channels still decode a name identically. - Retire the four xfails this closes and cover two more hostile names: a '=' in a name, and a name that merely looks like a percent-escape (it must not be double-decoded).
why: The temp-file + fsync + os.replace crash-safe writer was typed out three times (the store, the Claude hook, the Codex hook) -- three chances to forget the fsync or leak the temp file on error. The "resolve an agent source" reader and the attention ladder were split across query.py, workspace/status.py and agents/statusline.py, each with its own copy of the AgentSource union. what: - Add agents/_atomic.py `atomic_write_text`; the store and both hook installers write through it - Add agents/source.py: the AgentSource union, `agent_rows`, ATTENTION and the ATTENTION_ORDER derived from it -- one leaf module the query layer, the workspace status and the statusline all read - query.py re-exports AgentSource/ATTENTION, so its public surface is unchanged
why: hooks/base.py declared a "canonical" neutral event -> state map that nothing translates through. Every hook installer carries its own vocabulary map (_CLAUDE_EVENT_STATE, _CODEX_EVENT_STATE) and writes the state string straight into its config, so EVENT_STATE was a second source of truth with no reader -- and its only test asserted the literal it was declared with. what: - Drop EVENT_STATE from agents/hooks/base.py; the module is now just the AgentHook protocol, and says so - Drop test_event_state_map_is_canonical (a tautology over the deleted map; the per-agent maps are covered by the installer tests)
why: The workspace-set MCP surface lands on top of the plan tier, so it re-typed the same two shapes the plan tier had just consolidated: the "project a build result into a JSON outcome" step twice, and the "render the outcome as the JSON object a tool returns" dict twice more in the adapter. what: - Give WorkspaceSetOutcome a to_dict(); the adapter's sync and async build_workspaces tools return it - Add plan_tools._to_set_outcome; build_workspaces/abuild_workspaces project through it, matching _to_outcome on the plan path
| Back | FazBrowse Home | New Git URL |
Summary
Stacks on #690. Everything is additive under libtmux.experimental — no existing public API is touched, and the package is mypy-strict clean.
Changes by area
src/libtmux/experimental/agents/ (new package)
src/libtmux/experimental/engines/async_control_mode.py
src/libtmux/experimental/ops/_ops/refresh_client.py
src/libtmux/experimental/mcp/
CHANGES
Design decisions
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/agentsThe remote signal is written to the pane tty, not stdout (which agent hooks capture):
$ rg -n "/dev/tty" src/libtmux/experimental/agents/hooks/emit.pyAsync 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.pyMCP agent tools are gated behind an engine capability check:
$ rg -n "supports_monitor" src/libtmux/experimental/mcpTest plan
Refs #688, #689. Builds on #690.