paperclipai/paperclip · error · Error
device-login credential promotion rejected: the ${secretName
Error message
device-login credential promotion rejected: the ${secretName} secret conflict could not be resolved What it means
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.
Source
Thrown at server/src/routes/agents.ts:891
// The value just committed is correct at this instant, but a
// rotate queued behind the create's own lock can still commit a
// different value before the login service records its
// terminal state. Queue the same reconfirm `runTerminalCommit`
// runs for the two branches above.
pendingAccountHomeSecretCommits.set(context.sessionId, {
secretId: createdSecret.id,
secretName,
accountHomeDir,
});
} catch (err) {
if (err instanceof HttpError && err.status === 409) {
// A conflict means a concurrent login for the same account won the
// create race. Confirm the winning secret still names this
// account's own home before treating the race as a successful,
// idempotent login.
const winningSecret = await secretsSvc.getByName(context.companyId, secretName);
if (!winningSecret) {
throw new Error(
`device-login credential promotion rejected: the ${secretName} secret conflict could not be resolved`,
);
}
// Same lock and the same reasoning as the pre-existing-secret
// check above: an early fail-fast only, so also queue the
// same check for `runTerminalCommit` to run again, under a
// fresh lock acquisition it holds across the terminal commit.
await withAccountHomeSecretMutationLock(undefined, context.companyId, () =>
assertAccountHomeSecretMatches(secretsSvc, context.companyId, winningSecret, secretName, accountHomeDir),
);
pendingAccountHomeSecretCommits.set(context.sessionId, {
secretId: winningSecret.id,
secretName,
accountHomeDir,
});
return;
}
// The account home write failed for a reason other than a namingView on GitHub (pinned to 01ad858492)
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.
Example fix
// before (single read, then fail)
const winningSecret = await secretsSvc.getByName(companyId, secretName);
if (!winningSecret) throw new Error(`...conflict could not be resolved`);
// after (bounded retry to absorb read-after-write lag)
let winningSecret = null;
for (let i = 0; i < 3 && !winningSecret; i++) {
await new Promise((r) => setTimeout(r, 100));
winningSecret = await secretsSvc.getByName(companyId, secretName);
}
if (!winningSecret) throw new Error(`...conflict could not be resolved`); Defensive patterns
Strategy: retry
Validate before calling
// Before login, check whether another login already owns the secret
const existing = await secretsSvc.getByName(companyId, `CODEX_HOME_${handle}`);
if (existing) {
console.log('Secret already exists; login will take the idempotent path');
} Try / catch
try {
await promoteDeviceLogin(...);
} catch (e) {
if (e instanceof Error && e.message.includes('secret conflict could not be resolved')) {
// transient race: winner secret vanished mid-conflict; retry the login
await retryDeviceLogin({ attempts: 2, backoffMs: 500 });
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.
Related errors
- ${name} must be a JSON object
- Invalid ${name} JSON: ${err instanceof Error ? err.message :
- --file is required
- Request failed with status ${response.status}
- device-login credential promotion rejected: the existing ${s
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/4fc1e818f4e92b50.
Report an issue: GitHub.