When a Subagent Finished, It Used to Steal the Chat

You ask Hermes in Telegram to send two subagents off to research something, then go make coffee. When you come back the subagent report never arrived — but your original conversation has “ended”, and every message you send in that same window now gets answered by the subagent, as if something had quietly taken your seat.
That is not your imagination. The child really did take the chat.
The strange symptom
A subagent launched with delegate_task that starts a background process — or a long foreground command that gets promoted to the background once it runs long enough — reports a completion when it finishes. That is the moment things went wrong:
- Your real conversation was ended as
session_switch, which is a user-boundary event; - The chat route was moved onto the child’s own execution transcript;
- So the parent’s own delegation result was dropped as terminal when it arrived — you never saw the report;
- Your next message in that window was answered by the child instead of your session.
The same bug was reported on Discord (DMs, threads and channels, on macOS and Windows), Telegram (DMs and groups), Slack, Zulip and Feishu.
How a completion took the chat over
The short version: the system filed the completion under the wrong owner.
A background process started inside a delegate_task child records the child’s own session id as its completion pin. When it finishes, the gateway’s completion resolver sees a live pin pointing at a session and treats that session as the owner of the chat. The next step is switch_session: the chat route moves onto the child’s execution transcript, and the real conversation ends as a user boundary. A user boundary is terminal — at that point the parent’s own delegation result, still in flight, is judged to belong to a conversation that has ended, and is dropped.
Put differently: the child’s process notice was supposed to reach the person who delegated the work, and instead it looked up the child that was delegated to. One wrong link, and the whole chain leaned the wrong way.
The fix: a completion belongs to the person, not the child
The rule the fix establishes is simple: a pinned completion belongs to the human-facing session — the one reached through model_config._delegate_from. The chat route never moves onto a child.
Mechanically:
- Ownership is proven once before delivery and then handed to the resolver. The resolver re-reads ownership only if the route moved after that proof;
- Uncertain ownership is retried rather than guessed: before admission, a retry costs no delivery attempt; after admission, the completion goes back on the delivery queue;
- User boundaries stay terminal at every hop, so a retry can never resurrect a conversation that ended.
Child process notices were adjusted too: they now follow delegation.surface_child_process_notifications (off by default, as the delegation docs have always stated) and are delivered in the profile that owns the session, including a served profile’s raw api_server session. A process_manage handoff partway through makes the notice the parent’s again.
Why the fix lives in the gateway, not one platform
Each platform adapter does exactly one job: translate between its service and the gateway’s common message format. Which session owns a chat is decided afterwards in shared gateway code — one session store keyed by the chat’s routing key, one completion resolver, one switch_session.
A child’s completion walks that path on every platform, so every platform has the takeover, and the repair belongs in the shared code rather than in a single adapter. That is why this change contains no platform-specific patch.
What did not change
- User boundary events stay terminal; their semantics were not relaxed;
- Child process notifications are still suppressed by default: the child’s consolidated result is the deliverable, and the running commentary is not. Set
delegation.surface_child_process_notifications: trueto receive them, with subagent attribution; - Delegation results themselves were never suppressed — this work is about whether they can actually be delivered;
- The change is a salvage rebuild of #132827, keeping the original contributors’ commits with authorship intact.
Status and upgrade
This fix (PR #136225, labelled P1) merged to main on 2026-10-11. The current tag, v0.21.6 from October 8, does not contain it; the next release will.
How to verify after upgrading — in Discord or Telegram, delegate a subagent that runs a command which becomes a background process, then watch what happens when it finishes:
- The child’s result should land in your original conversation;
- Your next message should be answered by your own session, not by the child;
- With
delegation.surface_child_process_notificationsenabled, the process notice appears in that same conversation with subagent attribution.
Wrapping up
This is the classic “looks like the model lost its mind, is actually a bookkeeping error” bug: the completion notice addressed the wrong recipient, ownership of the chat was handed over, and every later turn went sideways. With it fixed, a long-running background job can no longer replace the conversation that started it — a boundary that matters most in multi-platform, multi-user deployments running several subagents at once.
Related reading: live subagent orchestration covers steering children while they run, the session boundaries guide explains where a conversation starts and ends, and gateway platform self-healing is the other recovery path when a platform connection goes wrong.