From 460f1ff68b56855862c6da2113c7f153fbaa96db Mon Sep 17 00:00:00 2001 From: Alex Kesling Date: Wed, 8 Jul 2026 13:50:42 -0400 Subject: [PATCH] fix(opencode): use the configured resolver in ConversationProvider::load_conversation --- CHANGELOG.md | 14 ++++ CLAUDE.md | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/toolpath-opencode/Cargo.toml | 2 +- crates/toolpath-opencode/src/provider.rs | 97 +++++++++++++++++++++++- site/_data/crates.json | 2 +- 7 files changed, 115 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce2d196..cd2306ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to the Toolpath workspace are documented here. +## Fix: opencode's trait path now honors the configured resolver — 2026-07-08 + +- **`toolpath-opencode`** (0.5.1): `ConversationProvider::load_conversation` + called `to_view(&s)`, which builds a fresh default `PathResolver::new()` + instead of using the provider's own configured resolver + (`self.resolver()`). With any non-default opencode data directory (e.g. + `$XDG_DATA_HOME` overridden, or a resolver built via + `OpencodeConvo::with_resolver` for testing or a non-standard install), + the snapshot git repo was never found via the trait entrypoint, so every + file diff silently degraded to the structural-only tool-input fallback + (no `raw` perspective). Fixed by routing through + `to_view_with_resolver(&s, self.resolver())`. `list_conversations` and + `list_metadata` don't build views, so they weren't affected. + ## Derive: resolve duplicate step ids — 2026-07-01 - **`toolpath-convo`** (0.11.1): `derive_path` now guarantees the derived diff --git a/CLAUDE.md b/CLAUDE.md index 01c70e9f..ac7c6698 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -182,7 +182,7 @@ Tests live alongside the code (`#[cfg(test)] mod tests`), plus `path-cli` has in - `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`: 161 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`: 80 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-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-opencode`: 53 unit + 19 integration + 1 doc test (SQLite reader, JSON payload serde, provider assembly, snapshot-based derive, tool-input fallback for gitignored paths, reasoning breakdown, trait-path resolver configuration) - `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-dot`: 30 unit + 2 doc tests (render, visual conventions, escaping) diff --git a/Cargo.lock b/Cargo.lock index a799d3c7..e04234ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4006,7 +4006,7 @@ dependencies = [ [[package]] name = "toolpath-opencode" -version = "0.5.0" +version = "0.5.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index cf3d6237..e781148b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ 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-opencode = { version = "0.5.1", path = "crates/toolpath-opencode" } 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" } diff --git a/crates/toolpath-opencode/Cargo.toml b/crates/toolpath-opencode/Cargo.toml index 82e90bd4..d934db29 100644 --- a/crates/toolpath-opencode/Cargo.toml +++ b/crates/toolpath-opencode/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "toolpath-opencode" -version = "0.5.0" +version = "0.5.1" edition.workspace = true license.workspace = true repository = "https://github.com/empathic/toolpath" diff --git a/crates/toolpath-opencode/src/provider.rs b/crates/toolpath-opencode/src/provider.rs index 32158689..28e02cb9 100644 --- a/crates/toolpath-opencode/src/provider.rs +++ b/crates/toolpath-opencode/src/provider.rs @@ -726,7 +726,7 @@ impl ConversationProvider for OpencodeConvo { let s = self .read_session(conversation_id) .map_err(|e| ConvoTraitError::Provider(e.to_string()))?; - Ok(to_view(&s)) + Ok(to_view_with_resolver(&s, self.resolver())) } fn load_metadata( @@ -886,6 +886,7 @@ mod tests { use super::*; use rusqlite::Connection; use std::fs; + use std::path::Path; use tempfile::TempDir; fn setup(body_sql: &str) -> (TempDir, OpencodeConvo) { @@ -1152,4 +1153,98 @@ mod tests { let v = ConversationProvider::load_conversation(&mgr, "", "ses_x").unwrap(); assert_eq!(v.turns.len(), 2); } + + /// Build a bare snapshot git repo for `(project_id, worktree)` with two + /// commits — an empty tree ("before") and a tree containing + /// `main.cpp` ("after") — and return their commit SHAs. Mirrors how + /// opencode's real snapshot repos are laid out (see `paths.rs`). + fn build_snapshot_repo( + resolver: &PathResolver, + project_id: &str, + worktree: &std::path::Path, + ) -> (String, String) { + let gitdir = resolver.snapshot_gitdir(project_id, worktree).unwrap(); + fs::create_dir_all(&gitdir).unwrap(); + let repo = git2::Repository::init_bare(&gitdir).unwrap(); + let sig = git2::Signature::now("test", "test@example.com").unwrap(); + + let empty_tree_id = repo.treebuilder(None).unwrap().write().unwrap(); + let empty_tree = repo.find_tree(empty_tree_id).unwrap(); + let snap_a = repo + .commit(None, &sig, &sig, "snap_a", &empty_tree, &[]) + .unwrap(); + + let blob_id = repo.blob(b"int main(){}\n").unwrap(); + let mut tb = repo.treebuilder(None).unwrap(); + tb.insert("main.cpp", blob_id, 0o100_644).unwrap(); + let tree_b = repo.find_tree(tb.write().unwrap()).unwrap(); + let snap_b = repo + .commit(None, &sig, &sig, "snap_b", &tree_b, &[]) + .unwrap(); + + (snap_a.to_string(), snap_b.to_string()) + } + + #[test] + fn trait_load_conversation_uses_configured_resolver_for_snapshots() { + let temp = TempDir::new().unwrap(); + let data = temp.path().join(".local/share/opencode"); + fs::create_dir_all(&data).unwrap(); + let resolver = PathResolver::new() + .with_home(temp.path()) + .with_data_dir(&data); + + // BASIC_SQL's session lives at project "proj" / worktree "/tmp/proj". + let (snap_a, snap_b) = build_snapshot_repo(&resolver, "proj", Path::new("/tmp/proj")); + let sql = BASIC_SQL + .replace("snap_a", &snap_a) + .replace("snap_b", &snap_b); + + let conn = Connection::open(data.join("opencode.db")).unwrap(); + conn.execute_batch(&format!( + r#" + CREATE TABLE project ( + id text PRIMARY KEY, worktree text NOT NULL, vcs text, name text, + icon_url text, icon_color text, + time_created integer NOT NULL, time_updated integer NOT NULL, + time_initialized integer, sandboxes text NOT NULL, commands text + ); + CREATE TABLE session ( + id text PRIMARY KEY, project_id text NOT NULL, parent_id text, + slug text NOT NULL, directory text NOT NULL, title text NOT NULL, + version text NOT NULL, share_url text, + summary_additions integer, summary_deletions integer, + summary_files integer, summary_diffs text, revert text, permission text, + time_created integer NOT NULL, time_updated integer NOT NULL, + time_compacting integer, time_archived integer, workspace_id text + ); + CREATE TABLE message ( + id text PRIMARY KEY, session_id text NOT NULL, + time_created integer NOT NULL, time_updated integer NOT NULL, + data text NOT NULL + ); + CREATE TABLE part ( + id text PRIMARY KEY, message_id text NOT NULL, session_id text NOT NULL, + time_created integer NOT NULL, time_updated integer NOT NULL, + data text NOT NULL + ); + {sql} + "# + )) + .unwrap(); + drop(conn); + + // Constructed with a resolver pointed at the temp data dir — this + // is the configuration the trait path must honor. + let convo = OpencodeConvo::with_resolver(resolver); + + let view = ConversationProvider::load_conversation(&convo, "", "ses_x").unwrap(); + assert!( + view.turns + .iter() + .flat_map(|t| &t.file_mutations) + .any(|m| m.raw_diff.is_some()), + "trait path must resolve snapshot diffs with the configured resolver" + ); + } } diff --git a/site/_data/crates.json b/site/_data/crates.json index 6b730be1..f4168edc 100644 --- a/site/_data/crates.json +++ b/site/_data/crates.json @@ -57,7 +57,7 @@ }, { "name": "toolpath-opencode", - "version": "0.5.0", + "version": "0.5.1", "description": "Derive from opencode SQLite databases", "docs": "https://docs.rs/toolpath-opencode", "crate": "https://crates.io/crates/toolpath-opencode",