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
- 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
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
- 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
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
- A different semantic result was already committed
- Invalid ${name} JSON: ${err instanceof Error ? err.message :
- Challenge secret is required. Pass --token or --token-env.
- Cannot build API path with an empty path segment.
- Report refresh output must be a new directory
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/bf4fa170132b0dc9.
Report an issue: GitHub.