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
- 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
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
- 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
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
- Discord correction draft ownership changed
- Failed to start worktree port reservation lock heartbeat at
- Timed out waiting for worktree port reservation lock at ${lo
- sandbox runtime asset key collides with a reserved runtime a
- DUPLEX_CHANNEL_ROUTE_BUSY
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/2fa81e6bf44a7045.
Report an issue: GitHub.