Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1c3d3d8
fix(derive): resolve duplicate step ids so paths keep unique ids
benbaarber Jul 1, 2026
8e5cd98
docs(formats): document harness compaction + capture real compaction …
benbaarber Jun 10, 2026
c75e647
feat(convo): enforce unique step ids in derive_path (keep-first)
benbaarber Jun 10, 2026
25924a6
refactor(convo): unify turns/events into ConversationView.items
benbaarber Jun 10, 2026
4f02cc9
feat(convo): emit conversation.compact steps for compaction boundaries
benbaarber Jun 10, 2026
26d7f7b
feat(providers): populate Item::Compaction from each harness marker
benbaarber Jun 10, 2026
6dbdd4a
feat(kind): add agent-coding-session v1.1.0 with conversation.compact
benbaarber Jun 10, 2026
9d75483
chore: version bumps for conversation-items + compaction feature
benbaarber Jun 10, 2026
306f7d5
feat(providers): project Item::Compaction back to harness markers
benbaarber Jun 10, 2026
395b0f0
fix(convo): error on duplicate step ids instead of silently dropping …
benbaarber Jun 11, 2026
e3c1853
docs(changelog): correct uniqueness behavior to error, not keep-first
benbaarber Jun 11, 2026
04fc735
feat(compaction): model kept as surviving turn-ids for cross-harness …
benbaarber Jun 11, 2026
1018fda
fix(derive): map Claude's <synthetic> model sentinel to tool:<provider>
benbaarber Jun 11, 2026
a25a6c5
test(matrix): check compaction survival as a cross-harness invariant
benbaarber Jun 11, 2026
d4d9860
refactor(claude): stop re-emitting kept turns on projection
benbaarber Jun 12, 2026
fbdcddb
fix(cursor): reconcile with the items/compaction API after rebase
benbaarber Jun 12, 2026
29e5cda
docs(cursor): document /summarize compaction; treat it like gemini
benbaarber Jun 22, 2026
29dd073
fix: reconcile compaction items IR with main's token-usage accounting
benbaarber Jun 22, 2026
67226ca
fix(opencode): pair each compaction with its own summary, not a sessi…
benbaarber Jun 24, 2026
e826c2a
docs(formats): note compaction projection coercions for pi and opencode
benbaarber Jun 24, 2026
e6f7f53
docs(test): correct stale compaction_roundtrip header
benbaarber Jun 24, 2026
5e13b32
feat(convo): resolve duplicate step ids in derive_path instead of err…
benbaarber Jun 25, 2026
6ded1e5
fix(compaction): resolve five round-trip provenance bugs found in review
benbaarber Jun 29, 2026
9b19f53
feat(derive): splice events/compactions onto the head's ancestry
benbaarber Jul 1, 2026
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
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,71 @@

All notable changes to the Toolpath workspace are documented here.

## Derive: resolve duplicate step ids — 2026-07-01

- **`toolpath-convo`**: `derive_path` guarantees the derived path's step ids
are unique. Sources reuse ids across distinct records (Claude Code reuses
`uuid` on `attachment` lines), and a duplicate breaks any consumer that keys
on the id (e.g. a `UNIQUE (path_id, step_id)` store). Collisions are resolved
inline as steps are emitted: a byte-identical re-emission is dropped, and a
same-id-but-different step is re-IDed to `<id>#<n>`. (Landed standalone in
#115; this branch builds on it.)

## Conversation items IR + compaction provenance — 2026-06-22

`ConversationView` now exposes a single ordered `items` stream (the new
`Item` enum) in place of the separate `turns`/`events` lists, giving every
provider one timeline to populate and every consumer one timeline to walk.
On top of that, compaction is now first-class provenance: when an agent
summarizes and drops earlier context, we record what was kept and why
instead of silently losing the boundary.

- **`toolpath-convo`** (BREAKING): `ConversationView.turns`/`events` are
unified into `ConversationView.items: Vec<Item>`, with `turns()`,
`events()`, and `compactions()` accessors for walking the stream. New
public types `Compaction` and `CompactionTrigger` model a
context-compaction boundary — `kept` is the set of prior turn ids that
survived, and `trigger` records what caused it. `derive_path` resolves
step-id collisions as it emits steps — a byte-identical re-emission is
dropped, a same-id-but-different step is re-IDed to `<id>#<n>` — so it
is infallible (returns `Path`, not `Result`) and the result is always
collision-free. The per-provider `derive::derive_path` / `derive_project`
(and pi's `derive_graph`) wrappers are likewise infallible now; only the
disk-reading entry points (e.g. pi's `derive_project`) still return
`Result`.
- **New step type**: `conversation.compact` records a compaction event as
a Step in the derived `Path`.
- **Per-provider compaction population**: `toolpath-claude`,
`toolpath-codex`, `toolpath-opencode`, and `toolpath-pi` emit
`Compaction` items from their on-disk compaction signals.
`toolpath-gemini` and `toolpath-cursor` participate in the items IR but
persist no compaction: Gemini records none, and Cursor's `/summarize`
writes only a boundary marker (the summary and kept set live server-side
and are unrecoverable from local data), so the marker is skipped on read.
- **`agent-coding-session` kind → v1.2.0**: the kind constant is now
`https://toolpath.net/kinds/agent-coding-session/v1.2.0`, extending
v1.1.0 (token usage) with the `conversation.compact` step type. The
v1.2.0 schema is bundled in both `toolpath` and `path-cli` alongside the
retained v1.0.0/v1.1.0 schemas; producers (the shared
`toolpath_convo::derive_path` and every provider built on it) emit the
new URI.
- **Token-usage idempotency fixes** (surfaced by the cross-harness
round-trip matrix once compaction fixtures were added): message totals
are canonicalized per `group_id` across the whole turn sequence rather
than per consecutive run, so a group interrupted by an intervening turn
no longer double-counts on re-read (`toolpath-claude`); consecutive
same-id Gemini lines (one split message) now share a `group_id` so their
repeated `tokens` snapshot is counted once (`toolpath-gemini`); and
Codex's cumulative `token_count` advances by a group's total once, on the
group's last turn (`toolpath-codex`).

Crates bumped: `toolpath` 0.8.0, `toolpath-convo` 0.12.0,
`toolpath-claude` 0.13.0, `toolpath-codex` 0.7.0, `toolpath-gemini`
0.7.0, `toolpath-opencode` 0.6.0, `toolpath-pi` 0.7.0,
`toolpath-cursor` 0.3.0, `path-cli` 0.15.0, `toolpath-cli` 0.15.0.
Unchanged: `toolpath-git`, `toolpath-github`, `toolpath-dot`,
`toolpath-md`, `pathbase-client`.

## Token usage: once per message, with per-step attribution + kind v1.1.0 — 2026-06-17

Fixes token over-counting in derived documents (~3× output-token
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,10 @@ Build the site after changes: `cd site && pnpm run build` (should produce 11 pag
- `toolpath-gemini` treats main file + sibling sub-agent UUID dir as one conversation. Sub-agent files are folded into `DelegatedWork` with populated `turns` (unlike `toolpath-claude`, whose sub-agent turns live in separate session files and stay empty). See `docs/agents/formats/gemini.md` for the full format reference.
- Provider-specific extras convention: `Turn.extra` and `WatcherEvent::Progress.data` use provider-namespaced keys (e.g. `extra["claude"]`, `extra["gemini"]`). `toolpath-claude` populates `Turn.extra["claude"]` from `ConversationEntry.extra`; `toolpath-gemini` populates `Turn.extra["gemini"]` with the full `tokens` struct, per-thought metadata, and tool-call status. This lets trait-only consumers access provider metadata without importing provider types.
- Shared derivation: `toolpath-convo` provides a provider-agnostic `ConversationView → Path` mapping via `toolpath_convo::derive_path`. New conversation providers should build on it rather than re-implementing the mapping.
- Path kinds: `toolpath::v1::PathMeta.kind` is an optional URI naming a hosted kind spec; URIs are immutable and semver-versioned. The only one defined so far is `https://toolpath.net/kinds/agent-coding-session/v1.1.0` (constant `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION`; `…_V1_0_0` names the superseded URI); every conversation → `Path` derivation sets it via the shared `toolpath_convo::derive_path` or each provider crate's own. Carried through the JSONL form via `PathOpen.meta` and `PathMeta` patch lines. Spec sources live in `site/kinds/<name>/<version>/{index.md,schema.json}` (schema.json is a symlink into `crates/path-cli/kinds/`, which `path p validate` bundles — both versions) and publish under `https://toolpath.net/kinds/`; the registry index is `site/kinds/index.md`. RFC: "Document Kind". JSON Schema: `$defs/pathMeta`.
- Path kinds: `toolpath::v1::PathMeta.kind` is an optional URI naming a hosted kind spec; URIs are immutable and semver-versioned. The only kind defined so far is `agent-coding-session`, currently at `https://toolpath.net/kinds/agent-coding-session/v1.2.0` (constant `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION`; `…_V1_0_0` names the superseded v1.0.0 URI); v1.2.0 adds the `conversation.compact` step type for context-compaction boundaries on top of v1.1.0's message-level token accounting, and the earlier v1.1.0 and v1.0.0 URIs stay registered (their schemas kept in `KIND_SCHEMAS`) and documented for backward compatibility. Every conversation → `Path` derivation sets it via the shared `toolpath_convo::derive_path` or each provider crate's own. Carried through the JSONL form via `PathOpen.meta` and `PathMeta` patch lines. Spec sources live in `site/kinds/<name>/<version>/{index.md,schema.json}` (schema.json is a symlink into `crates/path-cli/kinds/`, which `path p validate` bundles — all versions) and publish under `https://toolpath.net/kinds/`; the registry index is `site/kinds/index.md`. RFC: "Document Kind". JSON Schema: `$defs/pathMeta`.
- Token accounting (kind v1.1.0): two keys on `conversation.append`/`Turn`, both optional. `token_usage` = "the total for a message" (on the group's final step; `Σ` over a path = session total). `attributed_token_usage` = "this step's own attributed spend", populated only where the source genuinely reports per-step spend (its own key, so the sum is unaffected; remainder = group total − Σ attributed, computed not stored). One provider message can span several steps (Claude writes one JSONL line per content block); `Turn.group_id` groups them. `toolpath-claude` fills `group_id` from `message.id` and takes the **field-wise-max** group total (line order not trusted). Claude's per-line `usage` is a cumulative *streaming snapshot* (Anthropic streaming API: `message_start` seeds output near 0, `message_delta` is cumulative), NOT a per-block cost — so Claude emits no `attributed_token_usage`; the projector re-expands the total onto every line. `toolpath-codex` differences the cumulative `total_token_usage` (dedup-safe: never sum `last_token_usage` — Codex re-emits it stale; openai/codex #14489), attributes each per-call delta to the step it follows, and derives the round total from those attributions. pi/opencode decode all-zero wire counters as `None`. Never stamp a cumulative counter, a repeated message total, or zero-filled placeholders onto a step; never derive attribution from Claude's streaming snapshots.
- Token usage `breakdowns` (kind v1.1.0, additive): an optional third key on `TokenUsage` — a decomposition of a top-level class into named sub-classes, keyed by class (e.g. `"output"`), inner map sub-class → tokens (e.g. `breakdowns["output"]["reasoning"] = 243`). INFORMATIONAL ONLY: **never summed into any total** (the parent class already counts those tokens, so the session-total guarantee is untouched); invariant `Σ(inner) ≤ parent`; omitted when empty; rides both `token_usage` and `attributed_token_usage`. Per-provider reality: **Gemini** reports `thoughts` (reasoning) as an additive sibling that the derivation used to **drop** (under-counting output) — it's now folded into `output_tokens` *and* recorded as `breakdowns["output"]["reasoning"]`, with the projector un-folding it on the reverse path for a lossless round-trip (`Some(0)` preserved as a real Gemini-3 zero-reasoning signal). **OpenCode** folds `reasoning` into output and records the same breakdown. **Codex** differences `reasoning_output_tokens` (⊆ output, cumulative) into `breakdowns["output"]["reasoning"]` on both per-step `attributed_token_usage` and per-round `token_usage`. **Claude** records no breakdown (its JSONL `usage` doesn't itemize thinking tokens).
- Compaction boundary (kind v1.2.0): the `conversation.compact` step type — a context-compaction boundary recorded as its own step between the turns it separates (turns after the boundary parent on it, so the `head`-ancestry walk crosses it in order). `structural` fields are all optional but `type`: `trigger` (`auto`|`manual`), `summary`, `pre_tokens`, and `kept` (ids of prior turns surviving verbatim into the post-compaction window; non-contiguous allowed; empty = wholesale). It is not a turn (no `text`/`role`/`tool_uses`); `step.actor` is `tool:<provider>`. Populated by each provider's `Item::Compaction` derivation and projected back to harness markers.
- Pi provider: `toolpath-pi` reads Pi session JSONL from `~/.pi/agent/sessions/`. Sessions use a tree (id/parentId) in a single file, and may link to a parent file via `parentSession` in the header. The tree is preserved as a DAG in the derived `Path`.
- Codex provider: `toolpath-codex` reads Codex CLI rollout files from `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`. Sessions are date-bucketed (not project-keyed). File-change fidelity is excellent — Codex's `patch_apply_end` events carry either the unified diff (for updates) or the full file content (for adds), so the derived `Path` gets a real `raw` perspective on every file artifact. See `docs/agents/formats/codex.md` for the full format reference.
- opencode provider: `toolpath-opencode` reads a SQLite database at `~/.local/share/opencode/opencode.db` (opened read-only). Each session's messages and 12 typed part variants (text, reasoning, tool, step-start/-finish, snapshot, patch, file, agent, subtask, retry, compaction) land as one step per message with tool invocations attached. File diffs come from a sibling bare git repo at `snapshot/<project-id>/[<sha1(worktree)>]/` via `git2` tree↔tree diffs — opencode respects the user's `.gitignore`, so changes under gitignored paths fall back to tool-input-derived structural changes with no `raw` perspective. Project id is the SHA of the repo's first root commit. See `docs/agents/formats/opencode.md` for the full format reference.
Expand Down
18 changes: 9 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 @@ -23,19 +23,19 @@ edition = "2024"
license = "Apache-2.0"

[workspace.dependencies]
toolpath = { version = "0.7.0", path = "crates/toolpath" }
toolpath-convo = { version = "0.11.0", path = "crates/toolpath-convo" }
toolpath = { version = "0.8.0", path = "crates/toolpath" }
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.0", path = "crates/toolpath-claude", default-features = false }
toolpath-gemini = { version = "0.6.0", path = "crates/toolpath-gemini", default-features = false }
toolpath-codex = { version = "0.6.0", path = "crates/toolpath-codex" }
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-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.0", path = "crates/toolpath-pi" }
path-cli = { version = "0.14.0", path = "crates/path-cli" }
toolpath-pi = { version = "0.7.0", path = "crates/toolpath-pi" }
path-cli = { version = "0.15.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
12 changes: 8 additions & 4 deletions RFC.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,14 @@ unrecognized URIs should be treated as a generic path. Kind URIs are
immutable, semver-versioned, and revisions ship at a new version URI.

Defined kinds are listed at <https://toolpath.net/kinds/>. The only one defined
so far is `https://toolpath.net/kinds/agent-coding-session/v1.0.0` — a path
recording an AI coding conversation, where each conversational-turn step
carries a `"conversation.append"` structural change with the turn's role,
text, and so on. See the linked spec for the full contract.
so far is `agent-coding-session`, currently at
`https://toolpath.net/kinds/agent-coding-session/v1.2.0` — a path recording an
AI coding conversation, where each conversational-turn step carries a
`"conversation.append"` structural change with the turn's role, text, and so
on, and context-compaction boundaries carry a `"conversation.compact"` step.
The earlier `v1.1.0` URI (message-level token accounting, without
`conversation.compact`) and `v1.0.0` URI both remain valid and documented. See
the linked spec for the full contract.

#### Actor Definitions

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.14.0"
version = "0.15.0"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
Loading
Loading