Skip to content

Commit 941ca65

Browse files
committed
Point parent wakes at the send_input target field
1 parent 7534d8b commit 941ca65

6 files changed

Lines changed: 9 additions & 7 deletions

File tree

docs/ARCHITECTURE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,9 @@ Three distinct concepts (do not conflate them):
209209

210210
The **`spawn_agent`** tool starts a fleet agent on a separate inference source (tier/profile resolved from settings) and returns immediately with an `agent_id`; **`wait_agents`** collects reports later. Declared fan-out is unlimited: excess dispatches enqueue rather than fail. `run()` is admitted by `src/subagent/admission.ts` (default burst window of 8 is race-avoidance so a 429 freeze can fire before a herd — not a declared-spawn cap). Occupancy is the whole first `run()`, including `wait_agents`. Nested children of an already-admitted parent bypass **capacity** so a nested orchestrator cannot deadlock while holding a slot; they still wait on a provider 429 pause. Drain is FIFO among currently admissible jobs (a paused provider is skipped, not head-of-line for every provider). Resume and followup inference re-enter the same queue. Queued workers report wait/list status `queued` (live, not failed). Lowering capacity never cancels in-flight work. Retryable provider 429s freeze new admits via the shared retry remapper in `createCorbitsRetryPolicy`; `quota_exhausted` does not freeze. `list_agents` remains mailbox-scoped. The dispatch brief separates durable `context`, actionable `prompt`, and optional `goals` (checklist seeds for the _child's_ own `manage_tasks` list). Implement/review dispatches (and their default directors) fail closed without non-empty `success_criteria`. The child returns a structured report (`Summary` / `Findings` / `Blockers` / `Paths`) plus a tools-used footer. Parent and child never share a `manage_tasks` list.
211211

212-
Workers ask the spawning parent with **`ask_director`** (not the human). That parks a question while the worker stays `running`. **`wait_agents`** returns `awaiting_director` with a question payload — that is not terminal. The parent answers with **`send_input`**, then **`wait_agents`** again. The TUI runner publishes an authoritative snapshot of currently pending top-level questions on each store notification, including empty snapshots before fleet-count updates. During synchronous session rotation, a runner-owned barrier suppresses both publications before delivery-generation invalidation, transcript clearing, and worker cancellation; successful reset reconciles a fresh snapshot before resuming asynchronous backend rebuild. The bridge drops resolved, cancelled, replaced, terminal, and removed asks and delivers each session/question identity once while pending. A coalesced wake starts only when the parent is not processing and every operator gate is closed, including parent-idle fleet holds where the shell stays busy. Worker gates do not manufacture parent processing. Replies target the worker session ID through `send_input`, never its shared catalog ID. Synthetic wakes use `SessionPort.deliver` through queued-delivery's idle-send path without entering the user follow-up queue or composer `/feedback` capture. Escalate to the human with **`ask_operator`** only when the parent cannot resolve it.
212+
Workers ask the spawning parent with **`ask_director`** (not the human). That parks a question while the worker stays `running`. **`wait_agents`** returns `awaiting_director` with a question payload — that is not terminal. The parent answers with **`send_input`**, then **`wait_agents`** again. Escalate to the human with **`ask_operator`** only when the parent cannot resolve it.
213+
214+
When the parent TUI is not blocked in `wait_agents`, the runner publishes an authoritative snapshot of currently pending top-level questions on each store notification, including empty snapshots before fleet-count updates. During synchronous session rotation, a runner-owned barrier suppresses both publications before delivery-generation invalidation, transcript clearing, and worker cancellation; successful reset reconciles a fresh snapshot before resuming asynchronous backend rebuild. The bridge drops resolved, cancelled, replaced, terminal, and removed asks and delivers each session/question identity once while pending. A coalesced wake starts only when the parent is not processing and every operator gate is closed, including parent-idle fleet holds where the shell stays busy. Worker gates do not manufacture parent processing. Replies use `send_input`'s `target` field with the worker session ID, never its shared catalog ID. Synthetic wakes use `SessionPort.deliver` through queued-delivery's idle-send path without entering the user follow-up queue or composer `/feedback` capture.
213215

214216
When profiles exist (local `.agents/agents/` and/or enabled **`kind: "agent"`** plugins, including **data-only** markdown plugins with no `index.ts`), the chat model also receives **`search_agents`** — a lexical index over profile id, description, and role text so the model can discover ids before calling `spawn_agent(agent=...)`. Results include each match's full loaded system prompt / body so the parent can inspect plugin or Claude marketplace agents without `read_file` on paths outside the session cwd (path-escape blocks those roots by design; writes remain blocked). `spawn_agent` and `search_agents` are core tools on the primary session.
215217

docs/TUI.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ An `ask_director` lane stays live and reads as waiting on the director, not stal
224224
The runner snapshots currently pending root-worker questions, dropping resolved,
225225
cancelled, replaced, terminal, or removed asks before delivery. It sends one
226226
coalesced wake when the parent is not processing and all operator gates are closed,
227-
even while live workers hold the shell busy. Replies use `send_input` with the
228-
worker's session ID, not its shared catalog ID. Each session/question identity is
227+
even while live workers hold the shell busy. Replies use `send_input`'s `target`
228+
field with the worker's session ID, not its shared catalog ID. Each session/question identity is
229229
delivered once while pending; the strip never re-delivers it. Synthetic wakes use
230230
the idle delivery path, bypassing composer `/feedback` capture and leaving queued
231231
user follow-ups untouched.

src/subagent/fleet-report.ask-wake.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe("pendingAskWakeText", () => {
7777
expect(text).toContain("Which port?");
7878
expect(text).toContain("q1");
7979
expect(text).toContain("send_input");
80-
expect(text).toContain("targeting agent_id a1");
80+
expect(text).toContain("using target a1");
8181
expect(text.toLowerCase()).toContain("worker");
8282
});
8383
});

src/subagent/fleet-report.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export function pendingAskWakeText(wake: PendingAskWake): string {
165165
"",
166166
wake.question,
167167
"",
168-
`The worker — not the operator — raised this. Answer it with send_input (soft) targeting agent_id ${wake.sessionId}; do not relay to the operator unless it genuinely needs them.`,
168+
`The worker — not the operator — raised this. Answer it with send_input (soft) using target ${wake.sessionId}; do not relay to the operator unless it genuinely needs them.`,
169169
].join("\n");
170170
}
171171

src/tui/runner/wiring.ask-wake.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ test("same catalog workers answer by session, reconcile one resolution and repla
301301
ask("session-two", "replacement-question");
302302
bridge.handle({ type: "inference.done", data: {} });
303303
expect(sends).toHaveLength(1);
304-
expect(sends[0]).toContain("targeting agent_id session-two");
304+
expect(sends[0]).toContain("using target session-two");
305305
expect(sends[0]).toContain("replacement-question");
306306
expect(sends[0]).not.toContain("question-session-two");
307307
expect(sends[0]).not.toContain("question-session-one");

src/tui/runner/wiring.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function createFleetWakePublisher(
6464
// Reconcile even an empty snapshot before a fleet drop can settle the parent.
6565
const asks = pendingAskSnapshot(lanes, (id) => sessions.peekAsk(id));
6666
emitter.emit("event", { type: "agent-ask", asks });
67-
const fleet = liveFleetCount(sessions.list());
67+
const fleet = liveFleetCount(lanes);
6868
if (fleet !== lastLiveFleet) {
6969
lastLiveFleet = fleet;
7070
emitter.emit("event", { type: "fleet", running: fleet });

0 commit comments

Comments
 (0)