# Async-default delegation — dispatch returns after acceptance, results are pushed

The `delegate` tool blocks the parent orchestrator for the entire subagent run. During that time the conversation is dead air: pi delivers user steering only after the current assistant turn finishes its tool calls, so a ten-minute worker silences the session for ten minutes, and the orchestrator can neither respond nor re-dispatch. Herdsman solves this by construction — its `delegate` returns "after the task has been atomically recorded", the owner "remains available for other useful work and, for a lead Pi session, continued user interaction", and agent results "resume the session automatically". We adopt that model: dispatch is asynchronous by default, with blocking preserved as an explicit choice.

## Later refinement: live-session ownership (ADR-0020)

[ADR-0020](0020-live-session-run-lifecycle.md) refines the lifecycle and delivery claims below while preserving async-by-default dispatch. Its single live-session owner covers async, blocking, and sequential chain work; the widget projects that owner's state rather than owning a registry. Acceptance may precede spawning because bounded FIFO admission can queue work, and a parallel batch must be admitted atomically before any launch or acceptance.

Post-acceptance failure retains the original run id and an owned terminal outcome. Async delivery is confirmed only by observing the matching `custom_message` in the current parent's in-memory transcript, not by `pi.sendMessage` returning or a generic `message_end`. Unconfirmed outcomes remain visible and retained during the live session. There are no automatic blind retries or retry timers; explicit `retry-delivery` warns of possible duplicates and never reruns a Worker or resends a confirmed insertion. This is not a guarantee of model consumption, disk durability, or restart recovery. Blocking outcomes return only in the tool result; ESC and teardown stop queued as well as active work and suppress pushes. Historical implementation/test statements below describe the original decision, not verification of this refinement.

## Decision

- **Async is the default.** A dispatch spawns the child, registers the run, and returns an **acceptance** (accepted flag, run id, agent, task) immediately. The orchestrator's turn ends or continues with other work; it never waits.
- **One run registry, two wait modes.** The registry behind the fleet widget outlives the tool call — that is the actual refactor. `async: false` reproduces today's blocking behavior verbatim by waiting on the same registry entry: blocking is a mode of waiting, not a separate dispatch path. Async is per-call, never per-task: a parallel dispatch under async dispatches all N tasks asynchronously. Chain stays blocking always, because `{previous}` substitution needs the prior step's result in-process.
- **Results are pushed, not fetched.** A session-level run watcher delivers each settled child's outcome into the parent conversation: auto-starting a turn when the parent is idle, queueing as steering/follow-up when it is mid-turn. Every delivery echoes the run id, correlating it with its acceptance. Deliveries render through a registered custom-message renderer showing a compact push view (status icon, run id, agent, task, first output line; the full delivered text is what the model receives and what the view expands to) — deliberately lighter than the delegate tool's full result view, whose transcript already carries the run's tool-call detail. The orchestrator never polls — herdsman's doctrine: do not inspect, steer, or keep the turn alive merely to check progress.
- **Failure semantics split at acceptance.** Synchronous validation errors (unknown agent, invalid parameters, `async: true` with `chain`) return an error result at dispatch time — never an acceptance. Post-acceptance failures (child fails to boot, dies mid-run, turn budget exhausted) arrive as deliveries carrying ADR-0012's three-state status. No dispatch can leave the orchestrator waiting for a result that never comes.
- **New control and discovery actions.** `status` lists live runs (run id, agent, task, elapsed) without touching anything; `cancel` kills one run (SIGTERM, SIGKILL escalation — same as the fleet kill). Cancelling an unknown or already-finished run id returns an informative error result.
- **Child lifecycle decouples from the tool-call AbortSignal.** Today the delegate call's abort signal doubles as a child-lifecycle handle; under async the tool call ends before the children do. Abort handling therefore stays where it already belongs — the session level: ESC and session shutdown invoke the fleet kill (SIGTERM with SIGKILL escalation after 3s), unchanged.
- **Crash safety is child-side and untouched.** Children self-terminate within 5 seconds of parent death via the ppid heartbeat watchdog (index.ts) — SIGKILL-proof, because a crashed parent gets no cleanup callback. Accepted theoretical limit: PID reuse within the 5-second probe window defeats one probe. Stdin EOF is not usable as a death signal (RPC children are spawned with all three stdio pipes open), which is why the heartbeat exists.

## Later refinement: event-bus integration (ADR-0021)

[ADR-0021](0021-shared-wait-agent-leases.md) adds a session-scoped event-bus provider so other extensions can collect agent results through leases, preserving async defaults and parent-abort fleet cancellation.

## Considered options

- **Opt-in async parameter, blocking default** (the round-1 recommendation): rejected by product decision — it keeps the pain as the default and forks the tool-result shape in the common case instead of the escape hatch.
- **Always-async, push-only, no blocking mode**: rejected — quick lookups whose result integrates immediately are legitimately better in-hand; `async: false` preserves that cheaply, so blocking survives as a choice rather than disappearing.
- **Additional blocking action under always-async**: rejected as redundant once blocking survives as `async: false`.

## Consequences

- The delegation policy is rewritten around the new rhythm: dispatch, then end the turn or continue independent work; results arrive automatically; never poll or keep the turn alive waiting.
- The fleet widget reads the run registry, so live progress persists across turns and no longer ends when a tool call returns.
- Turn budget and stall watchdog enforcement (ADR-0006: soft grace, hard kill) is unchanged and applies identically under async dispatch: the timers live inside the child runner's closure, which already outlives the tool call, so only lifecycle ownership (kill handles, settle, delivery) moved into the run watcher — no timer relocation was needed.
- Flat orchestration stands (ADR-0002), reaffirmed without a new ADR: a blocked parent was the main pressure to nest, and an orchestrator that stays usable between dispatches coordinates phases itself. ADR-0002's revisit trigger ("large multi-phase tasks prove painful") never fired.
- Existing blocking-path tests pass unmodified — they are the regression guard proving `async: false` really is today's behavior.
