paperclipai/paperclip · error

A reusable lease handoff requires an execution workspace…

Error message

A reusable lease handoff requires an execution workspace and provider lease id.

What it means

environmentService's lease lifecycle validation: when an operation hands off a reusable lease (replacesReusableLeaseId or reusesReusableLeaseId is set), the input must also carry executionWorkspaceId and providerLeaseId. It throws when a handoff/reacquisition is requested without the workspace and provider lease identifiers needed to bind the new lease.

Solutions

  1. Populate both executionWorkspaceId and providerLeaseId whenever replacesReusableLeaseId or reusesReusableLeaseId is passed
  2. Fix the caller to only set the handoff flags when a real provider lease exists (check provider response before calling)
  3. If no handoff is intended, omit replacesReusableLeaseId/reusesReusableLeaseId entirely
  4. Backfill the missing workspace/provider lease ids from the provider API before retrying

Example fix

// before
acquireLease({ replacesReusableLeaseId: prevId })
// after
acquireLease({ replacesReusableLeaseId: prevId, executionWorkspaceId: ws.id, providerLeaseId: lease.providerLeaseId })
Defensive patterns

Strategy: validation

Validate before calling

if ((input.replacesReusableLeaseId || input.reusesReusableLeaseId) && (!input.executionWorkspaceId || !input.providerLeaseId)) {
  throw new Error('handoff requires executionWorkspaceId and providerLeaseId');
}
await acquireLease(input);

Type guard

function isCompleteLeaseHandoff(i: {replacesReusableLeaseId?:string;reusesReusableLeaseId?:string;executionWorkspaceId?:string|null;providerLeaseId?:string|null}): boolean {
  return !(i.replacesReusableLeaseId || i.reusesReusableLeaseId) || Boolean(i.executionWorkspaceId && i.providerLeaseId);
}

Try / catch

try {
  await environmentService.acquireReusableLease(input);
} catch (e) {
  if (e.message.includes('handoff requires')) {
    const providerLease = await provider.createLease(); // fetch missing ids then retry
    return environmentService.acquireReusableLease({ ...input, executionWorkspaceId: ws.id, providerLeaseId: providerLease.id });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling the lease acquire/replace path with replacesReusableLeaseId or reusesReusableLeaseId set but executionWorkspaceId or providerLeaseId null/undefined (e.g. seedReusablePluginSandboxLease or reusableLease callers omitting fields).

Common situations: Partial input construction after a refactor added the handoff fields; a caller migrating from non-reusable to reusable leases without provider lease ids; provider failed to return a lease id but the flow continued; DB row missing executionWorkspaceId.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-10). Data as JSON: /api/errors/c9104ef2fcfc1ae0. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/environments.ts:1448

        status: "active" as const,
        leasePolicy: input.leasePolicy ?? "ephemeral",
        provider: input.provider ?? null,
        providerLeaseId: input.providerLeaseId ?? null,
        acquiredAt: now,
        lastUsedAt: now,
        expiresAt: input.expiresAt ?? null,
        releasedAt: null,
        failureReason: null,
        cleanupStatus: null,
        metadata: input.metadata ?? null,
        createdAt: now,
        updatedAt: now,
      };
      if (
        (input.replacesReusableLeaseId || input.reusesReusableLeaseId) &&
        (!input.executionWorkspaceId || !input.providerLeaseId)
      ) {
        throw new Error(
          "A reusable lease handoff requires an execution workspace and provider lease id.",
        );
      }
      if (input.reusesReusableLeaseId && !input.heartbeatRunId) {
        throw new Error(
          "A same-run reusable lease reacquisition requires a heartbeat run id.",
        );
      }
      if (input.replacesReusableLeaseId && input.reusesReusableLeaseId) {
        throw new Error(
          "A reusable lease cannot be replaced and reacquired in the same operation.",
        );
      }
      const row =
        input.assertCompanyBinding ||
        input.replacesReusableLeaseId ||
        input.reusesReusableLeaseId
          ? await db.transaction(async (tx) => {

View on GitHub (pinned to 3f1d897a7c)