paperclipai/paperclip · error

runner_prp_session_released

runner_prp_session_released

Error message

runner_prp_session_released

What it means

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.

Source

Thrown at server/src/services/native-runtime/runner-prp-coordinator.ts:399

          }
          let timer: NodeJS.Timeout | null = null;
          try {
            return await Promise.race([
              terminalEvent,
              new Promise<never>((_resolve, reject) => {
                timer = setTimeout(
                  () => reject(new Error("runner_prp_terminal_timeout")),
                  timeoutMs,
                );
                timer.unref();
              }),
            ]);
          } finally {
            if (timer) clearTimeout(timer);
          }
        },
        waitForCommand: async (commandId, timeoutMs = 30_000) => {
          if (released) throw new Error("runner_prp_session_released");
          const deadline = Date.now() + timeoutMs;
          while (Date.now() < deadline) {
            const outcome = authority.commandOutcome(commandId);
            if (!outcome) throw new Error(`runner_prp_command_missing:${commandId}`);
            if (outcome.status === "completed") return outcome.result;
            if (outcome.status === "failed" || outcome.status === "rejected") {
              const message = outcome.result && typeof outcome.result.message === "string"
                ? outcome.result.message
                : `runner_prp_command_${outcome.status}:${commandId}`;
              throw new Error(message);
            }
            await new Promise<void>((resolve) => {
              const timer = setTimeout(resolve, 10);
              timer.unref();
            });
          }
          throw new Error(`runner_prp_command_timeout:${commandId}`);
        },

View on GitHub (pinned to 01ad858492)

Solutions

  1. Check session liveness before each use, or wrap calls in try/catch for /runner_prp_session_released/ and abort the loop instead of retrying.
  2. Keep a single owner of the session lifetime; cancel dependent waiters when release happens rather than sharing the handle across tasks.
  3. Re-acquire a fresh coordinator session (re-run runnerPrpCoordinator / re-register the authority) if the run is still active and commands must be sent.
  4. Verify the runner is still connected before issuing commands; a released session usually means the runner already disconnected.

Example fix

// before
const result = await session.waitForCommand(commandId, 30_000);

// after
let result;
try {
  result = await session.waitForCommand(commandId, 30_000);
} catch (e) {
  if (e.message === 'runner_prp_session_released') return; // session gone; stop
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (session.isReleased?.()) return; // track release state if exposed
const stillActive = !runFinished && runnerConnected; // guard with your own lifecycle flags

Type guard

const isSessionReleased = (e: unknown): e is Error =>
  e instanceof Error && e.message === 'runner_prp_session_released';

Try / catch

try {
  await session.waitForCommand(commandId);
} catch (e) {
  if (isSessionReleased(e)) {
    // session torn down: stop work, do NOT retry
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/5125828ffbe990a7. Report an issue: GitHub.