fork_session drops transcript entries containing U+2028/U+2029/U+0085 (str.splitlines), breaking the parentUuid chain

オープン 初心者向け
#1,441 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 2 日以内に返信

@mangeshraut712 がすでに取り組んでいます。

2026年10月25日 から。

  • #1442 @shasudo による — マージされずにクローズ
  • #1445 @mangeshraut712 による — オープン
  • #1452 @farhan629 による — オープン
  • #1495 @Sathwik-giddi による — オープン

評価

難易度
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 の本文から書いたものです。

説明

bug good first 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 を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

anthropics/claude-agent-sdk-python のほかの issue

anthropics/claude-agent-sdk-python の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。