feat: add tag/created_at to SDKSessionInfo + get_session_info#667
feat: add tag/created_at to SDKSessionInfo + get_session_info#667
Conversation
…n_info
Ports TS SDK changes from qing/sdk-session-info-expand (PR #21659).
- Add tag and agent_name fields to SDKSessionInfo dataclass
- Refactor inline field extraction into _parse_session_info_from_lite
helper for reuse between list_sessions and get_session_info
- Extract tag from tail via last-occurrence scan (empty string = cleared)
- Extract agent_name scoped to {type:'agent-name'} entries only; bare
tail-scan would pick up per-message agentName from swarm sessions
(handles both compact and spaced JSON variants)
- Add get_session_info(session_id, directory=None) for single-session
metadata lookup without O(n) directory scan; includes worktree fallback
when directory is provided (matches get_session_messages semantics)
- Export get_session_info from package root
Tests: 75 passed. Ruff + mypy clean.
Extracts creation timestamp from first entry's ISO timestamp field in the head buffer. Returns epoch ms for consistency with last_modified. More reliable than stat().birthtime which is unsupported on some filesystems. Python 3.10 compatibility: datetime.fromisoformat() in 3.10 doesn't support trailing 'Z', so we replace it with '+00:00' before parsing. Mirrors claude-cli-internal commit 2470efe469. Tests: 288 passed. Ruff + mypy clean.
agent_name is tied to feature-gated swarm mode — not a general SDK concept. Removing to keep SDKSessionInfo focused on universal metadata. Mirrors TS change in claude-cli-internal#21659. Tests: 286 passed. Ruff + mypy clean.
file_size is a local-JSONL concept with no remote-storage equivalent. Making optional now for forward-compat with pluggable storage backends. Local _parse_session_info_from_lite still populates it from fstat size; this is a type loosening only. Existing tests pass unchanged. Mirrors claude-cli-internal PR #21659.
…-expand-py # Conflicts: # src/claude_agent_sdk/__init__.py
There was a problem hiding this comment.
LGTM — clean TS port with comprehensive tests; the file_size optionality change and new get_session_info API follow established patterns.
Extended reasoning...
Overview
This PR adds two new fields (tag, created_at) to the SDKSessionInfo dataclass, makes file_size optional (int | None), introduces a new public get_session_info() function for single-session lookup, and extracts a shared _parse_session_info_from_lite() helper. Four files are modified: the types module, sessions implementation, public __init__.py exports, and tests.
Security risks
None. All changes involve read-only local file operations (reading JSONL session files from the user's own config directory). No auth, network, or injection surfaces are affected.
Level of scrutiny
Medium. While this adds a new public API function and modifies a public dataclass, the changes are a direct port from an already-reviewed TypeScript PR. The new get_session_info() function mirrors the existing get_session_messages() pattern almost exactly (same directory resolution, worktree fallback, UUID validation). The _parse_session_info_from_lite() extraction is a clean refactor — the old inline code in _read_sessions_from_dir is replaced by a call to the new helper with no behavioral changes. No CODEOWNERS file exists.
Other factors
- Test coverage: 20 new tests across three test classes (
TestTagExtraction,TestCreatedAtExtraction,TestGetSessionInfo) covering edge cases like empty tags, invalid timestamps, sidechain filtering, worktree fallback, and cross-project search. - Breaking change:
file_size: int→file_size: int | None = Noneis technically a type-level breaking change, but it's backward-compatible at runtime since it has a default value, and existing local JSONL storage still populates it. - Python 3.10 compat: The
Z→+00:00replacement fordatetime.fromisoformat()is a known workaround and is correctly applied.
Four fixes from a 43-agent adversarial review against the TS reference
(claude-cli-internal src/utils/listSessionsImpl.ts @ main):
1. Tag extraction scoped to {"type":"tag"} lines (sessions.py:455)
Bare _extract_last_json_string_field(tail, "tag") matches ANY "tag":"..."
in the 64KB tail — including tool_use inputs from Docker builds, git tag
via MCP, cloud resource tagging. Proven: test_tag_none_when_only_tool_use_tag
returned tag='prod' from tool input before the fix.
TS scopes to lines starting with '{"type":"tag"' at column 0
(listSessionsImpl.ts:132, sessionStorage.ts:629 with the canonical
comment about this exact collision class).
Tests now use separators=(",", ":") to match real on-disk format
(CLI jsonStringify, SDK tag_session, TS sessionMutationsImpl all write
compact JSON). 2 regression tests added.
2. custom_title chain: + head scan, + aiTitle fallback (sessions.py:426)
TS chain is tail.customTitle || head.customTitle || tail.aiTitle ||
head.aiTitle (listSessionsImpl.ts:97-102). Pre-existing drift from TS
PRs #21333 (aiTitle) and #20390 landing after Python #622 — fixing here
since this PR touches the exact block.
3. summary chain: + lastPrompt fallback (sessions.py:436)
TS inserts tail.lastPrompt between custom_title and summary
(listSessionsImpl.ts:115-119). Same drift class as (2).
4. created_at: float -> int (types.py:998, sessions.py:465)
last_modified is int; both are epoch-ms. Wrapped
datetime.timestamp() * 1000 in int().
|
Pushed
Also retitled (dropped ruff ✓ · mypy ✓ · 81/81 pass |
- Add is_dir() filter to get_session_info's projects_dir scan, matching _list_all_sessions/_list_sessions_for_project - Fix float literal in test_created_at_without_z_suffix (created_at is int | None)
Change _extract_json_string_field(head, "timestamp") to use first_line instead of the entire 64KB head buffer, avoiding false matches from later entries.
There was a problem hiding this comment.
All previous nits addressed; clean TS port with good test coverage.
Extended reasoning...
Overview
This PR adds tag and created_at fields to SDKSessionInfo, introduces get_session_info() for single-session metadata lookup, extracts _parse_session_info_from_lite() as a shared helper, and makes file_size optional. It touches 4 files: __init__.py (exports), sessions.py (core logic), types.py (dataclass), and test_sessions.py (20+ new tests).
Security risks
None. The code reads local JSONL session files and extracts metadata fields. No auth, crypto, network, or user-input-to-execution paths are involved.
Level of scrutiny
This is a TS port with clear scope. The design decisions (field naming, fallback chains, tag scoping) were already vetted in the TypeScript codebase. All four nits from my previous two review rounds have been addressed: is_dir() filter added, timestamp scoped to first_line, custom_title docstring updated, and test assertion fixed from float to int literal.
Other factors
- No bugs found by the bug hunting system.
- The
file_sizetype change (int→int | None) is technically breaking but well-motivated (forward-compat for remote backends) and backwards-compatible in practice (defaults toNone). - New
get_session_info()follows the exact pattern of existingget_session_messages()including worktree fallback. - Comprehensive test coverage: tag extraction (7 tests including tool_use isolation), created_at (6 tests including edge cases), and get_session_info (9 tests).
- The author performed an adversarial review against the TS implementation and fixed 4 TS-parity gaps before my reviews.
Mirrors TypeScript PR claude-cli-internal#21659.
Changes
SDKSessionInfodataclass +=tag: str | None,created_at: float | Nonelist_sessions()now extractstagfrom session tail (last occurrence wins; empty string → cleared)get_session_info(session_id, directory=None) -> SDKSessionInfo | None— single-session metadata lookup without O(n) directory scan_parse_session_info_from_lite()shared helper (matches TSparseSessionInfoFromLite)SDKSessionInfo.file_sizeis nowint | None(wasint) — forward-compat for pluggable storage backends where file size isn't meaningful. Local JSONL storage still populates it.Port notes
get_session_infoincludes worktree fallback per TS fix2b2d7b91f4, matchingget_session_messagessemantics.None(cleared) via existingor Nonepattern.created_atparsed from first entry's ISOtimestampfield → epoch ms. Python 3.10'sdatetime.fromisoformat()doesn't support trailingZ, so we replaceZ→+00:00before parsing. More reliable thanstat().birthtimewhich is unsupported on some filesystems.get_session_info()for single-session metadata lookuptagandcreated_atfields toSDKSessionInfo; makefile_sizeoptionalTests
20 new tests (TestTagExtraction, TestCreatedAtExtraction, TestGetSessionInfo). 286 total pass. Ruff + mypy clean.