Skip to content

Subagent permission decisions are silently dropped — subagent hangs forever #2

Description

@jonioliveira

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

  1. Start a Local agent.
  2. Give it work that spawns a subagent (any Task call).
  3. Have the subagent request a tool requiring approval (Bash / Write / Edit).
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions