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
- Use a fresh requestId for the new goal attempt instead of re-waiting on an id whose failure is already recorded.
- 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.
- 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.
- 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
- Never reuse requestIds across goal attempts; generate a new id per attempt.
- Check observedGoalRequests (or an equivalent outcome cache) before enqueueing/waiting on a goal request.
- Surface the replayed error message to logs — it is the original root-cause failure, not a new one.
- Design goal flows idempotently so a new request id is cheap and safe to issue.
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
- Public replay contains unprojected evidence
- Public replay contains an unprojected item
- Warm run transition permits only its exact cached attachment
- Paperclip run authentication is unavailable
- paperclip_runner_approval_not_found
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/706dc8084f4968ec.
Report an issue: GitHub.