paperclipai/paperclip · error · RunnerGoalConflictError

idempotency_key_conflict

idempotency_key_conflict

Error message

idempotency_key_conflict

What it means

`act` is idempotent via `requestId` stored in `agentSessionGoalActions`. If an action with the same requestId on the same session already exists but its stored payload differs from the incoming request (`!isSameGoalActionRequest`), it throws RunnerGoalConflictError with code `idempotency_key_conflict`. The same key must always carry the same request.

Source

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

      const currentGoal = storedGoal(session);
      const currentProjection: RunnerGoalProjection = {
        ...initialProjection,
        sessionId: session.id,
        capability: storedCapability(session, fallbackCapability),
        goal: currentGoal,
        workingNow: currentGoal?.workingNow ?? false,
        revision: session.goalRevision,
        observedAt: session.goalObservedAt?.toISOString() ?? null,
      };

      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"

View on GitHub (pinned to 01ad858492)

Solutions

  1. Send the exact same payload when retrying with the same requestId (idempotent replay)
  2. Generate a fresh unique requestId (e.g. UUID) whenever the request content changes
  3. Stop reusing requestId across actions — scope it per logical goal mutation
  4. Inspect the stored action row to confirm which payload the existing requestId was bound to

Example fix

// before
const requestId = agentId + ':' + issueId; // reused across different goals
// after
const requestId = crypto.randomUUID(); // new key per distinct request
await runnerGoals.act(companyId, issueId, { agentId, action, requestId, ...samePayload });
Defensive patterns

Strategy: retry

Validate before calling

// ensure request content is deterministic for a given requestId
const payload = canonicalize({ agentId, action, goal, tokenBudget, expectedRevision });
if (seenRequests.has(requestId) && seenRequests.get(requestId) !== payload) {
  requestId = crypto.randomUUID();
}

Type guard

function isReplayable(original, current) {
  return JSON.stringify(original) === JSON.stringify(current);
}

Try / catch

try {
  return await runnerGoals.act(companyId, issueId, req);
} catch (e) {
  if (e instanceof RunnerGoalConflictError && e.code === 'idempotency_key_conflict') {
    req = { ...req, requestId: crypto.randomUUID() }; // new key for new content
    return await runnerGoals.act(companyId, issueId, req);
  }
  throw e;
}

Prevention

When it happens

Trigger: Reusing an idempotency requestId for a different goal action (different goal text, action, tokenBudget, expectedRevision, etc.); a client generates non-unique requestIds by hashing over wrong fields; retrying with a mutated payload instead of a fresh requestId.

Common situations: Client retry logic replays the requestId but edits the goal text between attempts; shared request pool hands out duplicate keys; clock/revision differences make payloads compare unequal on retry.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/bf4fa170132b0dc9. Report an issue: GitHub.