can1357/oh-my-pi · error · AIError.ValidationError
No credential with id=${id}
Error message
No credential with id=${id} What it means
Thrown by the auth storage's credential refresh persistence path when the refreshed credential can no longer be found by id. After an async refresh completes, the code re-looks-up the row positionally because the array may have been reordered or shrunk while awaiting; if #replaceCredentialById returns -1, the credential was disabled or removed mid-refresh and there is no live row to persist into, so a ValidationError is raised instead of writing to a stale index.
Source
Thrown at packages/ai/src/auth-storage.ts:6756
type: "oauth",
access: refreshed.access,
refresh: refreshed.refresh,
expires: refreshed.expires,
accountId: refreshed.accountId ?? attempted.accountId,
email: refreshed.email ?? attempted.email,
projectId: refreshed.projectId ?? attempted.projectId,
enterpriseUrl: refreshed.enterpriseUrl ?? attempted.enterpriseUrl,
apiEndpoint: refreshed.apiEndpoint ?? attempted.apiEndpoint,
orgId: refreshed.orgId ?? attempted.orgId,
orgName: refreshed.orgName ?? attempted.orgName,
authorizedAt: refreshed.authorizedAt ?? attempted.authorizedAt,
};
// Persist by id: the array may have been reordered/shrunk while the
// refresh was in flight, so the pre-await positional index is unsafe. A
// -1 means the row was disabled/removed mid-refresh — surface that as a
// miss rather than implying a live row the snapshot won't contain.
if (this.#replaceCredentialById(provider, id, updated) === -1) {
throw new AIError.ValidationError(`No credential with id=${id}`);
}
return {
id,
provider,
credential: { ...updated, refresh: REMOTE_REFRESH_SENTINEL },
identityKey: resolveCredentialIdentityKey(provider, updated),
};
}
throw new AIError.ValidationError(`No credential with id=${id}`);
}
/**
* Disable the credential with the given id and emit a
* {@link CredentialDisabledEvent}. Used by the auth-broker server to honour
* `POST /v1/credential/:id/disable`. Returns `false` when no such row exists.
*/
disableCredentialById(id: number, disabledCause: string): boolean {
for (const [provider, entries] of this.#data) {View on GitHub (pinned to 9690622007)
Solutions
- Re-fetch the credential list and confirm the id still exists before refreshing
- Retry the refresh with a freshly resolved credential id, or re-authenticate to create a new credential
- Check whether another tool/process disabled or removed the credential (auth-broker disable endpoint) and restore it
- Wrap the refresh call in try-catch for AIError.ValidationError and treat it as 'credential gone' rather than retrying with the same id
Example fix
// before
const updated = await refresh(credential); // may take seconds
storage.replaceByIndex(originalIndex, updated); // stale index
// after
const updated = await refresh(credential);
try {
storage.updateCredential(provider, id, updated);
} catch (e) {
// credential was removed/disabled mid-refresh — re-acquire instead
const fresh = await authenticate(provider);
} Defensive patterns
Strategy: try-catch
Validate before calling
const exists = (await storage.listCredentials(provider)).some(c => c.id === id);
if (!exists) throw new Error(`Credential ${id} no longer exists; re-authenticate`); Try / catch
try {
await storage.updateCredential(provider, id, refreshed);
} catch (e) {
if (e instanceof AIError.ValidationError && /No credential with id=/.test(e.message)) {
const fresh = await authenticate(provider); // credential vanished mid-flight
} else throw e;
} Prevention
- Serialize credential mutations through a single owner/lock to avoid refresh vs. disable races
- Re-validate credential ids immediately before refresh
- Listen for CredentialDisabledEvent/removed events and cancel in-flight refreshes
- Persist provider+id references and re-resolve them on miss instead of failing hard
When it happens
Trigger: Calling the credential update/refresh API while another caller concurrently disables or removes the same credential id; refreshing a credential whose id no longer exists in the provider's credential array; a race between an auth-broker refresh and a concurrent credential list mutation.
Common situations: Multiple processes (e.g. the auth-broker server and a CLI session) sharing auth storage where one deletes or disables a credential while another is mid-refresh; stale UI or cached id passed after credential removal; credential rotation that drops rows during a long-running token refresh.
Related errors
- Failed to open auth database at '${dbPath}' after ${maxAttem
- OAuth refresh ownership was lost before persistence
- Qwen token/API key is required
- session header changed during stats cleanup: ${session.path}
- Daemon ${spec.name} is already starting
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/98e88b124e44b5da.
Report an issue: GitHub.