Repository navigation
Part B2 semantic-extraction subagents silently crash past the ~64k output-token turn limit on large/deep-mode chunks; SKILL.md chunking is file-count-based, not output-size-aware #1758
Description
A note on how this report was written: I'm not a professional developer,
and English isn't my first language. I used my AI coding assistant (Claude,
via Claude Code) to investigate this and write up the report below, based on
something we actually hit while using thegraphifyskill in a real
project. I checked the existing issue tracker first to make sure this
wasn't already reported. Happy to answer follow-up questions, most likely
with the assistant's help.
Summary
When GEMINI_API_KEY/GOOGLE_API_KEY are unset, SKILL.md Part B dispatches
semantic extraction to Claude Code Agent-tool subagents instead of a headless
LLM backend ("the host session itself is the LLM"). Each subagent must
produce its entire chunk's extraction (all nodes/edges/hyperedges for
20-25 files, per Step B1's fixed chunk size) as JSON in a single Write call
within a single conversational turn. On dense corpora, and especially with
--mode deep (richer INFERRED edges → more output per file), a chunk's
output can exceed the ~64,000-output-token ceiling on that single turn.
We directly observed the failure: Claude's response exceeded the 64000 output token maximum. When this happens, the subagent's turn ends without
ever writing graphify-out/.graphify_chunk_NN.json — no partial output, no
error surfaced to the orchestrating skill beyond "the file never appeared."
Why this looked like two different bugs before we found the root cause
From the orchestrator's side, a subagent that hits this ceiling is
indistinguishable from one that simply never responds:
- If you don't wait long enough, it looks like an infinite hang.
- If you wait, chunks that stay just under the ceiling still take a long
time to generate tens of thousands of tokens of JSON (12-28 minutes in our
runs), so it looks like extreme, unexplained per-chunk latency with no
correlation to chunk file-count. - Only once a chunk actually exceeds the ceiling does the real error surface
— and per Step B3, a missing chunk file is currently diagnosed as "the
subagent was likely dispatched as read-only (Explore type)", which is a
plausible-but-wrong explanation for this failure mode: the subagent type
was correct, it simply crashed mid-generation.
Root cause
Step B1 sizes chunks purely by input file count (20-25 files, hardcoded),
with no accounting for expected output volume — which varies a lot by
mode (--mode deep extracts materially more INFERRED edges per file) and by
document density. There's no equivalent, for this dispatch path, of the
adaptive-retry / bisection logic that the headless backends have for
finish_reason == "length" (see #730, #1372 for the claude/openai
backends' hardcoded-max_tokens truncation handling, and #1611/#1063 for the
claude-cli subprocess backend's hollow-response/bisection recovery) —
because this path never goes through graphify/llm.py at all. It's Claude
Code's own Task-tool subagent mechanism standing in as the LLM, and nothing
in SKILL.md estimates whether a given chunk's expected output fits in one
turn before dispatching it.
Impact
- Silent, total loss of a chunk's extraction (not just truncation — the file
never gets written, so none of that chunk's files get cached this run). - The wrong diagnostic in Step B3 sends users chasing "did I use the right
subagent_type" instead of "this chunk was too big." - Disproportionately hits exactly the corpora graphify's docs pitch hardest
for--mode deep(dense, cross-referenced, entity-rich documents) — deep
mode is the trigger, not an edge case.
Suggested fix
- Size Step B1's chunks by estimated output, not file count. A rough
heuristic (bytes-per-file × a mode-dependent multiplier, capped to a
token budget with margin under 64k) would catch most cases before
dispatch. The extraction-spec prompt could carry the same budget as
guidance to the subagent (e.g. "~60 nodes / ~80 edges per chunk, prioritize
the most cross-referenced entities") so the model self-limits even when
the pre-dispatch estimate is off. - Give Step B3 a second diagnosis branch for missing chunk files.
Alongside the existing "likely read-only dispatch" guess, surface
"chunk may have exceeded the model's output-token limit — retry with a
smaller chunk" when a chunk is missing and its file count/estimated
density is high. (If the Agent-tool result exposes a stop/error reason to
the orchestrating skill, prefer branching on that directly over guessing
from symptoms.) - Consider incremental writes for large chunks — have the subagent
append nodes in batches across multipleWrite/Editcalls instead of
one terminalWrite, so a chunk that would exceed the ceiling degrades to
"wrote N of M nodes" rather than losing the whole chunk. More invasive,
but it turns a hard failure into a partial one.
Relation to existing issues
- Hardcoded max_tokens=8192 causes truncation cascade on dense docs (3× cost overhead) #730 / OpenAI backend caps output at 8192 tokens (half the 16384 other backends use) → truncated semantic chunks on dense repos #1372: hardcoded
max_tokenstruncation in the headless
claude/openaiAPI backends (graphify/llm.py). Different code path —
those backends have adaptive retry that catches this class of failure;
our path (Agent-tool subagent dispatch, used when no API key is set) has
no such mechanism at all. - fix(claude-cli): deliver extraction instructions in the user turn (fixes infinite bisection on Claude Code >= 2.1) #1611 / Fix hollow-response loop in claude-cli backend (re-targeted to v8, clean diff, with tests) #1063: response-parsing and prompt-delivery fixes for the
claude-clisubprocess backend (_call_claude_clishelling out to the
claudebinary). Also a different mechanism from Claude-Code-native
Task-tool subagents — no subprocess is involved in Part B2's dispatch. - [RFC] Native
graphify updatefor the semantic layer — a scripted conductor with a pluggable LLM hook (we measured: the math is 0.89s–52s, the agent-walked workaround is 11–14 min) #1710: RFC observing that "agent-walked" execution of the update
conductor is slow (11-14 min) compared to a scripted equivalent (under a
minute). Consistent with what we saw, and a good argument for a scripted
path in general — but that RFC is about eliminating agent-walked execution
for the update conductor steps (detect/merge/cluster/label), not about
this specific extraction-subagent output-ceiling crash, which would still
need addressing even inside a scripted conductor if it still shells out to
an agent for the actual semantic-extraction step (as the RFC's own
proposal, step 3, does: "a pluggable LLM hook...claude -p/any agent CLI
supplied by the user").
Environment
graphifyy0.8.36- Semantic extraction via SKILL.md Part B2, Claude Code Agent-tool
subagents,subagent_type="general-purpose",model="sonnet" --mode deep- Windows 11
- Directly observed error:
Claude's response exceeded the 64000 output token maximum(one occurrence, during a 13-file incremental chunk)
Our workaround (not a library fix)
For the run where we hit this, we sidestepped re-extraction entirely: most
of the "uncached" files had only changed by line-ending normalization (CRLF),
so we re-keyed their existing cache entries under the new content hash
instead of re-extracting, and folded the small amount of genuinely new
content into hand-written inline fragments rather than a subagent dispatch.
Going forward we're manually holding chunk subagents to a "~60 nodes / ~80
edges" budget via prompt guidance and sizing chunks by estimated output
rather than file count — a prompt-level mitigation on our side, not a fix to
SKILL.md itself.
Activity
DataMindBI commentedon Jul 16, 2026
Confirming this independently — hit the identical failure mode running
/graphify(Agent-tool subagent dispatch, no Gemini key configured) over a ~370-file corpus split into 17 semantic-extraction chunks at the default Step B1 sizing (20-25 files/chunk):API Error: Claude's response exceeded the 64000 output token maximum.+1 on both suggested fixes:
Happy to share more detail on the corpus profile if useful for reproducing.
Abdallah01 commentedon Jul 27, 2026
Independent corroboration from a third corpus: this still reproduces on 0.9.27
(this report was filed against 0.8.36), and one detail in the original write-up
looks too narrow.
--mode deepis not required. The report says deep mode "is the trigger, not anedge case", which reads as though deep mode is needed to hit the ceiling. Our run
used default mode, no
--mode deep, and still lost two chunks toresponse exceeded the 64000 output token maximum. So the ceiling is reachable on aplain
/graphify .— deep mode raises the odds, it isn't a precondition.Density, not file count — measured. 10 chunks dispatched at default Step B1
sizing. The two that died were the two densest, and nothing else:
Same file count, roughly 10x the output. That lines up with the independent
confirmation above from a very different corpus profile, which I think strengthens
the case that the problem is the metric rather than any particular kind of
document.
Thirds worked where halves might not have. The suggested fix upthread is
auto-retry at half size. Worth noting we needed thirds: re-splitting each
failed 21-file chunk into three 7-file sub-chunks succeeded first try, both times.
A halved 21-file chunk would still have been ~10 dense files, and given how far over
the cap they were, I doubt that would have cleared. If auto-retry lands, a loop that
keeps subdividing until it fits would be safer than a single halving — and the retry
should be on the same chunk re-split, never the chunk re-sent unchanged, which
just burns the budget again.
Two smaller things that helped and are cheap to put in the prompt:
instead of whitespace. On dense chunks that is not a rounding error.
A chunk that returns 60 good nodes is recoverable; a chunk that dies returns
nothing and its files stay uncached for the whole run.
The Step B3 misdiagnosis is still present in 0.9.27. A missing chunk file is
still explained as "subagent may have been read-only. Re-run with general-purpose
agent." Both of our failures used
general-purposecorrectly, so that message sentus looking at
subagent_typefirst. Since the real cause is now known, it would helpif that branch listed the output-ceiling crash as the other cause of a missing chunk
file — one line, and it saves the wrong investigation.
Environment:
graphifyy0.9.27, Windows 11, Agent-tool subagent dispatch with noGemini key set,
subagent_type="general-purpose", default mode, mixed corpus ofsource plus project documentation (~210 non-code files dispatched, 62 images
deliberately skipped).
Separately filed #2229 for a different gap this same run surfaced — the #479
shrink-guard compares total node count, so the total collapse of a semantic layer
that this issue describes would not be caught by it if the AST layer happened to
grow in the same run.
maxhawk1 commentedon Aug 7, 2026
Fourth corpus confirming this, still present on 0.9.20. Adding three things that aren't yet in this thread: an implementation of suggestion #3 that does not degrade to partial output, a distinct second failure mode on the same
Writecall, and a gotcha for whoever implements it.(Same disclosure as the original report: investigation and write-up done with an AI assistant, based on a real run.)
1. Suggestion #3 works, and it doesn't have to be lossy
The write-up frames incremental writes as "more invasive, but it turns a hard failure into a partial one." In practice it turns it into a complete one. The batching goes on the write path, not the extraction — the model still extracts the whole chunk, it just doesn't have to emit it in a single turn's output budget.
Protocol we ran (verified end-to-end, including the failure paths):
mkdir -p "$(dirname CHUNK_PATH)" && : > 'CHUNK_PATH.parts.jsonl'$, backticks and backslashes inside labels.python3, deduping nodes byidand edges by(source, target, relation), so overlapping batches are safe.Three properties that matter: no single turn approaches the ceiling;
python3writes the bytes so no tooling sits between the model and the file; and a failure costs one batch rather than the chunk. Re-dispatching our three failed chunks under exactly this protocol recovered all three.We paired it with explicit caps in the extraction prompt (150 nodes / 250 edges / 3 hyperedges,
"truncated": truewhen the source genuinely exceeds them) and a hard cap on the subagent's final message. Worth noting the prompt currently opens with "Output ONLY valid JSON matching the schema below" and separately instructs aWriteto disk — that ambiguity invites the model to emit the payload into its output budget and write it, which burns the ceiling twice over. Making it unambiguous that the JSON only ever travels through tool calls is a cheap independent win.2. Distinct failure mode on the same call: the
Writeauto-formatter truncates valid outputSeparate from the token ceiling, we observed
Write's auto-formatter force-closing an intentionally-open JSON array, producing a file that exists and parses but is silently missing most of its content. Same chunk, same run:.graphify_chunk_05.json.truncated(formatter-mangled).graphify_chunk_05.json(valid re-run)This is a different symptom from the one this issue describes — the file is written, so Step B3's existence check passes and the short chunk merges cleanly. Nothing downstream flags it. It's arguably worse than the crash for that reason, and it's a second independent argument that
Writeis the wrong primitive for this payload regardless of chunk sizing.If Step B3 keeps an existence check, it may be worth also treating "file is short relative to sibling chunks and ends mid-structure" as a truncated write rather than a small chunk.
3. Implementation gotcha: sidecar naming vs the Part C merge glob
Part C globs
graphify-out/.graphify_chunk_*.json. Name the sidecarCHUNK_PATH + ".parts.jsonl"and it's safe —.graphify_chunk_04.json.parts.jsonldoes not match that pattern (the pattern requires the name to end in.json). We verified this explicitly, because getting it wrong would feed partial batch files straight into the merge.4. Loss rate
19 chunks over dense primary-source documents (SEC filings, regulatory dockets, testimony PDFs); chunks 04, 06 and 14 lost = 16%. Consistent with @DataMindBI's ~35% on a denser corpus. The cost profile is worth stating plainly: it dies after all file reading completes, so the full token cost of extraction is paid and nothing is written.
One small confirmation of the write-up's point about the misleading Step B3 diagnostic — chunk 04's last line before it died was:
Nothing about that looks like a read-only dispatch.
Scope
The
Write-tool instruction ships in 10references/extraction-spec.mdcopies across the packaged platform variants, so any fix here wants applying across all of them, not just the Claude skill.Merge upstream v8 into fix/top-impact-issues