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 naming

View on GitHub (pinned to 01ad858492)

Solutions

  1. Retry the device login: the race window is transient; a fresh run will either find the existing secret or create one cleanly.
  2. Check whether any concurrent process deletes CODEX_HOME_* secrets (failed-promotion cleanup, manual secret deletion) and serialize/remove that deletion.
  3. Verify the secrets store's read-after-write consistency; if getByName lags, retry the getByName before failing.
  4. 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

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


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/4fc1e818f4e92b50. Report an issue: GitHub.