{"record":{"id":"272e09b10cf892e5","repo":"can1357/oh-my-pi","slug":"invoketool-delegation-depth-exceeded-8-recursive","errorCode":null,"errorMessage":"invokeTool: delegation depth exceeded 8 (recursive invokeTool for \"${name}\"?)","messagePattern":"invokeTool: delegation depth exceeded 8 \\(recursive invokeTool for \"(.+?)\"\\?\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/extensibility/extensions/runner.ts","lineNumber":587,"sourceCode":"\t\tparams: Record<string, unknown>,\n\t\toptions?: {\n\t\t\tsignal?: AbortSignal;\n\t\t\tonUpdate?: AgentToolUpdateCallback<TDetails>;\n\t\t\tdepth?: number;\n\t\t\t/**\n\t\t\t * The caller tool's own context. Reused for the native call so metadata the native tool\n\t\t\t * reads — `toolCall` (write/edit LSP batch flushing) and provider metadata /\n\t\t\t * `providerSafetyApproved` (computer) — is preserved. Falls back to a fresh session tool\n\t\t\t * context only when the caller had none.\n\t\t\t */\n\t\t\tcallerContext?: AgentToolContext;\n\t\t},\n\t): Promise<AgentToolResult<TDetails>> {\n\t\tconst resolved = this.#nativeToolResolver?.(name);\n\t\tif (!resolved) throw new Error(`invokeTool: no native built-in named \"${name}\" to delegate to`);\n\t\tconst depth = options?.depth ?? 0;\n\t\tif (depth >= 8) {\n\t\t\tthrow new Error(`invokeTool: delegation depth exceeded 8 (recursive invokeTool for \"${name}\"?)`);\n\t\t}\n\t\tconst toolCallId = `invoke-${name}-${Date.now().toString(36)}-${depth}`;\n\t\treturn (await resolved.tool.execute(\n\t\t\ttoolCallId,\n\t\t\tparams as never,\n\t\t\toptions?.signal,\n\t\t\toptions?.onUpdate as never,\n\t\t\toptions?.callerContext ?? resolved.makeContext(),\n\t\t)) as AgentToolResult<TDetails>;\n\t}\n\n\tconstructor(\n\t\tprivate readonly extensions: Extension[],\n\t\tprivate readonly runtime: ExtensionRuntime,\n\t\t/** Ignored: `cwd` is always read live via the `cwd` getter below, not cached here. */\n\t\t_initialCwd: string,\n\t\tprivate readonly sessionManager: SessionManager,\n\t\tprivate readonly modelRegistry: ModelRegistry,","sourceCodeStart":569,"sourceCodeEnd":605,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/extensibility/extensions/runner.ts#L569-L605","documentation":"invokeTool tracks delegation depth and hard-caps it at 8 to stop recursive tool invocation (a tool whose execution invokes itself, directly or via a chain of extensions/tools). Exceeding the cap throws this Error naming the tool.","triggerScenarios":"An extension's invokeTool handler re-invokes the same tool, or a cycle of tools (A invokes B invokes A) nests more than 8 deep because options.depth is propagated.","commonSituations":"Extension wrapper intercepts a tool and calls invokeTool for the same tool name without a base-case condition; mutually recursive tool wrappers; default depth not threaded through a custom delegation chain.","solutions":["Break the recursion: in your handler, invoke the underlying tool directly (bypass invokeTool) or skip interception when options.depth > 0.","Pass/propagate depth correctly when re-invoking so the counter increments, and add an explicit depth guard in your handler.","Restructure so the intercepted path terminates (invoke the resolved built-in once, not through the dispatcher)."],"exampleFix":"// before\napi.onToolCall('bash', async evt => {\n  return runner.invokeTool('bash', evt.params); // re-enters dispatcher forever\n});\n// after\napi.onToolCall('bash', async evt => {\n  if ((evt.depth ?? 0) > 0) return; // let inner call pass through\n  return runner.invokeTool('bash', evt.params, { depth: 1 });\n});","handlingStrategy":"try-catch","validationCode":"if ((options?.depth ?? 0) >= 8) {\n  throw new Error('refusing to delegate: depth limit would be exceeded');\n}\nawait runner.invokeTool(name, params, { depth: (options?.depth ?? 0) + 1 });","typeGuard":"function canDelegate(depth: number | undefined): boolean {\n  return (depth ?? 0) < 8;\n}","tryCatchPattern":"try {\n  return await runner.invokeTool(name, params, { depth });\n} catch (err) {\n  if (err.message.includes('delegation depth exceeded 8')) {\n    logger.error('recursive invokeTool detected', { name, depth });\n    throw new Error(`circular tool delegation involving '${name}'`);\n  }\n  throw err;\n}","preventionTips":["In tool-call interceptors, never re-invoke the same tool through the dispatcher; call the underlying resolved tool directly.","Thread options.depth through every delegation hop so the counter is accurate.","Add a guard clause: skip interception when depth > 0.","Model tool chains as a DAG and test for cycles."],"tags":["extensions","tool-invocation","recursion"],"backgroundTag":"maximum-call-depth-exceeded","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}