
























@@ -39,6 +39,8 @@ const DEFAULT_SETUP_GRACE_TIMEOUT_MS = 30_000;
3939const DEFAULT_QUERY_MODE = "recent" as const;
4040const DEFAULT_QMD_SEARCH_MODE = "search" as const;
4141const DEFAULT_TRANSCRIPT_DIR = "active-memory";
42+const DEFAULT_CIRCUIT_BREAKER_MAX_TIMEOUTS = 3;
43+const DEFAULT_CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
4244const TOGGLE_STATE_FILE = "session-toggles.json";
4345const DEFAULT_PARTIAL_TRANSCRIPT_MAX_CHARS = 32_000;
4446const DEFAULT_TRANSCRIPT_READ_MAX_LINES = 2_000;
@@ -97,6 +99,8 @@ type ActiveRecallPluginConfig = {
9799recentAssistantChars?: number;
98100logging?: boolean;
99101cacheTtlMs?: number;
102+circuitBreakerMaxTimeouts?: number;
103+circuitBreakerCooldownMs?: number;
100104persistTranscripts?: boolean;
101105transcriptDir?: string;
102106qmd?: {
@@ -134,6 +138,8 @@ type ResolvedActiveRecallPluginConfig = {
134138recentAssistantChars: number;
135139logging: boolean;
136140cacheTtlMs: number;
141+circuitBreakerMaxTimeouts: number;
142+circuitBreakerCooldownMs: number;
137143persistTranscripts: boolean;
138144transcriptDir: string;
139145qmd: {
@@ -278,6 +284,44 @@ const MAX_LOG_VALUE_CHARS = 300;
278284279285const activeRecallCache = new Map<string, CachedActiveRecallResult>();
280286287+type CircuitBreakerEntry = {
288+consecutiveTimeouts: number;
289+lastTimeoutAt: number;
290+};
291+292+const timeoutCircuitBreaker = new Map<string, CircuitBreakerEntry>();
293+294+function buildCircuitBreakerKey(agentId: string, provider?: string, model?: string): string {
295+return `${agentId}:${provider ?? "unknown"}/${model ?? "unknown"}`;
296+}
297+298+function isCircuitBreakerOpen(key: string, maxTimeouts: number, cooldownMs: number): boolean {
299+const entry = timeoutCircuitBreaker.get(key);
300+if (!entry || entry.consecutiveTimeouts < maxTimeouts) {
301+return false;
302+}
303+if (Date.now() - entry.lastTimeoutAt >= cooldownMs) {
304+// Cooldown expired — reset and allow one attempt through.
305+timeoutCircuitBreaker.delete(key);
306+return false;
307+}
308+return true;
309+}
310+311+function recordCircuitBreakerTimeout(key: string): void {
312+const entry = timeoutCircuitBreaker.get(key);
313+if (entry) {
314+entry.consecutiveTimeouts++;
315+entry.lastTimeoutAt = Date.now();
316+} else {
317+timeoutCircuitBreaker.set(key, { consecutiveTimeouts: 1, lastTimeoutAt: Date.now() });
318+}
319+}
320+321+function resetCircuitBreaker(key: string): void {
322+timeoutCircuitBreaker.delete(key);
323+}
324+281325function parseOptionalPositiveInt(value: unknown, fallback: number): number {
282326const parsed =
283327typeof value === "number"
@@ -718,6 +762,18 @@ function normalizePluginConfig(pluginConfig: unknown): ResolvedActiveRecallPlugi
718762),
719763logging: raw.logging === true,
720764cacheTtlMs: clampInt(raw.cacheTtlMs, DEFAULT_CACHE_TTL_MS, 1000, 120_000),
765+circuitBreakerMaxTimeouts: clampInt(
766+raw.circuitBreakerMaxTimeouts,
767+DEFAULT_CIRCUIT_BREAKER_MAX_TIMEOUTS,
768+1,
769+20,
770+),
771+circuitBreakerCooldownMs: clampInt(
772+raw.circuitBreakerCooldownMs,
773+DEFAULT_CIRCUIT_BREAKER_COOLDOWN_MS,
774+5000,
775+600_000,
776+),
721777persistTranscripts: raw.persistTranscripts === true,
722778transcriptDir: normalizeTranscriptDir(raw.transcriptDir),
723779qmd: {
@@ -2181,6 +2237,39 @@ async function maybeResolveActiveRecall(params: {
21812237return cached;
21822238}
218322392240+// Circuit breaker: skip recall when the same agent/model has timed out
2241+// too many times in a row (#74054).
2242+const cbKey = buildCircuitBreakerKey(
2243+params.agentId,
2244+resolvedModelRef?.provider,
2245+resolvedModelRef?.model,
2246+);
2247+if (
2248+isCircuitBreakerOpen(
2249+cbKey,
2250+params.config.circuitBreakerMaxTimeouts,
2251+params.config.circuitBreakerCooldownMs,
2252+)
2253+) {
2254+const result: ActiveRecallResult = {
2255+status: "timeout",
2256+elapsedMs: 0,
2257+summary: null,
2258+};
2259+if (params.config.logging) {
2260+params.api.logger.info?.(
2261+`${logPrefix} skipped (circuit breaker open after consecutive timeouts)`,
2262+);
2263+}
2264+await persistPluginStatusLines({
2265+api: params.api,
2266+agentId: params.agentId,
2267+sessionKey: params.sessionKey,
2268+statusLine: `${buildPluginStatusLine({ result, config: params.config })} circuit-breaker`,
2269+});
2270+return result;
2271+}
2272+21842273if (params.config.logging) {
21852274params.api.logger.info?.(
21862275`${logPrefix} start timeoutMs=${String(params.config.timeoutMs)} queryChars=${String(params.query.length)}`,
@@ -2241,6 +2330,7 @@ async function maybeResolveActiveRecall(params: {
22412330debugSummary: buildPersistedDebugSummary(result),
22422331searchDebug: result.searchDebug,
22432332});
2333+recordCircuitBreakerTimeout(cbKey);
22442334return result;
22452335}
22462336@@ -2283,6 +2373,7 @@ async function maybeResolveActiveRecall(params: {
22832373if (shouldCacheResult(result)) {
22842374setCachedResult(cacheKey, result, params.config.cacheTtlMs);
22852375}
2376+resetCircuitBreaker(cbKey);
22862377return result;
22872378} catch (error) {
22882379if (controller.signal.aborted) {
@@ -2307,6 +2398,7 @@ async function maybeResolveActiveRecall(params: {
23072398debugSummary: buildPersistedDebugSummary(result),
23082399searchDebug: result.searchDebug,
23092400});
2401+recordCircuitBreakerTimeout(cbKey);
23102402return result;
23112403}
23122404const message = toSingleLineLogValue(error instanceof Error ? error.message : String(error));
@@ -2544,16 +2636,19 @@ export default definePluginEntry({
2544263625452637export const __testing = {
25462638 buildCacheKey,
2639+ buildCircuitBreakerKey,
25472640 buildMetadata,
25482641 buildPluginStatusLine,
25492642 buildPromptPrefix,
25502643 getCachedResult,
2644+ isCircuitBreakerOpen,
25512645 normalizePluginConfig,
25522646 readActiveMemorySearchDebug,
25532647 readPartialAssistantText,
25542648 shouldCacheResult,
25552649resetActiveRecallCacheForTests() {
25562650activeRecallCache.clear();
2651+timeoutCircuitBreaker.clear();
25572652lastActiveRecallCacheSweepAt = 0;
25582653minimumTimeoutMs = DEFAULT_MIN_TIMEOUT_MS;
25592654setupGraceTimeoutMs = DEFAULT_SETUP_GRACE_TIMEOUT_MS;
@@ -2565,4 +2660,7 @@ export const __testing = {
25652660setupGraceTimeoutMs = Math.max(0, Math.floor(value));
25662661},
25672662 setCachedResult,
2663+getCircuitBreakerEntry(key: string) {
2664+return timeoutCircuitBreaker.get(key);
2665+},
25682666};
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。