{"record":{"id":"8be589f91505065a","repo":"Hmbown/CodeWhale","slug":"runtime-has-reached-the-pending-dynamic-tool-call","errorCode":null,"errorMessage":"Runtime has reached the pending dynamic tool call limit ({MAX_PENDING_DYNAMIC_TOOL_CALLS})","messagePattern":"Runtime has reached the pending dynamic tool call limit \\((.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":3073,"sourceCode":"            .map(|(_, entry)| entry.request.clone())\n            .collect::<Vec<_>>();\n        user_inputs.sort_by(|left, right| {\n            left.turn_id\n                .cmp(&right.turn_id)\n                .then_with(|| left.id.cmp(&right.id))\n        });\n        (approvals, user_inputs)\n    }\n\n    fn register_pending_dynamic_tool(\n        &self,\n        params: DynamicToolCallParams,\n    ) -> Result<oneshot::Receiver<DynamicToolCallResult>> {\n        let (tx, rx) = oneshot::channel();\n        let (settlement_tx, _settlement_rx) = watch::channel(0);\n        let mut pending = self.pending_dynamic_tools.lock();\n        if pending.len() >= MAX_PENDING_DYNAMIC_TOOL_CALLS {\n            bail!(\n                \"Runtime has reached the pending dynamic tool call limit ({MAX_PENDING_DYNAMIC_TOOL_CALLS})\"\n            );\n        }\n        if pending.contains_key(&params.call_id) {\n            bail!(\"Dynamic tool call '{}' is already pending\", params.call_id);\n        }\n        pending.insert(\n            params.call_id.clone(),\n            PendingDynamicToolEntry {\n                params,\n                sender: Some(tx),\n                settlement_tx,\n                indeterminate: false,\n            },\n        );\n        Ok(rx)\n    }\n","sourceCodeStart":3055,"sourceCodeEnd":3091,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/runtime_threads.rs#L3055-L3091","documentation":"register_pending_dynamic_tool rejects a new dynamic tool call because the pending registry already holds MAX_PENDING_DYNAMIC_TOOL_CALLS (128) entries. The registry only drains when a call's terminal receipt is durably appended, so this means 128 calls are concurrently awaiting results, timeouts, or turn-termination settlement.","triggerScenarios":"A model turn issues more than 128 concurrent dynamic tool calls (register_dynamic_tool per call), or earlier calls were never settled because the caller never submitted results and no timeout fired. Checked at runtime_threads.rs:3072 before insertion.","commonSituations":"A fan-out agent loop that spawns tool calls without awaiting them; leaked calls from a turn whose completion path crashed; bursty workloads on a single runtime.","solutions":["Await or cancel outstanding dynamic tool calls so their terminal receipts are appended and registry slots free up","Cap the caller's own concurrency below 128 (e.g. a semaphore of ~64) so the runtime limit is never reached","If calls appear leaked, complete or terminate their turns - settle_dynamic_tools_for_terminal_turn drains all calls for the turn","Report a bug if fewer than 128 live calls are visible; a settlement leak is draining slots"],"exampleFix":"// before\nfor task in tasks {\n    let rx = manager.register_dynamic_tool_call(params(task)).await?; // can exceed 128\n}\n\n// after\nlet sem = Arc::new(tokio::sync::Semaphore::new(64));\nfor task in tasks {\n    let permit = sem.clone().acquire_owned().await?;\n    let rx = manager.register_dynamic_tool_call(params(task)).await?;\n    tokio::spawn(async move {\n        let _ = rx.await;\n        drop(permit);\n    });\n}","handlingStrategy":"retry","validationCode":"// Before registering, bound your own outstanding calls well under the limit.\nconst MAX_IN_FLIGHT: usize = 64; // runtime hard limit is 128\nif outstanding_dynamic_calls.load(Ordering::SeqCst) >= MAX_IN_FLIGHT {\n    return Err(anyhow::anyhow!(\"caller-side dynamic tool backpressure\"));\n}","typeGuard":null,"tryCatchPattern":"// On the limit error: wait for settlements, then retry once.\nmatch manager.register_dynamic_tool_call(params).await {\n    Ok(rx) => Ok(rx),\n    Err(e) if e.to_string().contains(\"pending dynamic tool call limit\") => {\n        wait_for_pending_settlements().await; // await outstanding oneshots/watch channels\n        manager.register_dynamic_tool_call(params).await\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Wrap registrations in a semaphore sized < 128","Always pair every registration with a terminal outcome (result submit, timeout, or turn end) so entries drain","Monitor the pending-call count if your runtime exposes it; saturation means a settlement leak"],"tags":["dynamic-tools","resource-limit","backpressure","runtime"],"backgroundTag":"pending-request-limit-reached","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}