{"record":{"id":"7baef7eafb8d334b","repo":"paperclipai/paperclip","slug":"duplex-channel-route-busy","errorCode":"DUPLEX_CHANNEL_ROUTE_BUSY","errorMessage":"DUPLEX_CHANNEL_ROUTE_BUSY","messagePattern":"DUPLEX_CHANNEL_ROUTE_BUSY","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"server/src/services/plugin-worker-manager.ts","lineNumber":1796,"sourceCode":"    route.buffered = [];\n    route.bufferedChars = 0;\n    route.preOpen = [];\n    route.preOpenExit = null;\n    clearDuplexChannelLifetimeTimer(route);\n    settleRouteWait(route, { exitCode: null });\n  }\n\n  // Open one live generic duplex channel route. Reserve the route before the open\n  // call, bind the worker session identifier one time on the first successful\n  // open reply, and return a session a caller drives. Terminalize the route on\n  // every open failure path.\n  async function openDuplexChannel(\n    input: DuplexChannelOpenInput,\n  ): Promise<DuplexChannelHostSession> {\n    if (duplexChannelRoute) {\n      // A route for this worker is not yet closed and confirmed. Reject the\n      // second open with one fixed non-secret error before it reaches the worker.\n      throw new Error(DUPLEX_CHANNEL_ROUTE_BUSY);\n    }\n    const hostRouteId = randomUUID();\n    let settleWait: (value: { exitCode: number | null }) => void = () => {};\n    const waitPromise = new Promise<{ exitCode: number | null }>((resolve) => {\n      settleWait = resolve;\n    });\n    const route: DuplexChannelRoute = {\n      hostRouteId,\n      state: \"reserved\",\n      workerSessionId: null,\n      listener: null,\n      buffered: [],\n      bufferedChars: 0,\n      preOpen: [],\n      preOpenExit: null,\n      pendingRequests: 0,\n      protocolErrors: 0,\n      totalDataBytes: 0,","sourceCodeStart":1778,"sourceCodeEnd":1814,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/server/src/services/plugin-worker-manager.ts#L1778-L1814","documentation":"Thrown by the plugin worker manager's openDuplexChannel when a duplex channel route already exists for the worker: each worker supports exactly one live generic duplex channel route, reserved before the open call and released only after close is confirmed (server/src/services/plugin-worker-manager.ts:1796, DUPLEX_CHANNEL_ROUTE_BUSY). A second open before the previous route reached its terminal state is rejected with this fixed, non-secret error before anything is sent to the worker.","triggerScenarios":"Calling openDuplexChannel twice on the same plugin worker without closing the first session — e.g. a client reconnects (dropped connection, retry loop) while the old route is still open/opening, or two callers stream into the same plugin concurrently.","commonSituations":"Reconnect logic that opens a new channel on transient failure without terminating the previous one; a leaked session whose close was never awaited; concurrent agents both granted duplex on the same plugin worker.","solutions":["Close and await the previous channel session (session close/terminal confirmation) before opening a new one.","Serialize channel opens per worker with a queue/mutex so only one route is live at a time.","If the previous session is unrecoverable, wait for its lifetime timer to expire and terminalize the route, then retry the open.","Design reconnect paths to reuse the existing session instead of opening a second route."],"exampleFix":"// before\nconst ch2 = await worker.openDuplexChannel(input); // DUPLEX_CHANNEL_ROUTE_BUSY while ch1 open\n\n// after\nawait ch1.close(); // route terminalized and confirmed\nconst ch2 = await worker.openDuplexChannel(input);","handlingStrategy":"retry","validationCode":"if (activeChannelForWorker(pluginId)) { await activeChannelForWorker(pluginId).close(); } // one live route per worker — close before open","typeGuard":null,"tryCatchPattern":"try { return await worker.openDuplexChannel(input); } catch (err) { if ((err as Error).message === \"DUPLEX_CHANNEL_ROUTE_BUSY\") { await existingSession.close(); return await worker.openDuplexChannel(input); } throw err; }","preventionTips":["Maintain exactly one duplex session per plugin worker; track it in the caller.","Always await session close before reconnecting after failures.","Serialize opens per worker (mutex/queue) in multi-caller code."],"tags":["duplex-channel","plugin-worker","resource-busy","concurrency","single-instance"],"backgroundTag":"resource-busy","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-21T17:58:32.592Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}