paperclipai/paperclip · error · Error
device-login credential promotion rejected: ${result.outcome
Error message
device-login credential promotion rejected: ${result.outcome} What it means
After promoteDeviceLoginCredential resolves, the promote() route accepts only the outcomes 'promoted' (credential written) or 'kept' (deliberate safe keep). Any other outcome — e.g. a reaper/expiry race revoked the session's sole active ownership of the company credential slot between the service transition and Decision H — fails closed and throws with the raw outcome embedded in the message. A resolved promise is explicitly not treated as an accepted promotion.
Source
Thrown at server/src/routes/agents.ts:823
// active owner of the company credential slot. The read runs
// inside the lock, so it observes a reaper reclaim that committed
// before this section acquired the lock.
const row = await adapterLoginStore.get(context.sessionId);
return row?.status === "promoting" && row.companyId === context.companyId;
},
log: (line) => {
// The promotion lines carry no token bytes and no raw account id,
// so it is safe to log them with the session identifier.
logger.info({ sessionId: context.sessionId }, line);
},
}),
);
// A resolved promotion is not necessarily an accepted promotion. In
// particular, a reaper/expiry race can revoke this session's sole
// ownership between the service transition and Decision H. Fail closed:
// only a credential write or a deliberate safe keep can authenticate.
if (result.outcome !== "promoted" && result.outcome !== "kept") {
throw new Error(`device-login credential promotion rejected: ${result.outcome}`);
}
// The account's own home is durable at this point (the promotion above
// wrote it fail-loud). Name it with a company secret, so any agent can
// bind to it. Reading the secret by name first keeps a repeat login for
// the same account idempotent: `create` throws a conflict when the name
// already exists.
const handle = result.accountId ? toAccountHandle(result.accountId) : null;
if (!handle || !result.accountHomeDir) {
throw new Error(
"device-login credential promotion rejected: the promotion carried no account home",
);
}
const secretName = `CODEX_HOME_${handle}`;
const accountHomeDir = result.accountHomeDir;
const existingSecret = await secretsSvc.getByName(context.companyId, secretName);
if (existingSecret) {
// A same-name secret already exists. Confirm it still names this
// account's own home before treating a repeat login as a success:View on GitHub (pinned to 01ad858492)
Solutions
- Restart the device-login flow from the beginning so a fresh `promoting` session row is created, then complete it promptly.
- Check adapter login session state (adapterLoginStore.get(sessionId).status) — if it is not 'promoting', the session was reclaimed and cannot be promoted.
- Avoid running two concurrent device logins for the same company+adapter; serialize them.
- If reaper expiry is too aggressive for slow login flows, tune the reaper's staleness threshold.
Defensive patterns
Strategy: retry
Validate before calling
// Before completing device login, confirm the session still owns the slot
const row = await adapterLoginStore.get(sessionId);
if (!row || row.status !== 'promoting' || row.companyId !== companyId) {
throw new Error('Login session expired or reclaimed; restart the device-login flow');
} Try / catch
try {
await promoteDeviceLogin(...);
} catch (e) {
if (e instanceof Error && e.message.startsWith('device-login credential promotion rejected:')
&& !e.message.includes('no account home')) {
// outcome-level rejection (reaper/expiry race): restart the login flow
await startNewDeviceLoginFlow();
} else throw e;
} Prevention
- Complete the device-login flow promptly; do not leave it idle until the session reaper fires
- Never run two device logins for the same company+adapter concurrently
- Monitor adapter login session ages and alert when sessions approach the reaper threshold
- After any rejection, always restart the flow — do not reuse a stale session id
When it happens
Trigger: Calling the device-login promote flow when the adapter-login session row is no longer status 'promoting' for this company (isSoleActiveOwner returns false): session expired and the reaper reclaimed the `promoting` row, a concurrent login for the same company/adapter took the slot, or the service returned an unusual outcome value like 'skipped'/'reclaimed'.
Common situations: User left the device-login flow idle until the session expired, then completed the flow; two parallel device logins for the same company+adapter racing for the single active credential slot; slow network causing the reaper window to open mid-promotion.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- device-login credential promotion rejected: the ${secretName
- Sign in again to connect your own account.
- ${name} must be a JSON object
- Invalid ${name} JSON: ${err instanceof Error ? err.message :
- --file is required
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/a62da9c55aa460fd.
Report an issue: GitHub.