Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src-tauri/src/parser/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ impl RawEntry {
return None;
}

// Skip non-full view mode entries (Codex v0.130.0+, PR #21566).
// The thread turns endpoint now exposes three view modes: "unloaded"
// (metadata-only stub), "summary" (partial), and "full" (complete).
// Only absent (legacy) or "full" entries carry complete turn data;
// any other view_mode is a placeholder and must be skipped so callers
// never receive silently truncated turn content.
if let Some(vm) = v.get("view_mode").and_then(|t| t.as_str()) {
if vm != "full" {
return None;
}
}

let entry_type = detect_entry_type(&v);
let timestamp = v
.get("timestamp")
Expand Down Expand Up @@ -159,6 +171,38 @@ mod tests {
assert_eq!(e.payload["permission_profile"], "full-auto");
}

// Codex v0.130.0 (PR #21566): thread turns endpoint now exposes three view
// modes. "unloaded" and "summary" entries are placeholders / partial stubs;
// only absent (legacy) or "full" entries contain complete turn data.

#[test]
fn view_mode_unloaded_returns_none() {
let line = r#"{"timestamp":"2026-05-08T10:00:00Z","type":"response_item","view_mode":"unloaded","payload":{"type":"function_call","name":"exec_command","call_id":"c1"}}"#;
assert!(RawEntry::parse(line).is_none());
}

#[test]
fn view_mode_summary_returns_none() {
let line = r#"{"timestamp":"2026-05-08T10:00:00Z","type":"response_item","view_mode":"summary","payload":{"type":"message","role":"assistant","content":"partial"}}"#;
assert!(RawEntry::parse(line).is_none());
}

#[test]
fn view_mode_full_is_parsed_normally() {
let line = r#"{"timestamp":"2026-05-08T10:00:00Z","type":"response_item","view_mode":"full","payload":{"type":"function_call","name":"exec_command","call_id":"c2"}}"#;
let e = RawEntry::parse(line).expect("view_mode:full must parse");
assert_eq!(e.entry_type, "response_item");
assert_eq!(e.payload["name"], "exec_command");
}

#[test]
fn absent_view_mode_is_parsed_normally() {
// Legacy entries (pre-v0.130.0) have no view_mode field; they must still parse.
let line = r#"{"timestamp":"2026-05-08T10:00:00Z","type":"response_item","payload":{"type":"message","role":"assistant","content":"hello"}}"#;
let e = RawEntry::parse(line).expect("legacy entry without view_mode must parse");
assert_eq!(e.entry_type, "response_item");
}

#[test]
fn log_db_log_writer_refactor_does_not_affect_jsonl_session_parser() {
// Codex v0.128.0 PRs #19234/#19959 refactored the internal log DB into a
Expand Down
39 changes: 39 additions & 0 deletions src-tauri/src/parser/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,45 @@ mod tests {
assert_eq!(turns[0].status, TurnStatus::Cancelled);
}

// Codex v0.130.0 (PR #21566): multi-page thread completeness.
// The thread turns endpoint now paginates large threads and writes "unloaded"
// stub entries as placeholders between pages. build_turns must ignore all
// non-full stubs so every real turn is present in the parsed output.
#[test]
fn multi_page_thread_all_turns_present_stubs_ignored() {
let entries = entries(&[
// session header
r#"{"timestamp":"2026-05-08T10:00:00Z","type":"session_meta","payload":{"id":"long-session","timestamp":"2026-05-08T10:00:00Z"}}"#,
// page 1 — turn 1 (full entries, no view_mode = legacy compat)
r#"{"timestamp":"2026-05-08T10:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1","turn_start_timestamp":1746691201.0}}"#,
r#"{"timestamp":"2026-05-08T10:00:02Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1746691202.0}}"#,
// unloaded stub that would appear between pages (view_mode:unloaded) — must be skipped
r#"{"timestamp":"2026-05-08T10:00:03Z","type":"event_msg","view_mode":"unloaded","payload":{"type":"task_started","turn_id":"turn-2"}}"#,
// summary stub — also must be skipped
r#"{"timestamp":"2026-05-08T10:00:04Z","type":"event_msg","view_mode":"summary","payload":{"type":"task_started","turn_id":"turn-2"}}"#,
// page 2 — turn 2 (full view_mode explicit)
r#"{"timestamp":"2026-05-08T10:00:05Z","type":"event_msg","view_mode":"full","payload":{"type":"task_started","turn_id":"turn-2","turn_start_timestamp":1746691205.0}}"#,
r#"{"timestamp":"2026-05-08T10:00:06Z","type":"event_msg","view_mode":"full","payload":{"type":"task_complete","turn_id":"turn-2","completed_at":1746691206.0}}"#,
// page 3 — turn 3 (legacy, no view_mode)
r#"{"timestamp":"2026-05-08T10:00:07Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-3","turn_start_timestamp":1746691207.0}}"#,
r#"{"timestamp":"2026-05-08T10:00:08Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-3","completed_at":1746691208.0}}"#,
]);

let turns = build_turns(&entries);

// All three real turns must be present; stubs must not create phantom turns
assert_eq!(
turns.len(),
3,
"expected exactly 3 complete turns, got {}",
turns.len()
);
assert_eq!(turns[0].turn_id, "turn-1");
assert_eq!(turns[1].turn_id, "turn-2");
assert_eq!(turns[2].turn_id, "turn-3");
assert!(turns.iter().all(|t| t.status == TurnStatus::Complete));
}

#[test]
fn unknown_event_types_are_ignored_gracefully() {
let entries = entries(&[
Expand Down