Workflow tool: dynamic-workflow args payload corruption causes 11→183 agent spawn multiplication, ~10M tokens burned in <10min; TaskStop doesn't clear in-flight state in UI
Summary
Using the Workflow tool's dynamic-workflow feature (a JS script run via agent()/parallel()), a moderately-sized args payload (~105 JSON objects, ~13KB) appears to have been corrupted somewhere between the tool call and the running script, causing individual agent() calls to receive tiny malformed text fragments instead of their intended slice of args. This seems to have triggered some kind of retry/multiplication behavior: only 11 agent() calls were requested (via parallel() over 11 batches), but 183 actual sub-agent processes were spawned. The result was an estimated ~7-10M tokens consumed in under 10 minutes before the user noticed via the cost/usage indicator and asked me to stop.
Additionally, after calling TaskStop on the parent workflow task (which succeeded — the task ID became unrecoverable via TaskOutput/TaskStop immediately after), 5 sub-agents remained visible in the run's journal.jsonl as started with no matching result event. Their individual transcripts show [Request interrupted by user] as the last message, confirming they were actually interrupted at the process level. However, the user-facing mobile app's "Background tasks" panel continued to show 2 of these as "Running" with a climbing elapsed timer, and tapping the in-app stop button did not change their displayed state. I was able to confirm via file-level evidence (agent transcript mtime, static agent-file count, journal.jsonl not growing for 7+ minutes) that no actual activity was occurring — so this looks like a UI/state-sync bug on top of the underlying spawn bug, not continued real execution.
Environment
claude --version: 2.1.204 (Claude Code)- Platform: macOS (Darwin)
- Feature:
Workflowtool, dynamic workflow (script passed viascriptPath, largeargsarray)
Reproduction shape
export const meta = {
name: 'open-issues-fleet',
description: '...',
phases: [{ title: 'Triage' }, { title: 'Implement' }],
}
const issueRefs = args // array of ~105 objects: {repo, number, title}
phase('Triage')
const BATCH_SIZE = 10
const batches = []
for (let i = 0; i < issueRefs.length; i += BATCH_SIZE) {
batches.push(issueRefs.slice(i, i + BATCH_SIZE))
}
const triageResults = await parallel(
batches.map((batch, idx) => () =>
agent(TRIAGE_PROMPT_HEADER + '\n\n' + JSON.stringify(batch) + '\n\nReturn via the schema.', {
schema: TRIAGE_SCHEMA,
phase: 'Triage',
label: `triage-batch-${idx}`,
})
)
)
args was passed to the Workflow tool call as a literal JSON array (~105 small objects, total ~13KB serialized). Expected: 11 agent() calls (one per batches entry via parallel()). Observed: 183 agent processes spawned for this run (per journal.jsonl started event count), most returning immediately with a StructuredOutput of {results: []} after reporting (in their own transcript text) that the prompt they received was truncated/malformed — e.g. one agent's received prompt tail was:
ISSUES TO FETCH AND TRIAGE (repo/number/title):
"itle\": \"[s"
another agent's:
ISSUES TO FETCH AND TRIAGE (repo/number/title):
"teryMobile"
i.e., instead of receiving its ~10-object JSON slice, the agent received a handful of characters that look like an arbitrary mid-string cut of the original JSON — consistent with the batch array (or the larger args binding it was sliced from) being truncated/corrupted somewhere in the pipeline between the Workflow tool call and the script's agent() invocation.
Observed symptoms (numbered for triage)
- Payload corruption:
agent()prompts built fromJSON.stringify(batch)(wherebatchis a slice of theargs-derived array) arrived at the sub-agent as tiny, arbitrarily-truncated string fragments rather than the full intended text. - Spawn multiplication: 11 requested
agent()calls (oneparallel()over 11 batches) resulted in 183 actual agent-transcript files for this run — a ~16.6x multiplication. I do not know the internal mechanism (retry-on-malformed-input storm? duplicate dispatch?) but the effect was a large, unexpected token burn with no corresponding increase in useful work (most of the 183 returned trivial/empty results). - TaskStop doesn't fully reach in-flight sub-agents:
TaskStopon the parent workflow's task ID succeeded (task became unrecoverable viaTaskOutput), butjournal.jsonlstill showed 5 agent IDsstartedwith noresultevent afterward, and those specific agent IDs were not recognized by a follow-upTaskStopcall ("No task found with ID: <agentId>"). Their own transcripts do show they were interrupted ([Request interrupted by user]), so the actual interruption happened, but there's no supported way to address these in-flight sub-agents directly by ID once the parent task handle is gone. - UI does not reflect the interrupted state: The user's mobile app "Background tasks" panel continued showing 2 of these tasks as "Running" with an increasing elapsed-time counter well after they were actually interrupted (confirmed via unchanged file mtimes for 7+ minutes), and tapping the in-app stop (⏹) button next to them produced no visible change.
Impact
Estimated 7-10M tokens consumed in under 10 minutes, per the user's own real-time cost/usage indicator, for a workflow run that produced essentially no useful output (most spawned agents received corrupted input and returned empty results). This is a severe, unexpected-cost bug class: a large-but-not-unreasonable args payload (~13KB, ~105 small objects) combined with a parallel()-driven agent() fan-out should not be able to multiply into 183 spawns or burn tokens at this rate from a script that only requested 11 concurrent calls.
Ask
- Investigate how the
argsparameter is transported into a running dynamic-workflow script for payloads in the ~10-15KB range — whether truncation/corruption can occur in that path. - Investigate why 11 requested
agent()calls (via oneparallel()fan-out) resulted in 183 actual sub-agent processes for this run — whether there's a retry-storm or duplicate-dispatch condition, particularly when agents receive malformed/empty-looking input. - Fix
TaskStop/state-sync so that once a parent workflow task is stopped, any still-in-flight sub-agents are (a) addressable/stoppable by their own agent ID if not already terminated, and (b) reliably reflected as stopped in both API-level task queries and the mobile app's "Background tasks" UI (including making the in-app stop button actually effective, or at minimum making the UI correctly reconcile to "stopped" once the underlying process has actually terminated).
Happy to share the full journal.jsonl / agent transcripts from this run privately if useful for repro — did not attach here as they may contain project-specific content.