Parse the newly-appended portion of one Cursor transcript file into a provider-neutral [`ParsedTranscript`]. Shared by the hook path ([`CursorEventSource`]) and the startup catch-up sweep ([`CursorSweepSource`]); both derive identical session/message ids for the same file (the hook event's `session_id` always equals the transcript file stem), so whichever runs second is an idempotent no-op.
(
event: &Value,
parent_session_id: &str,
path: &Path,
prev: StoredCursor,
max_new_bytes: Option<u64>,
)
| 283 | /// same file (the hook event's `session_id` always equals the transcript file |
| 284 | /// stem), so whichever runs second is an idempotent no-op. |
| 285 | fn parse_cursor_jsonl( |
| 286 | event: &Value, |
| 287 | parent_session_id: &str, |
| 288 | path: &Path, |
| 289 | prev: StoredCursor, |
| 290 | max_new_bytes: Option<u64>, |
| 291 | ) -> Option<ParsedTranscript> { |
| 292 | let new = stream_new_jsonl(path, prev, max_new_bytes)?; |
| 293 | let subagent = cursor_subagent_identity(path, parent_session_id); |
| 294 | let session_id = subagent.as_ref().map_or_else( |
| 295 | || parent_session_id.to_string(), |
| 296 | |(session_id, _agent_id)| session_id.clone(), |
| 297 | ); |
| 298 | let subagent_model = subagent.as_ref().and_then(|(_, agent_id)| { |
| 299 | parent_dispatch_model_for_subagent(path, parent_session_id, agent_id) |
| 300 | }); |
| 301 | let event_cwd = event_cwd(event); |
| 302 | let event_location_provenance = event_location_provenance(event); |
| 303 | let mut carry = TimestampCarry::new(i64::try_from(new.new_cursor.mtime).ok()); |
| 304 | let mut messages = Vec::new(); |
| 305 | for line in &new.lines { |
| 306 | let derived_timestamp = carry.observe(&line.value); |
| 307 | let context = CursorMessageContext { |
| 308 | transcript_path: path, |
| 309 | source_offset: line.offset, |
| 310 | derived_timestamp, |
| 311 | model_fallback: subagent_model.as_deref(), |
| 312 | event_cwd: event_cwd.as_deref(), |
| 313 | event_location_provenance, |
| 314 | }; |
| 315 | // The byte offset doubles as the message ordinal and source_offset, |
| 316 | // matching the original Cursor ingestion. |
| 317 | if let Some(message) = event_message(&line.value, event, &session_id, line.offset, context) |
| 318 | { |
| 319 | messages.push(message); |
| 320 | } |
| 321 | messages.extend(event_dispatch_messages( |
| 322 | &line.value, |
| 323 | event, |
| 324 | &session_id, |
| 325 | context, |
| 326 | )); |
| 327 | } |
| 328 | |
| 329 | // Defer the (filesystem-walking) project/title/metadata derivation until |
| 330 | // we actually have new messages; the driver ignores the draft otherwise. |
| 331 | let draft = if messages.is_empty() { |
| 332 | SessionDraft { |
| 333 | session_id, |
| 334 | project_key: String::new(), |
| 335 | project_path: String::new(), |
| 336 | title: None, |
| 337 | metadata_json: None, |
| 338 | parent_session_id: None, |
| 339 | is_subagent: false, |
| 340 | agent_id: None, |
| 341 | parent_tool_use_id: None, |
| 342 | } |
no test coverage detected