{"record":{"id":"bf4fa170132b0dc9","repo":"paperclipai/paperclip","slug":"idempotency-key-conflict","errorCode":"idempotency_key_conflict","errorMessage":"idempotency_key_conflict","messagePattern":"idempotency_key_conflict","errorType":"error_code","errorClass":"RunnerGoalConflictError","httpStatus":null,"severity":"error","filePath":"server/src/services/runner-goals.ts","lineNumber":404,"sourceCode":"\n      const currentGoal = storedGoal(session);\n      const currentProjection: RunnerGoalProjection = {\n        ...initialProjection,\n        sessionId: session.id,\n        capability: storedCapability(session, fallbackCapability),\n        goal: currentGoal,\n        workingNow: currentGoal?.workingNow ?? false,\n        revision: session.goalRevision,\n        observedAt: session.goalObservedAt?.toISOString() ?? null,\n      };\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\"","sourceCodeStart":386,"sourceCodeEnd":422,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/runner-goals.ts#L386-L422","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Send the exact same payload when retrying with the same requestId (idempotent replay)","Generate a fresh unique requestId (e.g. UUID) whenever the request content changes","Stop reusing requestId across actions — scope it per logical goal mutation","Inspect the stored action row to confirm which payload the existing requestId was bound to"],"exampleFix":"// before\nconst requestId = agentId + ':' + issueId; // reused across different goals\n// after\nconst requestId = crypto.randomUUID(); // new key per distinct request\nawait runnerGoals.act(companyId, issueId, { agentId, action, requestId, ...samePayload });","handlingStrategy":"retry","validationCode":"// ensure request content is deterministic for a given requestId\nconst payload = canonicalize({ agentId, action, goal, tokenBudget, expectedRevision });\nif (seenRequests.has(requestId) && seenRequests.get(requestId) !== payload) {\n  requestId = crypto.randomUUID();\n}","typeGuard":"function isReplayable(original, current) {\n  return JSON.stringify(original) === JSON.stringify(current);\n}","tryCatchPattern":"try {\n  return await runnerGoals.act(companyId, issueId, req);\n} catch (e) {\n  if (e instanceof RunnerGoalConflictError && e.code === 'idempotency_key_conflict') {\n    req = { ...req, requestId: crypto.randomUUID() }; // new key for new content\n    return await runnerGoals.act(companyId, issueId, req);\n  }\n  throw e;\n}","preventionTips":["Use a fresh UUID per distinct request; only reuse requestId for byte-identical retries","Never mutate payload fields while keeping the same requestId","Scope requestId generation to the logical mutation, not the client or session","On conflict, inspect the stored action instead of blindly retrying"],"tags":["idempotency","conflict","request-id"],"backgroundTag":"invalid-argument-value","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"}