Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions src/core/agent/ClaudianService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
buildContextFromHistory,
buildPromptWithHistoryContext,
getLastUserMessage,
isAuthenticationError,
isSessionExpiredError,
} from '../../utils/session';
import {
Expand Down Expand Up @@ -572,6 +573,12 @@ export class ClaudianService {
this.messageChannel.enqueue(messageToReplay);
return;
} catch (restartError) {
// If restart failed due to auth error, notify with actionable message
if (isAuthenticationError(restartError)) {
handler.onError(new Error('Authentication failed — your Claude OAuth token has expired. Please run `claude auth login` in your terminal to re-authenticate, then restart Claudian.'));
new Notice('Claude authentication expired. Run "claude auth login" in terminal to fix.', 10000);
return;
}
// If restart failed due to session expiration, invalidate session
// so next query triggers noSessionButHasHistory → history rebuild
if (isSessionExpiredError(restartError)) {
Expand All @@ -584,6 +591,12 @@ export class ClaudianService {

// Notify active handler of error
if (handler) {
// Check if original error is an auth error — give actionable message
if (isAuthenticationError(errorInstance)) {
handler.onError(new Error('Authentication failed — your Claude OAuth token has expired. Please run `claude auth login` in your terminal to re-authenticate, then restart Claudian.'));
new Notice('Claude authentication expired. Run "claude auth login" in terminal to fix.', 10000);
return;
}
handler.onError(errorInstance);
}

Expand All @@ -593,6 +606,11 @@ export class ClaudianService {
try {
await this.ensureReady({ force: true });
} catch (restartError) {
// If restart failed due to auth error, don't bother retrying
if (isAuthenticationError(restartError)) {
new Notice('Claude authentication expired. Run "claude auth login" in terminal to fix.', 10000);
return;
}
// If restart failed due to session expiration, invalidate session
// so next query triggers noSessionButHasHistory → history rebuild
if (isSessionExpiredError(restartError)) {
Expand Down Expand Up @@ -811,6 +829,14 @@ export class ClaudianService {
yield* this.queryViaPersistent(promptToSend, images, vaultPath, resolvedClaudePath, effectiveQueryOptions);
return;
} catch (error) {
// Authentication errors are non-recoverable — don't retry
if (isAuthenticationError(error)) {
this.closePersistentQuery('authentication error');
yield { type: 'error', content: 'Authentication failed — your Claude OAuth token has expired. Please run `claude auth login` in your terminal to re-authenticate, then restart Claudian.' };
new Notice('Claude authentication expired. Run "claude auth login" in terminal to fix.', 10000);
return;
}

if (isSessionExpiredError(error) && conversationHistory && conversationHistory.length > 0) {
this.sessionManager.invalidateSession();
const retryRequest = this.buildHistoryRebuildRequest(prompt, conversationHistory);
Expand All @@ -828,8 +854,13 @@ export class ClaudianService {
effectiveQueryOptions
);
} catch (retryError) {
const msg = retryError instanceof Error ? retryError.message : 'Unknown error';
yield { type: 'error', content: msg };
if (isAuthenticationError(retryError)) {
yield { type: 'error', content: 'Authentication failed — your Claude OAuth token has expired. Please run `claude auth login` in your terminal to re-authenticate, then restart Claudian.' };
new Notice('Claude authentication expired. Run "claude auth login" in terminal to fix.', 10000);
} else {
const msg = retryError instanceof Error ? retryError.message : 'Unknown error';
yield { type: 'error', content: msg };
}
} finally {
this.coldStartInProgress = false;
this.abortController = null;
Expand All @@ -850,6 +881,13 @@ export class ClaudianService {
try {
yield* this.queryViaSDK(promptToSend, vaultPath, resolvedClaudePath, images, effectiveQueryOptions);
} catch (error) {
// Authentication errors are non-recoverable — don't retry
if (isAuthenticationError(error)) {
yield { type: 'error', content: 'Authentication failed — your Claude OAuth token has expired. Please run `claude auth login` in your terminal to re-authenticate, then restart Claudian.' };
new Notice('Claude authentication expired. Run "claude auth login" in terminal to fix.', 10000);
return;
}

if (isSessionExpiredError(error) && conversationHistory && conversationHistory.length > 0) {
this.sessionManager.invalidateSession();
const retryRequest = this.buildHistoryRebuildRequest(prompt, conversationHistory);
Expand All @@ -864,8 +902,13 @@ export class ClaudianService {
effectiveQueryOptions
);
} catch (retryError) {
const msg = retryError instanceof Error ? retryError.message : 'Unknown error';
yield { type: 'error', content: msg };
if (isAuthenticationError(retryError)) {
yield { type: 'error', content: 'Authentication failed — your Claude OAuth token has expired. Please run `claude auth login` in your terminal to re-authenticate, then restart Claudian.' };
new Notice('Claude authentication expired. Run "claude auth login" in terminal to fix.', 10000);
} else {
const msg = retryError instanceof Error ? retryError.message : 'Unknown error';
yield { type: 'error', content: msg };
}
}
return;
}
Expand Down Expand Up @@ -1025,6 +1068,10 @@ export class ClaudianService {

// Check if an error occurred (assigned in onError callback)
if (state.error) {
// Re-throw authentication errors for outer handling (non-recoverable)
if (isAuthenticationError(state.error)) {
throw state.error;
}
// Re-throw session expired errors for outer retry logic to handle
if (isSessionExpiredError(state.error)) {
throw state.error;
Expand Down Expand Up @@ -1329,6 +1376,10 @@ export class ClaudianService {
}
}
} catch (error) {
// Re-throw authentication errors (non-recoverable)
if (isAuthenticationError(error)) {
throw error;
}
// Re-throw session expired errors for outer retry logic to handle
if (isSessionExpiredError(error)) {
throw error;
Expand Down
52 changes: 52 additions & 0 deletions src/utils/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,58 @@ export function isSessionExpiredError(error: unknown): boolean {
return false;
}

// ============================================
// Authentication Error Detection
// ============================================

const AUTH_ERROR_PATTERNS = [
'authentication_failed',
'authentication_error',
'oauth token has expired',
'failed to authenticate',
'obtain a new token',
'refresh your existing token',
'invalid api key',
'invalid x-api-key',
'api key not found',
] as const;

const AUTH_ERROR_COMPOUND_PATTERNS = [
{ includes: ['401', 'authentication'] },
{ includes: ['token', 'expired'] },
{ includes: ['oauth', 'expired'] },
] as const;

/**
* Detects authentication/OAuth errors from the Claude API.
* These are distinct from session expiry — retrying won't help
* because the underlying credentials are invalid.
*/
export function isAuthenticationError(error: unknown): boolean {
let msg = '';
if (error instanceof Error) {
msg = error.message.toLowerCase();
} else if (typeof error === 'string') {
msg = error.toLowerCase();
}

if (!msg) return false;

for (const pattern of AUTH_ERROR_PATTERNS) {
if (msg.includes(pattern)) {
return true;
}
}

for (const { includes } of AUTH_ERROR_COMPOUND_PATTERNS) {
if (includes.every(part => msg.includes(part))) {
return true;
}
}

return false;
}

// ============================================
// History Reconstruction
// ============================================
Expand Down
Loading