{"record":{"id":"ee0df993c8fd3570","repo":"CherryHQ/cherry-studio","slug":"cannot-stream-on-orphan-session-session-id-it","errorCode":null,"errorMessage":"Cannot stream on orphan session ${session.id} — its agent was deleted","messagePattern":"Cannot stream on orphan session (.+?) — its agent was deleted","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/ChannelMessageHandler.ts","lineNumber":792,"sourceCode":"    }\n    return agentSessionService.create({\n      agentId,\n      name: 'Channel session',\n      workspace: channelRow.workspace\n    })\n  }\n\n  private async collectStreamResponse(\n    session: AgentSessionEntity,\n    content: string,\n    abortController: AbortController,\n    adapter: ChannelAdapter,\n    chatId: string,\n    replyToMessageId?: string,\n    onAdmitted?: () => void\n  ): Promise<string> {\n    if (!session.agentId) {\n      throw new Error(`Cannot stream on orphan session ${session.id} — its agent was deleted`)\n    }\n\n    let resolveExecution!: (text: string) => void\n    let rejectExecution!: (err: unknown) => void\n    const executionDone = new Promise<string>((resolve, reject) => {\n      resolveExecution = resolve\n      rejectExecution = reject\n    })\n    let accumulatedText = ''\n    const sentinel: StreamListener = {\n      id: `channel-completion:${chatId}`,\n      onChunk(chunk) {\n        // `text-delta`'s field is `delta`, not `text` (AI SDK `UIMessageChunk`).\n        if (chunk.type === 'text-delta') accumulatedText += chunk.delta\n      },\n      onDone() {\n        resolveExecution(accumulatedText.trim())\n      },","sourceCodeStart":774,"sourceCodeEnd":810,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/ChannelMessageHandler.ts#L774-L810","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["When deleting an agent, also close/delete its channel sessions and evict them from the sessionTracker.","Before streaming, validate session.agentId and short-circuit (skip + log) instead of streaming.","Add a cleanup pass that prunes sessions whose agentId no longer resolves to an agent.","Ensure sessionTracker eviction runs on agent deletion, not only on session creation."],"exampleFix":"// before: stream regardless of agent binding\nawait collectStreamResponse(session, ...)\n\n// after: guard and clean up orphans\nif (!session.agentId) {\n  logger.warn('Skipping orphan channel session', { sessionId: session.id })\n  await agentSessionService.delete(session.id)\n  return\n}\nawait collectStreamResponse(session, ...)","handlingStrategy":"type-guard","validationCode":"if (!session.agentId) {\n  logger.warn('Orphan channel session; cleaning up', { sessionId: session.id })\n  await agentSessionService.delete(session.id).catch(() => {})\n  return // do not attempt to stream\n}","typeGuard":"function sessionHasAgent(s: { agentId?: string | null }): boolean {\n  return typeof s.agentId === 'string' && s.agentId.length > 0\n}","tryCatchPattern":"try {\n  await collectStreamResponse(session, content, controller, adapter, chatId, replyTo, onAdmitted)\n} catch (e) {\n  if (e instanceof Error && /orphan session/.test(e.message)) {\n    await agentSessionService.delete(session.id).catch(() => {})\n    // surface 'agent was deleted' to the channel instead of crashing\n  } else throw e\n}","preventionTips":["Delete/close channel sessions when their agent is deleted.","Evict orphan sessions from the sessionTracker on agent deletion.","Run a periodic cleanup of sessions whose agentId no longer resolves."],"tags":["channels","sessions","lifecycle","orphan","agents"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}