{"record":{"id":"1b1367de62e85a45","repo":"can1357/oh-my-pi","slug":"recipient-session-is-disposed","errorCode":null,"errorMessage":"Recipient session is disposed.","messagePattern":"Recipient session is disposed\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/coding-agent/src/session/irc-bridge.ts","lineNumber":120,"sourceCode":"\t\t\t\t}\n\t\t\t\tmessages.push({\n\t\t\t\t\tid,\n\t\t\t\t\tfrom,\n\t\t\t\t\tto: agentId,\n\t\t\t\t\tbody,\n\t\t\t\t\tts: record.timestamp,\n\t\t\t\t\t...(typeof replyTo === \"string\" ? { replyTo } : {}),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tthis.#interrupts = remainingInterrupts;\n\t\tthis.#asides = remainingAsides;\n\t\treturn messages;\n\t}\n\n\t/** Delivers an IRC message into the recipient session without awaiting any wake turn. */\n\tasync deliver(msg: IrcMessage, opts?: { expectsReply?: boolean }): Promise<\"injected\" | \"woken\"> {\n\t\tif (this.#host.isDisposed()) throw new Error(\"Recipient session is disposed.\");\n\t\tconst streaming = this.#host.isStreaming();\n\t\tconst planModeIdle = !streaming && this.#host.planModeEnabled();\n\t\tconst autoReply =\n\t\t\t(opts?.expectsReply ?? false) && ((streaming && !this.#host.settings.get(\"async.enabled\")) || planModeIdle);\n\t\tconst record: CustomMessage = {\n\t\t\trole: \"custom\",\n\t\t\tcustomType: \"irc:incoming\",\n\t\t\tcontent: prompt.render(ircIncomingTemplate, {\n\t\t\t\tfrom: msg.from,\n\t\t\t\tmessage: msg.body,\n\t\t\t\treplyTo: msg.replyTo ?? \"\",\n\t\t\t\tautoReplied: autoReply,\n\t\t\t\tinterrupting: streaming,\n\t\t\t}),\n\t\t\tdisplay: true,\n\t\t\tdetails: { id: msg.id, from: msg.from, message: msg.body, ...(msg.replyTo ? { replyTo: msg.replyTo } : {}) },\n\t\t\tattribution: \"agent\",\n\t\t\ttimestamp: msg.ts,","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/session/irc-bridge.ts#L102-L138","documentation":"IrcBridge.deliver() injects an IRC message into the recipient agent session. If the host session has already been disposed, delivery is impossible, so it throws synchronously before building the CustomMessage record. Callers should treat a disposed bridge as terminal and stop routing messages to it.","triggerScenarios":"Calling `ircBridge.deliver(msg, opts)` after the host session was disposed — typically an IRC server event arriving during/after shutdown, or a reply path firing for a session the user already closed.","commonSituations":"IRC network still delivering messages while the app is quitting; a user closes one session in a multi-session setup but the IRC channel keeps routing to it; race between dispose() and an in-flight PRIVMSG handler.","solutions":["Check host.isDisposed() (or a bridge-alive flag) before calling deliver and skip/queue the message","Catch this error in the IRC event handler, log it via logger.debug, and detach the bridge from the channel","Unsubscribe the IRC listener as the first step of session disposal to close the race window","If messages must survive, buffer them and replay into a replacement session instead of delivering to the disposed one"],"exampleFix":"// before\nircClient.on('message', async m => { await bridge.deliver(m, { expectsReply: true }); });\n// after\nircClient.on('message', async m => {\n  try {\n    if (session.isDisposed()) return; // or re-route\n    await bridge.deliver(m, { expectsReply: true });\n  } catch (e) {\n    if (!(e instanceof Error && e.message.startsWith('Recipient session is disposed'))) throw e;\n  }\n});","handlingStrategy":"validation","validationCode":"// before deliver:\n// if (bridge-host session isDisposed) skip or re-route the IRC message","typeGuard":null,"tryCatchPattern":"try {\n  await bridge.deliver(msg, { expectsReply: true });\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Recipient session is disposed')) {\n    logger.debug('dropped IRC message for disposed session', { from: msg.nick });\n    return;\n  }\n  throw err;\n}","preventionTips":["Detach IRC listeners at the start of session disposal to close the dispose/deliver race","Check isDisposed() on the host session in the IRC event handler before delivering","Route channel messages to a live session or an offline queue instead of a disposed one","Treat disposed-session delivery as expected traffic during shutdown, not a crash"],"tags":["irc","disposal","lifecycle","race-condition","bridge"],"backgroundTag":"operation-after-dispose","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}