Plugin API: a background agent from $.agent.spawn loses its report under acceptEdits (completion notification skipped as already-notified)
Environment: Claude Code 2.1.296, Windows 10, CLI (claude -p, plugin loaded with --plugin-dir).
Related: #98943 (SubagentHandback refused in bypassPermissions, report hidden), #101782 (plugin hooks skipped for $.agent.spawn).
Summary
A plugin starts a background subagent with $.agent.spawn, using a type it registered with $.agent.register. When the subagent finishes, the engine skips the normal completion notification to the parent. Then everything depends on the subagent calling SubagentHandback. In default and auto mode it does, and the report arrives. Under --permission-mode acceptEdits the subagent has no SubagentHandback tool, so the parent never sees the report, even though the work was done.
The built-in Agent tool with run_in_background: true under acceptEdits does not lose the report: it arrives as an ordinary task notification. The failure is specific to agents a plugin spawns.
Steps
- A plugin registers a type,
$.agent.register({ name, description, prompt, model: "sonnet", effort: "low", tools: ["Read"] }), and a tool whosetool.callhandler calls$.agent.spawn({ subagentType: "<plugin>:<name>", prompt, model })and returns the agentId at once. - Run
claude -p --permission-mode acceptEdits --plugin-dir <plugin>and have the model call that tool with a task that reads a file containing a marker and returns it. - Repeat in default mode and in auto mode.
Result
debug.log, all three modes, at the subagent's end:
[INFO] [Stall] agent_completion agentId=<id> agentType=<plugin>:<name> exitPath=completed
[DEBUG] [enqueueAgentNotification] skipped taskId=<id> status=completed taskPresent=true reason=already-notified
Nothing had notified the parent before this line. The parent's transcript has no notification for this agent at that point.
| Mode | SubagentHandback in the subagent's .prefix.json toolNames | Report reaches the parent |
|---|---|---|
| default | yes | yes, as an agent message from the handback |
| auto | yes | yes, as an agent message from the handback |
| acceptEdits | no (not deferred either) | no — the work is done and the report is silently lost |
| built-in Agent, background, acceptEdits | — | yes, as a task notification |
Reproduced in six separate acceptEdits runs.
Expected
A plugin-spawned background agent's completion reaches the parent the same way a built-in background agent's does, whatever the permission mode. Or SubagentHandback is offered to it in acceptEdits too.
Workaround in use
The plugin keeps the agentIds it spawned. A tool.call hook on SubagentHandback marks the ones that reported back. On turn.complete for one of its own agents that did not report, the plugin sends e.answer to the main loop with $.prompt.submit. That gives exactly one delivery in all three modes. But the plugin keeps that list in memory, so it is lost if the plugin reloads while the agent is still running, and every plugin that spawns agents has to repeat this logic.