{"record":{"id":"706dc8084f4968ec","repo":"paperclipai/paperclip","slug":"error","errorCode":null,"errorMessage":"${error}","messagePattern":"\\$\\{error\\}","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/runner-prp-coordinator.ts","lineNumber":422,"sourceCode":"            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        },\n        waitForGoalEvent: async (requestId, timeoutMs = 30_000) => {\n          if (released) throw new Error(\"runner_prp_session_released\");\n          if (observedGoalRequests.has(requestId)) {\n            const error = observedGoalRequests.get(requestId);\n            if (error) throw new Error(error);\n            return;\n          }\n          let timer: NodeJS.Timeout | null = null;\n          let resolveGoalEvent!: () => void;\n          let rejectGoalEvent!: (error: Error) => void;\n          const goalEvent = new Promise<void>((resolve, reject) => {\n            resolveGoalEvent = resolve;\n            rejectGoalEvent = reject;\n          });\n          const waiter = { resolve: resolveGoalEvent, reject: rejectGoalEvent };\n          const waiters = goalEventWaiters.get(requestId) ?? new Set<typeof waiter>();\n          waiters.add(waiter);\n          goalEventWaiters.set(requestId, waiters);\n          try {\n            await Promise.race([\n              goalEvent,\n              new Promise<never>((_resolve, reject) => {\n                timer = setTimeout(() => reject(new Error(\"runner_prp_goal_event_timeout\")), timeoutMs);","sourceCodeStart":404,"sourceCodeEnd":440,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/native-runtime/runner-prp-coordinator.ts#L404-L440","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait session.waitForGoalEvent(sameRequestId); // replays stored failure\n// after\nconst freshRequestId = crypto.randomUUID(); // new attempt, no cached error\nawait authority.queueCommand(\"goal\", { ...payload, requestId: freshRequestId });\nawait session.waitForGoalEvent(freshRequestId);","handlingStrategy":"retry","validationCode":"// consult recorded outcome before waiting\nconst recorded = observedGoalRequests.get(requestId);\nif (typeof recorded === \"string\") throw new Error(recorded); // will deterministically fail; use a new requestId instead","typeGuard":"function isReplayableFailure(err: unknown): err is Error {\n  return err instanceof Error && !err.message.startsWith(\"runner_prp_goal_event_timeout\");\n}","tryCatchPattern":"try {\n  await session.waitForGoalEvent(requestId);\n} catch (err) {\n  // stored replay: the original goal failure; retry only with a fresh requestId\n  const freshId = crypto.randomUUID();\n  await authority.queueCommand(\"goal\", { ...payload, requestId: freshId });\n  await session.waitForGoalEvent(freshId);\n}","preventionTips":["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."],"tags":["cached-failure","goal-event","native-runtime","replay"],"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"}