ctl C2: send (queue-until-idle) + status + subtree scoping + --here - #3
Merged
Conversation
…g + --here Second milestone of the amux-ctl control plane. A pane can now task and observe the hierarchy, not just build it. Stacked on C1 (feat/ctl-c1). Added: - `ctl send <target> <text>` — deliver a task to a worker as a submitted prompt. Queue-until-idle (Decision 4): if the target is mid-turn (agsess Working/ WaitingApproval), the text is held and delivered — text, then Enter after a short beat (mirrors the C0 spike) — once it reaches WaitingPrompt/Idle. An unbound/non-agent target gets a 2s fallback so the queue never wedges. Target is a pane id or role label (ctl::resolve_target). - `ctl status [target]` — agsess-backed: one target, or the caller's subtree. - `ctl spawn --here` — tile the worker beside its caller (layout::split_pane splits at the caller's pane, not just focus), so a lead + its ICs share a view. - Subtree scoping (Decision 3): send/status on a target are restricted to the caller's subtree; a root/operator pane controls all. Pure ctl::in_subtree guard; out-of-subtree is a clean JSON refusal. Run loop: a PendingSend queue flushed each tick (flush_sends); apply_ctl grew send/status handling, target resolution, scope_denied, and the --here split path (spawn_worker_here). No behavior change without --allow-ctl. Verified: `python dev.py check` green — 119 lib + 46 integration tests. New: resolve_target (id/role/ambiguous), in_subtree (parent-chain walk), split_pane, send/status build↔parse roundtrips, and live e2e — status reply, a real send delivered to a worker (checked across a window switch so the caller's command echo can't false-positive), and --here spawn. fmt + clippy clean. Not yet (C3): ctl kill, identity delegation scoping, audit log.
JSBtechnologies
added a commit
that referenced
this pull request
Aug 30, 2026
…(0.8.1) A ctl spawn (new window) and --here split sized the worker's pty to a rough estimate but never resized the window, so the agent painted short (content at top, blank below) until a manual terminal resize. Both paths now call resize_window right after spawning, matching the interactive split handlers. Fixes founder repaint reports #1 (new-window spawn) and #3 (--here didn't push to bottom). Verified: python dev.py check green (119 lib + 46 integ).
JSBtechnologies
added a commit
that referenced
this pull request
Sep 4, 2026
…fied on real Windows] Fills the src/reap.rs cfg(not(unix)) stub with the durable Windows teardown layer from the design spec: one Job Object per session with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. Every pane is assigned to it; amux holds the sole non-inheritable handle for its whole life; the kernel terminates every process in the job — children inherit it, so grandchildren too — the instant that handle closes, however amux exits, TerminateProcess included. That is the one death mode no unix layer can match. - SessionJob (src/reap.rs): no-op on unix; on Windows creates the job (kill-on-close, non-inheritable handle per R2), assign() adds a pane, Drop closes the handle. Win32 externs + constants + repr(C) structs (IoCounters / JobBasic / JobExtended) transcribed from winnt.h; usize for SIZE_T/ULONG_PTR (correct on 32- and 64-bit). - main.rs: session_job created once in run(), held for the session, each new pane assigned in the registry loop. spawn_watchdog + its var are now cfg(unix) — on Windows the job replaces the watchdog, which there could not signal a group and would block on its pipe forever (R5). amux reap stays as the near-inert manual path. - R1: create/assign failures degrade to a debug warning and a null-handle no-op; a pane is never refused. GetLastError captured immediately after each failing call (before CloseHandle) per review. VERIFIED ON REAL WINDOWS (what the spec's macOS author could not do): acceptance corruption), #2 clean-quit kills the pane's grandchild, #3 taskkill /F of amux still takes the tree down via kill-on-close, #6 full dev.py check green (237 tests). R4 confirmed: hard-killing amux leaves no conhost residue. Adversarially reviewed (rust-reviewer): verdict ship, layout exact on both bitwidths, handle inheritance correctly prevented, every failure path a safe no-op, no double-close. Also (pre-existing, distinct): fixed closing_an_overlay_repaints_the_pane, which shipped in the unix-teardown work typing a bash-only marker into cmd.exe and could never pass on Windows (confirmed failing at clean 43e7a6e). Branched the marker for cmd. Exactly the cargo-check-is-not-a-run gap the spec warned about.
JSBtechnologies
added a commit
that referenced
this pull request
Sep 4, 2026
…#3, #4] #3 — `flush_sends` wrote with `let _ = p.pty.write(text)`. `write(2)` returns a COUNT and may legitimately accept less than offered: a pty's input buffer is finite, and in canonical mode a single line is capped near 1 KB. The code threw that count away, marked the send delivered, and 400 ms later wrote the Enter that submits it — so a long task arrived as a fragment while ctl had already replied ok:true. The review reproduced it four times on its own dispatches: ~800-byte sends vanished, ~70-byte ones worked. PendingSend now carries `written`, advances by what was actually accepted, and stamps `text_written_at` only once the last byte lands, so Enter can never submit a fragment. Resuming across ticks rather than looping until complete is deliberate: this runs on the single event loop, and the review's own suggested `write_all` would block every pane until the target drained its buffer — trading silent truncation for a session-wide freeze, which is finding #5's class. #4 — `sanitize_spawn_argv` was a denylist of three literal names, and a denylist guarding this surface fails by omission. `--allowed-tools` (claude's own documented alias of the governed `--allowedTools`) passed straight through, and so did `--mcp-config`, `--plugin-dir`, `--plugin-url` and `--settings`, each of which reaches code execution OUTSIDE the tool-permission system: an stdio MCP server is a command claude launches at startup, a plugin carries hooks. None of those names appears anywhere else in amux, so a worker could pass one verbatim in every mode, plan included. Inverted to a per-vendor allowlist. `vet_spawn_argv` refuses an unlisted flag outright instead of quietly cleaning it, and refuses an agent stem amux has no trust posture for (gemini, aider, cursor-agent) since it could not cap what such an agent may do. Governed flags still strip-and-report rather than refuse — asking for one is not an attack, it is amux's to set. Scoped narrowly, after a first attempt was too broad: a NON-agent command (sh, cmd) carries no permission flags to escape through, and whether it may be spawned at all is the spawn allowlist's decision, not this function's. The first version refused shells and broke six integration tests that spawn `sh` as a worker — a supported, tested use. Deferring is both correct and narrower. macOS 26.6.2: ./dev.py check green — 310 tests, 0 failures, clippy clean. Type-checks for x86_64-pc-windows-msvc. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KoBz1RcGsUbYdMSCR7LaK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Second milestone of the amux
ctlcontrol plane — a pane can now task and observe the hierarchy, not just build it. Stacked on #2 (C1) — review/merge #2 first; this PR's base isfeat/ctl-c1, so its diff is C2-only.Severe/critical (extends the hosted-agent control surface) → PR + review.
Surface (all still behind
--allow-ctl)How
send= queue-until-idle (Decision 4): if the target is mid-turn (agsessWorking/WaitingApproval) the text is held; once it reachesWaitingPrompt/Idleamux writes the text, then the Enter after a short beat (mirrors the C0 spike). Unbound/non-agent targets get a 2s fallback so a queue never wedges. Flushed each tick viaflush_sends(aPendingSendqueue) — non-blocking, no thread.ctl::resolve_target, ambiguity is a clean error).statusfolds in liveagsessstatus (reuses the bar'sstatus_forpath).--heresplits at the caller's pane (layout::split_pane, not just focus) so a lead + ICs share one tiled window.send/statuson a target are limited to the caller's own subtree; a root/operator pane controls all. Purectl::in_subtreeguard; out-of-subtree → JSON refusal.Safety
No change without
--allow-ctl. Scoping means a worker can't reach a sibling's team.sendnever injects mid-turn. Still agents-only spawn (allowlist unchanged).Verification
python dev.py checkgreen — 119 lib + 46 integration, dep-guard OK, fmt + clippy clean. New coverage:resolve_target(id/role/ambiguous),in_subtree(parent-chain walk),split_pane, send/status build↔parse roundtrips; live e2e:statusreply, a realsenddelivered to a worker (verified across a window switch so the caller's command echo can't false-positive), and--herespawn.Not in C2 (C3)
ctl kill(subtree teardown), credential delegation scoping, audit log.