99from typing import Any , cast , get_args
1010
1111from models .record_data import RecordDataUnion
12- from models .session import MessageDict , RoleLiteral , SessionDict , SessionMetadataDict , ToolUseDict
12+ from models .session import (
13+ SESSION_METADATA_FIELD_NAMES ,
14+ MessageDict ,
15+ RoleLiteral ,
16+ SessionDict ,
17+ SessionMetadataDict ,
18+ ToolUseDict ,
19+ )
1320from models .tool_results import ToolResultUnion , is_tool_result_dict
1421from utils .jsonl_helpers import (
1522 entry_message as _entry_message ,
@@ -70,48 +77,15 @@ def _safe_int(val: Any) -> int:
7077 return 0
7178
7279
73- def _finalize_session_metadata (raw : dict [str , Any ]) -> SessionMetadataDict :
74- """Convert the mutable parse-time metadata builder into a SessionMetadataDict."""
75- return {
76- "session_id" : raw ["session_id" ],
77- "models_used" : sorted (raw ["models_used" ]),
78- "first_timestamp" : raw ["first_timestamp" ],
79- "last_timestamp" : raw ["last_timestamp" ],
80- "total_input_tokens" : raw ["total_input_tokens" ],
81- "total_output_tokens" : raw ["total_output_tokens" ],
82- "total_cache_read_tokens" : raw ["total_cache_read_tokens" ],
83- "total_cache_creation_tokens" : raw ["total_cache_creation_tokens" ],
84- "total_tool_calls" : raw ["total_tool_calls" ],
85- "tool_call_counts" : raw ["tool_call_counts" ],
86- "version" : raw ["version" ],
87- "cwd" : raw ["cwd" ],
88- "git_branch" : raw ["git_branch" ],
89- "permission_mode" : raw ["permission_mode" ],
90- "compactions" : raw ["compactions" ],
91- "total_ephemeral_5m_tokens" : raw ["total_ephemeral_5m_tokens" ],
92- "total_ephemeral_1h_tokens" : raw ["total_ephemeral_1h_tokens" ],
93- "service_tiers" : sorted (raw ["service_tiers" ]),
94- "session_wall_time_seconds" : raw ["session_wall_time_seconds" ],
95- "compact_boundaries" : raw ["compact_boundaries" ],
96- "api_errors" : raw ["api_errors" ],
97- "files_read" : sorted (raw ["files_read" ]),
98- "files_written" : sorted (raw ["files_written" ]),
99- "files_created" : sorted (raw ["files_created" ]),
100- "bash_commands" : raw ["bash_commands" ],
101- "web_fetches" : raw ["web_fetches" ],
102- "sidechain_messages" : raw ["sidechain_messages" ],
103- "stop_reasons" : raw ["stop_reasons" ],
104- "entry_counts" : raw ["entry_counts" ],
105- }
80+ # Set-backed fields are sorted when finalized; all other builder keys pass through as-is.
81+ _METADATA_SET_FIELDS = frozenset (
82+ {"models_used" , "service_tiers" , "files_read" , "files_written" , "files_created" }
83+ )
10684
10785
108- def parse_session (filepath : str ) -> SessionDict :
109- """Main entry point. Reads every line from a .jsonl file and builds up
110- a session dict with messages, metadata (tokens, models, tool counts),
111- and file/command activity."""
112- session_id = os .path .basename (filepath ).replace (".jsonl" , "" )
113- messages : list [MessageDict ] = []
114- metadata : dict [str , Any ] = {
86+ def _new_session_metadata_builder (session_id : str ) -> dict [str , Any ]:
87+ """Fresh mutable metadata accumulator for ``parse_session()``."""
88+ return {
11589 "session_id" : session_id ,
11690 "models_used" : set (),
11791 "total_input_tokens" : 0 ,
@@ -127,30 +101,42 @@ def parse_session(filepath: str) -> SessionDict:
127101 "git_branch" : None ,
128102 "permission_mode" : None ,
129103 "compactions" : 0 ,
130- # Extended token accounting
131104 "total_ephemeral_5m_tokens" : 0 ,
132105 "total_ephemeral_1h_tokens" : 0 ,
133106 "service_tiers" : set (),
134- # Timing
135107 "session_wall_time_seconds" : None ,
136- # Compaction details
137108 "compact_boundaries" : [],
138- # Error tracking
139109 "api_errors" : 0 ,
140- # File activity (from tool_use inputs)
141110 "files_read" : set (),
142111 "files_written" : set (),
143112 "files_created" : set (),
144113 "bash_commands" : [],
145114 "web_fetches" : [],
146- # Sidechain tracking
147115 "sidechain_messages" : 0 ,
148- # Stop reasons
149116 "stop_reasons" : {},
150- # Entry type counts
151117 "entry_counts" : {},
152118 }
153119
120+
121+ def _finalize_session_metadata (raw : dict [str , Any ]) -> SessionMetadataDict :
122+ """Convert the mutable parse-time metadata builder into a SessionMetadataDict."""
123+ finalized : dict [str , Any ] = {}
124+ for key in SESSION_METADATA_FIELD_NAMES :
125+ val = raw [key ]
126+ if key in _METADATA_SET_FIELDS :
127+ val = sorted (val )
128+ finalized [key ] = val
129+ return cast (SessionMetadataDict , finalized )
130+
131+
132+ def parse_session (filepath : str ) -> SessionDict :
133+ """Main entry point. Reads every line from a .jsonl file and builds up
134+ a session dict with messages, metadata (tokens, models, tool counts),
135+ and file/command activity."""
136+ session_id = os .path .basename (filepath ).replace (".jsonl" , "" )
137+ messages : list [MessageDict ] = []
138+ metadata = _new_session_metadata_builder (session_id )
139+
154140 with open (filepath , "r" , encoding = "utf-8" , errors = "replace" ) as f :
155141 for line in f :
156142 line = line .strip ()
@@ -172,7 +158,7 @@ def parse_session(filepath: str) -> SessionDict:
172158 if isinstance (snap , dict ):
173159 ts = snap .get ("timestamp" )
174160
175- if ts :
161+ if isinstance ( ts , str ) and ts :
176162 if metadata ["first_timestamp" ] is None :
177163 metadata ["first_timestamp" ] = ts
178164 metadata ["last_timestamp" ] = ts
0 commit comments