Summary
When a subagent (a node created by a Task/Agent tool call) requests a tool that needs approval, clicking Allow or Deny does nothing. The SDK's canUseTool promise never settles, so the subagent blocks indefinitely and the permission card never clears. The only way out is Halt / Remove from canvas.
No error surfaces anywhere: the POST returns { ok: false } and the UI ignores the response.
Root cause
sessions is keyed by the parent session id only — there is exactly one sessions.set call, in startLocalAgent:
sessions.set(agentId, session); // server/local-agent.ts:346
But a subagent is a first-class node with its own id, and the pending permission card is stored on that node:
const childId = nextId("sub"); // local-agent.ts:499
subagentByToolUse.set(block.id, childId);
registry.upsert({ id: childId, kind: "subagent", parentId: nodeId, ... });
// later, in canUseTool:
const nodeId = options.agentID
? (subagentByToolUse.get(options.agentID) ?? agentId) // :199-201
: agentId;
registry.patch(nodeId, { status: "waiting-user", pending: { requestId, ... } }); // :270
The client reads agent.pending off that subagent node and POSTs to its id:
await api(`/api/agents/${agentId}/permission`, { requestId: agent.pending.requestId, ... });
// lib/store.ts:912 (approve), :924 (deny), :938 (answer)
The route forwards that id straight through:
const ok = resolvePermission(req.params.id, String(requestId), { ... }); // server/index.ts:338-339
and resolvePermission looks it up in the parent-keyed map:
const session = sessions.get(agentId); // server/local-agent.ts:730 -> undefined for a subagent
if (!session) return false; // :731
So the promise created at local-agent.ts:303 is never resolved.
Reproduction
- Start a Local agent.
- Give it work that spawns a subagent (any
Task call).
- Have the subagent request a tool requiring approval (Bash / Write / Edit).
- A permission card appears on the subagent node. Click Allow (or Deny).
Expected: the tool runs (or is denied) and the card clears.
Actual: nothing happens. The card stays, the subagent hangs forever.
A second defect stacked in the same function
Even with the session lookup fixed, the card is cleared on the parent rather than the node that owns it:
registry.patch(agentId, { pending: undefined, status: "running" }); // :735
For a subagent request the card lives on the child node, so the stale card would survive and the parent's status gets wrongly overwritten. registry.pushChat(agentId, ...) at :738 and :750 misdirects the same way. Both the lookup and the patch/chat targets need to follow the node that owns the pending request.
Suggested fix
Resolve the owning session by walking up parentId, then patch the node the card is actually on. A loop rather than a single hop, because nesting is possible — the nodeId a subagent is parented to is itself resolved through subagentByToolUse (:499), so a subagent can spawn its own subagent:
export function resolvePermission(nodeId: string, requestId: string, decision) {
// A subagent's pending request belongs to its nearest ancestor's SDK session.
let sessionId: string | undefined = nodeId;
while (sessionId && !sessions.has(sessionId)) {
sessionId = registry.agents.get(sessionId)?.parentId;
}
const session = sessionId ? sessions.get(sessionId) : undefined;
if (!session) return false;
const pending = session.pendingByRequest.get(requestId);
if (!pending) return false;
session.pendingByRequest.delete(requestId);
// Clear the card on the node that owns it, not the session root.
registry.patch(nodeId, { pending: undefined, status: "running" });
// ...and route the status/user chat messages to nodeId as well.
}
Happy to open a PR for this if the approach looks right.
Summary
When a subagent (a node created by a
Task/Agenttool call) requests a tool that needs approval, clicking Allow or Deny does nothing. The SDK'scanUseToolpromise never settles, so the subagent blocks indefinitely and the permission card never clears. The only way out is Halt / Remove from canvas.No error surfaces anywhere: the POST returns
{ ok: false }and the UI ignores the response.Root cause
sessionsis keyed by the parent session id only — there is exactly onesessions.setcall, instartLocalAgent:But a subagent is a first-class node with its own id, and the pending permission card is stored on that node:
The client reads
agent.pendingoff that subagent node and POSTs to its id:The route forwards that id straight through:
and
resolvePermissionlooks it up in the parent-keyed map:So the promise created at
local-agent.ts:303is never resolved.Reproduction
Taskcall).Expected: the tool runs (or is denied) and the card clears.
Actual: nothing happens. The card stays, the subagent hangs forever.
A second defect stacked in the same function
Even with the session lookup fixed, the card is cleared on the parent rather than the node that owns it:
For a subagent request the card lives on the child node, so the stale card would survive and the parent's status gets wrongly overwritten.
registry.pushChat(agentId, ...)at:738and:750misdirects the same way. Both the lookup and the patch/chat targets need to follow the node that owns the pending request.Suggested fix
Resolve the owning session by walking up
parentId, then patch the node the card is actually on. A loop rather than a single hop, because nesting is possible — thenodeIda subagent is parented to is itself resolved throughsubagentByToolUse(:499), so a subagent can spawn its own subagent:Happy to open a PR for this if the approach looks right.