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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

All notable changes to the Toolpath workspace are documented here.

## Projected Claude sessions are resumable again — 2026-07-30

Two fixes found by live-resuming a projected session against the real
API. Together they broke resume for most projected sessions.

- **`toolpath-claude`** (0.12.2): `ContentPart::Thinking` serializes a
`None` signature as an **absent key** instead of `"signature": null`.
The Anthropic API rejects a null signature outright
(`400 …thinking.signature.str: Input should be a valid string`), so
any projected session containing thinking blocks — most real agent
sessions — failed on the first message after resume. An absent
signature is tolerated: the thinking block is dropped on replay, per
the documented writer contract in
`docs/agents/formats/claude-code/writing-compatible-jsonl.md`. Real
signatures still round-trip untouched.
- **`path-cli`** (0.16.1): the Claude JSONL export now ends
with a trailing newline. Claude Code appends to the projected file on
resume, and without one the first appended entry landed on the same
line as the last projected entry, corrupting the JSONL.

## `path p cache sync` — incremental session ingestion — 2026-07-16

Adds `path p cache sync [types…]`, the first step toward a cache that
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ license = "Apache-2.0"
toolpath = { version = "0.7.0", path = "crates/toolpath" }
toolpath-convo = { version = "0.11.1", path = "crates/toolpath-convo" }
toolpath-git = { version = "0.6.0", path = "crates/toolpath-git" }
toolpath-claude = { version = "0.12.1", path = "crates/toolpath-claude", default-features = false }
toolpath-claude = { version = "0.12.2", path = "crates/toolpath-claude", default-features = false }
toolpath-gemini = { version = "0.6.1", path = "crates/toolpath-gemini", default-features = false }
toolpath-codex = { version = "0.6.1", path = "crates/toolpath-codex" }
toolpath-copilot = { version = "0.1.0", path = "crates/toolpath-copilot" }
Expand All @@ -37,7 +37,7 @@ 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.1", path = "crates/toolpath-pi" }
path-cli = { version = "0.16.0", path = "crates/path-cli" }
path-cli = { version = "0.16.1", path = "crates/path-cli" }
pathbase-client = { version = "0.2.0", path = "crates/pathbase-client" }

reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/path-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "path-cli"
version = "0.16.0"
version = "0.16.1"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
6 changes: 5 additions & 1 deletion crates/path-cli/src/cmd_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,11 @@ fn serialize_jsonl(conv: &toolpath_claude::Conversation) -> Result<String> {
for entry in &conv.entries {
lines.push(serde_json::to_string(entry)?);
}
Ok(lines.join("\n"))
// Trailing newline matters: Claude Code appends to this file on resume,
// and without it the first appended entry lands on the last line.
let mut out = lines.join("\n");
out.push('\n');
Ok(out)
}

#[cfg(not(target_os = "emscripten"))]
Expand Down
2 changes: 1 addition & 1 deletion crates/toolpath-claude/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "toolpath-claude"
version = "0.12.1"
version = "0.12.2"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
27 changes: 26 additions & 1 deletion crates/toolpath-claude/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ pub enum ContentPart {
},
Thinking {
thinking: String,
#[serde(default)]
// Serialized as absent when None: the Anthropic API rejects
// `"signature": null` outright (400), while an absent signature just
// means the thinking block is dropped on resume — see
// docs/agents/formats/claude-code/writing-compatible-jsonl.md.
#[serde(default, skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
ToolUse {
Expand Down Expand Up @@ -731,6 +735,27 @@ mod tests {
assert_eq!(part.summary(), "[thinking]");
}

#[test]
fn test_thinking_none_signature_serializes_absent() {
let part = ContentPart::Thinking {
thinking: "deep thought".to_string(),
signature: None,
};
let json = serde_json::to_value(&part).unwrap();
assert!(
json.get("signature").is_none(),
"None signature must serialize as an absent key, not null: the \
API 400s on `\"signature\": null` but tolerates absence"
);

let signed = ContentPart::Thinking {
thinking: "deep thought".to_string(),
signature: Some("sig123".to_string()),
};
let json = serde_json::to_value(&signed).unwrap();
assert_eq!(json.get("signature").unwrap(), "sig123");
}

#[test]
fn test_content_part_summary_tool_use() {
let part = ContentPart::ToolUse {
Expand Down
4 changes: 2 additions & 2 deletions crates/toolpath-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "toolpath-cli"
version = "0.16.0"
version = "0.16.1"
edition = "2024"
license = "Apache-2.0"
repository = "https://github.com/empathic/toolpath"
Expand All @@ -14,7 +14,7 @@ name = "path"
path = "src/main.rs"

[dependencies]
path-cli = { path = "../path-cli", version = "0.16.0" }
path-cli = { path = "../path-cli", version = "0.16.1" }
anyhow = "1.0"

[workspace]
6 changes: 3 additions & 3 deletions site/_data/crates.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
{
"name": "toolpath-claude",
"version": "0.12.1",
"version": "0.12.2",
"description": "Derive from Claude conversation logs",
"docs": "https://docs.rs/toolpath-claude",
"crate": "https://crates.io/crates/toolpath-claude",
Expand Down Expand Up @@ -113,15 +113,15 @@
},
{
"name": "path-cli",
"version": "0.16.0",
"version": "0.16.1",
"description": "Unified CLI (binary: path)",
"docs": "https://docs.rs/path-cli",
"crate": "https://crates.io/crates/path-cli",
"role": "One binary called `path` that ties everything together. Porcelain at the top level (share, resume, query, show, track, auth); plumbing under `path p \u2026` (import, export, cache, list, render, merge, validate). Pathbase round-trip via `p import pathbase` / `p export pathbase` (authed default \u2192 secret pathstash; anon fallback when not logged in)."
},
{
"name": "toolpath-cli",
"version": "0.16.0",
"version": "0.16.1",
"description": "Deprecated alias for path-cli",
"docs": "https://docs.rs/toolpath-cli",
"crate": "https://crates.io/crates/toolpath-cli",
Expand Down
Loading