CherryHQ/cherry-studio · error · RecursiveDepthError
RECURSIVE_DEPTH_EXCEEDED
RECURSIVE_DEPTH_EXCEEDED
Error message
Maximum recursive depth (${maxDepth}) exceeded at depth ${currentDepth} What it means
Thrown by the recursive-call closure inside executeWithPlugins (the non-streaming generateText path) when context.recursiveDepth has already reached context.maxRecursiveDepth (default 10, set in createContext). The PluginEngine installs context.recursiveCall so plugins can re-invoke the pipeline with modified params; each invocation increments recursiveDepth. This guard prevents infinite recursion / stack overflow when a plugin's logic creates a self-sustaining recursive loop.
Source
Thrown at packages/aiCore/src/core/runtime/pluginEngine.ts:177
if (typeof model === 'string') {
// 字符串:需要通过插件解析
modelId = model
} else {
// 模型对象:直接使用
resolvedModel = model
modelId = model.modelId
}
// 创建类型安全的 context
const context = _context ?? createContext(this.providerId, model, params)
// ✅ 创建类型化的 manager(逆变安全)
const manager = new PluginManager<TParams, TResult>(this.basePlugins as AiPlugin<TParams, TResult>[])
// ✅ 递归调用泛型化,增加深度限制
context.recursiveCall = async <R = TResult>(newParams: Partial<TParams>): Promise<R> => {
if (context.recursiveDepth >= context.maxRecursiveDepth) {
throw new RecursiveDepthError(context.requestId, context.recursiveDepth, context.maxRecursiveDepth)
}
const previousDepth = context.recursiveDepth
const wasRecursive = context.isRecursiveCall
try {
context.recursiveDepth = previousDepth + 1
context.isRecursiveCall = true
return (await this.executeWithPlugins(
methodName,
{ ...params, ...newParams } as TParams,
executor,
context
)) as unknown as R
} finally {
// ✅ finally 确保状态恢复
context.recursiveDepth = previousDepthView on GitHub (pinned to 726446b54c)
Solutions
- Add a convergence/termination condition to the plugin's recursiveCall logic so it stops recursing once the goal is met (e.g., check a flag in context.metadata or inspect the result before re-calling).
- If deeper recursion is legitimate, raise the cap by setting context.maxRecursiveDepth to a higher value inside the plugin's configureContext hook before the first recursiveCall.
- Audit all plugins that call context.recursiveCall and verify each has a base case that exits without recursing.
- If using a retry/correction plugin, limit retries with a local counter in context.metadata instead of relying solely on the engine's depth guard.
Example fix
// before — plugin always recurses, no termination
transformResult: async (result, context) => {
if (result.text.includes('RETRY')) {
return context.recursiveCall({ prompt: 'try again' })
}
return result
}
// after — track attempt count and stop after 3 retries
transformResult: async (result, context) => {
const attempts = (context.metadata.custom?.attempts ?? 0) as number
if (result.text.includes('RETRY') && attempts < 3) {
context.metadata.custom = { ...(context.metadata.custom ?? {}), attempts: attempts + 1 }
return context.recursiveCall({ prompt: 'try again' })
}
return result
} Defensive patterns
Strategy: validation
Validate before calling
// Before calling context.recursiveCall in a plugin, check remaining depth budget
if (context.recursiveDepth + 1 >= context.maxRecursiveDepth) {
// stop recursing — return current result or throw a domain-specific error
return result
}
return context.recursiveCall(newParams) Type guard
function canRecurse(context: AiRequestContext, reserve: number = 1): boolean {
return context.recursiveDepth + reserve < context.maxRecursiveDepth
} Try / catch
try {
return await context.recursiveCall(newParams)
} catch (error) {
if (error instanceof RecursiveDepthError) {
// return last known good result or fall back to a non-recursive path
return fallbackResult
}
throw error
} Prevention
- Always pair context.recursiveCall with a local attempt counter in context.metadata and a convergence condition.
- Set context.maxRecursiveDepth to a value appropriate for your use case in the plugin's configureContext hook if the default 10 is insufficient.
- Prefer iterative loops over recursion for retry logic — use recursiveCall only when the pipeline must re-execute end-to-end.
- Test plugins that use recursiveCall with adversarial inputs that trigger the retry path repeatedly.
When it happens
Trigger: A plugin's hook (e.g. transformResult or transformParams) calls context.recursiveCall(newParams) unconditionally, and the modified params don't converge — each recursive call triggers another. The check fires when recursiveDepth >= maxRecursiveDepth before the 11th nested call. Only affects the non-streaming executeWithPlugins code path.
Common situations: A plugin that retries or re-prompts on certain results without a convergence condition (e.g., always re-calling when the output contains a particular token). A chain-of-thought or self-correction plugin with an unbounded retry loop. The default cap of 10 being too low for a legitimate deep-recursion use case.
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/dbfeddc911ca1592.
Report an issue: GitHub.