paperclipai/paperclip · error

${error}

Error message

${error}

What it means

In runnerPrpCoordinator (server/src/services/native-runtime/runner-prp-coordinator.ts:422), waitForGoalEvent rethrows a stored error for an already-observed goal request: if `observedGoalRequests.get(requestId)` yields an error string, that exact error message is thrown as-is. The `${error}` at line 422 is a dynamic replay of a previously recorded goal-event failure, so the concrete message depends on whatever error string was recorded against that requestId. It is a cached-failure replay mechanism: once a goal request failed, every subsequent waitForGoalEvent for that id throws the same failure.

Source

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

            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}`);
        },
        waitForGoalEvent: async (requestId, timeoutMs = 30_000) => {
          if (released) throw new Error("runner_prp_session_released");
          if (observedGoalRequests.has(requestId)) {
            const error = observedGoalRequests.get(requestId);
            if (error) throw new Error(error);
            return;
          }
          let timer: NodeJS.Timeout | null = null;
          let resolveGoalEvent!: () => void;
          let rejectGoalEvent!: (error: Error) => void;
          const goalEvent = new Promise<void>((resolve, reject) => {
            resolveGoalEvent = resolve;
            rejectGoalEvent = reject;
          });
          const waiter = { resolve: resolveGoalEvent, reject: rejectGoalEvent };
          const waiters = goalEventWaiters.get(requestId) ?? new Set<typeof waiter>();
          waiters.add(waiter);
          goalEventWaiters.set(requestId, waiters);
          try {
            await Promise.race([
              goalEvent,
              new Promise<never>((_resolve, reject) => {
                timer = setTimeout(() => reject(new Error("runner_prp_goal_event_timeout")), timeoutMs);

View on GitHub (pinned to 01ad858492)

Solutions

  1. Use a fresh requestId for the new goal attempt instead of re-waiting on an id whose failure is already recorded.
  2. Inspect the thrown message to learn the original goal failure, fix the underlying cause in the runner/goal pipeline, then retry with a new request id.
  3. Clear/reset the coordinator's observedGoalRequests state only if the recorded error is stale (e.g. after a session restart) — normally a new session is created instead.
  4. Add idempotency: check whether the request already failed before enqueuing the goal event and surface the error to the caller immediately.

Example fix

// before
await session.waitForGoalEvent(sameRequestId); // replays stored failure
// after
const freshRequestId = crypto.randomUUID(); // new attempt, no cached error
await authority.queueCommand("goal", { ...payload, requestId: freshRequestId });
await session.waitForGoalEvent(freshRequestId);
Defensive patterns

Strategy: retry

Validate before calling

// consult recorded outcome before waiting
const recorded = observedGoalRequests.get(requestId);
if (typeof recorded === "string") throw new Error(recorded); // will deterministically fail; use a new requestId instead

Type guard

function isReplayableFailure(err: unknown): err is Error {
  return err instanceof Error && !err.message.startsWith("runner_prp_goal_event_timeout");
}

Try / catch

try {
  await session.waitForGoalEvent(requestId);
} catch (err) {
  // stored replay: the original goal failure; retry only with a fresh requestId
  const freshId = crypto.randomUUID();
  await authority.queueCommand("goal", { ...payload, requestId: freshId });
  await session.waitForGoalEvent(freshId);
}

Prevention

When it happens

Trigger: Calling waitForGoalEvent(requestId) after the goal event for that requestId already arrived with a recorded error — observedGoalRequests holds an error entry for the id, so the stored error is thrown instead of waiting again.

Common situations: A goal request previously failed (e.g. runner reported an error for that goal), and the continuation/heartbeat code retries waitForGoalEvent for the same requestId, deterministically hitting the replayed failure; stale request ids from a prior run being re-waited.

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/706dc8084f4968ec. Report an issue: GitHub.