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 = previousDepth

View on GitHub (pinned to 726446b54c)

Solutions

  1. 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).
  2. 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.
  3. Audit all plugins that call context.recursiveCall and verify each has a base case that exits without recursing.
  4. 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

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.