{"record":{"id":"dbfeddc911ca1592","repo":"CherryHQ/cherry-studio","slug":"recursive-depth-exceeded","errorCode":"RECURSIVE_DEPTH_EXCEEDED","errorMessage":"Maximum recursive depth (${maxDepth}) exceeded at depth ${currentDepth}","messagePattern":"Maximum recursive depth \\((.+?)\\) exceeded at depth (.+?)","errorType":"error_code","errorClass":"RecursiveDepthError","httpStatus":null,"severity":"error","filePath":"packages/aiCore/src/core/runtime/pluginEngine.ts","lineNumber":177,"sourceCode":"    if (typeof model === 'string') {\n      // 字符串：需要通过插件解析\n      modelId = model\n    } else {\n      // 模型对象：直接使用\n      resolvedModel = model\n      modelId = model.modelId\n    }\n\n    // 创建类型安全的 context\n    const context = _context ?? createContext(this.providerId, model, params)\n\n    // ✅ 创建类型化的 manager（逆变安全）\n    const manager = new PluginManager<TParams, TResult>(this.basePlugins as AiPlugin<TParams, TResult>[])\n\n    // ✅ 递归调用泛型化，增加深度限制\n    context.recursiveCall = async <R = TResult>(newParams: Partial<TParams>): Promise<R> => {\n      if (context.recursiveDepth >= context.maxRecursiveDepth) {\n        throw new RecursiveDepthError(context.requestId, context.recursiveDepth, context.maxRecursiveDepth)\n      }\n\n      const previousDepth = context.recursiveDepth\n      const wasRecursive = context.isRecursiveCall\n\n      try {\n        context.recursiveDepth = previousDepth + 1\n        context.isRecursiveCall = true\n\n        return (await this.executeWithPlugins(\n          methodName,\n          { ...params, ...newParams } as TParams,\n          executor,\n          context\n        )) as unknown as R\n      } finally {\n        // ✅ finally 确保状态恢复\n        context.recursiveDepth = previousDepth","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/packages/aiCore/src/core/runtime/pluginEngine.ts#L159-L195","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — plugin always recurses, no termination\ntransformResult: async (result, context) => {\n  if (result.text.includes('RETRY')) {\n    return context.recursiveCall({ prompt: 'try again' })\n  }\n  return result\n}\n\n// after — track attempt count and stop after 3 retries\ntransformResult: async (result, context) => {\n  const attempts = (context.metadata.custom?.attempts ?? 0) as number\n  if (result.text.includes('RETRY') && attempts < 3) {\n    context.metadata.custom = { ...(context.metadata.custom ?? {}), attempts: attempts + 1 }\n    return context.recursiveCall({ prompt: 'try again' })\n  }\n  return result\n}","handlingStrategy":"validation","validationCode":"// Before calling context.recursiveCall in a plugin, check remaining depth budget\nif (context.recursiveDepth + 1 >= context.maxRecursiveDepth) {\n  // stop recursing — return current result or throw a domain-specific error\n  return result\n}\nreturn context.recursiveCall(newParams)","typeGuard":"function canRecurse(context: AiRequestContext, reserve: number = 1): boolean {\n  return context.recursiveDepth + reserve < context.maxRecursiveDepth\n}","tryCatchPattern":"try {\n  return await context.recursiveCall(newParams)\n} catch (error) {\n  if (error instanceof RecursiveDepthError) {\n    // return last known good result or fall back to a non-recursive path\n    return fallbackResult\n  }\n  throw error\n}","preventionTips":["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."],"tags":["recursion","plugin-engine","ai-core","non-streaming","safety-guard"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}