CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/API & SDK
API & SDK/2026-05-22Advanced

Why tool_result could not be submitted Keeps Coming Back, and How to Build a Recovery Handler That Actually Holds

Run a Claude agent long enough and one day it starts: 'tool_result could not be submitted', back to back, and retries change nothing. The error message hides four completely different root causes. Here is what I learned debugging this across the six auto-publishing pipelines I run as an indie developer, with the TypeScript recovery handler I now ship in production.

claude-api81tool-use22error-handling11agent-recoveryproduction111

Premium Article

Run a Claude agent long enough and one day this error walks in unannounced: tool_result could not be submitted. It fires back to back, retries change nothing, and within an hour another agent shows the same symptom. I have hit this myself across the six auto-publishing pipelines I run as an indie developer at Dolice — Claude Lab, Gemini Lab, Antigravity Lab, Rork Lab, plus the two long-running content sites Lacrima and Mystery. One evening four of them broke in a row, and I stayed up until sunrise carving the failure modes apart.

Anthropic's documentation gives almost no reproduction conditions for this one. After six months of running into it in production, I'm convinced of one thing: the message is singular, but it covers four completely different root causes that just happen to surface through the same string. Retries only fix one of them. The other three need you to rewrite the message array you're sending — they will not heal on their own.

This piece is the postmortem of those four causes, with the TypeScript recovery code I now ship.

The four root causes hiding behind one error string

Here is the taxonomy. The API does not label them; you separate them by looking at the message array you sent and the assistant turn just before it.

  1. tool_use_id and tool_result blocks are out of sync: the previous assistant turn emitted several tool_use blocks, and the tool_result blocks you sent back either miss one of those ids or carry one that was never requested
  2. Ordering of the message array is broken: a text block snuck into the user message that should have contained only tool_result blocks, or the tool_result ended up under the assistant role
  3. A parallel tool_use was interrupted mid-execution: five tool_use calls came back, you finished three, the process crashed, and on restart you advanced the conversation with a new user message before resolving the unfinished two
  4. One tool_result's content is too large (roughly above 200KB): a single block in a parallel batch exceeds the API ceiling, and the rejection bubbles up wearing this error's clothes

In my own pipelines the breakdown over six months was roughly 50% cause 1, 20% cause 2, 25% cause 3, 5% cause 4. Cause 1 is plain code bugs. Cause 2 is misreading the SDK. Cause 3 is bad streaming-recovery design. Cause 4 is undercompressed tool returns. Only cause 2 ever clears with a retry — the others require you to rewrite the message array itself before the next request.

Detecting and repairing tool_use_id mismatches

The most common one. When Claude emits several tool_use blocks in parallel and you mishandle even one id on the way back, the API rejects the whole message.

import Anthropic from "@anthropic-ai/sdk";
import type { MessageParam, ToolUseBlock } from "@anthropic-ai/sdk/resources/messages";
 
interface ToolInvocation {
  id: string;          // tool_use_id
  name: string;
  input: unknown;
  result?: string;
  error?: string;
}
 
/**
 * Pull every tool_use block out of the previous assistant message
 * and key them by tool_use_id. Execution results get written back into this map.
 */
function extractPendingToolUses(messages: MessageParam[]): Map<string, ToolInvocation> {
  const last = messages[messages.length - 1];
  if (last.role !== "assistant" || typeof last.content === "string") {
    return new Map();
  }
  const pending = new Map<string, ToolInvocation>();
  for (const block of last.content) {
    if (block.type === "tool_use") {
      const tu = block as ToolUseBlock;
      pending.set(tu.id, { id: tu.id, name: tu.name, input: tu.input });
    }
  }
  return pending;
}
 
/**
 * Before assembling the tool_result message, verify pending == executed.
 * Any missing id is padded with a synthetic skipped-execution result.
 * Sending an extra id that was never requested is just as fatal as missing one.
 */
function buildToolResultMessage(
  pending: Map<string, ToolInvocation>,
  executed: Map<string, ToolInvocation>
): MessageParam {
  const content = [];
  for (const [id, inv] of pending) {
    const done = executed.get(id);
    if (done) {
      content.push({
        type: "tool_result" as const,
        tool_use_id: id,
        content: done.result ?? "",
        is_error: Boolean(done.error),
      });
    } else {
      content.push({
        type: "tool_result" as const,
        tool_use_id: id,
        content: "tool execution skipped (client recovery)",
        is_error: true,
      });
    }
  }
  return { role: "user", content };
}

I now run the pending versus executed diff right before every send. In my pipelines that change alone dropped the incident rate from roughly 80 per month to under 5 per month — a 95% reduction. In dollar terms, each retry was costing about 18,000 input tokens on Claude Sonnet 4.6, so the saved API spend works out to roughly $32 per month per pipeline.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Tell apart the four real causes behind 'tool_result could not be submitted' (id mismatch, ordering, mid-stream interruption, oversize content) using request-shape evidence instead of guesswork
Take home the TypeScript recovery handler I run in production — extracted from a live auto-publishing pipeline serving six sites — including the checkpoint design that survives mid-stream crashes
See the actual monthly incident counts I observed before and after each fix, so you can calibrate how much engineering each cause deserves on your team
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

API & SDK2026-06-28
A Silent Drop to a Weaker Model Is Scarier Than an Error: Designing a Capability Floor for Claude API Fallback
When a model becomes unavailable in an unattended pipeline, automatically dropping to a weaker model is dangerous. Drawing on years of running automated indie pipelines, this is how to use per-task capability contracts and a degradation budget to decide where to stop.
API & SDK2026-05-26
Stabilizing Claude API Structured Responses in Production — Notes on tool_use, JSON Schema, and Layered Validation
Getting Claude to return JSON takes a few lines. Keeping that JSON usable in production is a different problem. Here is the layered design I landed on after running a wallpaper classification pipeline through Claude API, built around tool_use, JSON Schema, and domain validation.
API & SDK2026-05-09
A Five-Layer Preflight Design for Claude API — How I Cut Hundreds of 400/422/529 Errors to Zero
A production-tested five-layer preflight design that catches Claude API failures before the network call — schema, token budget, model capability, content policy, and spend cap — with full TypeScript implementation and one month of operational numbers.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →