My local-runtime agents call an in-process SDKCustomTool whose execute can run for many minutes. Sometimes the SDK calls execute again with the same context.toolCallId and args while the first invocation is still pending, and the stream re-emits a tool_call “running” event for that callId. The replay’s result becomes the tool result; the original’s later result is dropped. I get duplicate sub-agents, or “Agent already has active run” from resumeAgent as the result. Anthropic and xAI call IDs are both affected. Topic 170387 shows the same symptom in the IDE, blamed on window throttling, but my process is headless.
Steps to Reproduce
Evidence: across about 60 conversations since August, my logs show a second execute for the same toolCallId 40s to 30min after the first. On 2026-09-24, three pending calls from different agents in one process were replayed within 3 minutes (21:39:58 to 21:42:59 UTC). My app passes customTools straight to the SDK and never retries execute. Minimal detector: register a custom tool whose execute logs context.toolCallId with a timestamp, then awaits 30 minutes. Prompt a local agent to call it exactly once, and run about five such agents concurrently. Any toolCallId logged twice is the bug. I haven’t confirmed this detector triggers it in isolation because the timing is intermittent.
Expected Behavior
execute should run exactly once per toolCallId. If an exec request must be re-delivered, the SDK should dedupe by toolCallId and attach the replay to the in-flight invocation, returning its eventual result.
Hey, thanks for the detailed report. The logs and timestamps help a lot.
What you’re describing matches how local runs recover after losing the backend connection. If the connection drops while a tool call is still pending, the SDK reconnects and resumes from the last saved state (enableAgentRetries is on by default). In that state, your tool call is still marked as unfinished, so execute gets called again with the same toolCallId and args, and there’s nowhere for the result of the first call to go. There’s currently no in-flight toolCallId dedupe in the custom tool executor. I’ve passed this to the team.
For now, you can protect your tool by memoizing a single promise per toolCallId, so a retry returns the original result:
const calls = new Map<string, Promise<SDKCustomToolResult>>();
execute: (args, context) => {
const id = context.toolCallId;
if (!id) return runTool(args);
let call = calls.get(id);
if (!call) {
call = runTool(args);
calls.set(id, call);
call.finally(() => setTimeout(() => calls.delete(id), 60 * 60 * 1000));
}
return call;
}
There’s another option, local: { enableAgentRetries: false }, but that turns every disconnect into a hard run failure. So I’d recommend the memoization approach above instead of disabling retries.
If you want to confirm the cause on your machine, this should reproduce the issue on demand on Linux:
Register a custom tool whose execute logs context.toolCallId with a timestamp, then waits about 5 minutes.
Start one local agent and ask it to call this tool exactly once.
After the first log line, run sudo ss -tnp | grep node and note the remote IP for the agent’s connection on port 443.
Run sudo ss -K dst <that IP> dport = :443 to drop the connection.
Watch the logs for a few seconds.
Do you see a second log line with the same toolCallId after step 4? Let me know, and also whether the workaround above stops the duplicate subagents.