deepseek-ai/deepseek-harness · error · ApiRemoteSubagentSessionOwnership

agent-busy

agent-busy

Error message

session "${sessionId}" is a subagent session; use subagent delivery

What it means

Raised on the cold-resume path of the Host's shared Agent resolver when the persisted session header declares origin 'subagent' (the inspect-time check at agent-lookup.ts:148). Subagent child sessions belong to their parent agent's routing; generic Remote/legacy RPC must not resume them as top-level agents. The resolver converts it to the caller-facing error code 'agent-busy' with details.reason 'use subagent delivery for this child session' — the legacy fence shape is preserved on purpose.

Source

Thrown at packages/api/remotes/src/agent-lookup.ts:149

      return { error: apiRemoteSubagentOwnershipError(sessionId) }
    }
    return { agent: live }
  }

  const agentFor = async (sessionId: SessionId): Promise<ApiRemoteAgentResult> => {
    const fenced = fencedLiveAgent(sessionId)
    if (fenced !== undefined) return fenced
    const attached = ctx.sessions.get(sessionId)
    if (attached !== undefined && hasApiRemoteSubagentOwner(ctx, attached, undefined)) {
      return { error: apiRemoteSubagentOwnershipError(sessionId) }
    }
    let resume = resumes.get(sessionId)
    if (resume === undefined) {
      resume = (async () => {
        try {
          const inspected = await inspectApiRemoteSession(ctx, sessionId)
          if (hasApiRemoteSubagentOwner(ctx, { header: inspected.meta }, undefined)) {
            throw new ApiRemoteSubagentSessionOwnership(sessionId)
          }
          // Built from the inspected session before the published re-checks
          // below, so those stay adjacent to `resume` and a Host setup that
          // awaits (composing a preset, say) does not widen the collision
          // window.
          const setup = options.setup === undefined ? undefined : await options.setup(inspected)
          const publishedSession = ctx.sessions.get(sessionId)
          const publishedAgent = ctx.agents.get(sessionId)
          if (publishedSession !== undefined
            && hasApiRemoteSubagentOwner(ctx, publishedSession, publishedAgent)) {
            throw new ApiRemoteSubagentSessionOwnership(sessionId)
          }
          const handle = await ctx.agents.resume({
            resumeSessionId: sessionId,
            ...options.agentOptions === undefined ? {} : { agentOptions: options.agentOptions() },
            ...setup === undefined ? {} : { setup },
          })
          return handle.agent

View on GitHub (pinned to b150a551b8)

Solutions

  1. Route the request to the parent session instead: read parentSession from the child's header and resolve that id, letting subagent delivery manage the child.
  2. Use the subagent delivery mechanism for child sessions rather than generic session RPC.
  3. Filter origin === 'subagent' sessions out of client-facing session lists so they cannot be selected as continuation targets.

Example fix

// before
const result = await agentFor(childSessionId)

// after
const inspected = await inspectApiRemoteSession(ctx, childSessionId)
const target = inspected.meta.origin === 'subagent'
  ? inspected.meta.parentSession
  : childSessionId
const result = await agentFor(target)
Defensive patterns

Strategy: type-guard

Validate before calling

const inspected = await inspectApiRemoteSession(ctx, sessionId)
if (inspected.meta.origin === 'subagent') {
  throw new Error(`route via parent session ${inspected.meta.parentSession}`)
}

Type guard

const isSubagentFence = (r: ApiRemoteAgentResult): boolean =>
  'error' in r && r.error.code === 'agent-busy'
  && r.error.details.reason === 'use subagent delivery for this child session'

Try / catch

try {
  const agent = await coldResume(sessionId)
} catch (error) {
  if (error instanceof ApiRemoteSubagentSessionOwnership) {
    // permanent routing change: switch to the parent session, do not retry
  }
  throw error
}

Prevention

When it happens

Trigger: Calling agentFor (directly or via legacy API Proxy methods or Typert lookups) on a cold session id whose persisted header.origin === 'subagent', while no live agent for that id exists — the ownership check fires from the inspected metadata before any resume attempt.

Common situations: A client kept a child session id from a completed delegation and tries to continue it directly; a session-list UI shows subagent sessions next to top-level ones and a user selects one; a script iterates every session in the store and drives the generic session API against each.

Related errors


AI-assisted analysis of deepseek-ai/deepseek-harness@b150a551b8 (2026-08-24). Data as JSON: /api/errors/c7cc308d6de016f8. Report an issue: GitHub.