{"record":{"id":"5125828ffbe990a7","repo":"paperclipai/paperclip","slug":"runner-prp-session-released","errorCode":"runner_prp_session_released","errorMessage":"runner_prp_session_released","messagePattern":"runner_prp_session_released","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/runner-prp-coordinator.ts","lineNumber":399,"sourceCode":"          }\n          let timer: NodeJS.Timeout | null = null;\n          try {\n            return await Promise.race([\n              terminalEvent,\n              new Promise<never>((_resolve, reject) => {\n                timer = setTimeout(\n                  () => reject(new Error(\"runner_prp_terminal_timeout\")),\n                  timeoutMs,\n                );\n                timer.unref();\n              }),\n            ]);\n          } finally {\n            if (timer) clearTimeout(timer);\n          }\n        },\n        waitForCommand: async (commandId, timeoutMs = 30_000) => {\n          if (released) throw new Error(\"runner_prp_session_released\");\n          const deadline = Date.now() + timeoutMs;\n          while (Date.now() < deadline) {\n            const outcome = authority.commandOutcome(commandId);\n            if (!outcome) throw new Error(`runner_prp_command_missing:${commandId}`);\n            if (outcome.status === \"completed\") return outcome.result;\n            if (outcome.status === \"failed\" || outcome.status === \"rejected\") {\n              const message = outcome.result && typeof outcome.result.message === \"string\"\n                ? outcome.result.message\n                : `runner_prp_command_${outcome.status}:${commandId}`;\n              throw new Error(message);\n            }\n            await new Promise<void>((resolve) => {\n              const timer = setTimeout(resolve, 10);\n              timer.unref();\n            });\n          }\n          throw new Error(`runner_prp_command_timeout:${commandId}`);\n        },","sourceCodeStart":381,"sourceCodeEnd":417,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/native-runtime/runner-prp-coordinator.ts#L381-L417","documentation":"The PRP coordinator session object returned by runnerPrpCoordinator carries a 'released' flag that is set when the session's registration is released (runner disconnected, run finished, or teardown). Every public method — queueCommand, completeRun, waitForCommand, waitForGoalEvent — throws 'runner_prp_session_released' when invoked after release. It means the caller is using a stale session handle after its lifetime ended.","triggerScenarios":"Calling session.waitForCommand(commandId) (or queueCommand/completeRun/waitForTerminal/waitForGoalEvent) after the coordinator's release path ran — e.g. the runner websocket disconnected and registration.release() was invoked, or the run already completed and the coordinator tore the session down — while an in-flight async continuation still holds the old session object.","commonSituations":"Background polling loops that outlive the run; awaiting waitForCommand with a long timeout while a concurrent disconnect triggers release; retry logic reusing a captured session after a failed await; shutdown handlers releasing sessions while workers still reference them.","solutions":["Check session liveness before each use, or wrap calls in try/catch for /runner_prp_session_released/ and abort the loop instead of retrying.","Keep a single owner of the session lifetime; cancel dependent waiters when release happens rather than sharing the handle across tasks.","Re-acquire a fresh coordinator session (re-run runnerPrpCoordinator / re-register the authority) if the run is still active and commands must be sent.","Verify the runner is still connected before issuing commands; a released session usually means the runner already disconnected."],"exampleFix":"// before\nconst result = await session.waitForCommand(commandId, 30_000);\n\n// after\nlet result;\ntry {\n  result = await session.waitForCommand(commandId, 30_000);\n} catch (e) {\n  if (e.message === 'runner_prp_session_released') return; // session gone; stop\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"if (session.isReleased?.()) return; // track release state if exposed\nconst stillActive = !runFinished && runnerConnected; // guard with your own lifecycle flags","typeGuard":"const isSessionReleased = (e: unknown): e is Error =>\n  e instanceof Error && e.message === 'runner_prp_session_released';","tryCatchPattern":"try {\n  await session.waitForCommand(commandId);\n} catch (e) {\n  if (isSessionReleased(e)) {\n    // session torn down: stop work, do NOT retry\n    return null;\n  }\n  throw e;\n}","preventionTips":["Scope session usage to the run's lifetime; cancel dependent tasks on release.","Avoid storing session handles in long-lived registries or globals.","Subscribe to the coordinator's terminal/disconnect events to stop issuing commands proactively.","Never reuse a session object after completeRun resolves."],"tags":["session-lifecycle","race-condition","native-runtime"],"backgroundTag":"invalid-state-transition","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"}