[Workflow tool] resume cache is unreachable for nontrivial workflows because LLM dispatchers can't transcribe args byte-exactly
Summary
The Workflow tool's resume cache (resumeFromRunId) requires byte-identicalargs across invocations to actually fire. Today args is inline JSON only,
which means an LLM dispatcher has to perfectly transcribe the args object
across every turn — which doesn't hold up as the args grow. Filing this as a
gap; surfacing one possible shape of a fix at the bottom, but the right
solution is for the Claude Code team to figure out.
Background — the cache mechanic this is in tension with
Empirically (Claude Code 2.1.153), the Workflow tool's resume cache keys eachagent() call by the byte content of its prompt (and chains through prior
call results). Two prompts that differ by a single character get different
cache keys, miss the cache, run live, and — because the keys chain — every
downstream call misses too. This is fine and expected when the operator's
intent really did change.
The problem
Prompts are constructed inside the workflow script from the args global. Theargs parameter today is inline JSON only — there's no file reference. So the
dispatcher — in the common case, the Claude Code main loop itself, i.e. an
LLM — has to hand-carry the args object across every turn:
- LLM dispatcher reads the prior tool result.
- LLM dispatcher transcribes the args object into the new tool call.
- LLM dispatcher passes the new args inline.
This is an LLM transcribing structured data into a tool call. For trivially
small args (a few short strings) it can luck into byte-exactness. For args of
any real size — a multi-paragraph feature request, a JSON object holding
prior agent outputs, anything resembling a real workflow's working state —
the LLM will predictably:
- summarize prose fields it perceives as verbose
- reformat / re-indent JSON
- normalize quote styles or whitespace
- "clean up" punctuation, capitalization, or phrasing
- reorder object keys
None of these are bugs in the dispatcher; they're just what LLMs do when
copying text. Every one of them silently breaks the resume cache, for reasons
unrelated to anything the operator actually changed. There's no programmatic
guarantee that the args between turn 1 and turn 2 are byte-identical on the
fields the early prompts depend on, and no signal when they aren't.
The cache works, but it's only reachable for toy-sized args. That's the
structural gap.
Why this matters in practice
A common workflow shape is: clarify → plan → (return needs_input to ask
the operator a question) → operator answers → resume with the answer. The
resume cache is supposed to make this cheap on the unchanged prefix. Today it
does, if the dispatcher hits byte-exactness. When the dispatcher is an LLM
and the args are nontrivial, byte-exactness is not the realistic outcome —
the full pipeline re-runs, the failure is invisible (no error, just tokens
burned), and the bigger the workflow's working state grows, the more reliably
the cache misses.
Empirical data
A 3-step workflow with each agent's prompt referencing only the specific args
fields it needs, model: 'haiku' on all calls:
| Run | Args | Resume? | Tokens | What ran |
|-----|------|---------|--------|----------|
| 1 | {featureRequest:"add dark mode toggle to the right pane", plannerAnswers: null} | – | 79,923 | clarify + plan live, returned needs_input |
| 2 | same featureRequest, plannerAnswers: {color:"midnight blue"} | yes | 39,776 | clarify + plan cached, integrate live |
The cache fired in run 2 because the dispatcher (Claude Code's main loop, an
LLM) happened to transcribe the 9-word featureRequest string byte-perfectly.
This was a small-enough args that LLM-transcription held by accident. With a
realistic feature request — say, 400 lines of accumulated context, or a JSON
object holding prior agent outputs — the same dispatcher would have
summarized, reformatted, or re-serialized somewhere along the way, and the
cache would have silently missed.
Minimal repro workflow available on request.
One shape a fix could take (illustrative — you'll know better)
The core ask is: give the dispatcher some way to refer to the canonical args
without having to retype them every turn. The cheapest version of that we
could think of is an additional parameter that points at a file on disk —
something like argsPath: string — so the dispatcher writes the canonical
args once, then passes the path on subsequent invocations. The bytes flowing
into the cache layer become deterministic, and the LLM is no longer in the
byte-fidelity path at all.
But that's just one shape. There may be a better one — content-addressed
args, an "args handle" returned from the tool result, server-side
normalization with explicit guarantees, something else entirely. Whichever
shape closes the gap so that resume cache becomes reachable for non-toy
workflows.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗