can1357/oh-my-pi · info · AIError.AbortError

OAuth refresh ownership aborted by caller

Error message

OAuth refresh ownership aborted by caller

What it means

During the durable-lease based OAuth refresh loop, the library checks the caller's AbortSignal before each iteration. If the caller aborted the refresh (e.g. request cancelled or timeout), it throws AbortError labeled 'OAuth refresh ownership aborted by caller'. This is a cooperative-cancellation signal, not a refresh failure.

Source

Thrown at packages/ai/src/auth-storage.ts:2463

	/**
	 * Refresh one stored OAuth credential under durable row ownership.
	 */
	async refreshStoredOAuthCredential<T extends OAuthCredential = OAuthCredential>(
		provider: string,
		options: StoredOAuthRefreshOptions<T>,
	): Promise<StoredOAuthRefreshResult<T>> {
		const refreshSkewMs = options.refreshSkewMs ?? OAUTH_REFRESH_SKEW_MS;
		const hasDurableLease =
			!!this.#store.tryAcquireCredentialRefreshLease &&
			!!this.#store.getCredentialRefreshLeaseExpiresAt &&
			!!this.#store.releaseCredentialRefreshLease &&
			!!this.#store.renewCredentialRefreshLease;
		const owner = crypto.randomUUID();
		let leasedCredentialId: number | undefined;

		while (hasDurableLease) {
			if (options.signal?.aborted) throw new AIError.AbortError("OAuth refresh ownership aborted by caller");
			const rows = this.#store.listAuthCredentials(provider);
			this.#setStoredCredentials(
				provider,
				rows.map(row => ({ id: row.id, credential: row.credential })),
			);
			const row = rows.find(
				entry =>
					entry.credential.type === "oauth" &&
					(options.credentialId === undefined || entry.id === options.credentialId),
			);
			if (row?.credential.type !== "oauth") {
				return { credential: undefined, refreshed: false, removed: false };
			}
			const current = options.credentialFromRow(row.credential);
			if (!current) {
				return { credential: undefined, refreshed: false, removed: false };
			}
			const currentIsFresh = Date.now() + refreshSkewMs < current.expires;

View on GitHub (pinned to 9690622007)

Solutions

  1. Expected behavior when cancelling: catch AIError.AbortError and treat the refresh as cancelled
  2. If unintentional, audit what aborts your signal (timeout too short, request teardown) and extend the timeout
  3. Retry the refresh later; the lease system ensures another owner may have refreshed already
  4. Before retrying, re-read the stored credential — it may now be fresh from the other lease owner

Example fix

// before
await storage.refreshOAuthCredential(provider, { signal: controller.signal });
// after
try {
	await storage.refreshOAuthCredential(provider, { signal: controller.signal });
} catch (error) {
	if (error instanceof AIError.AbortError) return null; // caller cancelled
	throw error;
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (signal?.aborted) return null; // don't start a refresh that will be aborted

Try / catch

try {
	return await storage.refreshOAuthCredential(provider, { signal });
} catch (error) {
	if (error instanceof AIError.AbortError) return null; // cancelled by caller
	throw error;
}

Prevention

When it happens

Trigger: Passing an AbortSignal to a stored-OAuth-refresh operation (via StoredOAuthRefreshOptions.signal) and aborting it while the lease loop is running — including while polling for a lease held by another process/peer.

Common situations: Request timeout or user cancellation aborting a token refresh that is waiting on a refresh lease; shutdown paths cancelling in-flight refreshes; another process holds the lease so the poll loop runs long enough for the caller's timeout to fire.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/a8ac76cd7b064094. Report an issue: GitHub.