{"record":{"id":"3219ffff2fdb5de4","repo":"Hmbown/CodeWhale","slug":"dynamic-tool-call-is-already-pending","errorCode":null,"errorMessage":"Dynamic tool call '{}' is already pending","messagePattern":"Dynamic tool call '(.+?)' is already pending","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":3078,"sourceCode":"                .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\n    /// Atomically select the single terminal owner for a dynamic tool call.\n    ///\n    /// The registry entry intentionally remains present with an empty sender\n    /// while the winner commits its receipt. `get_thread_detail` therefore\n    /// cannot publish a cursor that has neither the pending request nor the","sourceCodeStart":3060,"sourceCodeEnd":3096,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/runtime_threads.rs#L3060-L3096","documentation":"register_pending_dynamic_tool found params.call_id already present in the pending registry. Call ids are the identity used to route results, timeouts, and turn-termination settlement, so a duplicate id while the first is still pending is rejected rather than allowed to alias two live calls.","triggerScenarios":"Calling register with a reused call_id (counter reset, deterministic id generation like \"call_1\" across retries, or a copy-pasted id) while the first call has not reached a terminal receipt. Checked at runtime_threads.rs:3077 before insert.","commonSituations":"Retrying a registration after a transient error with the same locally-generated id; parallel branches generating ids from the same seed; UUID v4 not used where the caller assumed uniqueness.","solutions":["Generate call ids with a collision-proof source (UUID v4 / ULID) instead of counters or deterministic derivation","If reusing an id intentionally, first wait for the prior call to settle (submit its result or let the turn end) so the registry slot is released","On catching this error, regenerate a fresh call_id and retry the registration once","Audit retry logic that replays the exact same DynamicToolCallParams"],"exampleFix":"// before\nlet call_id = format!(\"call_{}\", attempt); // collides on retry\n\n// after\nlet call_id = uuid::Uuid::new_v4().to_string(); // unique per registration","handlingStrategy":"validation","validationCode":"// Generate ids from a collision-proof source; never reuse across retries.\nfn fresh_call_id() -> String {\n    uuid::Uuid::new_v4().to_string()\n}","typeGuard":null,"tryCatchPattern":"// Duplicate id while pending: regenerate and retry once.\nmatch manager.register_dynamic_tool_call(params).await {\n    Ok(rx) => Ok(rx),\n    Err(e) if e.to_string().contains(\"already pending\") => {\n        let mut params = params;\n        params.call_id = fresh_call_id();\n        manager.register_dynamic_tool_call(params).await\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Use UUID v4/ULID for call_id, not counters or deterministic derivation","On registration retry after a transient error, prefer awaiting the original receiver over re-registering","Audit retry wrappers that replay identical DynamicToolCallParams"],"tags":["dynamic-tools","duplicate-id","idempotency","runtime"],"backgroundTag":"duplicate-request-id","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}