paperclipai/paperclip · error · Error

Daytona duplex channel: no cached sandbox resolves the provi

Error message

Daytona duplex channel: no cached sandbox resolves the provider lease.

What it means

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.

Source

Thrown at packages/plugins/sandbox-providers/daytona/src/plugin.ts:2808

  // terminal is closed. The worker never keys the close on the worker session id.
  async onSetupTokenPtyClose(params) {
    const entry = daytonaSetupTokenPtyByRoute.get(params.hostRouteId);
    if (entry) {
      forgetDaytonaSetupTokenPty(entry);
      await entry.session.close().catch(() => undefined);
    }
    return { hostRouteId: params.hostRouteId };
  },

  // Open one persistent duplex channel. Resolve the cached sandbox by the provider
  // lease id, run the gateway command on a raw pseudo-terminal, and register the
  // channel under the host route id. Stream the raw data and the exit through
  // `ctx.duplexChannel`, bound to the returned worker session id. Fail closed when
  // no cached sandbox matches the lease.
  async onDuplexChannelOpen(params) {
    const sandbox = await sandboxHandleCache.findByProviderLeaseId(params.providerLeaseId);
    if (!sandbox) {
      throw new Error(
        "Daytona duplex channel: no cached sandbox resolves the provider lease.",
      );
    }
    const session = await openDuplexChannelSession(
      sandbox.process as unknown as DaytonaPtyProcess,
      params.command,
    );
    const workerSessionId = `duplex-${randomUUID()}`;
    const entry: DaytonaDuplexChannelEntry = {
      hostRouteId: params.hostRouteId,
      workerSessionId,
      providerLeaseId: params.providerLeaseId,
      session,
    };
    daytonaDuplexChannelByRoute.set(params.hostRouteId, entry);
    daytonaDuplexChannelBySession.set(workerSessionId, entry);
    // Register the data listener before the first write, so no early data chunk is
    // lost. The client stamps the worker session id, so the host binds the data to

View on GitHub (pinned to a7e689b3c3)

Solutions

  1. Re-provision the sandbox (acquire a fresh provider lease) and retry the channel open on the process that owns it.
  2. If the server restarted, re-run sandbox creation so the cache repopulates before any duplex channel open.
  3. Check the Daytona sandbox for the lease still exists; if it was removed, let the lease expire and create a new one.
  4. Ensure channel-open requests route to the same server/plugin process that handled sandbox creation (single-owner routing).

Example fix

// before
const session = await openDuplexChannelSession(sandbox.process, params.command);

// after (host-side guard before opening)
if (!(await sandboxHandleCache.findByProviderLeaseId(leaseId))) {
  await reProvisionSandbox(leaseId); // recreate sandbox + cache entry, then retry open
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Host side, before requesting the channel:
const cached = await sandboxHandleCache.findByProviderLeaseId(providerLeaseId);
if (!cached) {
  await reProvisionSandbox(providerLeaseId); // recreate sandbox, then open the channel
}

Try / catch

try {
  return await plugin.definition.onDuplexChannelOpen?.(params);
} catch (error) {
  if (error instanceof Error && error.message.includes("no cached sandbox resolves the provider lease")) {
    // lease/cache skew: re-provision the sandbox on THIS process, then retry once
    await provisionSandboxForLease(params.providerLeaseId);
    return await plugin.definition.onDuplexChannelOpen?.(params);
  }
  throw error;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-21). Data as JSON: /api/errors/aac633ea169db74f. Report an issue: GitHub.