fork_session drops transcript entries containing U+2028/U+2029/U+0085 (str.splitlines), breaking the parentUuid chain
メンテナーはふだん 2 日以内に返信
@mangeshraut712 がすでに取り組んでいます。
2026年10月25日 から。
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 78/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- python
調査の方向性
src/claude_agent_sdk/_internal/session_mutations.py の601行目あたりにある _parse_fork_transcript から始め、その後、fork_session のトランスクリプト解析を対象とするテストを確認してください。U+2028、U+2029、またはU+0085を含むJSONLレコードで再現し、後続のエントリがparentUuidを保持することを確認してください。すべてのレコードが解析され、forkが親の連鎖を保持できれば完了です。関連するテストを実行してください。
索引モデルが issue の本文から書いたものです。
説明
Summary
fork_session silently drops transcript entries whose JSON contains U+2028 (LINE SEPARATOR), U+2029 (PARAGRAPH SEPARATOR) or U+0085 (NEL). The entry after a dropped one loses its parent (parentUuid becomes None), so the forked session's chain breaks there, and resuming the fork loads only the part after the break.
Cause
_parse_fork_transcript splits the file with str.splitlines() (_internal/session_mutations.py line 601 on main):
for line in content.decode("utf-8", errors="replace").splitlines():
str.splitlines() treats U+2028, U+2029 and U+0085 as line boundaries. Claude Code writes the JSONL with JavaScript's JSON.stringify, which leaves these characters unescaped inside strings (that is valid JSON). So a single entry containing one of them is cut into pieces, each piece fails json.loads, and the except branch skips them without a trace. _build_fork_lines then cannot resolve the next entry's parent and writes parentUuid: None.
Splitting on "\n" only (the record separator Claude Code actually writes) avoids this. An equivalent fix is to escape the three characters to \uXXXX before parsing: inside JSON they can only occur in strings, so the meaning is unchanged.
Reproduction
import json, os, tempfile, uuid
from pathlib import Path
from claude_agent_sdk import fork_session
home = Path(tempfile.mkdtemp())
os.environ["CLAUDE_CONFIG_DIR"] = str(home)
project = home / "projects" / "-w"
project.mkdir(parents=True)
session, ids = str(uuid.uuid4()), [str(uuid.uuid4()) for _ in range(3)]
texts = ["first", "a\u2028b", "third"] # the middle message contains U+2028 LINE SEPARATOR
records = [
{"type": "user" if i % 2 == 0 else "assistant", "uuid": ids[i], "parentUuid": ids[i - 1] if i else None,
"sessionId": session, "cwd": "/w",
"message": {"role": "user" if i % 2 == 0 else "assistant", "content": [{"type": "text", "text": t}]}}
for i, t in enumerate(texts)
]
# Claude Code writes JSONL with JS JSON.stringify, which leaves U+2028 unescaped; ensure_ascii=False does the same here.
(project / f"{session}.jsonl").write_text("".join(json.dumps(r, ensure_ascii=False) + "\n" for r in records), encoding="utf-8")
forked = fork_session(session, directory="/w").session_id
copied = [json.loads(l) for l in (project / f"{forked}.jsonl").read_text(encoding="utf-8").split("\n") if l.strip()]
messages = [r for r in copied if r.get("type") in ("user", "assistant")]
print("source messages:", len(records))
print("forked messages:", len(messages), [r["message"]["content"][0]["text"] for r in messages])
print("parentUuid of the last one:", messages[-1]["parentUuid"])
Output with claude-agent-sdk 0.2.168 (main still has the same line; latest release is v0.2.174):
source messages: 3
forked messages: 2 ['first', 'third']
parentUuid of the last one: None
Impact
We fork live sessions with fork_session(..., up_to_message_id=...) and resume the fork through ClaudeSDKClient so that it continues with the full history. One source session contained a single tool call whose input had a raw U+2028. After forking, that entry was missing, the following tool result had parentUuid: None, and the resumed fork's first request carried 27 messages instead of the full conversation (cache read 0). Nothing reported an error.
Environment
- claude-agent-sdk 0.2.168 (Python 3.13), Claude Code CLI 2.1.310, Linux
- 主要言語
- Python
- スター
- 8.5k
- フォーク
- 1.4k
- 平均マージ
- 3日 10時間
- マージ済み PR(30日)
- 13
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
anthropics/claude-agent-sdk-python のほかの issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
anthropics/claude-agent-sdk-python#1509 ·
メンテナーはふだん 2 日以内に返信
-
`parse_message` raises raw `TypeError` instead of `MessageParseError` on non-dict nested payloadsオープンbug good first issue
難易度 2/5 半日 初心者へのやさしさ 76/100
anthropics/claude-agent-sdk-python#1508 ·
メンテナーはふだん 2 日以内に返信
-
bug good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
anthropics/claude-agent-sdk-python#1507 ·
メンテナーはふだん 2 日以内に返信
-
`fork_session` hangs forever on a cyclic `parentUuid` chain (no cycle guard in `_build_fork_lines`)オープンbug good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
anthropics/claude-agent-sdk-python#1506 ·
メンテナーはふだん 2 日以内に返信
-
ClaudeSDKClient.connect() with an async iterable closes stdin, so follow-up query() fails対応中かも @m4xkushnir が 19 日前に担当しました。 オープンbug
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
anthropics/claude-agent-sdk-python#1443 ·
メンテナーはふだん 2 日以内に返信
anthropics/claude-agent-sdk-python の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
dbt-labs/spark-utils#51 ·
-
bug pydanty:is-working
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
pydantic/pydantic-ai#13444 ·
メンテナーはふだん 1 日以内に返信
-
good first issue help wanted
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
-
good first issue help wanted
難易度 1/5 1時間未満 初心者へのやさしさ 72/100
-
changelog investigate
難易度 1/5 1時間未満 初心者へのやさしさ 68/100
ramnes/notion-sdk-py#502 ·