Hmbown/CodeWhale · error
Dynamic tool call '{}' is already pending
Error message
Dynamic tool call '{}' is already pending What it means
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.
Source
Thrown at crates/tui/src/runtime_threads.rs:3078
.then_with(|| left.id.cmp(&right.id))
});
(approvals, user_inputs)
}
fn register_pending_dynamic_tool(
&self,
params: DynamicToolCallParams,
) -> Result<oneshot::Receiver<DynamicToolCallResult>> {
let (tx, rx) = oneshot::channel();
let (settlement_tx, _settlement_rx) = watch::channel(0);
let mut pending = self.pending_dynamic_tools.lock();
if pending.len() >= MAX_PENDING_DYNAMIC_TOOL_CALLS {
bail!(
"Runtime has reached the pending dynamic tool call limit ({MAX_PENDING_DYNAMIC_TOOL_CALLS})"
);
}
if pending.contains_key(¶ms.call_id) {
bail!("Dynamic tool call '{}' is already pending", params.call_id);
}
pending.insert(
params.call_id.clone(),
PendingDynamicToolEntry {
params,
sender: Some(tx),
settlement_tx,
indeterminate: false,
},
);
Ok(rx)
}
/// Atomically select the single terminal owner for a dynamic tool call.
///
/// The registry entry intentionally remains present with an empty sender
/// while the winner commits its receipt. `get_thread_detail` therefore
/// cannot publish a cursor that has neither the pending request nor theView on GitHub (pinned to 0c42157ee5)
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
Example fix
// before
let call_id = format!("call_{}", attempt); // collides on retry
// after
let call_id = uuid::Uuid::new_v4().to_string(); // unique per registration Defensive patterns
Strategy: validation
Validate before calling
// Generate ids from a collision-proof source; never reuse across retries.
fn fresh_call_id() -> String {
uuid::Uuid::new_v4().to_string()
} Try / catch
// Duplicate id while pending: regenerate and retry once.
match manager.register_dynamic_tool_call(params).await {
Ok(rx) => Ok(rx),
Err(e) if e.to_string().contains("already pending") => {
let mut params = params;
params.call_id = fresh_call_id();
manager.register_dynamic_tool_call(params).await
}
Err(e) => Err(e),
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Dynamic tool call '{call_id}' has an indeterminate terminal
- Runtime has reached the pending dynamic tool call limit ({MA
- User-input request '{input_id}' has an indeterminate termina
- Agent Mail message id '{}' already exists with different del
- {error}
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/3219ffff2fdb5de4.
Report an issue: GitHub.