1010
1111from models .record_data import RecordDataUnion
1212from models .session import (
13- SESSION_METADATA_FIELD_NAMES ,
1413 MessageDict ,
1514 RoleLiteral ,
1615 SessionDict ,
16+ SessionMetadataBuilderDict ,
1717 SessionMetadataDict ,
1818 ToolUseDict ,
1919)
@@ -77,13 +77,7 @@ def _safe_int(val: Any) -> int:
7777 return 0
7878
7979
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- )
84-
85-
86- def _new_session_metadata_builder (session_id : str ) -> dict [str , Any ]:
80+ def _new_session_metadata_builder (session_id : str ) -> SessionMetadataBuilderDict :
8781 """Fresh mutable metadata accumulator for ``parse_session()``."""
8882 return {
8983 "session_id" : session_id ,
@@ -118,15 +112,53 @@ def _new_session_metadata_builder(session_id: str) -> dict[str, Any]:
118112 }
119113
120114
121- def _finalize_session_metadata (raw : dict [ str , Any ] ) -> SessionMetadataDict :
115+ def _finalize_session_metadata (raw : SessionMetadataBuilderDict ) -> SessionMetadataDict :
122116 """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 )
117+ return {
118+ "session_id" : raw ["session_id" ],
119+ "models_used" : sorted (raw ["models_used" ]),
120+ "first_timestamp" : raw ["first_timestamp" ],
121+ "last_timestamp" : raw ["last_timestamp" ],
122+ "total_input_tokens" : raw ["total_input_tokens" ],
123+ "total_output_tokens" : raw ["total_output_tokens" ],
124+ "total_cache_read_tokens" : raw ["total_cache_read_tokens" ],
125+ "total_cache_creation_tokens" : raw ["total_cache_creation_tokens" ],
126+ "total_tool_calls" : raw ["total_tool_calls" ],
127+ "tool_call_counts" : raw ["tool_call_counts" ],
128+ "version" : raw ["version" ],
129+ "cwd" : raw ["cwd" ],
130+ "git_branch" : raw ["git_branch" ],
131+ "permission_mode" : raw ["permission_mode" ],
132+ "compactions" : raw ["compactions" ],
133+ "total_ephemeral_5m_tokens" : raw ["total_ephemeral_5m_tokens" ],
134+ "total_ephemeral_1h_tokens" : raw ["total_ephemeral_1h_tokens" ],
135+ "service_tiers" : sorted (raw ["service_tiers" ]),
136+ "session_wall_time_seconds" : raw ["session_wall_time_seconds" ],
137+ "compact_boundaries" : raw ["compact_boundaries" ],
138+ "api_errors" : raw ["api_errors" ],
139+ "files_read" : sorted (raw ["files_read" ]),
140+ "files_written" : sorted (raw ["files_written" ]),
141+ "files_created" : sorted (raw ["files_created" ]),
142+ "bash_commands" : raw ["bash_commands" ],
143+ "web_fetches" : raw ["web_fetches" ],
144+ "sidechain_messages" : raw ["sidechain_messages" ],
145+ "stop_reasons" : raw ["stop_reasons" ],
146+ "entry_counts" : raw ["entry_counts" ],
147+ }
148+
149+
150+ def _entry_timestamp (entry : dict [str , Any ]) -> str | None :
151+ """Return a usable ISO timestamp string from an entry, or None."""
152+ ts = entry .get ("timestamp" )
153+ if isinstance (ts , str ) and ts :
154+ return ts
155+ if entry .get ("type" ) == "file-history-snapshot" :
156+ snap = entry .get ("snapshot" )
157+ if isinstance (snap , dict ):
158+ snap_ts = snap .get ("timestamp" )
159+ if isinstance (snap_ts , str ) and snap_ts :
160+ return snap_ts
161+ return None
130162
131163
132164def parse_session (filepath : str ) -> SessionDict :
@@ -151,14 +183,9 @@ def parse_session(filepath: str) -> SessionDict:
151183 continue
152184
153185 entry_type = entry .get ("type" )
154- ts = entry .get ("timestamp" )
155- # file-history-snapshot stores timestamp inside snapshot
156- if not ts and entry_type == "file-history-snapshot" :
157- snap = entry .get ("snapshot" )
158- if isinstance (snap , dict ):
159- ts = snap .get ("timestamp" )
160-
161- if isinstance (ts , str ) and ts :
186+ ts = _entry_timestamp (entry )
187+
188+ if ts :
162189 if metadata ["first_timestamp" ] is None :
163190 metadata ["first_timestamp" ] = ts
164191 metadata ["last_timestamp" ] = ts
@@ -210,7 +237,7 @@ def parse_session(filepath: str) -> SessionDict:
210237
211238
212239def _process_user (
213- entry : dict [str , Any ], messages : list [MessageDict ], metadata : dict [ str , Any ]
240+ entry : dict [str , Any ], messages : list [MessageDict ], metadata : SessionMetadataBuilderDict
214241) -> None :
215242 """Pull out text, tool results, and session-level metadata (cwd, version, etc.)
216243 from a user entry."""
@@ -257,7 +284,7 @@ def _process_user(
257284
258285
259286def _process_assistant (
260- entry : dict [str , Any ], messages : list [MessageDict ], metadata : dict [ str , Any ]
287+ entry : dict [str , Any ], messages : list [MessageDict ], metadata : SessionMetadataBuilderDict
261288) -> None :
262289 """Handle assistant responses -- splits content into text, thinking blocks,
263290 and tool_use calls, and accumulates token/model/tool stats."""
@@ -353,7 +380,7 @@ def _process_assistant(
353380
354381
355382def _process_system (
356- entry : dict [str , Any ], messages : list [MessageDict ], metadata : dict [ str , Any ]
383+ entry : dict [str , Any ], messages : list [MessageDict ], metadata : SessionMetadataBuilderDict
357384) -> None :
358385 """Handle system entries (mostly compact_boundary markers from context
359386 compaction)."""
0 commit comments