CherryHQ/cherry-studio · error · Error

Cannot stream on orphan session ${session.id} — its agent wa

Error message

Cannot stream on orphan session ${session.id} — its agent was deleted

What it means

Thrown by ChannelMessageHandler.collectStreamResponse when the session being streamed has no agentId (falsy). Agent sessions can outlive their agent: when an agent is deleted, lingering channel sessions may keep rows with a null/cleared agentId. Streaming a completion requires a live agent, so an orphan session is a hard error rather than a silent no-op.

Source

Thrown at src/main/ai/channels/ChannelMessageHandler.ts:792

    }
    return agentSessionService.create({
      agentId,
      name: 'Channel session',
      workspace: channelRow.workspace
    })
  }

  private async collectStreamResponse(
    session: AgentSessionEntity,
    content: string,
    abortController: AbortController,
    adapter: ChannelAdapter,
    chatId: string,
    replyToMessageId?: string,
    onAdmitted?: () => void
  ): Promise<string> {
    if (!session.agentId) {
      throw new Error(`Cannot stream on orphan session ${session.id} — its agent was deleted`)
    }

    let resolveExecution!: (text: string) => void
    let rejectExecution!: (err: unknown) => void
    const executionDone = new Promise<string>((resolve, reject) => {
      resolveExecution = resolve
      rejectExecution = reject
    })
    let accumulatedText = ''
    const sentinel: StreamListener = {
      id: `channel-completion:${chatId}`,
      onChunk(chunk) {
        // `text-delta`'s field is `delta`, not `text` (AI SDK `UIMessageChunk`).
        if (chunk.type === 'text-delta') accumulatedText += chunk.delta
      },
      onDone() {
        resolveExecution(accumulatedText.trim())
      },

View on GitHub (pinned to 726446b54c)

Solutions

  1. When deleting an agent, also close/delete its channel sessions and evict them from the sessionTracker.
  2. Before streaming, validate session.agentId and short-circuit (skip + log) instead of streaming.
  3. Add a cleanup pass that prunes sessions whose agentId no longer resolves to an agent.
  4. Ensure sessionTracker eviction runs on agent deletion, not only on session creation.

Example fix

// before: stream regardless of agent binding
await collectStreamResponse(session, ...)

// after: guard and clean up orphans
if (!session.agentId) {
  logger.warn('Skipping orphan channel session', { sessionId: session.id })
  await agentSessionService.delete(session.id)
  return
}
await collectStreamResponse(session, ...)
Defensive patterns

Strategy: type-guard

Validate before calling

if (!session.agentId) {
  logger.warn('Orphan channel session; cleaning up', { sessionId: session.id })
  await agentSessionService.delete(session.id).catch(() => {})
  return // do not attempt to stream
}

Type guard

function sessionHasAgent(s: { agentId?: string | null }): boolean {
  return typeof s.agentId === 'string' && s.agentId.length > 0
}

Try / catch

try {
  await collectStreamResponse(session, content, controller, adapter, chatId, replyTo, onAdmitted)
} catch (e) {
  if (e instanceof Error && /orphan session/.test(e.message)) {
    await agentSessionService.delete(session.id).catch(() => {})
    // surface 'agent was deleted' to the channel instead of crashing
  } else throw e
}

Prevention

When it happens

Trigger: collectStreamResponse is invoked with a session whose agentId is null/undefined — the agent was deleted but the session row (and its channel binding) was not cleaned up.

Common situations: Agent deleted while channel sessions referencing it still exist; cascading delete that cleared agentId but left the session rows; orphaned session tracker entries pointing at agent-less sessions.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/ee0df993c8fd3570. Report an issue: GitHub.