{"record":{"id":"4fc1e818f4e92b50","repo":"paperclipai/paperclip","slug":"device-login-credential-promotion-rejected-the","errorCode":null,"errorMessage":"device-login credential promotion rejected: the ${secretName} secret conflict could not be resolved","messagePattern":"device-login credential promotion rejected: the (.+?) secret conflict could not be resolved","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/routes/agents.ts","lineNumber":891,"sourceCode":"              // The value just committed is correct at this instant, but a\n              // rotate queued behind the create's own lock can still commit a\n              // different value before the login service records its\n              // terminal state. Queue the same reconfirm `runTerminalCommit`\n              // runs for the two branches above.\n              pendingAccountHomeSecretCommits.set(context.sessionId, {\n                secretId: createdSecret.id,\n                secretName,\n                accountHomeDir,\n              });\n            } catch (err) {\n              if (err instanceof HttpError && err.status === 409) {\n                // A conflict means a concurrent login for the same account won the\n                // create race. Confirm the winning secret still names this\n                // account's own home before treating the race as a successful,\n                // idempotent login.\n                const winningSecret = await secretsSvc.getByName(context.companyId, secretName);\n                if (!winningSecret) {\n                  throw new Error(\n                    `device-login credential promotion rejected: the ${secretName} secret conflict could not be resolved`,\n                  );\n                }\n                // Same lock and the same reasoning as the pre-existing-secret\n                // check above: an early fail-fast only, so also queue the\n                // same check for `runTerminalCommit` to run again, under a\n                // fresh lock acquisition it holds across the terminal commit.\n                await withAccountHomeSecretMutationLock(undefined, context.companyId, () =>\n                  assertAccountHomeSecretMatches(secretsSvc, context.companyId, winningSecret, secretName, accountHomeDir),\n                );\n                pendingAccountHomeSecretCommits.set(context.sessionId, {\n                  secretId: winningSecret.id,\n                  secretName,\n                  accountHomeDir,\n                });\n                return;\n              }\n              // The account home write failed for a reason other than a naming","sourceCodeStart":873,"sourceCodeEnd":909,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/routes/agents.ts#L873-L909","documentation":"When creating the CODEX_HOME_<handle> secret fails with a 409 conflict (a concurrent login for the same account won the create race), promote() re-fetches the winning secret by name and fails closed if that re-fetch returns nothing. A conflict without a resolvable winner means the idempotent-login fast path cannot be verified, so the login cannot safely proceed. Normally this only happens if the winning secret was deleted between the create conflict and the re-fetch.","triggerScenarios":"Two device logins for the same Codex account race; the loser's secretsSvc.create throws HttpError 409, but secretsSvc.getByName(companyId, secretName) then returns null — e.g. a concurrent secret deletion, a cleanup/failed-promotion delete removing the winner's secret, or eventual-consistency in the secrets provider.","commonSituations":"Parallel logins on two browser tabs for the same account while something (cleanup scan, manual deletion, another failed login's directory cleanup) removes the just-created secret; secrets backend with read-after-write lag.","solutions":["Retry the device login: the race window is transient; a fresh run will either find the existing secret or create one cleanly.","Check whether any concurrent process deletes CODEX_HOME_* secrets (failed-promotion cleanup, manual secret deletion) and serialize/remove that deletion.","Verify the secrets store's read-after-write consistency; if getByName lags, retry the getByName before failing.","If a genuine orphan conflict persists, delete any conflicting CODEX_HOME_<handle> secret and log in again to recreate it."],"exampleFix":"// before (single read, then fail)\nconst winningSecret = await secretsSvc.getByName(companyId, secretName);\nif (!winningSecret) throw new Error(`...conflict could not be resolved`);\n// after (bounded retry to absorb read-after-write lag)\nlet winningSecret = null;\nfor (let i = 0; i < 3 && !winningSecret; i++) {\n  await new Promise((r) => setTimeout(r, 100));\n  winningSecret = await secretsSvc.getByName(companyId, secretName);\n}\nif (!winningSecret) throw new Error(`...conflict could not be resolved`);","handlingStrategy":"retry","validationCode":"// Before login, check whether another login already owns the secret\nconst existing = await secretsSvc.getByName(companyId, `CODEX_HOME_${handle}`);\nif (existing) {\n  console.log('Secret already exists; login will take the idempotent path');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await promoteDeviceLogin(...);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('secret conflict could not be resolved')) {\n    // transient race: winner secret vanished mid-conflict; retry the login\n    await retryDeviceLogin({ attempts: 2, backoffMs: 500 });\n  } else throw e;\n}","preventionTips":["Serialize device logins for the same account across tabs/machines","Do not delete CODEX_HOME_* secrets while logins for that account may be in flight","Ensure the failed-promotion cleanup cannot run concurrently with another login's secret create","Verify the secrets backend provides read-after-write consistency for getByName after a 409"],"tags":["secrets","race-condition","device-login","conflict"],"backgroundTag":"entity-not-found","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}