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

Đang mở Phù hợp với người mới
#1,441 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 2 ngày

@mangeshraut712 đang làm issue này rồi.

Từ ngày 25/10/2026.

  • #1442 của @shasudo — đã đóng, không merge
  • #1445 của @mangeshraut712 — đang mở
  • #1452 của @farhan629 — đang mở
  • #1495 của @Sathwik-giddi — đang mở

Đánh giá

Độ khó
2/5
Thời gian dự kiến
1-3 giờ
Mức phù hợp với người mới
78/100
Loại issue
Lỗi
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
python

Hướng nghiên cứu

Bắt đầu tại src/claude_agent_sdk/_internal/session_mutations.py, ở _parse_fork_transcript quanh dòng 601, sau đó kiểm tra các bài kiểm thử bao quát việc phân tích cú pháp bản ghi hội thoại của fork_session. Tái hiện lỗi bằng một bản ghi JSONL chứa U+2028, U+2029 hoặc U+0085 và kiểm tra xem các mục tiếp theo có giữ nguyên parentUuid hay không. Công việc được xem là hoàn tất khi tất cả bản ghi đều được phân tích cú pháp và fork giữ nguyên chuỗi cha; hãy chạy các bài kiểm thử liên quan.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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
Ngôn ngữ chính
Python
Star
8.5k
Fork
1.4k
Merge trung bình
3 ngày 10 giờ
Pull request đã merge (30 ngày)
13

Chuẩn bị môi trường

Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của anthropics/claude-agent-sdk-python

Tất cả issue của anthropics/claude-agent-sdk-python

Issue tương tự

Thêm issue về Python

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.