A normal agent turn can call spawn_agent when delegation helps or the person
requests it. Delegation is asynchronous: the tool returns the child's ID, and the
main agent can work, reply or yield without polling. The person can continue the
conversation while children work.
spawn_agent: prompt, optional title, write, isolate, skills.send_agent: id, prompt; continues the same task with its own prior messageslist_agents, cancel_agent, close_agent, merge_agent: operate only on theSettings exposes Chat sub-agents and the maximum simultaneous children:
1–16, default 3. This limit is independent of Self-Driving's parallel plan steps.
Excess child turns wait in the persisted start queue. Lowering the limit leaves
already running turns intact and prevents new starts until capacity is available.
Every new completion is saved before it reserves a place in the main chat's FIFO.
Text, streamed image/file sends and child-result reviews use the same FIFO. A
second completion arriving during the first review waits; user messages arriving
later cannot start a concurrent main turn. Different conversations remain
independent.
The completion mailbox has monotonic IDs and pending/inflight/interrupted
states. The main agent claims one record before reviewing it. A successful final
assistant message contains its completion marker. The mailbox acknowledges it
only after reading that marker from the saved session file, never solely from
memory. Duplicate notifications and a crash after session save but before ACK do
not execute the review twice. Stopped, failed or unsaved reviews retain their
result and require Resume main review in the child pane; this resumes the
review without rerunning the child. An interrupted review does not hold later pending
results: those continue automatically in arrival order, with at most one review
in flight per chat. Explicit resume requeues that saved result after already
pending work while retaining its completion ID. Background reviews may use delegated coding/checking and child-coordination tools without an invisible permission dialog; explicit denies still win. Unrelated actions are denied and must be requested in an interactive turn.
Child tasks and completion records live in config.DataPath("subagents"), as an
atomic private state file and managed worktrees. They are temporary local recovery
records, git-ignored and excluded from the existing backup manifest. Stream text
is coalesced and checkpointed every 250 ms; completed output, tool events and
queue transitions save immediately. A crash can lose the latest unsaved stream
tail, but a finalized result remains stored. Running children recover as
interrupted and require an explicit follow-up; queued children can start normally.
Ordinary pending HTTP sends remain tied to their client connection.
The chat tray shows only running and queued children. Agent history opens a
chat-scoped panel for completed, failed, interrupted, cancelled and closed tasks;
selecting a task opens its assigned prompt, output and tool events. Back returns
to the history list. The history selection never carries into another chat.
Accepting a child with close_agent closes it against further instructions while
retaining its prompt, output, messages and events in this local history. The
internal system prompt and skill list are cleared. Older records already cleared
by a previous version cannot recover their details. Unacknowledged completion
records are retained independently. Failed or unmerged isolated work is not
deleted automatically, and the existing merge-before-close guard still applies.
The main agent always sees the real state. Every main turn carries a<memo-subagent-status> block (appended to the current user message, never the
system prompt, so the prompt cache is untouched) listing each non-closed child:
state, current activity ("running tool run_command", "waiting for the model after
tool edit_file"), elapsed and idle time, tool-call/error counts, the error of a
failed child, a recovery hint, and any result main has not reviewed yet.list_agents returns the same compact summary per child plus recent tools and
the tail of its output; it no longer returns whole transcripts, andlist_agents {id} adds that child's full final output. A failed,
interrupted or cancelled child reaches main through the same durable mailbox as a
success, and the delivered record includes that summary (task), not only the
error string.
Why children used to fail at exactly 20 minutes. runChatSubAgent applied
the interactive turn budget (agent_mode.turn_budget_secs, 1200 s) to background
children, so a healthy coder mid-edit was killed with a bare context deadline exceeded (two live children with 315 and 377 events). Children now fail only
when they make no progress (no model output or tool activity) for 15 minutes,
with the reason in the error, or after a 6 hour backstop. The earlier failures
(no provider configured or all providers failed) were the provider-inheritance
bug fixed in 90f52c63.
Transient provider errors are retried. A 5xx, timeout or rate limit during a
child's run is retried up to three times (30 s, 2 min, 5 min, or the provider's
retry hint, at most 10 min). Each retry shows as a retry event, and the model is
told that part of the work may already be on disk (git status/git diff) so it
continues instead of starting over. Configuration, authentication and
unsupported-model errors are not retried. A failure with no final reply still
hands main the last tool results.
Limitations: a silent tool that runs longer than the idle limit (a very long
build) is still treated as stalled. A stopped result still needs explicit resume
for its own review; later results are delivered automatically. When a delivery
state save fails, the mailbox retries parking an orphaned claim after storage
recovers. It checks dispatcher ownership under the manager lock so explicit
resume cannot cause a newly active review to be interrupted.
For writable children, omitted isolate defaults to true. Each gets a separatememo-agent-<id> branch and worktree. The source Git worktree must be clean,
including untracked files, and checked out on main; the runtime does not stash,
switch branches or discard local edits for the user. A non-Git project can use an
explicit isolate:false shared writer. A shared writer holds an ancestor-aware
project mutation lease: main can converse/read, but overlapping writes/commands
wait. Separate worktrees can write concurrently.
The child is instructed to test and commit its work locally. Main reviews the
result and requests corrections through send_agent as needed. merge_agent
serializes integration into clean main, validates the combined tree and only then
commits the merge. Validation checks whitespace and detected affected Go modules,
Flutter packages and npm test scripts (including mixed backend/frontend repos).
Toolchains must be available on the backend process's PATH. Unknown project types
still require main to perform their appropriate checks before requesting merge.
Conflicts or failed checks abort the attempted merge and preserve the child branch
and worktree. Cleanup verifies merge ancestry, removes the managed worktree and
uses safe branch deletion. Cleanup can resume after interruption. The runtime
never pushes branches, tags or releases.
Child cards appear above the composer. Selecting a card opens a right pane with
its task prompt, own conversation, output and tool events; narrow windows use the
pane as a full-width view. Follow-up, cancellation, acceptance and interrupted
main-review recovery are available there. Polling is chat-scoped, gated by login,
nonoverlapping and disposed on navigation. Background main replies refresh only
when doing so cannot overwrite a user stream or a newer user turn.
Tests cover durable FIFO delivery, simultaneous completions, concurrency limits,
follow-up context, cancellation/restart, explicit denies, disk-save failure,
worktree isolation, merge conflict/validation failure/cleanup retry, streamed
attachments, real HTTP/fake-provider delegation and main resumption, and Flutter
panel/settings/recovery behavior. No live vendor-model quality or native
Windows/macOS execution is established by those tests.
The two review findings were treated as separate checkpoints. The guarantees
come from the approved plan above and the review's model-selection race report.
| Guarantee | Test / evidence | Result |
|---|---|---|
| Early subscription endpoint registration cannot write during a model-selection transaction, and reads the selection after acquiring the lock | TestSyncSubscriptions_EarlyRegistrationWaitsForModelSelection |
RED on the previous code: early registration wrote provider config while a model-selection transaction held subscriptionModelMu; GREEN after protecting the early read/write |
| Model selection remains usable while network discovery is pending | TestSyncSubscriptions_PreservesModelSelectedDuringDiscovery and other TestSyncSubscriptions_ cases |
CGO_ENABLED=1 go test -tags sqlite_fts5 ./internal/app -race -run '^TestSyncSubscriptions_' -count=1: PASS, 9.501s |
| Two isolated children actually write source/tests, run tests and commit; main reuses a child, merges and closes through real tools | TestBenchmark_SubagentsFakeProject |
CGO_ENABLED=1 go test -tags sqlite_fts5 ./internal/e2e -race -run '^TestBenchmark_SubagentsFakeProject$' -count=1 -v -timeout=90s: PASS, 2.654s |
| Child 2 completing during the blocked first review stays pending and reviews follow increasing completion IDs; accepted changes land through sequential merges and both branches/worktrees disappear | Same benchmark, channel-controlled ordering and final Git assertions | PASS |
| A broken child writer cannot produce a green benchmark | Temporary Go build overlay removing write_file from the child registry; no production mutation committed |
Previous benchmark: PASS, 1.364s. New benchmark: expected FAIL, Error: Unknown tool: write_file for both writers |
Only fixture initialization writes files from the test process. All child changes,
follow-up edits, tests and commits use agent tool calls. Tool results must be
present and successful before the fake provider advances to its next step. The
fixture is a dependency-free Go module so merge validation also runs its tests.
The existing TestChatSubAgents_ResultWhileMainBusyQueuesWithHumanTurn covers a
human message arriving during main review; this benchmark controls child-result
order and writer isolation. Model quality and native desktop UX remain untested.
Verification: Go build/vet passed. The full ./... -race run passed the agent,
subagent and E2E packages, but failedTestEnsureTailscaleWebServerBind_RebindsFromLoopback withweb server not running after rebind. A comparison of race test binaries under
12 identical busy-loop processes, 150 runs per version, reproduced that same
failure 8 times before these fixes and 2 times after them. This is recorded as a
separate pre-existing server lifecycle problem; the full suite is not claimed
to be green. Flutter tests passed 815/815; analysis reported only the 3 existing
BuildContext infos. No Flutter source changed.
Coverage measurement:CGO_ENABLED=1 go test -tags sqlite_fts5 ./internal/app -run '^TestSyncSubscriptions_' -coverprofile=/tmp/memo-subagent-fixes-coverage.out
passed (9.306s). syncSubscriptions and SetProviderModel each measured 75%;
the intentionally narrow selection measured 0.9% of the whole app package.
These figures do not establish 80% app-wide coverage.
A stopped main review previously remained at the mailbox head and silently held
all later results. A new user message could read current child status even while
automatic delivery remained blocked. The supplied log shows child turns ending
at 21:46:59 and 21:47:14 (UTC+03), followed by no new parent work until the user's
21:48:30 message. The supplied tail does not include the saved mailbox state, so
it does not by itself prove which earlier delivery was interrupted.
The regression test reproduces the blocking path through real App/HTTP/SSE:
first main review returns HTTP 503, a second child finishes successfully, and
main must save its review without another user send. It failed before the fix
and passes afterward. The failed old review is retained and neither the child
nor that review is automatically replayed. Manager tests also cover pending
FIFO, explicit retry during a live review, restart recovery and isolated chats.
A separate app regression fails a delivery-state save, restores storage and
checks recovery without restart or replay; an active dispatcher is preserved.
The mailbox now logs completion ID, child ID and chat ID when scheduling and
starting main review, so future field logs can distinguish delivery delay from
model/tool execution without exposing the result contents.
Verification: CGO_ENABLED=1 go build -tags sqlite_fts5 ./... and go vet passed;
full CGO_ENABLED=1 go test -tags sqlite_fts5 ./... -race passed (app 59.946s,
E2E 91.831s, subagents 2.958s). Focused mailbox regressions passed (app 1.755s,
E2E 1.562s); orphan/retry atomicity tests passed 30 repetitions under -race.
Flutter: 826 tests passed; analyze reported only three existinguse_build_context_synchronously infos. No Dart source changed; the touched-file
UI literal check and git diff --check were empty. This proves the reproduced
mailbox path; a real-provider UI replay on the reporting machine remains untested.