From ec6692808b7d04cc73118f6f188b7347c0434158 Mon Sep 17 00:00:00 2001 From: decolua Date: Fri, 28 Aug 2026 16:18:46 +0700 Subject: [PATCH] fix(search): scope failure locks so search cannot take chat offline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two problems on the credentialFallback path, where a search provider borrows a chat provider's connection: - the lock was attributed to the search provider id, but the connection belongs to the chat provider, so markAccountUnavailable looked it up under the wrong provider and read a stale backoffLevel - with no model argument the lock key is `modelLock___all`, which isModelLockActive treats as blocking every model — one failing search would have taken the shared glm key offline for chat as well Attribute the lock to the provider that owns the connection, and scope it to `websearch:`, passed to getProviderCredentials too so the lock is read back under the same key. --- src/sse/handlers/search.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/sse/handlers/search.js b/src/sse/handlers/search.js index 2e464d18..131095f6 100644 --- a/src/sse/handlers/search.js +++ b/src/sse/handlers/search.js @@ -154,14 +154,24 @@ async function handleSingleProviderSearch(body, providerInput, request, apiKey, // fall back to the linked provider's credentials. const fallbackProviderId = resolvedProvider.credentialFallback; + // Lock scope for this handler. Without it markAccountUnavailable would write + // an account-wide `__all` lock, which on the credentialFallback path takes + // the shared chat key (e.g. glm) offline for chat as well. Must be passed to + // getProviderCredentials too, so the lock is read back under the same key. + const searchLockKey = `websearch:${providerId}`; + while (true) { - let credentials = await getProviderCredentials(providerId, excludeConnectionIds); + // Provider that actually owns the connection in use — differs from + // providerId once we fall back, and error locks must be attributed to it. + let credentialProviderId = providerId; + let credentials = await getProviderCredentials(providerId, excludeConnectionIds, searchLockKey); // Fall back to the related chat provider's credentials when this search // provider has none of its own (one key, chat + search). if (!credentials && fallbackProviderId) { - credentials = await getProviderCredentials(fallbackProviderId, excludeConnectionIds); + credentials = await getProviderCredentials(fallbackProviderId, excludeConnectionIds, searchLockKey); if (credentials) { + credentialProviderId = fallbackProviderId; log.info("AUTH", `\x1b[32m${providerId} reusing ${fallbackProviderId} credentials\x1b[0m`); } } @@ -206,7 +216,7 @@ async function handleSingleProviderSearch(body, providerInput, request, apiKey, if (result.success) return result.response; - const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, providerId); + const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, credentialProviderId, searchLockKey); if (shouldFallback) { log.warn("AUTH", `Account ${credentials.connectionName} unavailable (${result.status}), trying fallback`);