Skip to content
Draft
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
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,63 @@

All notable changes to the Toolpath workspace are documented here.

## Turns and events become one ordered item stream — 2026-07-27

`ConversationView` replaces its parallel `turns`/`events` vecs with a
single ordered `items` stream that preserves the source's exact
interleaving, and `derive_path` emits `conversation.event` steps so
non-turn entries survive import/export.

- **`toolpath-convo`** (0.12.0): **Breaking** —
`ConversationView.turns`/`.events` are replaced by
`items: Vec<Item>` (`Turn | Event`), with `turns()`/`events()`
iterators. `derive_path` emits `conversation.event` steps
(previously dropped — a Claude session lost its attachments and
system entries on import/export), resolves duplicate step ids by
renaming to `<id>#<n>`, recognizes byte-identical wire replays at
the source level and drops them (the Claude chain-merge shape,
including replays of steps that were spliced or renamed), and
splices events onto the head's ancestry so they don't dangle as
false dead ends. Event steps always stamp `source_parent` (`null` =
positional chaining) so `extract_conversation` restores source
linkage exactly. New proptest suite pins unique step ids,
derive → extract → derive stability, and replay no-ops over
randomized interleavings with id collisions.
- **`toolpath-claude`** (0.13.0): the projector emits events inline at
their item position — real Claude interleaves attachments and system
entries with turns; the old trailing pass regrouped them at end of
file. `wire_order_roundtrip` pins the projected entry-type sequence
to the captured fixture. Byte-identical duplicate-uuid replays (the
compaction re-emission block) are stripped at read time, with
compact boundaries exempt.
- **`toolpath-codex`** (0.7.0): events interleave with turns at their
rollout position — a `compacted` marker now derives between its
surrounding turns instead of after them, pinned through
derive → extract. Empty carrier turns that carry token accounting
survive via a keep-mask, and the opening `turn_context` placement
matches native rollouts.
- **`toolpath-gemini`** (0.7.0): all-zero token usage is dropped,
split assistant messages group via `group_id` with the snapshot
counted once, and colliding wire ids dedup with `#N` suffixes.
- **`toolpath-opencode`** (0.6.0): compaction parts become in-position
`part.compaction` events parented on the preceding turn (previously
a trailing events vec). Empty compaction-host user messages are
suppressed with parents redirected past them; attachment-only user
messages still emit turns. Projected timestamps are monotonized so
a re-read keeps emission order.
- **`toolpath-cursor`** (0.3.0): `/summarize` marker bubbles
(`capabilityType` 22) become `summarization` events and project
back to well-formed marker bubbles — the marker survives a full
cursor → toolpath → cursor round-trip instead of being dropped.
- **`toolpath-copilot`** (0.2.0): non-turn events carry a turn
watermark and merge into `items` in source order; assistant
messages whose only content is token usage survive as turns.
- **`toolpath-pi`** (0.7.0): tree parents resolve past discarded
entries (model changes, labels, folded tool results, the virtual
root) via `resolve_item_parent`; compaction stays a System turn.
- **`path-cli`** (0.17.0), **`toolpath-cli`** (0.17.0): dependency
bumps for all of the above. 0.17.0 rather than 0.16.0 because
0.16.0 had already been released from main.
## `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
16 changes: 8 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,16 @@ the server publishes that operation.
Tests live alongside the code (`#[cfg(test)] mod tests`), plus `path-cli` has integration tests in `tests/`. Per-crate counts:

- `toolpath`: 69 unit + 11 doc tests (serde roundtrip, builders, query)
- `toolpath-convo`: 118 unit + 4 doc tests (types, enrichment, display, ConversationView -> Path derivation, message-group usage accounting, breakdowns)
- `toolpath-convo`: 130 unit + 3 property + 4 doc tests (types, enrichment, display, ConversationView -> Path derivation, message-group usage accounting, breakdowns, proptests over randomized item interleavings with id collisions)
- `toolpath-git`: 33 unit + 3 doc tests (derive, branch detection, diffstat)
- `toolpath-github`: 32 unit + 3 doc tests (mapping, DAG construction, fixtures)
- `toolpath-claude`: 229 unit + 18 integration + 6 doc tests (path resolution, conversation reading, query, chaining, watcher, derive, metadata first-user-message, group_id grouping + once-per-message usage totals)
- `toolpath-gemini`: 165 unit + 29 integration + 5 doc tests (path resolution, chat-file parsing, query, watcher, derive, provider, round-trip fidelity, thoughts-folded-into-output + reasoning breakdown round-trip)
- `toolpath-codex`: 83 unit + 51 integration + 2 doc tests (rollout parsing, provider assembly, patch-fidelity derive, real-session fixture, source→path fidelity invariants, JSON wire-level round-trip, per-turn token deltas from cumulative counters, reasoning breakdown)
- `toolpath-copilot`: 63 unit + 8 integration + 1 doc test (`events.jsonl` envelope/event-type classification incl. `session.start` nested `context` + `tool.execution` `result.content` + `assistant.message` `reasoningText`/`outputTokens`, `session.shutdown` `tokenDetails`, path resolution incl. legacy `history-session-state/`, reader malformed-line tolerance without env races, tolerant `workspace.yaml` parse, `to_view` turn/tool/delegation assembly + per-turn token usage + shutdown-total merge, id-based **and** id-less positional tool pairing, position-stable turn ids, native file-state diff from `result.detailedContent`, `CopilotProjector` round-trip + foreign-tool-name remap). Ships a **real captured feature-elicit session** at `tests/fixtures/real-session.jsonl` (also `test-fixtures/copilot/convo.jsonl`) driving `real_fixture_roundtrip.rs` (forward invariants, projection round-trip fidelity, wire-level serde value-identity). The projector is exercised by the cross-harness matrix in `path-cli`.
- `toolpath-opencode`: 52 unit + 19 integration + 1 doc test (SQLite reader, JSON payload serde, provider assembly, snapshot-based derive, tool-input fallback for gitignored paths, reasoning breakdown)
- `toolpath-cursor`: 78 unit + 8 integration round-trip + 1 real-DB sanity + 1 doc test (state.vscdb SQLite reader, bubble store + composer header parsing, content-addressed blob lookup, projector with full TOOL_TABLE coverage, JSONL transcript ingest in `examples/dump_fixture.rs`)
- `toolpath-pi`: 133 unit + 26 integration + 5 doc tests (types, paths, error, reader, io, provider)
- `toolpath-claude`: 233 unit + 22 integration + 6 doc tests (path resolution, conversation reading, query, chaining, watcher, derive, metadata first-user-message, group_id grouping + once-per-message usage totals)
- `toolpath-gemini`: 170 unit + 29 integration + 5 doc tests (path resolution, chat-file parsing, query, watcher, derive, provider, round-trip fidelity, thoughts-folded-into-output + reasoning breakdown round-trip)
- `toolpath-codex`: 86 unit + 52 integration + 2 doc tests (rollout parsing, provider assembly, patch-fidelity derive, real-session fixture, source→path fidelity invariants, JSON wire-level round-trip, per-turn token deltas from cumulative counters, reasoning breakdown)
- `toolpath-copilot`: 68 unit + 8 integration + 1 doc test (`events.jsonl` envelope/event-type classification incl. `session.start` nested `context` + `tool.execution` `result.content` + `assistant.message` `reasoningText`/`outputTokens`, `session.shutdown` `tokenDetails`, path resolution incl. legacy `history-session-state/`, reader malformed-line tolerance without env races, tolerant `workspace.yaml` parse, `to_view` turn/tool/delegation assembly + per-turn token usage + shutdown-total merge, id-based **and** id-less positional tool pairing, position-stable turn ids, native file-state diff from `result.detailedContent`, `CopilotProjector` round-trip + foreign-tool-name remap). Ships a **real captured feature-elicit session** at `tests/fixtures/real-session.jsonl` (also `test-fixtures/copilot/convo.jsonl`) driving `real_fixture_roundtrip.rs` (forward invariants, projection round-trip fidelity, wire-level serde value-identity). The projector is exercised by the cross-harness matrix in `path-cli`.
- `toolpath-opencode`: 59 unit + 20 integration + 1 doc test (SQLite reader, JSON payload serde, provider assembly, snapshot-based derive, tool-input fallback for gitignored paths, reasoning breakdown)
- `toolpath-cursor`: 83 unit + 9 integration round-trip + 1 real-DB sanity + 1 doc test (state.vscdb SQLite reader, bubble store + composer header parsing, content-addressed blob lookup, projector with full TOOL_TABLE coverage, `/summarize` marker event round-trip, JSONL transcript ingest in `examples/dump_fixture.rs`)
- `toolpath-pi`: 136 unit + 26 integration + 5 doc tests (types, paths, error, reader, io, provider)
- `toolpath-dot`: 30 unit + 2 doc tests (render, visual conventions, escaping)
- `path-cli`: 353 unit + 119 integration tests (import/export/cache, track sessions, merge, validate, roundtrip, render-md snapshots, deprecation aliases, pathbase HTTP mock-server tests, fzf-friendly TSV output, `path resume` orchestration with injectable `ExecStrategy`, `path query`/`path kind` jaq filters + kind-selector matching + step wrapping over a `$TOOLPATH_CONFIG_DIR` cache sandbox, streaming-planner recognition + streamed-output-equals-slurp equality checks, `p cache sync` incremental ingestion — stat fingerprints, refresh-overwrite, per-type filtering, failure tallying — over resolver-injected provider fixtures, and import/share manifest recording end-to-end). For an end-to-end check against a real Pathbase deployment, run `scripts/test-pathbase-live.sh <url>` — it does an anon round-trip in a sandboxed config dir and, if you're logged into that URL, an authed pathstash round-trip too.
- `toolpath-cli`: 0 tests (it's a one-line `path_cli::run()` shim crate that exists only so `cargo install toolpath-cli` keeps installing the `path` binary)
Expand Down
71 changes: 62 additions & 9 deletions Cargo.lock

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

18 changes: 9 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,19 @@ license = "Apache-2.0"

[workspace.dependencies]
toolpath = { version = "0.7.0", path = "crates/toolpath" }
toolpath-convo = { version = "0.11.1", path = "crates/toolpath-convo" }
toolpath-convo = { version = "0.12.0", 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-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" }
toolpath-opencode = { version = "0.5.0", path = "crates/toolpath-opencode" }
toolpath-cursor = { version = "0.2.0", path = "crates/toolpath-cursor" }
toolpath-claude = { version = "0.13.0", path = "crates/toolpath-claude", default-features = false }
toolpath-gemini = { version = "0.7.0", path = "crates/toolpath-gemini", default-features = false }
toolpath-codex = { version = "0.7.0", path = "crates/toolpath-codex" }
toolpath-copilot = { version = "0.2.0", path = "crates/toolpath-copilot" }
toolpath-opencode = { version = "0.6.0", path = "crates/toolpath-opencode" }
toolpath-cursor = { version = "0.3.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.1", path = "crates/toolpath-pi" }
path-cli = { version = "0.16.0", path = "crates/path-cli" }
toolpath-pi = { version = "0.7.0", path = "crates/toolpath-pi" }
path-cli = { version = "0.17.0", 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.17.0"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
33 changes: 14 additions & 19 deletions crates/path-cli/tests/cross_harness_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,10 +509,7 @@ mod invariants {
}

fn meaningful_turns(view: &ConversationView) -> Vec<&Turn> {
view.turns
.iter()
.filter(|t| !is_system_envelope(t))
.collect()
view.turns().filter(|t| !is_system_envelope(t)).collect()
}

pub fn turn_count_and_role_sequence(
Expand Down Expand Up @@ -690,8 +687,7 @@ mod invariants {
// on input/output — the fields every wire carries (codex has no
// cache_write analog, cursor carries no cache counters at all).
let usage_seq = |v: &ConversationView| -> Vec<(Option<u32>, Option<u32>)> {
v.turns
.iter()
v.turns()
.filter(|t| matches!(t.role, Role::Assistant))
.filter_map(|t| t.token_usage.as_ref())
.map(|u| (u.input_tokens, u.output_tokens))
Expand Down Expand Up @@ -745,13 +741,11 @@ mod invariants {
failures: &mut Vec<String>,
) {
let pre: Vec<&Turn> = before_target
.turns
.iter()
.turns()
.filter(|t| matches!(t.role, Role::Assistant))
.collect();
let post: Vec<&Turn> = after_target
.turns
.iter()
.turns()
.filter(|t| matches!(t.role, Role::Assistant))
.collect();
for (i, (a, b)) in pre.iter().zip(post.iter()).enumerate() {
Expand Down Expand Up @@ -813,8 +807,7 @@ mod invariants {
failures: &mut Vec<String>,
) {
let edges = |v: &ConversationView| -> BTreeSet<(String, Option<String>)> {
v.turns
.iter()
v.turns()
.map(|t| (t.id.clone(), t.parent_id.clone()))
.collect()
};
Expand Down Expand Up @@ -864,7 +857,7 @@ mod invariants {
failures: &mut Vec<String>,
) {
let count =
|v: &ConversationView| -> usize { v.turns.iter().map(|t| t.delegations.len()).sum() };
|v: &ConversationView| -> usize { v.turns().map(|t| t.delegations.len()).sum() };
let o = count(original);
let f = count(final_);
if o != f {
Expand All @@ -875,7 +868,7 @@ mod invariants {
return;
}

for (i, (a, b)) in original.turns.iter().zip(final_.turns.iter()).enumerate() {
for (i, (a, b)) in original.turns().zip(final_.turns()).enumerate() {
if a.delegations.len() != b.delegations.len() {
failures.push(format!(
"turn {} delegation count diverged: first={} second={}",
Expand Down Expand Up @@ -938,14 +931,12 @@ mod invariants {
failures: &mut Vec<String>,
) {
let agent_ids = |v: &ConversationView| -> BTreeSet<String> {
v.turns
.iter()
v.turns()
.flat_map(|t| t.delegations.iter().map(|d| d.agent_id.clone()))
.collect()
};
let tool_use_ids = |v: &ConversationView| -> BTreeSet<String> {
v.turns
.iter()
v.turns()
.flat_map(|t| t.tool_uses.iter().map(|tu| tu.id.clone()))
.collect()
};
Expand Down Expand Up @@ -1099,7 +1090,11 @@ fn matrix_translation() {
h.name()
)
});
eprintln!("loaded {} fixture: {} turns", h.name(), view.turns.len());
eprintln!(
"loaded {} fixture: {} turns",
h.name(),
view.turns().count()
);
sources.push((h.name().to_string(), view));
}
run_matrix("matrix (real fixtures)", &sources);
Expand Down
1 change: 1 addition & 0 deletions crates/path-cli/tests/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ fn kind_lists_bundled_kinds() {
.assert()
.success()
.stdout(predicate::str::contains("agent-coding-session"))
.stdout(predicate::str::contains("v1.0.0"))
.stdout(predicate::str::contains("v1.1.0"));
}

Expand Down
Loading
Loading