fix: fork subagents recursed forever in the Claude Code export

A fork subagent's transcript opens with a copy of the parent turn that spawned it, its own Agent call included; folding that call re-entered the same fork until RecursionError, failing every daily sync since 2026-09-24. Skip a spawn call while its own subagent is being expanded, and strip the <fork-boilerplate> preamble.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbnmGHnFqDjyhPcCg1SEfF
This commit is contained in:
JesseMarkowitz
2026-10-05 07:14:16 -04:00
co-authored by Claude Opus 5.5
parent b6ce636891
commit 64068bb19b
3 changed files with 74 additions and 1 deletions
+15 -1
View File
@@ -118,7 +118,10 @@ _HARNESS_TAG_RE = re.compile(
r"|<command-args>.*?</command-args>"
r"|<command-contents>.*?</command-contents>"
r"|<local-command-stdout>.*?</local-command-stdout>"
r"|<system-reminder>.*?</system-reminder>",
r"|<system-reminder>.*?</system-reminder>"
# A fork subagent's first user turn: generic worker rules ahead of the
# fork's actual "Your directive: …", which is kept.
r"|<fork-boilerplate>.*?</fork-boilerplate>",
re.DOTALL,
)
@@ -530,6 +533,7 @@ def _extract_messages(
policy: str,
subagents: dict | None = None,
include_sidechain: bool = False,
expanding: frozenset[str] = frozenset(),
) -> list[dict]:
"""Normalize Claude Code records into messages.
@@ -540,6 +544,13 @@ def _extract_messages(
those recursive subagent passes (subagent records are flagged
``isSidechain``); the top-level pass keeps skipping sidechain records so a
subagent is never also emitted as a stray top-level turn.
``expanding`` holds the spawn ids of the subagents being folded around
this pass. A ``fork`` subagent's transcript opens with a copy of the parent
turn that spawned it, its own spawn call included; that copy is dropped,
since the enclosing subagent block already stands for it. Expanding it
again recursed without end (RecursionError, every daily sync from
2026-09-24).
"""
subagents = subagents or {}
messages: list[dict] = []
@@ -620,6 +631,8 @@ def _extract_messages(
elif item_type == "tool_use":
name = item.get("name") or "tool"
tool_id = item.get("id")
if tool_id in expanding:
continue
if name in _SUBAGENT_TOOL_NAMES and tool_id in subagents:
# A Task/Agent spawn: fold its separate transcript inline
# instead of collapsing it. Its own tool traffic is
@@ -633,6 +646,7 @@ def _extract_messages(
policy,
subagents,
include_sidechain=True,
expanding=expanding | {tool_id},
)
blocks.append(
make_subagent_block(