paperclipai/paperclip · error
A reusable lease cannot be replaced and reacquired in the sa
Error message
A reusable lease cannot be replaced and reacquired in the same operation.
What it means
A lease operation may either replace a reusable lease with a new one (replacesReusableLeaseId) or reacquire the same lease (reusesReusableLeaseId), but not both at once. environmentService throws when both flags are set, because the two handoff semantics are mutually exclusive and the resulting state would be ambiguous.
Source
Thrown at server/src/services/environments.ts:1400
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) => {
if (input.assertCompanyBinding) {
// Lock the environment row first. Managed reconciliation locks the
// same sandbox environment rows with `for update` before it writes a
// company binding, so this lock serializes the two transactions on
// this row and closes the time-of-check to time-of-use window.
await tx
.select({ id: environments.id })
.from(environments)
.where(eq(environments.id, input.environmentId))
.for("update");View on GitHub (pinned to 01ad858492)
Solutions
- Decide the intended semantics and pass exactly one of replacesReusableLeaseId or reusesReusableLeaseId
- Stop blindly spreading previous lease options into the new request; build options explicitly per operation
- In retry logic, recompute the handoff mode instead of reusing stale flags
- Add a caller-side assertion/validation to reject both-set inputs early
Example fix
// before
updateLease({ replacesReusableLeaseId: oldId, reusesReusableLeaseId: oldId })
// after
updateLease({ reusesReusableLeaseId: oldId }) // reacquire OR replace, never both Defensive patterns
Strategy: validation
Validate before calling
if (input.replacesReusableLeaseId && input.reusesReusableLeaseId) {
throw new Error('choose either replace or reacquire, not both');
}
await updateLease(input); Type guard
function isExclusiveHandoff(i: {replacesReusableLeaseId?:string;reusesReusableLeaseId?:string}): boolean {
return !(i.replacesReusableLeaseId && i.reusesReusableLeaseId);
} Try / catch
try {
await environmentService.updateLease(input);
} catch (e) {
if (e.message.includes('replaced and reacquired')) {
// Prefer reacquire when both were set with the same id
const { replacesReusableLeaseId, ...rest } = input;
return environmentService.updateLease({ ...rest, reusesReusableLeaseId: input.reusesReusableLeaseId ?? replacesReusableLeaseId });
}
throw e;
} Prevention
- Build lease request options explicitly instead of spreading prior request objects
- Model replace vs reacquire as a discriminated union type so both cannot be set at compile time
- Zod schema with .refine() rejecting both-set inputs at the boundary
- Review retry logic to recompute handoff mode rather than replaying stale flags
When it happens
Trigger: A single environmentService lease call passing both replacesReusableLeaseId and reusesReusableLeaseId — e.g. caller code that copies all prior lease fields into a new request, carrying over the old replace id while also adding reuse.
Common situations: Request builders that spread previous options into the next call; a retry path that re-sent the original replace flag while the run switched to reacquire semantics; confused migration code between the two lease modes.
Related errors
- A reusable lease handoff requires an execution workspace and
- A same-run reusable lease reacquisition requires a heartbeat
- Invalid status '${String(rawStatus)}'. Must be one of: ${PLU
- "tool" is required and must be a string
- "runContext" is required and must be an object
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/96f7f70d1c18c76b.
Report an issue: GitHub.