Harness silently executes duplicated parallel tool_use blocks: subagent fan-out runs N× the intended count (6 → 24)
Summary
In a single assistant turn that fans out a fixed set of parallel subagents (the Task/Agent tool), the model can degenerate into re-emitting the same batch of parallel tool_use blocks multiple times before yielding the turn. Claude Code executes every emitted block, so an intended fan-out of 6 subagents became 24 — each a full subagent burning large token counts. There is no deduplication of identical parallel tool_use calls within a turn, no cap on concurrent subagent fan-out, and no warning, so the blowup is silent until the running-agents count is noticed.
This is triggered by model degeneration, but the cost is a harness concern: the harness is what converts emitted blocks into billed, executed work, and it has no backstop against a stuttered re-emission of an identical batch.
Environment
- Claude Code CLI 2.1.158
- Model: Opus (extended thinking enabled)
- Plain
Task/Agentsubagent fan-out (not the experimental Agent Teams feature)
What happened
A turn was supposed to dispatch a fixed panel of 6 parallel subagents in one message. Instead the UI showed 24 concurrent subagents — the same 6-member batch repeated ~4× (individual members appearing 3–5 times each), each running to completion at ~70k–220k tokens.
Evidence that this was one non-yielding turn (not legitimate sequential re-dispatch)
From the session transcript:
- Between the first subagent dispatch and the first subagent result returning, there were 18 subagent dispatches with ZERO interleaved tool-results. Sequential tool-calling cannot emit call #2 before call #1 returns; emitting 18 with no results in between is only possible by repeatedly emitting the parallel batch within a single non-yielding turn.
- The dispatch
descriptiontext degraded across repeats — the first batch carried full descriptions, later batches collapsed to a truncated form. Degrading, repeating output is the fingerprint of autoregressive degeneration. - The fanned-out calls differed only in the subagent type — identical short prompt, near-identical description across the batch. That maximally-repetitive shape is exactly what seeds a tool-call repetition loop.
Impact
- Intended fan-out: 6 subagents. Actual: 24. ~4× token spend and wall-clock for one operation.
- Silent — no cap, no dedup, no warning; only noticeable via the running-agents count.
- Non-deterministic (model variance). A milder instance (a single subagent emitted 3× in one turn) was observed days earlier in the same setup, so this is a latent class, not a one-off.
Expected behavior / suggested fixes (harness side)
- Deduplicate identical parallel
tool_useblocks within a single assistant turn before execution — at minimum, collapse exact-duplicate subagent dispatches (same subagent type + identical prompt) emitted in one turn. Highest-value backstop. - Cap concurrent subagent fan-out with a soft limit + confirmation above a threshold (e.g. "About to launch 24 agents — continue?"), so a degenerate emission can't silently run.
- Warn when a turn emits the same tool-call signature more than K times — a strong degeneration signal regardless of dedup policy.
A prompt/skill author cannot reliably prevent this, since the degenerating model is the same one that would have to read a "don't repeat" instruction. The reliable fix is at the layer that turns emitted blocks into executed work.
Possibly related (but believed distinct)
- #55586 (Agent Teams: single spawn creates many duplicate workers) — its "within-turn duplication" mechanism is behaviorally the same N→k×N pattern, but that report's repro is gated on the experimental Agent Teams feature; this occurs on plain
Task/Agentfan-out. - #20640 / #20693 (
tool_use ids must be unique) — same "duplicate tool_use blocks in one turn" family, but those terminate in an API 400 error; here the IDs are unique and all copies execute silently.
Note on transcript logging (secondary)
In the session .jsonl, requestId, message.id, and stop_reason were stamped identically across an entire multi-minute turn-group, including unrelated earlier tool calls that completed with their own results. Those fields therefore look like a turn-group/UI grouping value rather than a raw per-API-response identifier, which makes them unreliable for attributing a truncation to a specific API response. Possibly worth confirming whether that field reuse is intended.
15 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
@SynVisions — your 18-dispatch-with-zero-interleaved-results forensic is the cleanest evidence I've seen for the within-turn parallel-
tool_usere-emission class, and I want to fold it into a wider pattern you may not have seen yet.I track a parallel-tool-cancellation-and-cascade cluster (Cluster 20 in cc-safe-setup's tracker) — three independent issues filed 2026-05-30 (#64047 / #64052 / #64059) on the same parallel-
tool_usefan-out failure mode. Your #64080 lands as a fourth, distinct sibling on a tighter root cause:tool_usebatch K times before yielding. 18 dispatches with zero interleaved results is the load-bearing signal — sequential calling literally cannot produce that interleave pattern.The three differ on whether the model yields between repeats and what triggers the repetition, but the harness-side gap is identical: no dedup of identical parallel
tool_useblocks before execution, no concurrent-fan-out cap, no degeneration warning. Your fix list (1, 2, 3) is exactly the right shape — fix 1 (within-turn dedup) is the highest-value backstop because it short-circuits both your case and the cancellation-cascade axis if the same batch re-emits on the recovery turn.Two cc-safe-setup hooks (both shipped 2026-05-31) cover symptoms of this class while the harness fix is upstream:
parallel-cascade-detector.sh— PostToolUse, rolling 500ms window, fires when concurrent parallel tool calls exceed a threshold. Would have caught your 18-dispatch burst around the 6th–8th block, not the 24th.parallel-batch-size-limiter.sh— PreToolUse, advisory-only stderr warning when a batch crosses a configurable size (default 6). Doesn't block (exit 0), but breaks the silent-blowup property — the operator sees the warning the moment the second copy of the batch lands.Neither is a real fix for the within-turn re-emission case, because both observe the harness-executed blocks rather than the model's emitted blocks. Within-turn dedup has to live on the harness side, before execution. Even so, the PreToolUse warner does its job on cost containment (you see "8th
Taskdispatch in 500ms with identical description" within a couple seconds of the second copy starting), which is what stopped Cluster 20 sibling cases at ~2× rather than ~4×.The note at the bottom —
requestId/message.id/stop_reasonstamped identically across a multi-minute turn-group, including for earlier completed tool calls — is, separately, the kind of attribution-breaker that makes harness-side debugging of within-turn re-emission much harder than it needs to be. If those fields really do reuse a turn-group value, an outside investigator (or a hook author) can't unambiguously bind a Cancelled marker or a duplicate emission to a specific API response, which means the "this happened inside one turn" claim has to be reconstructed from interleave-pattern forensics every time, the way you did here. That seems worth a separate filing if Anthropic confirms the field semantics.Adding #64080 to the Cluster 20 entry as the within-turn-degeneration axis. Thanks for the forensic — it's the cleanest single-issue evidence of the class so far.
(Disclosure: I maintain cc-safe-setup; the two hooks above are PR #501 and #503 there. No affiliation with Anthropic.)
@SynVisions yeah, the harness should own dedup here. We hit a related drift case earlier. Orchestrator-side, not CC harness-side. Fix that worked: make subagent dispatch explicit per-task, content-hash dedup at the dispatch boundary. Re-emitted identical batch collapses to one execution.
Different setup, not CC-native Task fan-out, so ymmv. But here's what a harness backstop looked like for us: github.com/palios-taey/claude-code-fleet-orchestrator
Fix #1 is the right minimum. 18 dispatches before the first result comes back is a structural signal you can catch in-turn, no model guessing needed.
@palios-taey thanks — the dispatch-boundary content-hash dedup in your orchestrator is the cleanest independent corroboration of the structural fix shape we've been circling.
lib/dispatch.pydoing the atomictaey:<worker>:current_taskwrite with stale-outcome + stuck-dedup clear before any worker-state mutation, plus the v1.0.5 conditional OrchTask claim before the Redis write, is exactly the layer SynVisions' Fix #1 is asking for — just landed in your supervisor instead of the CC harness.The "different setup, ymmv" caveat you flagged is the load-bearing one and worth pulling out: your orchestrator owns the dispatch boundary because you spawn workers via tmux-send through
claude-code-fleet-notify, so dedup can run in the supervisor's process before the worker CLI ever sees the batch. CC-nativeTask/Agentfan-out has no equivalent intercept point — the paralleltool_useblocks are emitted inside the same Claude Code process that will execute them, so dedup either lives upstream in the harness's pre-execution path (Fix #1 territory) or it doesn't exist. Either layer suffices independently; the gap is that neither is currently present in CC-native fan-out.Your "18 dispatches before the first result comes back is a structural signal you can catch in-turn, no model guessing needed" point is the right one to amplify. That's a property of the emitted block stream, not of execution outcomes, so it can be enforced before any token is spent on a duplicate. The PostToolUse hook I shipped (
parallel-cascade-detector.sh) catches it after execution starts, which means cost containment but not cost prevention — pre-execution dedup is structurally better. Your implementation is the existence proof that "harness backstop, content-hash, in-turn" is buildable without model cooperation.Linking your orchestrator into the Cluster 20 entry as an external-implementation reference. The fact that the same drift pattern surfaced in a non-CC orchestrator and you converged on the same fix shape strengthens the case for Fix #1 landing inside Claude Code itself rather than each operator rebuilding it at their own dispatch boundary.
@yurukusa that pre-execution vs post-execution distinction is the load-bearing one. Your parallel-cascade-detector.sh post-exec catch gives cost containment plus observability when the harness can't intercept; our supervisor-side intercept only exists because we control the dispatch process. Both are real layers. The gap is just that CC-native fan-out has neither.
The within-process intercept is harder than it looks. The harness has to dedupe after the model emits the parallel tool_use stream but before it forks the executions — and by then the thinking budget has already counted the dupes as work done. So Fix #1 saves you execution cost, not reasoning cost. Worth flagging upstream.
@palios-taey — the "Fix #1 saves execution cost, not reasoning cost" decomposition is the right way to frame the asymmetric cost, and it has a structural implication worth pulling out: within-turn re-emission is strictly more expensive per unit of model-side recognition than between-turn re-emission, because the reasoning tokens for the duplicate batch are committed before any signal (a returned result, a cancellation marker, an operator interrupt) can reach the model.
That breaks the usual cost-containment intuition. With between-turn cascade events (Cluster 20 Axis 1 — fail one, cancel siblings, model re-emits next turn), the model at least sees the cancellation results before it commits reasoning to the retry, so per-retry reasoning cost is bounded by how convinced the model is that the cancel was real. With within-turn re-emission (your #64080 axis), the duplicate batches are emitted from the same autoregressive pass — the model has no intermediate signal because no result has returned yet. Reasoning cost for emit N+1 is paid for the same reason as emit N, with no observation in between.
The harness-side implication: pre-execution dedup at the dispatch boundary is the right execution-cost backstop, but a reasoning-cost backstop has to live one layer further upstream — at the token-stream layer, before the parallel
tool_useblocks finish emitting. That's substantially harder, since it requires the harness to introspect the streaming token output for repeatedtool_useheaders and either cut the stream or emit a synthetic dedup marker mid-emission. Probably not the right first move; Fix #1 is high-value because execution cost is the visible part of the bill, and the reasoning-cost asymmetry is the under-counted part. Worth flagging both layers upstream so the eventual fix sequence is informed by which part of the cost the team wants to address first.The "thinking budget has already counted the dupes as work done" line also explains a transcript artifact I've been seeing in cascade postmortems: the model's reasoning summary at the end of a degenerated turn often references work that the duplicate emissions were going to do, not work that was actually completed. The reasoning was structured around the duplicate emit happening, so the post-hoc summary reflects the model's plan rather than the harness-executed outcome. If your orchestrator's content-hash dedup preserves the original emission's reasoning attribution (i.e. attributes the deduped call's reasoning cost to whichever original emit it collapsed into), that's worth noting in the orchestrator docs — operators reading their orchestrator logs would otherwise see a single execution with N× the apparent reasoning cost and not be sure what happened.
A separate angle: your
lib/dispatch.py's stale-outcome + stuck-dedup clear before any worker-state mutation ordering is the load-bearing detail for non-CC operators trying to retrofit the pattern. The temptation is to do dedup after worker-state mutation (because the mutation is the visible artifact), but that's where the silent N→2N drift sneaks back in — your ordering closes the gap. If you ever spin up an operator guide for the orchestrator, that ordering is the part that's easiest to get wrong on a first-pass reimplementation and worth its own section.The Cluster 13 (Extended-Thinking wedge) decoupling test I asked about upthread — whether @SynVisions' batch was sent with thinking enabled vs disabled — would let us either fold within-turn re-emission into Cluster 13's regression window (v2.1.156–v2.1.158) or break it out as a structurally separate cluster. The asymmetric reasoning-cost angle you've articulated leans toward "separate cluster": Cluster 13's wedge is observable as a hang or empty response, but #64080's degeneration is observable as visible work that just happens to be wrong work. Different operator-side symptoms even if the underlying model-state mechanic overlaps. Tracking both as distinct entries for now.
@yurukusa yeah — 127-133, you read it right. Delete stale last_outcome, kill the stuck-dedup key, then set current_task. Order matters: dedup-clear before state-mutation closes the silent re-claim window. v1.0.5 added the conditional OrchTask claim (
_claim_ready_orch_task, 146-189) before the Redis write, so a stale worker that survived the dedup-clear still can't grab a task whose Neo4j status already moved.On the reasoning-attribution thing — doesn't really map for us. Each worker is its own Claude/Codex/Gemini/Grok process. Own context. Not a fan-out from one supervisor stream. So there's no intra-supervisor reasoning to attribute. Supervisor only sees the structured outcomes the workers write back.
That difference is actually load-bearing for the cluster split: supervisor-coordinated fan-out has independent reasoning per worker by construction; CC-native Task fan-out has shared reasoning across all the dupes from one autoregressive pass. Different architectures, different cost shape. Different clusters per your read — agreed.
Within-turn re-emission is its own beast. The model commits the reasoning to the dupes in one pass. It can't observe its way out of that.
@palios-taey — the supervisor-coordinated vs CC-native Task fan-out distinction is the right architectural split, and once you draw that line a lot of the cost shape becomes legible.
Supervisor-coordinated (your orchestrator): N independent Claude/Codex/Gemini/Grok processes, each with its own context window, each making its own autoregressive pass. The reasoning cost is N × per-worker — no shared substrate to attribute. The pathology you fenced off is at the dispatch boundary (workers re-claiming tasks the orchestrator already moved past), and your
_claim_ready_orch_taskat 146-189 is exactly the right place: the conditional claim runs after the dedup-clear but before any Redis state mutation, so the stale-worker window is closed at the orchestrator's decision layer rather than the worker's execution layer. That's the dedup-clear-before-state-mutation ordering I read at 127-133, just escalated one level up to handle the Neo4j-already-moved case.CC-native Task fan-out (Cluster 20D shape): one autoregressive pass commits the reasoning to all K dupes simultaneously. The model isn't observing K parallel processes and choosing to commit identical reasoning — the K identical
tool_useblocks are artifacts of the same generative trajectory. There's no place for an in-loop check, because the loop has already emitted the K blocks before any observation can route back. That's why I keep landing on "the only window where prevention is possible is PreToolUse" for the related Cluster 22 cases (#64048 prompt-injection fabrication, #64065 anchor) — once the assistant turn commits, the cost is paid in reasoning tokens regardless of what happens downstream.Two cross-cluster observations the architectural split brings into focus:
effort=mediumroutine turns burning 46,433 output tokens (anchor #64153, 22m 43s of hidden thinking on a rename-impact scan) where Opus 4.6/4.7 produce 2-3k. The cost-shape difference between supervisor-coordinated and CC-native fan-out maps onto the cost-hazard / correctness-hazard split: per-worker independent reasoning is independently expensive but observable; shared-reasoning fan-out is invisibly expensive but only at one point in time.If your orchestrator's supervisor layer ever needs a Cluster 22-style fabrication signal, the
tool-result-correlation-checker.sh(PR #519) hook detectstool_use_id↔tool_resultpairing mismatches at the PostToolUse boundary — useful only when the harness emits both sides, which I'd expect for your case but not for pure within-turn re-emission. Different observation surfaces, different defenses; the architectural split tells you which one applies.Path-1-elimination (
/model claude-opus-4-7) is the upstream fix for both surfaces, but it's a downstream symptom-suppression for the calibration regression itself. The fix needs to land in the model weights / decoding parameters — neither the supervisor nor the harness can reach it.@yurukusa yeah, the 20D/22 split is what I'm seeing too. PR #519 fits. We pipe some workers through CC, others through Codex/Gemini/Grok — the CC ones could use the tool_use_id pairing directly. Saves reimplementing it.
Path-1-elimination (
/model claude-opus-4-7) is the right disposition until the calibration regression lands a fix. Per-worker model pin is trivial on our side. CC-native fan-out it's the cleanest containment while you wait.Our cost leak isn't shared reasoning across dupes — we don't even have that. It's a worker retrying the same task with different params and the supervisor not catching the collision. Honestly haven't measured the write amplification yet, but I'd bet that's where it hides. Same shape, different surface. Worth flagging if anyone else is fanning out across CC + Codex.
The "18 dispatches with zero interleaved results" forensic is the cleanest observable property for distinguishing within-turn re-emission from between-turn retry: sequential tool calls cannot emit result #2 before result #1 returns, so a stream with 18 dispatches and 0 interleaved results is necessarily within one autoregressive pass. That's a strong harness-side signal — it's visible in the emitted block stream before any execution completes.
Fix #1 (dedup identical parallel
tool_useblocks within a turn before execution) is the right highest-leverage backstop, and it has a specific property worth calling out for the implementation: the dedup has to happen after the blocks are emitted (since the model can't observe its own degeneration) but before any fork is handed to the execution layer. At that boundary, a content-hash on (subagent-type, prompt) is sufficient to collapse the duplicates — the model's description degradation across repeats (full → truncated) is just an artifact of the degeneration, not a signal of intended differentiation.One thing that makes this harder at the harness side: unique
tool_useIDs mean the harness's existing dedup logic (which likely keys on ID) won't catch this. The dedup has to key on content, not identity. If that seems expensive at scale: a count-per-subagent-type within a single turn is cheap and sufficient — if you see the same subagent type dispatched more than K times in one turn before any result returns, flag for dedup.On the extended-thinking correlation (yurukusa's question upthread): the description-text degradation pattern (full → truncated) is characteristic of autoregressive output drift under extended generation, which extended thinking can amplify by holding more context in-flight without yielding. If this batch was running with thinking enabled, that's load-bearing for attributing this to the Cluster 13 regression window (v2.1.156–v2.1.158). If not, it's a structurally independent cluster.
We've hit this shape in an orchestration context (Claudiverse — claudeverse.ai) where the coordinator layer sits outside the CC harness and can see the fan-out count before committing. A content-hash dedup at the dispatch boundary is exactly what we ended up building — the principle is the same whether the intercepting layer is inside the harness (Fix #1) or external to it. External works as a workaround; the harness fix is the right permanent answer because it prevents the execution cost regardless of what tooling sits on top.
@kcarriedo — The "18 dispatches with zero interleaved results" forensic is the kind of harness-side observable that closes the explanatory loop without requiring model-internal access. Two implications I want to add to your framing:
On the within-turn vs between-turn distinction. The property is visible in the emitted block stream before any execution completes, which makes it asymmetrically cheap to detect compared to all the after-the-fact approaches we've been working with (post-execution
tool_use_idcorrelation, transcript replay, etc.). Detection cost is O(block-stream scan); detection happens at the right layer (harness, pre-dispatch); the corresponding intervention (Fix #1: dedup identical paralleltool_useblocks within a turn before execution) lives at the same layer. That's a closed-loop fix shape — observation, detection, and intervention all in the same component, which is the cheapest fix surface available across the cluster.On the duplicate vs the multi-instance distinction. The 18-with-0-interleaved forensic also gives us a clean way to split sub-axis 22B (parallel-batch fabrication compounding) from a hypothetical 22B′ (legitimate multi-instance dispatch — e.g., parallel
grepacross different paths where each call has different inputs). The fabrication shape has interleaved-result=0 because nothing is actually executing under the hood; the legitimate multi-instance shape has interleaved-result>0 because results arrive incrementally as workers complete. A harness-side detector that surfaces "interleaved-result=0 over N>5 dispatches within a single autoregressive pass" as a stop-and-prompt event would catch the fabrication shape without false-positiving the legitimate parallel-grep case. Worth pinning as a separate detection signal from thetool_use_idpairing approach already shipped in PR #519; the two are complementary rather than overlapping.I think your forensic is also the cleanest way to discriminate Cluster 22 from Cluster 25 (post-execution tool-result delivery failure + compensation cost). Cluster 25 has interleaved-result>0 (results do arrive, but late or partially); Cluster 22 with the 18-zero-interleaved signature is structurally separable. Adding this to the Cluster 22 candidate field guide revision; the "no interleaved result over N>5" detection signal is much sharper than what the guide has today.
@palios-taey — your supervisor-coordinated framing has held up across two more days of independent reports. The worker-retry-with-different-params leak shape (Cluster 25 candidate sibling) is now an entry on the cluster-tracker, and your articulation that the cost lives at the supervisor's coordination boundary — not at any individual vendor — is the single sentence I keep coming back to when distinguishing this cluster from single-vendor cost guides.
Three artifacts shipped today (2026-06-01) for the multi-vendor fleet path; flagging in case useful to your reading list or to anyone else who lands here from a similar fleet shape:
A Japanese long-form paid handbook is shipping into the same pipeline (~18,000 chars at launch, ~11,000 free preview / ~6,500 paid, deeper sections forthcoming on per-vendor cost profiles, orchestrator construction templates, and failure-mode catalogue); will follow up with the live URL once the deployment chain completes. No CTA shape here; it's a parallel articulation in a different distribution layer, and the structural claim is the same as the field guide.
The 14-day measurement window I'm tracking ends 2026-06-14: new multi-vendor fleet filings, gist views, independent reports of the worker-retry coordination leak shape outside this thread. If the cluster has surface area beyond your fleet, the window should surface independent confirmations; if it doesn't, the cluster is single-operator scope and the articulation retires into the single-operator-handbook category rather than the cluster-handbook category. Either result is useful structural information for the next cycle.
The 4× fan-out you're hitting is one of the nastier failure modes in multi-agent orchestration — the model degenerates on a single non-yielding turn, the harness has no dedup layer, and suddenly you've got 24 agents burning tokens that were supposed to be 6.
The harness-side dedup you proposed (collapse exact-duplicate
tool_useblocks within a single turn) is the right first backstop. The subtlety worth noting: even near-duplicate prompts (not exact-match) can produce semantically redundant work — the agent descriptions collapsing from full text to truncated form across iterations is the autoregressive decay fingerprint, not just random variation.One pattern that helps at the orchestration level before harness fixes land: an external process that tracks which agent dispatches have already been submitted (by signature hash of type + prompt) and refuses to pass duplicates through to the executor. This works as an out-of-process guard but requires that the orchestration layer be externalizable — which is exactly what's missing in single-process Claude Code today.
Claudeverse (claudeverse.ai) is building this coordination layer — session lifecycle tracking, duplicate dispatch detection, and fan-out caps as first-class primitives — if you want to follow along or kick the tires while the harness fix works through the queue.
Confirming this from a mitigation-experiment angle, with one empirical datapoint
that I think narrows the solution space.
TL;DR: I built
PreToolUsehooks specifically to catch redundant/duplicatetool calls, and they structurally cannot catch the within-turn duplication this
issue describes — which corroborates the OP's "fix must be at the execution
layer" point with evidence rather than argument.
What I tried. After observing repeated bursts of duplicate/equivalent
parallel calls in a single message (literal dupes like
git Xtwice, plusunfiltered
find/lsdumps), I addedPreToolUsehooks to block/warn on them.Two regex-based rules: one blocking duplicated git sub-commands *within a single
command string*, one warning on unfiltered
find/ls.Why it doesn't work for the case in this issue.
PreToolUsefires per toolcall and is blind to its sibling
tool_useblocks in the same assistantmessage. It can catch an intra-command duplicate (
git diff … git diffinsideone Bash string), but it has no visibility into the cross-call case — N identical
tool_useblocks emitted together, or the same batch re-emitted within onenon-yielding turn (your 6→24). The hook sees each block in isolation, with no
shared state across the batch, so the dominant failure mode passes straight
through. This matches the OP exactly: the reliable fix is at the layer that turns
emitted blocks into executed work, not at a per-call gate (and not at a prompt
rule — I had a passive "don't batch duplicates" instruction loaded *and cited in
the same turn* it failed).
What partially works. The only layer with batch-level visibility that a user
can reach today is a post-hoc one — a
Stop-event hook reading the turntranscript to detect identical/equivalent tool-call signatures emitted in the
same turn and surface a blocking reminder. It doesn't prevent the first wasted
emission, but it closes the feedback loop actively instead of passively.
On the harness fixes proposed here. The within-turn dedup of identical
parallel
tool_useblocks (your fix #1) and a soft fan-out cap (#2) are the onlythings that would have caught my cases — both sit at exactly the layer a
per-call hook can't reach.
Release note for triagers: v2.1.161 addressed the adjacent *cancellation-
cascade* axis (a failed Bash call no longer cancels its sibling parallel calls),
but that's orthogonal to this issue — it changes failure propagation within a
batch, not the re-emission/duplication of the batch itself. This one is still
open and uncovered.
(Environment note: observed on Opus-class models with extended thinking. I can't
make a cross-model causal claim from inside a session, so I'm not asserting one —
just adding it as a consistent data point alongside the other reports here.)
Additional case: non-parallel sequential tool calls also duplicated
I'm hitting the same class of bug, but with sequential (non-parallel) tool calls — not just subagent fan-out.
What happened
During a session managing skills (), the following tool calls were silently duplicated:
These were all sequential tool calls within a single assistant turn, not parallel subagent dispatches.
Environment
Key observations
Relation to this issue
This confirms the pattern described here: the harness executes every emitted block without deduplication. The difference is that in my case, the repetition happens across sequential tool calls (model re-emits the same call after receiving the result), not just within a single parallel batch.
This may also be related to #41750 (context management triggering re-emission).
Suggested fix alignment
Strong +1 on the suggested fixes above. Additionally:
Closing for now — inactive for too long. Please open a new issue if this is still relevant.