paperclipai/paperclip · error · Error

Daytona duplex channel: no cached sandbox resolves the…

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.

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@3f1d897a7c (2026-08-21). Data as JSON: /api/errors/aac633ea169db74f. Report an issue: GitHub.

Appendix: source

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

    const scope = {
      driverKey: params.driverKey,
      companyId: params.companyId,
      environmentId: params.environmentId,
      providerLeaseId: params.lease.providerLeaseId,
      config,
    };
    // Collect the advisory read-write destinations for this scope. This records
    // intent only; it does not change the transfer below.
    sandboxHandleWritableDirs.recordWritableTargets(scope, params.operations);
    return await withSandboxActivityGate(scope, async () => {
      const sandbox = await getSandbox(scope, { bypassTeardownGate: true });
      await ensureSandboxStarted(sandbox, timeoutSeconds);
      const result = await performSyncIn({
        sandbox,
        operations: params.operations,
        remoteDir,
        timeoutSeconds,
      });
      sandboxHandleCache.markFresh(scope);
      return result;
    });
  },

  // Opt-in native outbound transfer. See onEnvironmentSyncIn.
  async onEnvironmentSyncOut(
    params: PluginEnvironmentSyncOutParams,
  ): Promise<PluginEnvironmentSyncResult> {
    if (!params.lease.providerLeaseId) {
      throw new Error("Daytona syncOut requires a provider lease ID.");
    }
    const config = parseDriverConfig(params.config);
    const remoteDir = resolveSyncRemoteDir(params.lease);
    const timeoutSeconds = toTimeoutSeconds(config.timeoutMs);
    const scope = {
      driverKey: params.driverKey,
      companyId: params.companyId,

View on GitHub (pinned to 3f1d897a7c)