{"record":{"id":"aac633ea169db74f","repo":"paperclipai/paperclip","slug":"daytona-duplex-channel-no-cached-sandbox-resolves","errorCode":null,"errorMessage":"Daytona duplex channel: no cached sandbox resolves the provider lease.","messagePattern":"Daytona duplex channel: no cached sandbox resolves the provider lease\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/daytona/src/plugin.ts","lineNumber":2808,"sourceCode":"  // terminal is closed. The worker never keys the close on the worker session id.\n  async onSetupTokenPtyClose(params) {\n    const entry = daytonaSetupTokenPtyByRoute.get(params.hostRouteId);\n    if (entry) {\n      forgetDaytonaSetupTokenPty(entry);\n      await entry.session.close().catch(() => undefined);\n    }\n    return { hostRouteId: params.hostRouteId };\n  },\n\n  // Open one persistent duplex channel. Resolve the cached sandbox by the provider\n  // lease id, run the gateway command on a raw pseudo-terminal, and register the\n  // channel under the host route id. Stream the raw data and the exit through\n  // `ctx.duplexChannel`, bound to the returned worker session id. Fail closed when\n  // no cached sandbox matches the lease.\n  async onDuplexChannelOpen(params) {\n    const sandbox = await sandboxHandleCache.findByProviderLeaseId(params.providerLeaseId);\n    if (!sandbox) {\n      throw new Error(\n        \"Daytona duplex channel: no cached sandbox resolves the provider lease.\",\n      );\n    }\n    const session = await openDuplexChannelSession(\n      sandbox.process as unknown as DaytonaPtyProcess,\n      params.command,\n    );\n    const workerSessionId = `duplex-${randomUUID()}`;\n    const entry: DaytonaDuplexChannelEntry = {\n      hostRouteId: params.hostRouteId,\n      workerSessionId,\n      providerLeaseId: params.providerLeaseId,\n      session,\n    };\n    daytonaDuplexChannelByRoute.set(params.hostRouteId, entry);\n    daytonaDuplexChannelBySession.set(workerSessionId, entry);\n    // Register the data listener before the first write, so no early data chunk is\n    // lost. The client stamps the worker session id, so the host binds the data to","sourceCodeStart":2790,"sourceCodeEnd":2826,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/packages/plugins/sandbox-providers/daytona/src/plugin.ts#L2790-L2826","documentation":"Thrown by the Daytona provider's onDuplexChannelOpen when sandboxHandleCache.findByProviderLeaseId(providerLeaseId) returns nothing. The duplex channel must run on a sandbox this plugin process previously created and cached; a lease with no cached sandbox means the channel cannot be opened safely, so the provider fails closed instead of guessing.","triggerScenarios":"The host sends a duplexChannelOpen RPC for a lease that was never provisioned in this process, whose sandbox entry was evicted after removal/expiry, or after a plugin/server restart that cleared the in-memory sandboxHandleCache while the host still routes by the old lease id.","commonSituations":"Sandbox deleted or expired on the Daytona side between lease grant and channel open; server restart or plugin reload losing cache state; multiple server replicas where the channel-open lands on a process that never created the sandbox; stale host-side routing metadata after a lease rotation.","solutions":["Re-provision the sandbox (acquire a fresh provider lease) and retry the channel open on the process that owns it.","If the server restarted, re-run sandbox creation so the cache repopulates before any duplex channel open.","Check the Daytona sandbox for the lease still exists; if it was removed, let the lease expire and create a new one.","Ensure channel-open requests route to the same server/plugin process that handled sandbox creation (single-owner routing)."],"exampleFix":"// before\nconst session = await openDuplexChannelSession(sandbox.process, params.command);\n\n// after (host-side guard before opening)\nif (!(await sandboxHandleCache.findByProviderLeaseId(leaseId))) {\n  await reProvisionSandbox(leaseId); // recreate sandbox + cache entry, then retry open\n}","handlingStrategy":"try-catch","validationCode":"// Host side, before requesting the channel:\nconst cached = await sandboxHandleCache.findByProviderLeaseId(providerLeaseId);\nif (!cached) {\n  await reProvisionSandbox(providerLeaseId); // recreate sandbox, then open the channel\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await plugin.definition.onDuplexChannelOpen?.(params);\n} catch (error) {\n  if (error instanceof Error && error.message.includes(\"no cached sandbox resolves the provider lease\")) {\n    // lease/cache skew: re-provision the sandbox on THIS process, then retry once\n    await provisionSandboxForLease(params.providerLeaseId);\n    return await plugin.definition.onDuplexChannelOpen?.(params);\n  }\n  throw error;\n}","preventionTips":["Route duplexChannelOpen to the same process that created the sandbox.","Re-acquire leases after server or plugin restarts before reusing cached route ids.","Treat sandbox removal/expiry events as invalidations of pending channel opens.","Do not disable the fail-closed check; a wrong sandbox would leak the channel to another tenant."],"tags":["daytona","sandbox","duplex-channel","cache-miss","lease"],"backgroundTag":"stale-resource-handle","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-21T17:58:32.592Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}