{"record":{"id":"4d78e3b4559898f8","repo":"paperclipai/paperclip","slug":"photon-poll-already-claimed","errorCode":null,"errorMessage":"Photon poll already claimed","messagePattern":"Photon poll already claimed","errorType":"validation","errorClass":"PhotonAnswerValidationError","httpStatus":null,"severity":"error","filePath":"server/src/services/photon/interactions.ts","lineNumber":291,"sourceCode":"    type PollSend = {\n      schema: 1;\n      phase: \"prepared\" | \"creating\" | \"created\";\n      receipt?: {\n        pollMessageGuid: string;\n        options: Array<{ optionIdentifier: string; text: string }>;\n      };\n    };\n    let saved = await adapter.state.read<PollSend>(stateKey);\n    if (saved?.phase === \"creating\" && !input.retryUnknown)\n      throw new PhotonError(\n        \"delivery_unknown\",\n        \"Photon poll creation is unknown; reconcile this publication before retrying\",\n      );\n    if (!saved || saved.phase !== \"created\") {\n      await assertCurrent();\n      await adapter.state.update<PollSend>(stateKey, (current) => {\n        if (current?.phase === \"creating\" && !input.retryUnknown)\n          throw new PhotonAnswerValidationError(\"Photon poll already claimed\");\n        return { schema: 1, phase: \"creating\" };\n      });\n      const poll = await adapter.client.polls\n        .create(\n          adapter.decodeThreadId(input.threadId).chatGuid,\n          title,\n          choices.map((choice) => choice.label),\n          {\n            clientMessageId: createHash(\"sha256\")\n              .update(`${adapter.state.scope.endpointId}:${stateKey}`)\n              .digest(\"hex\"),\n          },\n        )\n        .catch(async (error) => {\n          const failure = photonFailure(error, true);\n          if (failure.code !== \"delivery_unknown\")\n            await adapter.state.update<PollSend>(stateKey, () => ({\n              schema: 1,","sourceCodeStart":273,"sourceCodeEnd":309,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/photon/interactions.ts#L273-L309","documentation":"Inside publishPhotonPrompt, before creating a native poll, the code performs a compare-and-swap on adapter.state: if the current record is already phase \"creating\" and retryUnknown is falsy, another concurrent attempt has claimed the send, so a PhotonAnswerValidationError(\"Photon poll already claimed\") is thrown. This is a concurrency guard ensuring only one poll-creation attempt is in flight per stateKey.","triggerScenarios":"Two concurrent invocations of publishPhotonPrompt for the same publicationId/questionIndex both reach the adapter.state.update(stateKey) CAS; the loser sees current.phase === \"creating\" and throws. Also thrown when a stale creating record from a crashed prior attempt is hit during the CAS despite the earlier read check passing.","commonSituations":"Duplicate webhook/redelivery triggering two publishPhotonPrompt calls in parallel; retried request racing the original still in flight; a crashed process leaving phase creating and a later attempt bypassing the read check via race timing.","solutions":["Serialize publishing per publicationId (queue or lock) so only one attempt runs at a time.","After reconciling the unknown-outcome prior attempt, reset state to prepared and retry with retryUnknown: true.","Check for duplicate triggers (double delivery of the same interaction) and deduplicate upstream.","If a stale creating record is the cause, manually update state to {schema:1, phase:\"prepared\"} before retrying."],"exampleFix":"// before — concurrent duplicate call\npublishPhotonPrompt(input); publishPhotonPrompt(input); // second throws\n// after\nconst once = memoizePerPublication(publishPhotonPrompt);\nawait Promise.all([once(input), once(input)]); // single create","handlingStrategy":"try-catch","validationCode":"const claimed = (await adapter.state.read<{phase:string}>(`poll:${publicationId}:${qIndex}`))?.phase === \"creating\";\nif (claimed) await reconcileBeforePublish(publicationId);","typeGuard":"function isUnclaimed(current: {phase: string} | undefined): boolean {\n  return current?.phase !== \"creating\";\n}","tryCatchPattern":"try {\n  await publishPhotonPrompt(input);\n} catch (e) {\n  if (e instanceof PhotonAnswerValidationError && e.message === \"Photon poll already claimed\") {\n    // another attempt owns the send; back off and poll state until created/prepared\n    await waitForPollSettled(`poll:${input.publicationId}:${qIndex}`);\n  } else throw e;\n}","preventionTips":["Deduplicate interaction deliveries before invoking publishPhotonPrompt.","Use a per-publication mutex/queue around publishing.","Add idempotency keys upstream so duplicate triggers collapse to one call.","Clean up creating state left by crashed processes during startup recovery."],"tags":["photon","concurrency","state-machine","poll-delivery"],"backgroundTag":"invalid-state-transition","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}