From c31ec774d7a7c36ffc003287c658308d6a7fa8a3 Mon Sep 17 00:00:00 2001 From: Alex Kesling Date: Wed, 8 Jul 2026 13:46:59 -0400 Subject: [PATCH] fix(pi): skip unparseable session lines instead of failing the read --- CHANGELOG.md | 8 +++++ Cargo.lock | 2 +- Cargo.toml | 2 +- crates/toolpath-pi/Cargo.toml | 2 +- crates/toolpath-pi/src/reader.rs | 57 +++++++++++++++++++++++++++++--- site/_data/crates.json | 2 +- 6 files changed, 65 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce2d196..e609d34f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the Toolpath workspace are documented here. +## Pi: skip unparseable lines instead of failing the read — 2026-07-08 + +- **`toolpath-pi`** (0.6.1): Crashed or truncated Pi session files now load + gracefully. Previously, a truncated final line (invalid JSON) would fail + the entire session read; now unparseable lines are skipped with a warning + (matching `toolpath-codex`'s documented safe-reader policy). Header parsing + remains a hard error — a session without a parseable header has no identity. + ## Derive: resolve duplicate step ids — 2026-07-01 - **`toolpath-convo`** (0.11.1): `derive_path` now guarantees the derived diff --git a/Cargo.lock b/Cargo.lock index a799d3c7..e0f454cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4024,7 +4024,7 @@ dependencies = [ [[package]] name = "toolpath-pi" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index cf3d6237..b98b6b84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ toolpath-cursor = { version = "0.2.0", path = "crates/toolpath-cursor" } toolpath-github = { version = "0.6.0", path = "crates/toolpath-github" } toolpath-dot = { version = "0.5.0", path = "crates/toolpath-dot" } toolpath-md = { version = "0.7.0", path = "crates/toolpath-md" } -toolpath-pi = { version = "0.6.0", path = "crates/toolpath-pi" } +toolpath-pi = { version = "0.6.1", path = "crates/toolpath-pi" } path-cli = { version = "0.14.0", path = "crates/path-cli" } pathbase-client = { version = "0.2.0", path = "crates/pathbase-client" } diff --git a/crates/toolpath-pi/Cargo.toml b/crates/toolpath-pi/Cargo.toml index 8ce22347..72b1e236 100644 --- a/crates/toolpath-pi/Cargo.toml +++ b/crates/toolpath-pi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toolpath-pi" -version = "0.6.0" +version = "0.6.1" edition.workspace = true license.workspace = true repository = "https://github.com/empathic/toolpath" diff --git a/crates/toolpath-pi/src/reader.rs b/crates/toolpath-pi/src/reader.rs index 3f6b0595..01424f24 100644 --- a/crates/toolpath-pi/src/reader.rs +++ b/crates/toolpath-pi/src/reader.rs @@ -220,10 +220,12 @@ pub fn read_session_from_file(path: &Path) -> Result { continue; } Err(_) => { - return Err(PiError::invalid_session_file( - path.to_path_buf(), - format!("line {line_no}: {parse_err}"), - )); + eprintln!( + "warning: skipping unparseable line at {}:{} (truncated write?): {parse_err}", + path.display(), + line_no + ); + continue; } } } @@ -837,6 +839,53 @@ mod tests { let ids: Vec<&str> = s.message_entries().map(|(b, _)| b.id.as_str()).collect(); assert_eq!(ids, vec!["a", "b"]); } + + #[test] + fn truncated_final_line_is_skipped_with_warning() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("s.jsonl"); + // valid header + one valid entry + a truncated JSON fragment + let content = format!( + "{}\n{}\n{{\"type\":\"mess", + header_line("sess-1"), + msg_line("a", None, "2026-04-16T00:00:01Z", "hi") + ); + fs::write(&path, content).unwrap(); + let session = read_session_from_file(&path).unwrap(); + // Header + 1 message; truncated line skipped + assert_eq!(session.entries.len(), 2); + } + + #[test] + fn garbage_mid_file_line_is_skipped_and_later_entries_survive() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("s.jsonl"); + write_jsonl( + &path, + &[ + &header_line("sess-1"), + "not json at all", + &msg_line("a", None, "2026-04-16T00:00:01Z", "hi"), + ], + ); + let session = read_session_from_file(&path).unwrap(); + // Header + 1 message; garbage line skipped + assert_eq!(session.entries.len(), 2); + } + + #[test] + fn malformed_header_still_hard_fails() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("s.jsonl"); + // first non-empty line is invalid JSON -> Err (unchanged behavior) + fs::write(&path, "not json at all").unwrap(); + let err = read_session_from_file(&path).unwrap_err(); + // Should be an InvalidSessionFile or MalformedHeader error + assert!(matches!( + err, + PiError::InvalidSessionFile { .. } | PiError::MalformedHeader(_) + )); + } } // Small fallback helper for tests to set mtimes without adding a new dep. diff --git a/site/_data/crates.json b/site/_data/crates.json index 6b730be1..ac4bd4b6 100644 --- a/site/_data/crates.json +++ b/site/_data/crates.json @@ -65,7 +65,7 @@ }, { "name": "toolpath-pi", - "version": "0.6.0", + "version": "0.6.1", "description": "Derive Toolpath provenance documents from Pi (pi.dev) agent session logs", "docs": "https://docs.rs/toolpath-pi", "crate": "https://crates.io/crates/toolpath-pi",