paperclipai/paperclip · critical

Daytona sandbox handle mismatch: handle ${sandbox.id} does n

Error message

Daytona sandbox handle mismatch: handle ${sandbox.id} does not belong to lease ${providerLeaseId}.

What it means

Thrown by assertHandleMatchesLease when a cached/resolved Daytona Sandbox handle's id does not equal the lease's providerLeaseId. This belt-and-suspenders guard prevents a handle from standing in for a different sandbox than the lease asked for, defending against a provider returning a renamed/substituted sandbox or a future key collision.

Source

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

    .digest("hex");
}

function sandboxHandleCacheKey(scope: SandboxScope): string {
  return stableStringify({
    driverKey: scope.driverKey,
    companyId: scope.companyId,
    environmentId: scope.environmentId,
    providerLeaseId: scope.providerLeaseId,
    account: sandboxAccountDiscriminator(scope.config),
  });
}

function assertHandleMatchesLease(sandbox: Sandbox, providerLeaseId: string): void {
  // C2: a handle must never stand in for a different sandbox than the lease
  // asked for. Belt-and-suspenders against a provider that returns a renamed or
  // substituted sandbox, and against any future key collision.
  if (sandbox.id !== providerLeaseId) {
    throw new Error(
      `Daytona sandbox handle mismatch: handle ${sandbox.id} does not belong to lease ${providerLeaseId}.`,
    );
  }
}

// A cached `Sandbox` carries the provider state captured when it was last
// fetched/refreshed. Daytona auto-stops an idle sandbox after `autoStopInterval`
// minutes, at which point that snapshot ("started") no longer matches reality
// and `ensureSandboxStarted` would wrongly skip the restart, sending every
// subsequent exec/sync at a stopped sandbox. Before reusing a handle that has
// gone untouched for this fraction of the auto-stop interval we re-read the live
// state so the restart decision is made against the truth. Reusing a handle for
// an operation resets Daytona's idle clock, so an actively-used lease stays well
// inside the window and never pays the refresh — only a lease resumed after an
// idle gap does.
const STALE_HANDLE_REFRESH_SAFETY_FRACTION = 0.5;

function staleHandleRefreshThresholdMs(autoStopIntervalMinutes: number | null): number | null {

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Release the current lease and acquire a fresh sandbox to get a consistent id.
  2. Verify the providerLeaseId and sandbox.id match in Daytona's dashboard/API.
  3. Check that the cache key (driverKey/companyId/environmentId/providerLeaseId/account discriminator) is unique per lease.
  4. If recurring, audit sandboxAccountDiscriminator inputs (apiUrl/target/apiKey) for unintended sharing.
Defensive patterns

Strategy: validation

Validate before calling

function handleMatchesLease(sandboxId: string, providerLeaseId: string): boolean {
  return sandboxId === providerLeaseId;
}

Try / catch

try {
  assertHandleMatchesLease(sandbox, providerLeaseId);
} catch (e) {
  if (e instanceof Error && e.message.includes('handle mismatch')) {
    // release and acquire a new lease; do not reuse the mismatched handle
  }
  throw e;
}

Prevention

When it happens

Trigger: getSandbox returns a Sandbox whose .id differs from scope.providerLeaseId, then assertHandleMatchesLease throws. Can occur if the cache key collides, the provider returns a renamed sandbox, or a stale handle is reused for a recycled lease id.

Common situations: Daytona recycled/substituted a sandbox under the same lease; a cache key collision after config changes (apiUrl/target/apiKey); manual lease id reuse across environments; a bug in cache key construction.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/794dcc4c2b946078. Report an issue: GitHub.