{"record":{"id":"2fa81e6bf44a7045","repo":"paperclipai/paperclip","slug":"stale-revision","errorCode":"stale_revision","errorMessage":"stale_revision","messagePattern":"stale_revision","errorType":"error_code","errorClass":"RunnerGoalConflictError","httpStatus":null,"severity":"warning","filePath":"server/src/services/runner-goals.ts","lineNumber":414,"sourceCode":"      };\n\n      const [existingAction] = await tx.select().from(agentSessionGoalActions).where(and(\n        eq(agentSessionGoalActions.sessionId, session.id),\n        eq(agentSessionGoalActions.requestId, request.requestId),\n      )).limit(1);\n      if (existingAction) {\n        if (!isSameGoalActionRequest(existingAction.payloadJson, request)) {\n          throw new RunnerGoalConflictError(\"idempotency_key_conflict\", currentProjection);\n        }\n        return {\n          repeated: true,\n          session,\n          status: existingAction.status,\n          result: asRecord(existingAction.resultJson),\n        };\n      }\n      if (session.goalRevision !== request.expectedRevision) {\n        throw new RunnerGoalConflictError(\"stale_revision\", currentProjection);\n      }\n      const capability = storedCapability(session, fallbackCapability);\n      const requiredCapabilityAction = request.action === \"pause\"\n        ? \"pause\"\n        : request.action === \"resume\"\n          ? \"resume\"\n          : request.action === \"clear\"\n            ? \"clear\"\n            : \"set\";\n      if (capability.availability !== \"available\" || !capability.actions.includes(requiredCapabilityAction)) {\n        throw new RunnerGoalActionError(\n          capability.reasonCode ?? \"goal_action_unsupported\",\n          capability.reason ?? `${request.action} is unsupported by this agent session.`,\n        );\n      }\n      if (request.tokenBudget != null && !capability.tokenBudgetControl) {\n        throw new RunnerGoalActionError(\n          \"goal_token_budget_unsupported\",","sourceCodeStart":396,"sourceCodeEnd":432,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/runner-goals.ts#L396-L432","documentation":"Optimistic-concurrency control: `act` compares `session.goalRevision` to `request.expectedRevision` and throws RunnerGoalConflictError with code `stale_revision` when they differ. Another goal action already advanced the revision, so the caller's view is stale.","triggerScenarios":"Two clients act on the same session concurrently; the caller cached expectedRevision from an older projection while another action (set/pause/clear/etc.) incremented goalRevision; automatic retries after the first request actually succeeded.","commonSituations":"Board UI and an automated runner both mutating goals; double-submitted forms; retry loops that don't refresh the projection between attempts.","solutions":["Re-read the projection to get the current goalRevision, then retry with that value as expectedRevision","Implement a retry-on-conflict loop that refreshes the projection on each attempt","Serialize goal mutations through a single actor per session","Check recent goal actions on the session to see who advanced the revision"],"exampleFix":"// before\nawait act(companyId, issueId, { ...req, expectedRevision: cachedRevision });\n// after\nfor (let i = 0; i < 3; i++) {\n  const proj = await projection(companyId, issueId, agentId);\n  try { return await act(companyId, issueId, { ...req, expectedRevision: proj.goalRevision }); }\n  catch (e) { if (e.code !== 'stale_revision') throw e; }\n}","handlingStrategy":"retry","validationCode":"const proj = await runnerGoals.projection(companyId, issueId, agentId);\nconst expectedRevision = proj.goalRevision;","typeGuard":"function revisionIsCurrent(proj, expectedRevision) {\n  return proj != null && proj.goalRevision === expectedRevision;\n}","tryCatchPattern":"try {\n  await runnerGoals.act(companyId, issueId, req);\n} catch (e) {\n  if (e instanceof RunnerGoalConflictError && e.code === 'stale_revision') {\n    const proj = await runnerGoals.projection(companyId, issueId, agentId);\n    return await runnerGoals.act(companyId, issueId, { ...req, expectedRevision: proj.goalRevision });\n  }\n  throw e;\n}","preventionTips":["Always fetch expectedRevision from a fresh projection right before acting","Cap retry attempts to avoid loops under heavy contention","Serialize goal mutations to one writer per session","Re-read the projection after any conflict instead of reusing revisions"],"tags":["concurrency","conflict","revision","optimistic-locking"],"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-14T05:17:10.506Z"}