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

Login cancelled

Error message

Login cancelled

What it means

During Kilo's device-flow polling loop, loginKilo checks callbacks.signal?.aborted before each poll and throws LoginCancelledError when the caller's AbortSignal has fired. This is the library's normal way to surface a user-initiated or programmatic cancellation of the pending device login — it is not a provider failure.

Source

Thrown at packages/ai/src/registry/kilo.ts:61

	const userCode = initiateData.code;
	const verificationUrl = initiateData.verificationUrl;
	const expiresInSeconds = initiateData.expiresIn;
	if (!userCode || !verificationUrl || typeof expiresInSeconds !== "number" || expiresInSeconds <= 0) {
		throw new AIError.OAuthError("Kilo device authorization response missing required fields", {
			kind: "validation",
			provider: "kilo",
		});
	}

	callbacks.onAuth?.({
		url: verificationUrl,
		instructions: `Enter code: ${userCode}`,
	});

	const deadline = Date.now() + expiresInSeconds * 1000;
	while (Date.now() < deadline) {
		if (callbacks.signal?.aborted) {
			throw new AIError.LoginCancelledError();
		}

		const pollResponse = await fetchImpl(`${KILO_DEVICE_AUTH_BASE_URL}/codes/${encodeURIComponent(userCode)}`);
		if (pollResponse.status === 202) {
			await Bun.sleep(POLL_INTERVAL_MS);
			continue;
		}
		if (pollResponse.status === 403) {
			throw new AIError.OAuthError("Authorization was denied", { kind: "device-auth", provider: "kilo" });
		}
		if (pollResponse.status === 410) {
			throw new AIError.OAuthError("Authorization code expired. Please try again.", {
				kind: "device-auth",
				provider: "kilo",
			});
		}
		if (!pollResponse.ok) {
			throw new AIError.OAuthError(`Failed to poll device authorization: ${pollResponse.status}`, {

View on GitHub (pinned to 9690622007)

Solutions

  1. If cancellation was intentional, catch LoginCancelledError and treat login as cleanly aborted — no retry needed.
  2. If it was accidental, re-run login without aborting the signal and complete the code entry at the verification URL.
  3. Ensure the AbortController isn't shared with unrelated operations that abort earlier.
  4. Check UI wiring so the abort only fires on genuine cancel actions.

Example fix

// before
await loginKilo(callbacks); // unhandled LoginCancelledError on Ctrl+C
// after
try {
  await loginKilo(callbacks);
} catch (err) {
  if (err instanceof AIError.LoginCancelledError) return; // user cancelled
  throw err;
}
Defensive patterns

Strategy: try-catch

Type guard

function isLoginCancelled(err: unknown): err is AIError.LoginCancelledError {
  return err instanceof AIError.LoginCancelledError || (err instanceof Error && err.name === "LoginCancelledError");
}

Try / catch

try {
  await loginKilo(callbacks);
} catch (err) {
  if (isLoginCancelled(err)) return null; // clean cancel, not an error path
  throw err;
}

Prevention

When it happens

Trigger: The AbortSignal passed via callbacks.signal is aborted (e.g. user pressed Ctrl+C / Esc in the UI, or code called AbortController.abort()) while loginKilo is between polls waiting for authorization within the expiresIn deadline.

Common situations: User cancels the login prompt in the TUI; an upstream timeout aborts the signal; application code aborts pending OAuth when a screen closes or a different login method is chosen.

Related errors


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