paperclipai/paperclip · warning · RunnerGoalConflictError

stale_revision

stale_revision

Error message

stale_revision

What it means

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.

Source

Thrown at server/src/services/runner-goals.ts:414

      };

      const [existingAction] = await tx.select().from(agentSessionGoalActions).where(and(
        eq(agentSessionGoalActions.sessionId, session.id),
        eq(agentSessionGoalActions.requestId, request.requestId),
      )).limit(1);
      if (existingAction) {
        if (!isSameGoalActionRequest(existingAction.payloadJson, request)) {
          throw new RunnerGoalConflictError("idempotency_key_conflict", currentProjection);
        }
        return {
          repeated: true,
          session,
          status: existingAction.status,
          result: asRecord(existingAction.resultJson),
        };
      }
      if (session.goalRevision !== request.expectedRevision) {
        throw new RunnerGoalConflictError("stale_revision", currentProjection);
      }
      const capability = storedCapability(session, fallbackCapability);
      const requiredCapabilityAction = request.action === "pause"
        ? "pause"
        : request.action === "resume"
          ? "resume"
          : request.action === "clear"
            ? "clear"
            : "set";
      if (capability.availability !== "available" || !capability.actions.includes(requiredCapabilityAction)) {
        throw new RunnerGoalActionError(
          capability.reasonCode ?? "goal_action_unsupported",
          capability.reason ?? `${request.action} is unsupported by this agent session.`,
        );
      }
      if (request.tokenBudget != null && !capability.tokenBudgetControl) {
        throw new RunnerGoalActionError(
          "goal_token_budget_unsupported",

View on GitHub (pinned to 01ad858492)

Solutions

  1. Re-read the projection to get the current goalRevision, then retry with that value as expectedRevision
  2. Implement a retry-on-conflict loop that refreshes the projection on each attempt
  3. Serialize goal mutations through a single actor per session
  4. Check recent goal actions on the session to see who advanced the revision

Example fix

// before
await act(companyId, issueId, { ...req, expectedRevision: cachedRevision });
// after
for (let i = 0; i < 3; i++) {
  const proj = await projection(companyId, issueId, agentId);
  try { return await act(companyId, issueId, { ...req, expectedRevision: proj.goalRevision }); }
  catch (e) { if (e.code !== 'stale_revision') throw e; }
}
Defensive patterns

Strategy: retry

Validate before calling

const proj = await runnerGoals.projection(companyId, issueId, agentId);
const expectedRevision = proj.goalRevision;

Type guard

function revisionIsCurrent(proj, expectedRevision) {
  return proj != null && proj.goalRevision === expectedRevision;
}

Try / catch

try {
  await runnerGoals.act(companyId, issueId, req);
} catch (e) {
  if (e instanceof RunnerGoalConflictError && e.code === 'stale_revision') {
    const proj = await runnerGoals.projection(companyId, issueId, agentId);
    return await runnerGoals.act(companyId, issueId, { ...req, expectedRevision: proj.goalRevision });
  }
  throw e;
}

Prevention

When it happens

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

Common situations: Board UI and an automated runner both mutating goals; double-submitted forms; retry loops that don't refresh the projection between attempts.

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/2fa81e6bf44a7045. Report an issue: GitHub.