Skip to content

Commit bec1b17

Browse files
Unmount wait_agents on TUI primary and nested orchestrators (#943)
* Unmount wait_agents on TUI primary and nested orchestrators * Scope remaining wait-path copy to the exec primary surface TUI and nested runs collect through mailbox mail, so every prompt, doc, and comment that still teaches the wait path as live now says which surface it applies to. Drops the readerless queued-steer yield flag whose comment cited the unmounted wait. * Reword leftover TUI wait-yield copy as exec-scoped session wake * Correct wait_agents yield copy to supported-but-unwired actuality
1 parent 94d7cc4 commit bec1b17

36 files changed

Lines changed: 367 additions & 105 deletions

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1515

1616
### Changed
1717

18+
- `wait_agents` is now exec-primary opt-in (`mountWaitAgents` in
19+
`createAgentToolset`; CL-7678). The TUI primary and nested orchestrators no
20+
longer mount it and collect fleet results through mailbox mail instead:
21+
spawn then idle, do not poll. Headless `corbits exec` keeps it mounted with
22+
an advertised allow.
1823
- Headless `corbits exec` unmounts `ask_operator` when stdin/stdout are not
1924
TTYs instead of advertising a cancel stub. TUI Skywalker still mounts it;
2025
TTY exec still prompts on stdin.

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ Details live in `docs/PRODUCT.md` (safety model) and `docs/ARCHITECTURE.md`
154154
Corbits Code is a single-process CLI built on Interchange primitives. The primary
155155
session is always the **orchestrator** (Skywalker): it can act directly and
156156
delegates substantial work through a closed director fleet via `spawn_agent`
157-
then idle (mailbox mail inbound), `search_agents`, and optional `wait_agents`
158-
for nested orchestrators.
157+
then idle (mailbox mail inbound) and `search_agents`. `wait_agents` stays
158+
mounted only on headless `corbits exec` primary runs.
159159

160160
```
161161
CLI (src/index.ts)

‎docs/ARCHITECTURE.md‎

Lines changed: 11 additions & 11 deletions
Large diffs are not rendered by default.

‎docs/IMPLEMENTATION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ Unmatched shell auto-allows, including contained non-force `git worktree add`/`r
201201

202202
- **Alt+Enter** queues a follow-up (kind `"queue"`) delivered only on **session-idle** — parent-idle **and** no live fleet lanes (`run` goes idle). Session-idle Alt+Enter is a no-op. **Ctrl+C** stops the run.
203203

204-
Idle-with-fleet is shipped: after a non-blocking `spawn_agent` dispatch the parent turn can settle while workers keep running. The runner emits a `fleet` event carrying the live-lane count; the bridge holds the run busy on that count, so mid-hold Enter upgrades to a new primary turn (sent immediately) instead of queueing a steer, follow-ups keep waiting for true session-idle, and any steer left pending at the hold's engagement delivers immediately — the parent it was steering has already stopped. While the hold is up and the parent is not processing, occupancy flushes mailbox mail (`driveMailboxMail` + `buildMailboxMailMessage`) on store subscribe and idle-with-fleet settle — one child done while siblings run is enough. Skip that shot when a fleet-dry open-task continuation is latched. TUI-primary `wait_agents` gets `shouldYieldWait` (queued steer or uncollected mail/ask) and finishes as a timeout without taking workers.
204+
Idle-with-fleet is shipped: after a non-blocking `spawn_agent` dispatch the parent turn can settle while workers keep running. The runner emits a `fleet` event carrying the live-lane count; the bridge holds the run busy on that count, so mid-hold Enter upgrades to a new primary turn (sent immediately) instead of queueing a steer, follow-ups keep waiting for true session-idle, and any steer left pending at the hold's engagement delivers immediately — the parent it was steering has already stopped. While the hold is up and the parent is not processing, occupancy flushes mailbox mail (`driveMailboxMail` + `buildMailboxMailMessage`) on store subscribe and idle-with-fleet settle — one child done while siblings run is enough. Skip that shot when a fleet-dry open-task continuation is latched. Exec-primary `wait_agents` mounts with no yield predicate (`createWaitAgentsTool({ sessions, fleetRecords })`) and blocks to ready/timeout/abort; `shouldYieldWait` remains a supported-but-unwired tool option (no production mount passes it).
205205

206206
`src/tui/stream-event-map.ts` maps reactor events onto the bridge's inbound events, and `src/tui/turn-state.ts` tracks the turn's status. `src/tui/turns-to-blocks.ts` hydrates a resumed session's stored turns into the same content blocks.
207207

‎docs/PRODUCT.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ The evidence is in how the product fails today: the personas already produce exc
4141
5. **Resume capability** — Runs persist to a git-backed store and resume from the last point after interruption.
4242
6. **Legible loop** — A live event log, working-tree diff panel, plan tracker, and real-time cost meter show what happened, when, and why.
4343
7. **Operator-in-the-loop** — The agent can call `ask_operator` to pause and ask a clarifying question; the operator answers from a modal (TUI). Headless `corbits exec` unmounts `ask_operator` when stdin/stdout are not TTYs. TTY exec still reads a single line from stdin.
44-
8. **Mid-run steering** — Two modes while the agent is running, keyed to **whose** idle. **Parent-idle** is when the primary Skywalker turn is not inside an in-flight parent tool; **session-idle** is parent-idle **and** no live fleet lanes. **Enter** soft-steers while the parent is busy — delivers at the next **parent** `tool.boundary` without stopping the current run; a long parent `run_shell` or an awaiting `wait_agents` is parent-busy, so Enter is a queued steer, not a new turn. An in-flight TUI-primary `wait_agents` yields as a timeout when that steer is queued so occupancy can deliver it. Idle-with-fleet is shipped: after a non-blocking `spawn_agent` dispatch the parent goes idle while workers keep running, mailbox mail arrives as inbound when a worker finishes or fails, and mid-hold Enter starts a new primary turn instead of queueing a steer. **Alt+Enter** queues a follow-up delivered only on session-idle (`run` goes idle; does not interrupt). Session-idle Alt+Enter is a no-op. **Ctrl+C** stops the run outright. The notice row shows distinct `steer N` / `follow-up M` badges; when steers are pending and a parent tool has been in flight a few seconds, the notice names that command. Shortcuts are listed in `/help` (`Enter` soft-steer · `Alt+Enter` follow-up · `Ctrl+C` stop).
45-
9. **Orchestrator-only (TUI + exec)** — The primary session is always the orchestrator: it can act directly and delegates via `spawn_agent` (then idle; mailbox mail inbound) / `search_agents`. Nested orchestrators still collect with `wait_agents`. Long jobs belong on workers — a parent that runs them itself stays parent-busy and holds Enter steers. Single-agent session mode, the first-run mode picker, and Settings → Session are gone (CL-5814). Legacy `sessionMode` values on disk are ignored.
44+
8. **Mid-run steering** — Two modes while the agent is running, keyed to **whose** idle. **Parent-idle** is when the primary Skywalker turn is not inside an in-flight parent tool; **session-idle** is parent-idle **and** no live fleet lanes. **Enter** soft-steers while the parent is busy — delivers at the next **parent** `tool.boundary` without stopping the current run; a long parent `run_shell` is parent-busy, so Enter is a queued steer, not a new turn. A queued steer delivers at the next parent `tool.boundary` so occupancy can pick it up. Idle-with-fleet is shipped: after a non-blocking `spawn_agent` dispatch the parent goes idle while workers keep running, mailbox mail arrives as inbound when a worker finishes or fails, and mid-hold Enter starts a new primary turn instead of queueing a steer. **Alt+Enter** queues a follow-up delivered only on session-idle (`run` goes idle; does not interrupt). Session-idle Alt+Enter is a no-op. **Ctrl+C** stops the run outright. The notice row shows distinct `steer N` / `follow-up M` badges; when steers are pending and a parent tool has been in flight a few seconds, the notice names that command. Shortcuts are listed in `/help` (`Enter` soft-steer · `Alt+Enter` follow-up · `Ctrl+C` stop).
45+
9. **Orchestrator-only (TUI + exec)** — The primary session is always the orchestrator: it can act directly and delegates via `spawn_agent` (then idle; mailbox mail inbound) / `search_agents`. Nested orchestrators collect through mailbox mail the same way. Long jobs belong on workers — a parent that runs them itself stays parent-busy and holds Enter steers. Single-agent session mode, the first-run mode picker, and Settings → Session are gone (CL-5814). Legacy `sessionMode` values on disk are ignored.
4646

4747
## User Experience
4848

@@ -170,7 +170,7 @@ Corbits Code fans work out to short-lived **fleet agents** — workers with thei
170170

171171
- **Agents** are runtime entities (primary session or child).
172172
- **Tasks** are checklist items owned by one agent via `manage_tasks`.
173-
- **Fleet agents** are spawned with `spawn_agent`. On the TUI primary, mailbox mail arrives as inbound when a worker finishes or fails — do not poll `wait_agents`. Nested orchestrators still collect with `wait_agents`. Workers ask the parent with `ask_director`. That parks a question while the worker stays `running`. Nested `wait_agents` returns `awaiting_director` with a question payload — that is not terminal. Once a parked ask is surfaced, `list_agents` fails closed until `send_input` answers or the ask is dropped — do not poll `list_agents`. The parent answers with `send_input` (`target` = the worker's session id). When the parent TUI is not blocked in `wait_agents`, a parked question arrives as a synthetic idle-send wake. Escalate to the human only with `ask_operator`.
173+
- **Fleet agents** are spawned with `spawn_agent`. On the TUI primary, mailbox mail arrives as inbound when a worker finishes or fails — spawn then idle; do not poll. Nested orchestrators collect through mailbox mail the same way. Workers ask the parent with `ask_director`. That parks a question while the worker stays `running`. On an exec-primary run, `wait_agents` returns `awaiting_director` with a question payload — that is not terminal. Once a parked ask is surfaced, `list_agents` fails closed until `send_input` answers or the ask is dropped — do not poll `list_agents`. The parent answers with `send_input` (`target` = the worker's session id). On the TUI primary, a parked question arrives as a synthetic idle-send wake. Escalate to the human only with `ask_operator`.
174174

175175
Dispatch uses a structured brief (context / goal / optional goals seed) and returns a structured report. The TUI Agents strip and fleet board show who is running; live tool progress updates the status bar without dumping the child transcript into the parent chat. There is no turn budget. A tool-less final turn completes only with the four-heading report envelope. Printed `<tool_call>` markup in assistant text gets one corrective nudge to issue a real tool call and does not count as the wrap-up; without the envelope, one incomplete-report nudge is given and a second tool-less turn without the envelope salvages as `incomplete-report-stop`. Counsel and `intent=plan` also require plan substance in Findings (files/paths, acceptance criteria, non-goals, risks, ordered steps); headings-only or stub Findings salvage as `incomplete-report`, not an attachable plan. A silent worker (no activity for `stallTimeoutMs`, opt-in) gets one continuation nudge, then salvages as `stalled` only after a full `stallTimeoutMs` grace with still no activity — queued checks inside that window wait, they do not salvage. An opt-in `deadlineMs`, or an operator cancel, can also end a run early. Each of these returns a salvage report so a runaway or idle child cannot quietly burn a large token budget or look done after prose alone.
176176

‎docs/TUI.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -628,9 +628,9 @@ Two mid-run gestures, two delivery times (CL-6290):
628628
- **Enter, mid-run** — soft steer: enqueues kind `"steer"` and delivers at the
629629
next **parent** `tool.boundary` (the parent tool finishing, not a child) via
630630
`Agent.deliver` into the live reactor, not a new `send`. A
631-
long parent `run_shell` or an awaiting `wait_agents` is parent-busy and holds
632-
steers. An in-flight TUI-primary `wait_agents` yields as a timeout when a
633-
steer is queued so occupancy can deliver it. The transcript row says
631+
long parent `run_shell` is parent-busy and holds steers. A queued steer
632+
delivers at the next parent `tool.boundary` so occupancy can pick it up.
633+
The transcript row says
634634
`[will steer next]` while pending and
635635
`[steering]` once delivered (`submitPrompt`, `drainSteersAtBoundary` in
636636
`runtime-bridge.ts`). If the captured target agent is already closed when

‎scripts/eval-capability.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,8 @@ export async function buildEvalDiagnostics(
686686
// Capability evals run through exec; they are non-TTY, so ask_operator
687687
// is unmounted the same way the runner does when interactive is false.
688688
operatorAvailable: false,
689+
// ...and wait_agents stays mounted the way the exec runner mounts it.
690+
waitAgentsMounted: true,
689691
});
690692
return {
691693
advertisedTools,

‎src/agent/agent-search.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function formatAgentSearchResults(
111111
"",
112112
...entries.flatMap((entry, i) => (i === 0 ? [entry] : ["", entry])),
113113
"",
114-
"Spawn with spawn_agent(description, prompt, agent=<id>). For a team, call spawn_agent once per member (parallel in one turn when independent), then reply and idle — mailbox mail arrives as inbound. Nested orchestrators still collect with wait_agents.",
114+
"Spawn with spawn_agent(description, prompt, agent=<id>). For a team, call spawn_agent once per member (parallel in one turn when independent), then reply and idle — mailbox mail arrives as inbound. wait_agents is mounted on exec-primary runs only.",
115115
].join("\n"),
116116
);
117117
}

‎src/agent/directors/greybeard/package.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ describe("greybeardPackage", () => {
104104
const allow = greybeardPackage.tools?.allow ?? [];
105105
expect(allow).not.toContain("task");
106106
expect(allow).toContain("spawn_agent");
107-
expect(allow).toContain("wait_agents");
107+
// CL-7678: nested orchestrators collect through mailbox mail; wait_agents
108+
// is exec-primary opt-in, so it stays off the Greybeard allow.
109+
expect(allow).not.toContain("wait_agents");
108110
// CL-7051: search_agents is Skywalker-only — nested directors spawn from allowlist.
109111
expect(allow).not.toContain("search_agents");
110112
expect(allow).toContain("write_file");

‎src/agent/directors/greybeard/package.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Judge the approach:
3333
4. Rank risks for long-term maintainability and backward compatibility.
3434
5. Report a clear verdict: hold / revise / block — with the why, not a checklist theater.
3535
36-
Spawn only when a concrete unknown blocks that judgment. Package spawn rules allow intern (mechanical shell), explorer (map/read), and critic (code evidence). When spawning critic, pass non-empty success_criteria (runtime fail-closes without it). intern and explorer remain optional. Prefer doing the review yourself with mounted read/search tools. Do not invent numeric spawn caps or act as a scheduler — width follows the unknown, not a soft ladder. Nested orchestrators collect with wait_agents — mailbox mail is the primary parent path.
36+
Spawn only when a concrete unknown blocks that judgment. Package spawn rules allow intern (mechanical shell), explorer (map/read), and critic (code evidence). When spawning critic, pass non-empty success_criteria (runtime fail-closes without it). intern and explorer remain optional. Prefer doing the review yourself with mounted read/search tools. Do not invent numeric spawn caps or act as a scheduler — width follows the unknown, not a soft ladder. Spawn then idle; reports arrive as mailbox mail — do not poll.
3737
3838
Blinders: do not call search_agents to discover the fleet (even when nested). You already know the limited spawn set; stay inside it. Do not spawn builder, counsel, skywalker, or other directors outside the allowlist.
3939

0 commit comments

Comments
 (0)