MemoDocumentation
EN

Sub-agents in normal agent chats

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.

Execution and context

  • spawn_agent: prompt, optional title, write, isolate, skills.
  • send_agent: id, prompt; continues the same task with its own prior messages
    and bounded tool evidence. Use a new child for unrelated work.
  • list_agents, cancel_agent, close_agent, merge_agent: operate only on the
    requesting chat's children. No model argument selects a different parent chat.
  • Read-only children have file inspection/search and web tools, with no shell,
    file mutation, personal tools or further delegation.
  • Coding children additionally have file mutation and command tools. Explicit
    permission denies remain effective; task tools outside the delegated coding
    set and selected skills are denied rather than opening an invisible prompt.
  • Children receive the configured Code Mode build prompt, their assigned task,
    their own task conversation and explicitly selected skill instructions. They
    receive no personal memory, RAG, persona or parent conversation. Project skills
    must first be enabled by the person in the parent chat. Explicit skill tools
    can be used by writable children; read-only children receive instructions only.
  • Provider name, model and effort are selected from the actual parent turn and
    retained for the child. A removed provider or stopped local model fails clearly
    instead of silently substituting another model.

Settings 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.

Main-agent FIFO and crash recovery

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.

Live state, failures and timing

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, and
list_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.

Coding isolation and integration

For writable children, omitted isolate defaults to true. Each gets a separate
memo-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.

Interface and evidence

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.

Review regressions verified on 2026-10-23

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 failed
TestEnsureTailscaleWebServerBind_RebindsFromLoopback with
web 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.

Completion wakeup regression (2026-10-24)

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 existing
use_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.


Source document