feat(websearch): add managed mcp runtime and provider cooldowns

- switch ccs-websearch MCP to stdio-first framing and keep legacy compatibility

- add cooldown persistence and bounded retries for provider failures

- surface cooldown status in readiness output and regression tests
This commit is contained in:
Tam Nhu Tran committed 2026-03-31 14:22:11 -04:00
1 parent cae5b710b8
commit 7f83e041b7
10 files changed
+1115 -63

No files matched your search

+12 -1
View File
@@ -20,6 +20,13 @@ interface ClaudeUserConfig {
[key: string]: unknown;
}
interface ManagedWebSearchMcpConfig {
type: 'stdio';
command: 'node';
args: [string];
env: Record<string, string>;
}
function getCcsMcpDir(): string {
return path.join(getCcsDir(), 'mcp');
}
@@ -91,9 +98,11 @@ function readClaudeUserConfig(configPath: string): ClaudeUserConfig | null {
function writeClaudeUserConfig(configPath: string, config: ClaudeUserConfig): boolean {
const tempPath = getTempPath(configPath);
const fileMode = fs.existsSync(configPath) ? fs.statSync(configPath).mode & 0o777 : 0o600;
try {
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
fs.chmodSync(tempPath, fileMode);
fs.renameSync(tempPath, configPath);
return true;
} finally {
@@ -252,9 +261,11 @@ export function ensureWebSearchMcpConfig(): boolean {
config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
? (config.mcpServers as Record<string, unknown>)
: {};
const desiredServerConfig = {
const desiredServerConfig: ManagedWebSearchMcpConfig = {
type: 'stdio',
command: 'node',
args: [getWebSearchMcpServerPath()],
env: {},
};
const currentConfig = existingServers[WEBSEARCH_MCP_SERVER_NAME];
+91 -1
View File
@@ -6,18 +6,105 @@
* @module utils/websearch/status
*/
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { ok, warn, fail, info } from '../ui';
import { getWebSearchConfig } from '../../config/unified-config-loader';
import { getCcsDir } from '../config-manager';
import { getGeminiCliStatus, isGeminiAuthenticated } from './gemini-cli';
import { getGrokCliStatus } from './grok-cli';
import { getOpenCodeCliStatus } from './opencode-cli';
import { getWebSearchApiKeyStates } from './provider-secrets';
import type { WebSearchCliInfo, WebSearchStatus } from './types';
const PROVIDER_STATE_FILE = 'websearch-provider-state.json';
type ProviderCooldown = {
reason: string;
until: number;
};
function hasEnvValue(name: string): boolean {
return (process.env[name] || '').trim().length > 0;
}
function getProviderStatePath(): string {
return join(getCcsDir(), 'cache', PROVIDER_STATE_FILE);
}
function readProviderCooldowns(now = Date.now()): Record<string, ProviderCooldown> {
try {
const statePath = getProviderStatePath();
if (!existsSync(statePath)) {
return {};
}
const parsed = JSON.parse(readFileSync(statePath, 'utf8')) as {
cooldowns?: Record<string, { reason?: unknown; until?: unknown }>;
};
const nextCooldowns: Record<string, ProviderCooldown> = {};
for (const [providerId, entry] of Object.entries(parsed.cooldowns || {})) {
const until = Number.parseInt(String(entry?.until || ''), 10);
if (!Number.isFinite(until) || until <= now) {
continue;
}
nextCooldowns[providerId] = {
reason: typeof entry?.reason === 'string' ? entry.reason : 'rate_limited',
until,
};
}
return nextCooldowns;
} catch {
return {};
}
}
function formatCooldownDuration(until: number, now = Date.now()): string {
const remainingSec = Math.max(1, Math.ceil((until - now) / 1000));
if (remainingSec >= 3600) {
return `~${Math.ceil(remainingSec / 3600)}h`;
}
if (remainingSec >= 60) {
return `~${Math.ceil(remainingSec / 60)}m`;
}
return `~${remainingSec}s`;
}
function formatCooldownReason(reason: string): string {
switch (reason) {
case 'quota_exhausted':
return 'quota exhaustion';
case 'rate_limited':
return 'rate limiting';
default:
return 'a temporary provider error';
}
}
function applyCooldownStatus(
provider: WebSearchCliInfo,
cooldowns: Record<string, ProviderCooldown>,
now = Date.now()
): WebSearchCliInfo {
if (!(provider.enabled && provider.available)) {
return provider;
}
const cooldown = cooldowns[provider.id];
if (!cooldown) {
return provider;
}
return {
...provider,
available: false,
detail: `Cooling down ${formatCooldownDuration(cooldown.until, now)} after ${formatCooldownReason(cooldown.reason)}`,
};
}
function getLegacyProviderStatuses(): WebSearchCliInfo[] {
const wsConfig = getWebSearchConfig();
const geminiStatus = getGeminiCliStatus();
@@ -86,6 +173,7 @@ function getLegacyProviderStatuses(): WebSearchCliInfo[] {
export function getWebSearchCliProviders(): WebSearchCliInfo[] {
const wsConfig = getWebSearchConfig();
const apiKeyStates = getWebSearchApiKeyStates();
const cooldowns = readProviderCooldowns();
const providers: WebSearchCliInfo[] = [
{
id: 'exa',
@@ -152,7 +240,9 @@ export function getWebSearchCliProviders(): WebSearchCliInfo[] {
},
];
return [...providers, ...getLegacyProviderStatuses()];
return [...providers, ...getLegacyProviderStatuses()].map((provider) =>
applyCooldownStatus(provider, cooldowns)
);
}
/**