fix: address Greptile P1 + CodeRabbit follow-ups (cycle 9)
- waitForSlot now accepts an optional AbortSignal. When the signal fires (e.g. session.abortController.abort() during shutdown or cancel), the queued waiter is removed from slotWaiters and the promise rejects immediately, instead of hanging until a slot naturally opens. Restores the cancellation guarantee that the removed 60s timeout used to provide. ClaudeProvider.startSession now passes session.abortController.signal at the call site. - EnvManager: a bare ANTHROPIC_BASE_URL now also short-circuits the OAuth lookup. Tokenless gateways (allowed by the new install flow) were otherwise being authenticated against api.anthropic.com via the injected OS-keychain OAuth token. - install.ts: resolveClaudeAuthMethod now reads the raw stored CLAUDE_MEM_CLAUDE_AUTH_METHOD value via a direct settings.json read (readRawStoredAuthMethod), bypassing SettingsDefaultsManager's default backfill. Without this, getSetting() always returned 'subscription' for unmigrated installs and the env-based fallback never ran — so the previous fix only addressed the optics, not the actual misclassification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -588,15 +588,22 @@ type ProviderId = 'claude' | 'gemini' | 'openrouter';
|
||||
type ClaudeAccessMode = 'subscription' | 'api-key';
|
||||
type ClaudeApiMode = 'direct' | 'gateway';
|
||||
|
||||
function resolveClaudeAuthMethod(): 'subscription' | 'api-key' | 'gateway' {
|
||||
const stored = getSetting('CLAUDE_MEM_CLAUDE_AUTH_METHOD') as
|
||||
| 'subscription'
|
||||
| 'api-key'
|
||||
| 'gateway'
|
||||
| undefined;
|
||||
if (stored === 'subscription' || stored === 'api-key' || stored === 'gateway') {
|
||||
return stored;
|
||||
function readRawStoredAuthMethod(): 'subscription' | 'api-key' | 'gateway' | undefined {
|
||||
try {
|
||||
if (!existsSync(USER_SETTINGS_PATH)) return undefined;
|
||||
const raw = JSON.parse(readFileSync(USER_SETTINGS_PATH, 'utf-8')) as Record<string, unknown>;
|
||||
const flat = (raw.env && typeof raw.env === 'object' ? raw.env : raw) as Record<string, unknown>;
|
||||
const value = flat.CLAUDE_MEM_CLAUDE_AUTH_METHOD;
|
||||
if (value === 'subscription' || value === 'api-key' || value === 'gateway') return value;
|
||||
return undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveClaudeAuthMethod(): 'subscription' | 'api-key' | 'gateway' {
|
||||
const stored = readRawStoredAuthMethod();
|
||||
if (stored) return stored;
|
||||
const env = loadClaudeMemEnv();
|
||||
if (env.ANTHROPIC_BASE_URL?.trim()) return 'gateway';
|
||||
if (env.ANTHROPIC_API_KEY?.trim()) return 'api-key';
|
||||
|
||||
@@ -150,7 +150,7 @@ export class ClaudeProvider {
|
||||
|
||||
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
||||
const maxConcurrent = parseInt(settings.CLAUDE_MEM_MAX_CONCURRENT_AGENTS, 10) || 2;
|
||||
await waitForSlot(maxConcurrent);
|
||||
await waitForSlot(maxConcurrent, session.abortController.signal);
|
||||
|
||||
const isolatedEnv = sanitizeEnv(await buildIsolatedEnvWithFreshOAuth());
|
||||
const authMethod = getAuthMethodDescription();
|
||||
|
||||
@@ -231,8 +231,14 @@ export async function buildIsolatedEnvWithFreshOAuth(
|
||||
if (!includeCredentials) return isolatedEnv;
|
||||
|
||||
// If the user already configured explicit Anthropic/gateway credentials in
|
||||
// ~/.claude-mem/.env, honor those and skip OAuth lookup entirely.
|
||||
if (isolatedEnv.ANTHROPIC_API_KEY || isolatedEnv.ANTHROPIC_AUTH_TOKEN) {
|
||||
// ~/.claude-mem/.env, honor those and skip OAuth lookup entirely. A bare
|
||||
// ANTHROPIC_BASE_URL counts because gateways may be tokenless, and falling
|
||||
// back to OAuth would silently route requests to api.anthropic.com.
|
||||
if (
|
||||
isolatedEnv.ANTHROPIC_API_KEY ||
|
||||
isolatedEnv.ANTHROPIC_BASE_URL ||
|
||||
isolatedEnv.ANTHROPIC_AUTH_TOKEN
|
||||
) {
|
||||
clearStaleMarker();
|
||||
return isolatedEnv;
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ function notifySlotAvailable(): void {
|
||||
if (waiter) waiter();
|
||||
}
|
||||
|
||||
export async function waitForSlot(maxConcurrent: number): Promise<void> {
|
||||
export async function waitForSlot(maxConcurrent: number, signal?: AbortSignal): Promise<void> {
|
||||
getProcessRegistry().pruneDeadEntries();
|
||||
const activeCount = getActiveSdkCount();
|
||||
if (activeCount >= TOTAL_PROCESS_HARD_CAP) {
|
||||
@@ -457,26 +457,45 @@ export async function waitForSlot(maxConcurrent: number): Promise<void> {
|
||||
|
||||
if (activeCount < maxConcurrent) return;
|
||||
|
||||
if (signal?.aborted) {
|
||||
throw new Error('waitForSlot aborted before queuing');
|
||||
}
|
||||
|
||||
logger.info('PROCESS', `Pool limit reached (${activeCount}/${maxConcurrent}), waiting for slot...`);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let recheckTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let abortHandler: (() => void) | null = null;
|
||||
const cleanup = () => {
|
||||
if (recheckTimer) clearInterval(recheckTimer);
|
||||
if (abortHandler && signal) signal.removeEventListener('abort', abortHandler);
|
||||
const idx = slotWaiters.indexOf(onSlot);
|
||||
if (idx >= 0) slotWaiters.splice(idx, 1);
|
||||
};
|
||||
const onSlot = () => {
|
||||
const count = getActiveSdkCount();
|
||||
if (count >= TOTAL_PROCESS_HARD_CAP) {
|
||||
if (recheckTimer) clearInterval(recheckTimer);
|
||||
cleanup();
|
||||
reject(new Error(`Hard cap exceeded: ${count} processes in registry (cap=${TOTAL_PROCESS_HARD_CAP}). Refusing to spawn more.`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (count < maxConcurrent) {
|
||||
if (recheckTimer) clearInterval(recheckTimer);
|
||||
cleanup();
|
||||
resolve();
|
||||
} else {
|
||||
slotWaiters.push(onSlot);
|
||||
}
|
||||
};
|
||||
|
||||
if (signal) {
|
||||
abortHandler = () => {
|
||||
cleanup();
|
||||
reject(new Error('waitForSlot aborted'));
|
||||
};
|
||||
signal.addEventListener('abort', abortHandler, { once: true });
|
||||
}
|
||||
|
||||
slotWaiters.push(onSlot);
|
||||
recheckTimer = setInterval(() => {
|
||||
const removed = getProcessRegistry().pruneDeadEntries();
|
||||
|
||||
Reference in New Issue
Block a user