Skip to content

fix(sessions): pick the last JSON field match by position, not per pattern - #1208

Open
chintan-diwakar wants to merge 1 commit into
anthropics:mainfrom
chintan-diwakar:fix/last-json-field-position
Open

fix(sessions): pick the last JSON field match by position, not per pattern#1208
chintan-diwakar wants to merge 1 commit into
anthropics:mainfrom
chintan-diwakar:fix/last-json-field-position

Conversation

@chintan-diwakar

@chintan-diwakar chintan-diwakar commented Aug 15, 2026

Copy link
Copy Markdown

Summary

_extract_last_json_string_field (_internal/sessions.py:229) scans the two accepted serializations one after the other, "key":" then "key": ", and lets the second pattern's last match overwrite the first's unconditionally. So it returns the last match per pattern, not the last match by position, contradicting its own docstring ("finds the LAST occurrence").

When a transcript holds both spacings, an earlier "key": "value" beats a later "key":"value". The user-visible result is that rename_session() is silently ignored: the new title is physically the last line of the file, and get_session_info() / list_sessions() keep returning the old one permanently.

Both spacings occur in practice. rename_session() appends compact JSON (session_mutations.py:98 uses separators=(",", ":")), while anything that serialized the transcript with a bare json.dumps(entry) writes the spaced form, since Python's defaults are ", " / ": ". This repo's own fixtures do exactly that at tests/test_sessions.py:88.

>>> _extract_last_json_string_field('{"customTitle": "old"}\n{"customTitle":"new"}', "customTitle")
'old'      # expected 'new'

_parse_session_info_from_lite routes customTitle, aiTitle, lastPrompt, summary, gitBranch and tag through this helper, so tag_session() and the last-prompt, summary and branch fields go stale the same way.

Fix

  • src/claude_agent_sdk/_internal/sessions.py: track the winning match's start index and only overwrite when a later one is found. Four lines. Scanning, escape handling and the truncated-line break are unchanged, so files using a single spacing behave exactly as before.
  • tests/test_sessions.py: test_extract_last_json_string_field_mixed_spacing covers both orderings at the unit level.
  • tests/test_session_mutations.py: test_rename_wins_over_earlier_spaced_title is the end-to-end regression, renaming over a pre-existing spaced title and asserting list_sessions() reports the rename.

Tests

  • Both new tests fail on main and pass with the fix (2 failed, 152 passed with sessions.py reverted).
  • Full suite: python -m pytest tests/: 1368 passed, 5 skipped, same baseline as main.
  • python -m ruff check src/ tests/, ruff format --check and python -m mypy src/: clean.

A public-API reproduction, using only rename_session, get_session_info, list_sessions and project_key_for_directory, is in the first comment below.

Note on the sibling helper

_extract_json_string_field (sessions.py:205) has the mirrored flaw: it documents "returns the first match" but tries the compact pattern to completion first, so a later compact match can beat an earlier spaced one. It feeds created_at, cwd and the gitBranch head fallback. I left it out to keep this to one defect with one demonstrated symptom, and am happy to fold in the same-shape fix if you would prefer both together.

Used AI assistance; reviewed and tested by me.

@chintan-diwakar chintan-diwakar changed the title Fix rename_session being silently ignored on mixed-spacing transcripts fix(sessions): pick the last JSON field match by position, not per pattern Aug 15, 2026
@chintan-diwakar

Copy link
Copy Markdown
Author

Public-API reproduction. Uses only rename_session, get_session_info, list_sessions and project_key_for_directory, plus CLAUDE_CONFIG_DIR.

import json, os, tempfile
from pathlib import Path

tmp = tempfile.mkdtemp(prefix="sdk-repro-")
config_dir = Path(tmp) / "claude-config"
project = Path(tmp) / "myproject"
project.mkdir()
os.environ["CLAUDE_CONFIG_DIR"] = str(config_dir)

from claude_agent_sdk import (  # env var must be set first
    get_session_info, list_sessions, project_key_for_directory, rename_session,
)

sid = "550e8400-e29b-41d4-a716-446655440000"

# A host tool writes the transcript with plain json.dumps -> SPACED form.
entries = [
    {"type": "user", "uuid": "u1", "sessionId": sid,
     "timestamp": "2026-08-01T10:00:00Z", "cwd": str(project),
     "message": {"role": "user", "content": "help me refactor the parser"}},
    {"type": "custom-title", "customTitle": "Imported Title", "sessionId": sid},
]
f = config_dir / "projects" / project_key_for_directory(project) / f"{sid}.jsonl"
f.parent.mkdir(parents=True)
f.write_text("".join(json.dumps(e) + "\n" for e in entries))

# The user renames through the SDK. This appends a COMPACT line at EOF.
rename_session(sid, "Renamed Title", directory=str(project))

print("last line of file:", f.read_text().rstrip().rsplit("\n", 1)[-1])
info = get_session_info(sid, directory=str(project))
print("get_session_info custom_title:", info.custom_title)
print("list_sessions[0] summary:     ",
      list_sessions(directory=str(project), include_worktrees=False)[0].summary)

On main:

last line of file: {"type":"custom-title","customTitle":"Renamed Title","sessionId":"550e8400-e29b-41d4-a716-446655440000"}
get_session_info custom_title: Imported Title
list_sessions[0] summary:      Imported Title

With this PR:

last line of file: {"type":"custom-title","customTitle":"Renamed Title","sessionId":"550e8400-e29b-41d4-a716-446655440000"}
get_session_info custom_title: Renamed Title
list_sessions[0] summary:      Renamed Title

…ttern

_extract_last_json_string_field scans the two accepted serializations one
after the other ('"key":"' then '"key": "'). The second pattern's last match
overwrote the first's unconditionally, so in a transcript that contains both
spacings an EARLIER spaced-form value beat a LATER compact-form one, which
contradicts the function's documented "finds the LAST occurrence" contract.

User-visible effect: rename_session() and tag_session() append compact JSON,
but a transcript written by a host tool with a bare json.dumps carries the
spaced form. When both are present the rename is silently ignored and
get_session_info()/list_sessions() keep reporting the stale title forever.
lastPrompt, summary and gitBranch go stale the same way.

Track the winning match's index and only overwrite when the new match starts
later in the text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chintan-diwakar
chintan-diwakar force-pushed the fix/last-json-field-position branch from f3cee4f to e365f7c Compare August 15, 2026 08:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant