{"record":{"id":"32a91cde3f4a7555","repo":"paperclipai/paperclip","slug":"a-same-run-reusable-lease-reacquisition-requires-a","errorCode":null,"errorMessage":"A same-run reusable lease reacquisition requires a heartbeat run id.","messagePattern":"A same-run reusable lease reacquisition requires a heartbeat run id\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/environments.ts","lineNumber":1395,"sourceCode":"        lastUsedAt: now,\n        expiresAt: input.expiresAt ?? null,\n        releasedAt: null,\n        failureReason: null,\n        cleanupStatus: null,\n        metadata: input.metadata ?? null,\n        createdAt: now,\n        updatedAt: now,\n      };\n      if (\n        (input.replacesReusableLeaseId || input.reusesReusableLeaseId) &&\n        (!input.executionWorkspaceId || !input.providerLeaseId)\n      ) {\n        throw new Error(\n          \"A reusable lease handoff requires an execution workspace and provider lease id.\",\n        );\n      }\n      if (input.reusesReusableLeaseId && !input.heartbeatRunId) {\n        throw new Error(\n          \"A same-run reusable lease reacquisition requires a heartbeat run id.\",\n        );\n      }\n      if (input.replacesReusableLeaseId && input.reusesReusableLeaseId) {\n        throw new Error(\n          \"A reusable lease cannot be replaced and reacquired in the same operation.\",\n        );\n      }\n      const row =\n        input.assertCompanyBinding ||\n        input.replacesReusableLeaseId ||\n        input.reusesReusableLeaseId\n          ? await db.transaction(async (tx) => {\n              if (input.assertCompanyBinding) {\n                // Lock the environment row first. Managed reconciliation locks the\n                // same sandbox environment rows with `for update` before it writes a\n                // company binding, so this lock serializes the two transactions on\n                // this row and closes the time-of-check to time-of-use window.","sourceCodeStart":1377,"sourceCodeEnd":1413,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/environments.ts#L1377-L1413","documentation":"When reusing an existing reusable lease within the same run (reusesReusableLeaseId set), the operation must include heartbeatRunId so the reacquisition is attributable and scoped to the current run. It throws when a same-run reacquisition is requested without that run id.","triggerScenarios":"Calling environmentService lease reacquisition with reusesReusableLeaseId but heartbeatRunId missing/undefined — typically a heartbeat-driven reacquire path that lost its run context.","commonSituations":"Caller invoked outside a heartbeat run context (manual script or retry path) but still flagged as same-run reuse; run id not propagated through the service layer after refactor; test harness omitting heartbeatRunId.","solutions":["Pass the current heartbeatRunId whenever reusesReusableLeaseId is set","If there is genuinely no run, drop reusesReusableLeaseId and take the normal acquisition path instead","Propagate run context through the calling service so the id reaches environmentService","Check heartbeat wiring to confirm the reacquire call happens inside an active run"],"exampleFix":"// before\nreacquireLease({ reusesReusableLeaseId: leaseId })\n// after\nreacquireLease({ reusesReusableLeaseId: leaseId, heartbeatRunId: run.id })","handlingStrategy":"validation","validationCode":"if (input.reusesReusableLeaseId && !input.heartbeatRunId) {\n  throw new Error('same-run reacquisition requires heartbeatRunId');\n}\nawait reacquireLease(input);","typeGuard":"function isSameRunReacquire(i: {reusesReusableLeaseId?:string;heartbeatRunId?:string|null}): boolean {\n  return !i.reusesReusableLeaseId || Boolean(i.heartbeatRunId);\n}","tryCatchPattern":"try {\n  await environmentService.reacquireLease(input);\n} catch (e) {\n  if (e.message.includes('heartbeat run id')) {\n    if (currentRun) return environmentService.reacquireLease({ ...input, heartbeatRunId: currentRun.id });\n    return environmentService.acquireLease({ ...input, reusesReusableLeaseId: undefined }); // fall back to fresh acquire\n  }\n  throw e;\n}","preventionTips":["Thread heartbeatRunId through the run context so every lease call inside a run has it","When calling outside a run, never set reusesReusableLeaseId","Write an integration test for the heartbeat reacquire path","Validate inputs at the route/service boundary before deeper calls"],"tags":["leases","validation","environments"],"backgroundTag":"missing-required-argument","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}