stablyai/orca · error · Error

agent_session_identity_required

Error message

agent_session_identity_required

What it means

When createOrAttach carries an agentSessionEnsure payload, both its 'claim' and 'surface' fields must pass strict shape validation (isAgentSessionExecutionClaim and isAgentSessionSurfaceBinding). The claim requires a correct digestVersion, base64url digests of exact length, and a resumable agent; the surface requires a valid tabId, leafId, and a terminalHandle prefixed 'term_'. If either fails, the daemon refuses to bind an agent session identity, since a malformed claim cannot be trusted for session ownership.

Source

Thrown at src/main/daemon/daemon-server.ts:999

        // too late if a session was accepted in between — that session is then reachable by
        // nobody, and the user sees a terminal that acknowledges input and never runs it.
        // Why creation only: an attach reaches a session this daemon already hosts, over a
        // connection that already exists. Refusing that would break the drain a retiring daemon
        // depends on, and it strands nothing — the session is already here.
        if (!attachOnly && this.hasLostEndpointOwnership()) {
          this.requestRetirementForLostEndpoint()
          throw new Error(DAEMON_ENDPOINT_LOST_MESSAGE)
        }
        this.createOrAttachInFlight++
        let routedSessionId = p.sessionId
        let result: Awaited<ReturnType<TerminalHost['createOrAttach']>>
        try {
          if (
            p.agentSessionEnsure !== undefined &&
            (!isAgentSessionExecutionClaim(p.agentSessionEnsure.claim) ||
              !isAgentSessionSurfaceBinding(p.agentSessionEnsure.surface))
          ) {
            throw new Error('agent_session_identity_required')
          }
          if (!attachOnly) {
            await this.preparePtySpawnUnlessCanceled(p.sessionId, clientId)
          }
          if (p.historySeed !== undefined && p.historySeedTransferId !== undefined) {
            throw new Error('Multiple terminal history seed sources')
          }
          const historySeedChunks =
            p.historySeedTransferId !== undefined
              ? this.historySeedTransfers.take(clientId, p.historySeedTransferId)
              : p.historySeed !== undefined
                ? [p.historySeed]
                : undefined
          result = await this.host.createOrAttach({
            sessionId: p.sessionId,
            cols: p.cols,
            rows: p.rows,
            cwd: p.cwd,

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Re-derive the agentSessionEnsure on the client using the host's authority helpers so claim and surface match the current protocol version.
  2. Validate the payload with isAgentSessionExecutionClaim() and isAgentSessionSurfaceBinding() locally before sending createOrAttach.
  3. If you do not need an agent session claim, omit agentSessionEnsure entirely rather than sending a placeholder.
  4. On version mismatch, negotiate protocol version in hello and construct the claim in the negotiated shape.

Example fix

// before: send an unchecked agentSessionEnsure
await daemon.rpc('createOrAttach', { ..., agentSessionEnsure: raw })

// after: validate before sending, omit if invalid
import { isAgentSessionExecutionClaim, isAgentSessionSurfaceBinding } from '../../shared/agent-session-host-authority'
if (raw && isAgentSessionExecutionClaim(raw.claim) && isAgentSessionSurfaceBinding(raw.surface)) {
  await daemon.rpc('createOrAttach', { ..., agentSessionEnsure: raw })
} else {
  await daemon.rpc('createOrAttach', { ... })
}
Defensive patterns

Strategy: validation

Validate before calling

import { isAgentSessionExecutionClaim, isAgentSessionSurfaceBinding } from '../../shared/agent-session-host-authority'
function agentSessionEnsureIsValid(e: { claim: unknown; surface: unknown } | undefined): boolean {
  if (e === undefined) return true
  return isAgentSessionExecutionClaim(e.claim) && isAgentSessionSurfaceBinding(e.surface)
}

Type guard

import { isAgentSessionExecutionClaim, isAgentSessionSurfaceBinding, type AgentSessionExecutionClaim, type AgentSessionSurfaceBinding } from '../../shared/agent-session-host-authority'
function isAgentSessionEnsure(v: unknown): v is { claim: AgentSessionExecutionClaim; surface: AgentSessionSurfaceBinding } {
  if (typeof v !== 'object' || v === null) return false
  const o = v as { claim?: unknown; surface?: unknown }
  return isAgentSessionExecutionClaim(o.claim) && isAgentSessionSurfaceBinding(o.surface)
}

Try / catch

try {
  await daemon.rpc('createOrAttach', { ..., agentSessionEnsure })
} catch (e) {
  if (e instanceof Error && e.message === 'agent_session_identity_required') {
    // drop the claim and retry without it, or rebuild it correctly
    await daemon.rpc('createOrAttach', { ... })
  } else { throw e }
}

Prevention

When it happens

Trigger: createOrAttach with p.agentSessionEnsure present where claim.digestVersion is wrong, identityDigest/worktreeScopeDigest are not 43-char base64url SHA-256, keyId contains non-base64url chars, surface.tabId/leafId/terminalHandle do not match their formats, or the agent field is not a recognized resumable TuiAgent.

Common situations: A client sending a stale agentSessionEnsure shape from an older protocol version (digestVersion mismatch); hand-constructed or truncated digest fields; a surface binding whose terminalHandle lacks the 'term_' prefix; cross-version client/host where the claim format changed.

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/dcaf47c72a4af267. Report an issue: GitHub.