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
- 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).
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
- 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.
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
- Sandbox driver does not support duplex channels for this…
- CreateOS does not yet support leases with a guaranteed…
- CreateOS execution requires a lease from this environment.
- CreateOS lease cleanup must finish before resume.
- CreateOS lease is closing or requires cleanup.
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)