diff --git a/.cargo/config.toml b/.cargo/config.toml index 1e6e9bc1..3673390a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -8,10 +8,8 @@ CXXFLAGS = { value = "-DNDEBUG", force = false } # Global accounting (savings ledger, worldwide-counter flushes) is ON by # default for installed binaries, but `cargo test` / `cargo run` would then -# write temp-project rows into the developer's real ~/.tracedecay/global.db -# (or a legacy ~/.tokensave/global.db when present). Keep cargo-launched -# processes hermetic; tests that exercise the ledger opt back in with -# TRACEDECAY_ENABLE_GLOBAL_DB=1 or legacy TOKENSAVE_ENABLE_GLOBAL_DB=1. +# write temp-project rows into the developer's real ~/.tracedecay/global.db. +# Keep cargo-launched processes hermetic; tests that exercise the ledger opt +# back in with TRACEDECAY_ENABLE_GLOBAL_DB=1. # Installed binaries run outside cargo and are unaffected. TRACEDECAY_DISABLE_GLOBAL_DB = { value = "1", force = false } -TOKENSAVE_DISABLE_GLOBAL_DB = { value = "1", force = false } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f31fd2ab..e55264c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,6 @@ jobs: name: Test ${{ matrix.name }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository || contains(fromJSON('["master","feature/holographic-memory"]'), github.event.pull_request.base.ref) }} runs-on: ${{ fromJSON(matrix.runner) }} - env: - CARGO_BUILD_JOBS: "2" strategy: fail-fast: false matrix: @@ -65,39 +63,8 @@ jobs: ${{ runner.os }}-cargo- - name: Run tests - if: runner.os != 'Windows' run: cargo test --workspace - - name: Run tests (Windows, serialized) - if: runner.os == 'Windows' - env: - RUST_TEST_THREADS: "1" - run: | - $ErrorActionPreference = "Stop" - - cargo test --workspace --lib --bins -- --test-threads=1 - - $testFiles = @(Get-ChildItem -Path tests -Filter *.rs | - ForEach-Object { $_.BaseName } | - Sort-Object) - - $integrationArgs = @("test", "--workspace") - foreach ($test in ($testFiles | Where-Object { $_ -ne "branch_db_safety_test" })) { - $integrationArgs += @("--test", $test) - } - $integrationArgs += @("--", "--test-threads=1") - cargo @integrationArgs - - $cases = cargo test --test branch_db_safety_test -- --list | - Where-Object { $_ -match ': test$' } | - ForEach-Object { ($_ -split ':')[0] } - - foreach ($case in $cases) { - cargo test --test branch_db_safety_test $case -- --exact --test-threads=1 - } - - cargo test --workspace --doc -- --test-threads=1 - clippy: name: Clippy if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository || contains(fromJSON('["master","feature/holographic-memory"]'), github.event.pull_request.base.ref) }} @@ -158,6 +125,10 @@ jobs: holographic/dist/style.css lcm/dist/index.js lcm/dist/style.css + graph/dist/index.js + graph/dist/style.css + savings/dist/index.js + savings/dist/style.css steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index e24363ab..01d2c181 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -172,7 +172,7 @@ jobs: done # NOTE: the Homebrew tap (ScriptedAlchemy/homebrew-tap) is an external - # repo — any legacy Formula/tokensave-beta.rb there must be removed + # repo — any previous beta formula there must be removed # separately; this step only writes Formula/tracedecay-beta.rb. - name: Update formula env: @@ -243,7 +243,7 @@ jobs: echo "sha256_win64=${SHA256}" >> "$GITHUB_OUTPUT" # NOTE: the Scoop bucket (ScriptedAlchemy/scoop-bucket) is an external - # repo — any legacy bucket/tokensave-beta.json there must be removed + # repo — any previous beta bucket manifest there must be removed # separately; this step only writes bucket/tracedecay-beta.json. - name: Update Scoop manifest env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4607d53..d1231975 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -203,7 +203,7 @@ jobs: done # NOTE: the Homebrew tap (ScriptedAlchemy/homebrew-tap) is an external - # repo — the old Formula/tokensave.rb there must be removed/renamed + # repo — the previous formula there must be removed/renamed # separately; this step only writes the new Formula/tracedecay.rb. - name: Update formula env: @@ -245,7 +245,6 @@ jobs: cd tap # Clean up the legacy pre-rebrand formula if it still exists. - git rm -f --ignore-unmatch Formula/tokensave.rb git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add Formula/tracedecay.rb @@ -275,7 +274,7 @@ jobs: echo "sha256_win64=${SHA256}" >> "$GITHUB_OUTPUT" # NOTE: the Scoop bucket (ScriptedAlchemy/scoop-bucket) is an external - # repo — the old bucket/tokensave.json there must be removed separately; + # repo — the previous bucket manifest there must be removed separately; # this step only writes the new bucket/tracedecay.json. - name: Update Scoop manifest env: @@ -316,7 +315,6 @@ jobs: mkdir -p bucket cp ../tracedecay.json bucket/tracedecay.json # Clean up legacy pre-rebrand manifests (root-level and bucket/). - git rm -f --ignore-unmatch tokensave.json bucket/tokensave.json git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add bucket/tracedecay.json diff --git a/.gitignore b/.gitignore index 50c3266e..9dd6a6a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ /target .DS_Store .codegraph -.tokensave .tracedecay .mcp.json !codex-plugin/.mcp.json @@ -18,4 +17,3 @@ generated-illustrations/ *.log *~ *# - diff --git a/AGENTS.md b/AGENTS.md index 63a8a60b..f32532d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,11 +15,11 @@ - Memory deletion is permanent by design: no archive/soft-delete/restore features anywhere; dashboard curation hard-deletes facts. - In Hermes, tracedecay is both the memory provider and the context-engine provider for every profile; the provider name is "tracedecay" (not "lcm"), replacing the legacy hermes-lcm and holographic_plus plugins. -- Hermes profiles bound to a project use that project's repo-level `.tracedecay` databases (legacy `.tokensave` directories are still honored as a fallback); only the default profile stores tracedecay data at the profile level under `~/.hermes`. +- Hermes profiles bound to a project use the user-level TraceDecay store, scoped to the current project by default. Repo-level TraceDecay databases are not active runtime stores. - The Hermes tracedecay plugin must keep working against stock, uncustomized Hermes — verified by a CI job that installs stock Hermes; the user's Hermes fork only adds optional extras. -- The canonical repo is ScriptedAlchemy/tracedecay (renamed from ScriptedAlchemy/tokensave; GitHub redirects the old URL): never push or open PRs to the aovestdipaperino upstream; only the tokensave-large-treesitters dependency intentionally stays pointed at upstream. +- The canonical repo is ScriptedAlchemy/tracedecay: never push or open PRs to the aovestdipaperino upstream; only the tree-sitter grammar dependency intentionally stays pointed at upstream. - The standalone `tracedecay dashboard` server is the canonical dashboard implementation; the Hermes plugin wraps and reuses it, layering Hermes-only extras (e.g. LLM-based curation) on top. -- `tracedecay install --local` scopes the database to the repo's `.tracedecay/`; otherwise storage lives at the user/profile level. +- `tracedecay install --local` writes project integration files only; TraceDecay storage lives at the user/profile level and is scoped to the current project by default. ## Workspace Guidance @@ -30,14 +30,14 @@ Before reading source files or scanning the codebase, use the tracedecay MCP tools (`tracedecay_context`, `tracedecay_search`, `tracedecay_callers`, `tracedecay_callees`, `tracedecay_impact`, `tracedecay_node`, `tracedecay_files`, `tracedecay_affected`). They provide instant semantic results from a pre-built knowledge graph and are faster than file reads. -If a code analysis question cannot be fully answered by tracedecay MCP tools, try querying the SQLite database directly at `.tracedecay/tracedecay.db` (tables: `nodes`, `edges`, `files`). Use SQL to answer complex structural queries that go beyond what the built-in tools expose. +If a code analysis question cannot be fully answered by tracedecay MCP tools, use `tracedecay_storage_status` or `tracedecay_active_project` to get the resolved active graph database path before querying SQLite directly (tables: `nodes`, `edges`, `files`). Use SQL to answer complex structural queries that go beyond what the built-in tools expose. If you discover a gap where an extractor, schema, or tracedecay tool could be improved to answer a question natively, propose to the user that they open an issue at https://github.com/ScriptedAlchemy/tracedecay describing the limitation. **Remind the user to strip any sensitive or proprietary code from the bug description before submitting.** ## Cargo contention (concurrent Kanban workers) -Cargo holds an exclusive lock on its target dir for the whole build; concurrent workers hitting the same dir block (`Blocking waiting for file lock on build directory`) and look stale to the dispatcher. Every cargo-heavy card must export, **before its first cargo command**: +Cargo holds an exclusive lock on its target dir for the whole build; concurrent workers hitting the same dir block (`Blocking waiting for file lock on build directory`) and look stale to the dispatcher. Every cargo-heavy card must export a target outside the project tree, **before its first cargo command**: - export CARGO_TARGET_DIR="$HERMES_KANBAN_WORKSPACE/.tracedecay/target/$HERMES_KANBAN_TASK" + export CARGO_TARGET_DIR="/tmp/tracedecay-target/$HERMES_KANBAN_TASK" -Never run bare `cargo` against the repo `target/` — it is the user's ~399 GB interactive dir and is contended. Leave `.tracedecay/target/` (no suffix) to the tracedecay MCP diagnostic tools. Full-workspace integration checks use the serialized `.tracedecay/target/integration/` lane (one at a time). Reclaim disk with `rm -rf "$CARGO_TARGET_DIR"` before completing. Full policy: `docs/CARGO-CONTENTION-POLICY.md`. +Never run bare `cargo` against the repo `target/` — it is the user's ~399 GB interactive dir and is contended. Full-workspace integration checks should use their own target dir so they do not block unrelated workers. Reclaim disk with `rm -rf "$CARGO_TARGET_DIR"` before completing. Full policy: `docs/CARGO-CONTENTION-POLICY.md`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 407ac38b..a6da5a13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,37 +44,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.0.2] - 2026-06-18 ### Changed -- **Project renamed: TokenSave → TraceDecay.** The crate, binary, CLI command, and MCP server are now `tracedecay`, and the MCP tools are prefixed `tracedecay_*`. The data directory is now `.tracedecay/` (an existing `.tokensave/` directory is still honored as a fallback), and environment variables use the `TRACEDECAY_*` prefix (legacy `TOKENSAVE_*` variables are still honored as a fallback). All entries below predate the rename and intentionally retain the historical `tokensave`/`TokenSave` names. +- **Project renamed to TraceDecay.** The crate, binary, CLI command, and MCP server are now `tracedecay`, and the MCP tools are prefixed `tracedecay_*`. The data directory is now `.tracedecay/`, and environment variables use the `TRACEDECAY_*` prefix. All entries below predate the rename and intentionally retain their historical names. - **Version reset for the rebranded crate.** `tracedecay` now restarts at `0.0.2` as a fresh crate line; `0.0.1` is already occupied on crates.io by the name-reservation placeholder, so `0.0.2` is the first publishable release number. ### Added -- **Local web dashboard (`tokensave dashboard` + `tokensave_dashboard` MCP tool).** A self-contained axum server with compile-time-embedded UI assets serving three tabs: **Holographic Memory** (fact/entity/bank inspector, 2D PCA semantic map, association graph, phase-cosine similarity explorer with brushable histogram, and feature-flagged curation), **LCM** (overview, FTS search with role/source/session facets, session/node drilldowns, timeline, compression analytics over the global DB), and **Code Graph** (overview analytics plus a force-directed canvas explorer with search-to-focus, progressive neighbor expansion, callers/callees, filters, and shortest-path mode). CLI flags: `--path`, `--host`, `--port` (0 = auto, parseable URL on stdout), `--open`. The `tokensave_dashboard` MCP tool starts/stops the same server as a background task and returns the URL. `GET /api/capabilities` advertises feature flags (`memory`, `lcm`, `graph`, `curation`, `llm_curation`) for host/UI feature detection. Dark + light themes, responsive down to ~420px. +- **Local web dashboard (`tracedecay dashboard` + `tracedecay_dashboard` MCP tool).** A self-contained axum server with compile-time-embedded UI assets serving three tabs: **Holographic Memory** (fact/entity/bank inspector, 2D PCA semantic map, association graph, phase-cosine similarity explorer with brushable histogram, and feature-flagged curation), **LCM** (overview, FTS search with role/source/session facets, session/node drilldowns, timeline, compression analytics over the global DB), and **Code Graph** (overview analytics plus a force-directed canvas explorer with search-to-focus, progressive neighbor expansion, callers/callees, filters, and shortest-path mode). CLI flags: `--path`, `--host`, `--port` (0 = auto, parseable URL on stdout), `--open`. The `tracedecay_dashboard` MCP tool starts/stops the same server as a background task and returns the URL. `GET /api/capabilities` advertises feature flags (`memory`, `lcm`, `graph`, `curation`, `llm_curation`) for host/UI feature detection. Dark + light themes, responsive down to ~420px. - **Memory curation with hard-delete semantics.** `POST /api/plugins/holographic/curate` proposes (dry-run) or applies similarity-based deduplication: the lower-trust fact of each `likely_duplicate` pair is permanently deleted via the canonical store path (FK-cascaded entity links, FTS trigger cleanup, bank dirty-marking) — no archive state, no restore. `POST /curate/apply` exposes a generic delete/merge ops contract for external (e.g. LLM-backed) planners; per-op failures are reported per-op. Migration v13 only cleans up a never-shipped archive-column experiment from local dev databases. -- **Hermes wrapper for the dashboard.** `dashboard/hermes-wrapper/` (canonical; deployed to the hermes-agent working tree) reverse-proxies `/holographic/*`, `/lcm/*`, and `/graph/*` to a spawned or externally configured tokensave dashboard, re-hosts the same UI bundles under the combined "TokenSave" tab, layers an optional LLM curation planner on the `/curate/apply` contract, and hardens the subprocess lifecycle (stderr drain, parent-death signal, spawn-failure backoff). +- **Hermes wrapper for the dashboard.** `dashboard/hermes-wrapper/` (canonical; deployed to the hermes-agent working tree) reverse-proxies `/holographic/*`, `/lcm/*`, and `/graph/*` to a spawned or externally configured tracedecay dashboard, re-hosts the same UI bundles under the combined "TraceDecay" tab, layers an optional LLM curation planner on the `/curate/apply` contract, and hardens the subprocess lifecycle (stderr drain, parent-death signal, spawn-failure backoff). - **Dashboard build + test infrastructure.** `dashboard/` npm workspace (esbuild) building all bundles, 16 frontend unit tests (`node run-unit-tests.mjs`), Playwright smoke (`npm run smoke -- --expect-lcm=empty|non-empty`), Rust integration suites (`tests/dashboard_api_test.rs`, `dashboard_lcm_fixes_test.rs`, `dashboard_graph_api_test.rs`, `mcp_dashboard_tool_test.rs`), a dashboard CI job, and `build.rs` rerun-if-changed guards so frontend dist changes force re-embedding. -- **Curation previews survive dashboard restarts.** The last dry-run curation plan is mirrored to a `.tokensave/dashboard/curation_preview.json` sidecar and re-hydrated when the server starts; applying curation (or any `/curate/apply` mutation) clears both the in-memory copy and the sidecar. The `GET /curation/preview` API shape is unchanged, and staleness is still recomputed against the live fact count on every read. -- **`tokensave install --agent hermes` deploys the dashboard plugin page.** The Hermes wrapper (manifest, `plugin_api.py` reverse proxy, and the UI bundles — all embedded in the binary, no source checkout needed) is now written to `/plugins/tokensave/dashboard/` as part of the default install, where Hermes' dashboard-plugin discovery (stock and forked) picks it up as a "TokenSave" tab with Memory / LCM / Code Graph / Savings sub-tabs. The deployed proxy bakes in the installing binary path and the profile's pinned `project_root` as spawn-mode defaults (`TOKENSAVE_BIN` / `TOKENSAVE_DASHBOARD_PROJECT` env vars still win); reinstalls preserve the pin, `--no-dashboard` opts out (and removes a previous deploy), and uninstall cleans the page up. The wrapper also gained the Savings sub-tab (`/savings/*` proxy to `/api/plugins/savings/*`). On Hermes versions without dashboard-plugin discovery the deployed directory is inert. +- **Curation previews survive dashboard restarts.** The last dry-run curation plan is mirrored to a `.tracedecay/dashboard/curation_preview.json` sidecar and re-hydrated when the server starts; applying curation (or any `/curate/apply` mutation) clears both the in-memory copy and the sidecar. The `GET /curation/preview` API shape is unchanged, and staleness is still recomputed against the live fact count on every read. +- **`tracedecay install --agent hermes` deploys the dashboard plugin page.** The Hermes wrapper (manifest, `plugin_api.py` reverse proxy, and the UI bundles — all embedded in the binary, no source checkout needed) is now written to `/plugins/tracedecay/dashboard/` as part of the default install, where Hermes' dashboard-plugin discovery (stock and forked) picks it up as a "TraceDecay" tab with Memory / LCM / Code Graph / Savings sub-tabs. The deployed proxy bakes in the installing binary path and the profile's pinned `project_root` as spawn-mode defaults (`TRACEDECAY_BIN` / `TRACEDECAY_DASHBOARD_PROJECT` env vars still win); reinstalls preserve the pin, `--no-dashboard` opts out (and removes a previous deploy), and uninstall cleans the page up. The wrapper also gained the Savings sub-tab (`/savings/*` proxy to `/api/plugins/savings/*`). On Hermes versions without dashboard-plugin discovery the deployed directory is inert. - **Dashboard assets build themselves on fresh checkouts.** When the embedded `dashboard/*/dist` bundles are missing, `build.rs` now runs the frontend build automatically (`npm ci`, falling back to `npm install`, then `npm run build`) with progress reported as build warnings — so `cargo build` / `cargo install --path .` work from a clean clone. If npm is unavailable, the build still fails fast with actionable instructions. `Cargo.toml` switched to an explicit `package.include` whitelist that ships the prebuilt dist bundles inside the crate package, making `cargo package`/`cargo publish` verifiable and letting crates.io/docs.rs builds proceed with no Node.js toolchain. The release workflows (`release.yml` build + publish-crate jobs, `release-beta.yml`) gained the same dashboard prebuild step as CI. - **Tokenizer-backed cost tier for the Savings & Cost tab (`token-counting` feature, on by default).** When transcripts carry no usage counters (all Cursor stores — verified to contain none — plus cline/vibe and any Codex/Claude rows without usage), stored message text is now counted with a real BPE tokenizer (tiktoken-rs, `o200k_base`/`cl100k_base`) instead of the chars/4 heuristic: exact for OpenAI-family models, a labeled `≈` approximation for vendors without a public tokenizer (Claude/Gemini). The API gains a third `cost_basis` value `"tokenized"` (between `"actual"` and `"estimated"`; `"mixed"` semantics unchanged), additive `tokenized` token blocks, `tokenized_messages` counts, and per-model `tokenizer` provenance (`{"encoder", "exact"}`); the UI shows tier badges and an updated methodology note. Counts are cached per message (in-process map + a `dashboard_token_counts` sidecar table in the global accounting DB, keyed by message identity with a text-length guard) and pre-warmed in the background at dashboard startup, so 15k+-message stores pay the BPE pass once instead of per request. Disable the feature for a leaner binary (~4 MB embedded vocabularies, lazily decoded) — everything degrades to the chars/4 tier. ### Fixed -- **The savings ledger records by default again — the Savings tab is no longer empty while lifetime counters grow.** The holographic-fact-store commit made the MCP server's global accounting DB opt-in via `TOKENSAVE_ENABLE_GLOBAL_DB`, which silently disabled `savings_ledger` writes (and worldwide-counter flushes) for every default install: tool calls still printed `tokensave_metrics` lines and CLI paths kept growing `projects.tokens_saved`, but the dashboard showed "ledger calls: 0 / no events yet". Global accounting is now **on by default**; opt out with `TOKENSAVE_DISABLE_GLOBAL_DB=1` (set automatically for cargo-launched processes via `.cargo/config.toml` so test runs stay hermetic) or `TOKENSAVE_ENABLE_GLOBAL_DB=0`, with an explicit `TOKENSAVE_ENABLE_GLOBAL_DB=1` always winning. The dashboard now also surfaces the gate verdict (`savings.recording` in the overview API, a `recording: on/off` badge, and an honest explanation when the ledger is empty — including the "restart your MCP server to pick this up" case) instead of an unconditional "no events yet". Covered by a default-on ledger regression test plus env-precedence unit tests; long-running MCP servers must be restarted/reloaded to pick up the fix. -- **Hermes wrapper spawn mode no longer drops its child server after idle periods.** The wrapper's Linux parent-death guard (`PR_SET_PDEATHSIG`) fires when the *thread* that forked the child exits — and FastAPI sync endpoints run on anyio threadpool workers that are reaped after ~10s idle, so the spawned `tokensave dashboard` was SIGTERMed shortly after quiet spells (surfacing as intermittent 502 "connection reset by peer" on the next tab click). `plugin_api.py` now spawns from a single long-lived worker thread, binding the child's lifetime to the Hermes host process as intended. +- **The savings ledger records by default again — the Savings tab is no longer empty while lifetime counters grow.** The holographic-fact-store commit made the MCP server's global accounting DB opt-in via `TRACEDECAY_ENABLE_GLOBAL_DB`, which silently disabled `savings_ledger` writes (and worldwide-counter flushes) for every default install: tool calls still printed `tracedecay_metrics` lines and CLI paths kept growing `projects.tokens_saved`, but the dashboard showed "ledger calls: 0 / no events yet". Global accounting is now **on by default**; opt out with `TRACEDECAY_DISABLE_GLOBAL_DB=1` (set automatically for cargo-launched processes via `.cargo/config.toml` so test runs stay hermetic) or `TRACEDECAY_ENABLE_GLOBAL_DB=0`, with an explicit `TRACEDECAY_ENABLE_GLOBAL_DB=1` always winning. The dashboard now also surfaces the gate verdict (`savings.recording` in the overview API, a `recording: on/off` badge, and an honest explanation when the ledger is empty — including the "restart your MCP server to pick this up" case) instead of an unconditional "no events yet". Covered by a default-on ledger regression test plus env-precedence unit tests; long-running MCP servers must be restarted/reloaded to pick up the fix. +- **Hermes wrapper spawn mode no longer drops its child server after idle periods.** The wrapper's Linux parent-death guard (`PR_SET_PDEATHSIG`) fires when the *thread* that forked the child exits — and FastAPI sync endpoints run on anyio threadpool workers that are reaped after ~10s idle, so the spawned `tracedecay dashboard` was SIGTERMed shortly after quiet spells (surfacing as intermittent 502 "connection reset by peer" on the next tab click). `plugin_api.py` now spawns from a single long-lived worker thread, binding the child's lifetime to the Hermes host process as intended. - **Hermes wrapper cold starts no longer 502.** After spawning, the wrapper now waits (bounded, 30s) for the engine's `/api/capabilities` to answer before proxying the first request, returns a clear `503` with `Retry-After` if the engine truly fails to come up, and transparently retries GET proxies once after re-resolving the upstream (which reaps and respawns a dead child). POSTs are never retried so curation applies cannot run twice. -- **Fallback branch DBs are now read-only for sync/index writes.** `tokensave sync`, lazy single-file syncs, and full indexing now refuse to write when the active git branch is being served from an ancestor branch database, preventing branch-only files from being indexed into the fallback DB. -- **`tokensave install --agent hermes` generates a plugin that loads on newer Hermes hosts.** Four generator/installer fixes: (1) the generated `TokenSaveContextEngine` implements the now-abstract `update_from_response(usage)` method (normalizes `prompt/input`, `completion/output`, and `total` token counts into `last_*_tokens` attributes), so plugin load no longer dies with `Can't instantiate abstract class`; (2) the skill registers under the bare name `tokensave` — newer Hermes derives the namespace from the plugin and rejects `:` in skill names; (3) the installer now matches the existing indentation of `plugins.enabled`/`plugins.disabled` lists (Hermes writes 2-space items) instead of always inserting 4-space items, which produced unparseable mixed-indent YAML; and (4) flow-style empty lists (`disabled: []`, which Hermes itself writes) are accepted instead of failing with "unsupported Hermes plugins config" — an empty `enabled: []` is rewritten to a block list. The generated context engine additionally honors a `project_root` config key so profiles can pin the indexed project (explicit host kwargs win; the session cwd stays the last fallback). -- **`tokensave install --agent hermes --project-root ` pins a profile's plugin to one project.** The pin is written into the generated plugin (`PINNED_PROJECT_ROOT` in `tools.py`): every plugin tool call then passes `--project ` so memory + LCM stores resolve to `/.tokensave/` regardless of the Hermes process cwd, and the context engine uses it ahead of cwd inference (kwargs > config > pin > cwd). Reinstalls without the flag preserve an existing pin; the flag is hermes-only, requires an absolute path, and conflicts with `--all-profiles` (pins are per-profile). -- **`tokensave tool` now walks up from subdirectories to the nearest initialised project** when `--project` is not given, matching how `sync`, `status`, `serve`, and `dashboard` resolve project roots. +- **Fallback branch DBs are now read-only for sync/index writes.** `tracedecay sync`, lazy single-file syncs, and full indexing now refuse to write when the active git branch is being served from an ancestor branch database, preventing branch-only files from being indexed into the fallback DB. +- **`tracedecay install --agent hermes` generates a plugin that loads on newer Hermes hosts.** Four generator/installer fixes: (1) the generated `TraceDecayContextEngine` implements the now-abstract `update_from_response(usage)` method (normalizes `prompt/input`, `completion/output`, and `total` token counts into `last_*_tokens` attributes), so plugin load no longer dies with `Can't instantiate abstract class`; (2) the skill registers under the bare name `tracedecay` — newer Hermes derives the namespace from the plugin and rejects `:` in skill names; (3) the installer now matches the existing indentation of `plugins.enabled`/`plugins.disabled` lists (Hermes writes 2-space items) instead of always inserting 4-space items, which produced unparseable mixed-indent YAML; and (4) flow-style empty lists (`disabled: []`, which Hermes itself writes) are accepted instead of failing with "unsupported Hermes plugins config" — an empty `enabled: []` is rewritten to a block list. The generated context engine additionally honors a `project_root` config key so profiles can pin the indexed project (explicit host kwargs win; the session cwd stays the last fallback). +- **`tracedecay install --agent hermes --project-root ` pins a profile's plugin to one project.** The pin is written into the generated plugin (`PINNED_PROJECT_ROOT` in `tools.py`): every plugin tool call then passes `--project ` so memory + LCM stores resolve to `/.tracedecay/` regardless of the Hermes process cwd, and the context engine uses it ahead of cwd inference (kwargs > config > pin > cwd). Reinstalls without the flag preserve an existing pin; the flag is hermes-only, requires an absolute path, and conflicts with `--all-profiles` (pins are per-profile). +- **`tracedecay tool` now walks up from subdirectories to the nearest initialised project** when `--project` is not given, matching how `sync`, `status`, `serve`, and `dashboard` resolve project roots. - **Cursor hook hints use the quote-aware shell parser.** `tool_hints` classified search commands with a naive `split_whitespace`, so a quoted pattern like `grep "needle -r" file` leaked a fake `-r` flag and misclassified as a recursive search. It now shares `hooks.rs`'s quote/escape-aware `shell_words` parser (single shared implementation, regression-tested). -- **Hermes generated plugin files are written atomically.** `write_text_file` now uses the write-to-`.new`-then-rename pattern (like the config writer), so a mid-write crash can no longer leave a truncated `__init__.py`/`tools.py` behind. Unsupported-config errors during install also name the exact retry command (`tokensave install --agent hermes`). +- **Hermes generated plugin files are written atomically.** `write_text_file` now uses the write-to-`.new`-then-rename pattern (like the config writer), so a mid-write crash can no longer leave a truncated `__init__.py`/`tools.py` behind. Unsupported-config errors during install also name the exact retry command (`tracedecay install --agent hermes`). - **v13 archive-column cleanup handles generated-column dev databases.** The migration enumerated columns with `PRAGMA table_info`, which hides GENERATED columns — so a dev DB where the abandoned archive revision left `superseded_by` as a generated column referencing `merged_into` skipped that drop and then failed with `no such column: merged_into`. The migration now uses `PRAGMA table_xinfo` and drops the columns in reverse-addition order (dependent generated columns first). Covered by a regression test seeding exactly that odd state. - **Archive-semantics purge (policy: deleted memories are permanently hard-deleted).** Removed the last UI remnants of the never-shipped archive feature: the CurationPanel no longer recognizes/renders `archive` or `supersede` ops (neither planner can produce them; the curate ops contract is delete/merge only), the `archive` action field is gone from the frontend types, and stale Hermes-wrapper docstrings naming `archive`/`archive/{fact_id}/restore` routes were corrected. A new store-level test pins the full hard-delete cascade: `MemoryStore::remove_fact` removes the fact row, its FTS mirror row, its entity links, and its feedback events, and marks the fact's banks dirty. ## [6.1.3] - 2026-06-04 ### Fixed -- **Write/exec MCP tools no longer advertise `readOnlyHint: true` (#94).** `tokensave_replace_symbol`, `tokensave_insert_at_symbol`, and `tokensave_run_affected_tests` mutate source files or run a `cargo test` subprocess, but were annotated read-only via the shared `def()` helper — so harnesses that auto-approve read-only tools could edit files or compile and execute project code without prompting. They now use a new `def_rw()` helper that stamps `readOnlyHint: false`, matching the other edit tools. A regression test asserts every write/exec tool is non-read-only. +- **Write/exec MCP tools no longer advertise `readOnlyHint: true` (#94).** `tracedecay_replace_symbol`, `tracedecay_insert_at_symbol`, and `tracedecay_run_affected_tests` mutate source files or run a `cargo test` subprocess, but were annotated read-only via the shared `def()` helper — so harnesses that auto-approve read-only tools could edit files or compile and execute project code without prompting. They now use a new `def_rw()` helper that stamps `readOnlyHint: false`, matching the other edit tools. A regression test asserts every write/exec tool is non-read-only. ## [6.1.2] - 2026-05-30 @@ -88,14 +88,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.1.1] - 2026-05-26 ### Added -- **Borrowed-worktree detection on every MCP read tool.** When a git worktree is nested inside the main checkout (e.g. agent tooling that puts worktrees under `.claude/worktrees//` or `.worktrees//`), tokensave's `discover_project_root` walks up and silently resolves the MAIN checkout's `.tokensave/` — returning results for the wrong branch with no warning, while symbols changed only in the worktree are invisible. New `src/worktree.rs` runs `git rev-parse --show-toplevel` on the caller's CWD and on the resolved index root; when they belong to different working trees, the verbose warning is included in `tokensave_status` and a one-line notice is prefixed to every read tool response. Detection runs once at server startup (≤2 `git rev-parse` spawns total per session). Ported from codegraph #312. +- **Borrowed-worktree detection on every MCP read tool.** When a git worktree is nested inside the main checkout (e.g. agent tooling that puts worktrees under `.claude/worktrees//` or `.worktrees//`), tracedecay's `discover_project_root` walks up and silently resolves the MAIN checkout's `.tracedecay/` — returning results for the wrong branch with no warning, while symbols changed only in the worktree are invisible. New `src/worktree.rs` runs `git rev-parse --show-toplevel` on the caller's CWD and on the resolved index root; when they belong to different working trees, the verbose warning is included in `tracedecay_status` and a one-line notice is prefixed to every read tool response. Detection runs once at server startup (≤2 `git rev-parse` spawns total per session). Ported from codegraph #312. - **Catch-up sync on MCP connect.** `McpServer::new` now spawns a non-blocking task that runs `find_stale_files` + `sync_if_stale_silent` + `refresh_file_token_map` once at startup, bypassing the 30 s cooldown. Picks up changes made while the server was down — terminal `git pull`, IDE edits before the agent launched, files touched by another tool — so the first tool call sees a fresh index instead of waiting through the cooldown. Ported from codegraph #414. - **`scripts/prepare-release.py` to auto-promote `[Unreleased]` → `[]` in CHANGELOG.md.** Idempotently renames the `[Unreleased]` block to a dated `[]` block at release time (Case A), or merges into a pre-existing `[]` block by sub-section (Case B). Avoids the codegraph v0.9.5 failure mode where a sparse hand-staged `[]` block silently shadowed the much-larger `[Unreleased]` section above it during release-notes extraction. Ported from codegraph #436. Wire into the release workflow when a version bump lands. ### Changed - **Embedded MCP watcher replaced with on-demand staleness check (#80).** The `notify-debouncer-full` watcher was the source of severe CPU and memory pressure on large monorepos: top-level-only filtering of `IGNORED_DIRS` meant nested `apps/*/node_modules`, `packages/*/target`, `**/dist` were watched at the OS level, producing event storms and unbounded `RecommendedCache` growth (one user reported the process climbing to 19 GB before being killed). The watcher is now gone — along with the `notify-debouncer-full` dependency. Index freshness is maintained by a lazy `find_stale_files` walk (same gitignore-aware logic `sync()` uses) gated by a 30-second cooldown and invoked at the top of every MCP `tools/call`. Cost: walks on the cold tool call after a quiet window add tens of ms to milliseconds depending on repo size; in exchange, the unbounded-memory class of bug is structurally gone. Reported by @AGiorgetti and @ottob. -- **Per-file staleness banner replaces the binary "STALE INDEX" warning.** Tool responses that referenced files whose in-line sync couldn't refresh now get a focused banner naming exactly those files with their edit ages (e.g. `src/foo.rs (edited 3m ago)`) and an explicit instruction to `Read` those files directly — while telling the agent the rest of the response is authoritative. Replaces the previous all-or-nothing wording that made agents distrust the entire response. The machine-readable `tokensave_graph_stale` marker is preserved. Ported from codegraph #428. -- **Kiro steering is loaded as a resource.** The Kiro installer now writes `~/.kiro/steering/tokensave.md`, loads it from the managed agent's `resources` list with an absolute `file://` URI, leaves the custom-agent `prompt` unset so Kiro's default prompt is preserved, keeps MCP approval policy out of `mcp.json`, and installs permissive `tools: ["*"]` plus `allowedTools: ["@builtin", "@tokensave"]` defaults for the managed agent. +- **Per-file staleness banner replaces the binary "STALE INDEX" warning.** Tool responses that referenced files whose in-line sync couldn't refresh now get a focused banner naming exactly those files with their edit ages (e.g. `src/foo.rs (edited 3m ago)`) and an explicit instruction to `Read` those files directly — while telling the agent the rest of the response is authoritative. Replaces the previous all-or-nothing wording that made agents distrust the entire response. The machine-readable `tracedecay_graph_stale` marker is preserved. Ported from codegraph #428. +- **Kiro steering is loaded as a resource.** The Kiro installer now writes `~/.kiro/steering/tracedecay.md`, loads it from the managed agent's `resources` list with an absolute `file://` URI, leaves the custom-agent `prompt` unset so Kiro's default prompt is preserved, keeps MCP approval policy out of `mcp.json`, and installs permissive `tools: ["*"]` plus `allowedTools: ["@builtin", "@tracedecay"]` defaults for the managed agent. ### Fixed - **Windows: same physical file no longer indexed twice under both path-separator variants (#87).** On Windows, sync entry points that accepted caller-supplied relative paths (`sync_if_stale`, `sync_if_stale_silent`, `sync_single_files`, `check_file_staleness`) did not normalize backslashes before hitting the DB. When an MCP tool response carried OS-native paths (`src\foo.py`) into the staleness check, the lookup missed the canonical row stored under `src/foo.py`, the file got re-indexed under the backslash variant, and the `files` table grew a duplicate row for every physical source file. Downstream tools (`god_class`, `complexity`, `dead_code`, `redundancy`, `unused_imports`) returned doubled results; the `health` `redundancy` dimension reported ~0.39 instead of ~0.50+. Fix: defensive `normalize_rel_path` (single `replace('\\', "/")`) applied at all four sync entry points, with regression tests. Reported by @xaerogonzo. @@ -103,30 +103,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.1.0] - 2026-05-25 ### Added -- **`tokensave tool ` — schema-driven CLI dispatcher.** Every MCP tool is now reachable from the command line through a single dynamic subcommand that introspects each tool's JSON schema and coerces `--key value` flags accordingly. `tokensave tool` (no args) lists tools grouped by category; `tokensave tool --help` prints schema-derived parameters. Reserved flags: `--json` (raw response), `--project `, `--args `, `-h`/`--help`. Positional args bind to required string properties (e.g. `tokensave tool search foo`). `@file` values are read from disk for multi-line strings. Replaces seven hand-rolled subcommands (`query`, `context`, `body`, `impact`, `callers`, `files`, `affected`); `query` is kept as an alias for the renamed `search`. New file: `src/tool_command.rs` (~729 LoC). -- **`tokensave_find_exact_symbol` MCP tool.** Bare-name lookup against `idx_nodes_name` — a single O(log n) index probe with no BM25 ranking, no fuzzy match, no qualified-name suffix walk. Use this when the symbol name is already known; use `tokensave_search` for relevance-ranked discovery. ~30–200 µs per lookup vs. ~700 µs for the BM25 path. -- **`tokensave_call_chain` MCP tool.** Finds the shortest *directed* call chain between two node IDs along outgoing `calls` edges only. New `find_path_directed` BFS in `graph/traversal.rs` (the existing `find_path` is bidirectional and wrong for "how does A reach B" questions). Bounded by `max_depth` (default 8, max 20). -- **`tokensave_file_dependents` MCP tool.** Lists every indexed file that imports or otherwise depends on the given file. Thin wrapper around the existing `TokenSave::get_file_dependents` that was previously only reachable through `tokensave_affected`'s test-rollup path. -- **`tokensave_replace_symbol` MCP tool.** Symbol-aware body replacement: resolves a name via exact qualified-name match, narrows to callable kinds on ambiguity, and refuses the edit rather than picking the wrong site if more than one callable matches. Reads the file, splices the symbol's `start_line..=end_line` range with `new_source`, writes back, and reindexes the touched file. Plays the role of token-savior's `replace_symbol_source`. -- **`tokensave_insert_at_symbol` MCP tool.** Inserts content immediately before or after a named symbol's source range — same resolution semantics as `tokensave_replace_symbol`. `position` is `"before"` or `"after"` (default after). Plays the role of token-savior's `insert_near_symbol`. -- **`tokensave serve --timings` flag.** When set, every `tools/call` response gains a `_meta.duration_us` field reporting the handler's pure execution time in microseconds. Lets clients (and benchmarks) attribute latency to actual query work vs. JSON-RPC / stdio / Python-parse overhead. Toggleable at runtime through the new `McpServer::set_timings_enabled` setter so embedders can flip it per-session. -- **Indexer benchmark harness** at `benchmarks/run_benchmarks.py` — adapts `Mibayy/token-savior`'s `run_benchmarks.py` to drive both tools side-by-side on the same clone of FastAPI, sharing a random symbol sample (seed=42) so per-query rows are directly comparable. tokensave is driven through a long-lived `tokensave serve --timings` MCP session for the query column. Latest report (`benchmarks/comparison-report.md`): cold index 2.9× faster, impact analysis 43× faster than token-savior. -- **tsbench fork** at `benchmarks/tsbench/` — patch + reproduction README + per-run summary for running `Mibayy/tsbench` (token-savior's own 96-task agent benchmark) against tokensave. First-attempt untuned result: 184/192 = 95.8% vs. token-savior's audited 97.9%. The harness rewrites `SYSTEM_PROMPT_TS` to map each token-savior tool to its tokensave equivalent and relaxes the `--disallowedTools` list to allow `Read`/`Edit` fallback on the four task categories tokensave has no direct tool for. -- **`docs/TOKENSAVE-VS-TOKENSAVIOR.md`** — full capability + performance comparison document covering parsing strategy (regex annotators vs. tree-sitter grammars), the 11 health-analytics tools that have no token-savior equivalent, query-latency numbers (apples-to-apples find / body / impact), the tsbench 184/192 result with per-task failure analysis, and an honest "when to use which" guide. +- **`tracedecay tool ` — schema-driven CLI dispatcher.** Every MCP tool is now reachable from the command line through a single dynamic subcommand that introspects each tool's JSON schema and coerces `--key value` flags accordingly. `tracedecay tool` (no args) lists tools grouped by category; `tracedecay tool --help` prints schema-derived parameters. Reserved flags: `--json` (raw response), `--project `, `--args `, `-h`/`--help`. Positional args bind to required string properties (e.g. `tracedecay tool search foo`). `@file` values are read from disk for multi-line strings. Replaces seven hand-rolled subcommands (`query`, `context`, `body`, `impact`, `callers`, `files`, `affected`); `query` is kept as an alias for the renamed `search`. New file: `src/tool_command.rs` (~729 LoC). +- **`tracedecay_find_exact_symbol` MCP tool.** Bare-name lookup against `idx_nodes_name` — a single O(log n) index probe with no BM25 ranking, no fuzzy match, no qualified-name suffix walk. Use this when the symbol name is already known; use `tracedecay_search` for relevance-ranked discovery. ~30–200 µs per lookup vs. ~700 µs for the BM25 path. +- **`tracedecay_call_chain` MCP tool.** Finds the shortest *directed* call chain between two node IDs along outgoing `calls` edges only. New `find_path_directed` BFS in `graph/traversal.rs` (the existing `find_path` is bidirectional and wrong for "how does A reach B" questions). Bounded by `max_depth` (default 8, max 20). +- **`tracedecay_file_dependents` MCP tool.** Lists every indexed file that imports or otherwise depends on the given file. Thin wrapper around the existing `TraceDecay::get_file_dependents` that was previously only reachable through `tracedecay_affected`'s test-rollup path. +- **`tracedecay_replace_symbol` MCP tool.** Symbol-aware body replacement: resolves a name via exact qualified-name match, narrows to callable kinds on ambiguity, and refuses the edit rather than picking the wrong site if more than one callable matches. Reads the file, splices the symbol's `start_line..=end_line` range with `new_source`, writes back, and reindexes the touched file. Plays the role of token-savior's `replace_symbol_source`. +- **`tracedecay_insert_at_symbol` MCP tool.** Inserts content immediately before or after a named symbol's source range — same resolution semantics as `tracedecay_replace_symbol`. `position` is `"before"` or `"after"` (default after). Plays the role of token-savior's `insert_near_symbol`. +- **`tracedecay serve --timings` flag.** When set, every `tools/call` response gains a `_meta.duration_us` field reporting the handler's pure execution time in microseconds. Lets clients (and benchmarks) attribute latency to actual query work vs. JSON-RPC / stdio / Python-parse overhead. Toggleable at runtime through the new `McpServer::set_timings_enabled` setter so embedders can flip it per-session. +- **Indexer benchmark harness** at `benchmarks/run_benchmarks.py` — adapts `Mibayy/token-savior`'s `run_benchmarks.py` to drive both tools side-by-side on the same clone of FastAPI, sharing a random symbol sample (seed=42) so per-query rows are directly comparable. tracedecay is driven through a long-lived `tracedecay serve --timings` MCP session for the query column. Latest report (`benchmarks/comparison-report.md`): cold index 2.9× faster, impact analysis 43× faster than token-savior. +- **tsbench fork** at `benchmarks/tsbench/` — patch + reproduction README + per-run summary for running `Mibayy/tsbench` (token-savior's own 96-task agent benchmark) against tracedecay. First-attempt untuned result: 184/192 = 95.8% vs. token-savior's audited 97.9%. The harness rewrites `SYSTEM_PROMPT_TS` to map each token-savior tool to its tracedecay equivalent and relaxes the `--disallowedTools` list to allow `Read`/`Edit` fallback on the four task categories tracedecay has no direct tool for. +- **`docs/TRACEDECAY-VS-TOKENSAVIOR.md`** — full capability + performance comparison document covering parsing strategy (regex annotators vs. tree-sitter grammars), the 11 health-analytics tools that have no token-savior equivalent, query-latency numbers (apples-to-apples find / body / impact), the tsbench 184/192 result with per-task failure analysis, and an honest "when to use which" guide. ### Fixed -- **`tokensave serve` no longer blocks MCP `initialize` on the watcher's filesystem walk (#84).** Constructing the embedded `notify_debouncer_full` watcher does a synchronous `walkdir` over every registered subtree to seed its file-id map. On a large JS/TS monorepo with multi-gigabyte `node_modules` / `.next` / `dist` trees this can take 30+ seconds — long enough to blow the client's `initialize` timeout. Fix: `ProjectWatcher::new` now runs inside `tokio::task::spawn_blocking` from a detached `tokio::spawn`, so `McpServer::new` returns immediately and the MCP stdio loop can answer `initialize` / `tools/list` in milliseconds. The `CancellationToken` is stored on the server up front so `shutdown` can cancel mid-walk if the agent disconnects before the watcher finishes initialising. Reported by @ottob with a sample-trace and an FSEvents-sandbox repro that left zero ambiguity about root cause. -- **`tokensave serve` no longer runs pre-serve maintenance work (#84).** `Commands::Serve` was running `try_flush` (synchronous HTTP round-trip to the worldwide counter), `check_install_stale`, and the silent-reinstall loop over every tracked agent before the MCP stdio loop even started. All three are now gated behind `should_skip_agent_install_maintenance`, alongside `Install` / `Reinstall` / `Uninstall` / `Doctor`. Same maintenance still runs on the user's next interactive `tokensave …` invocation. -- **`tokensave install --agent antigravity` now registers in both the IDE config and the CLI plugin directory (#85).** Previously only `~/.gemini/antigravity/mcp_config.json` was written, leaving the Antigravity CLI (`agy`) unable to see tokensave in `/mcp`. New: also writes `~/.gemini/antigravity-cli/plugins/tokensave.json` with the same `{"mcpServers": {"tokensave": {...}}}` shape. `uninstall` removes both, `doctor` reports both, `is_detected` triggers on either path. Reported by @ottob. -- **MCP `last synced N ago` warning no longer fires after a no-change sync (#86).** The warning was reading `MAX(files.indexed_at)`, which only advances when a file is actually reindexed. On quiet repos a successful `tokensave sync` (0 added / 0 modified / 0 removed) left `indexed_at` stuck and the warning fired forever. New: the warning is computed from the `last_sync_at` metadata key, which `sync()` writes unconditionally on every successful invocation. Falls back to `MAX(indexed_at)` only when the metadata key is missing (e.g. a freshly-initialised project that has never been synced). New `TokenSave::last_sync_timestamp()` helper exposes this for embedders. Reported by @uwe-sure. +- **`tracedecay serve` no longer blocks MCP `initialize` on the watcher's filesystem walk (#84).** Constructing the embedded `notify_debouncer_full` watcher does a synchronous `walkdir` over every registered subtree to seed its file-id map. On a large JS/TS monorepo with multi-gigabyte `node_modules` / `.next` / `dist` trees this can take 30+ seconds — long enough to blow the client's `initialize` timeout. Fix: `ProjectWatcher::new` now runs inside `tokio::task::spawn_blocking` from a detached `tokio::spawn`, so `McpServer::new` returns immediately and the MCP stdio loop can answer `initialize` / `tools/list` in milliseconds. The `CancellationToken` is stored on the server up front so `shutdown` can cancel mid-walk if the agent disconnects before the watcher finishes initialising. Reported by @ottob with a sample-trace and an FSEvents-sandbox repro that left zero ambiguity about root cause. +- **`tracedecay serve` no longer runs pre-serve maintenance work (#84).** `Commands::Serve` was running `try_flush` (synchronous HTTP round-trip to the worldwide counter), `check_install_stale`, and the silent-reinstall loop over every tracked agent before the MCP stdio loop even started. All three are now gated behind `should_skip_agent_install_maintenance`, alongside `Install` / `Reinstall` / `Uninstall` / `Doctor`. Same maintenance still runs on the user's next interactive `tracedecay …` invocation. +- **`tracedecay install --agent antigravity` now registers in both the IDE config and the CLI plugin directory (#85).** Previously only `~/.gemini/antigravity/mcp_config.json` was written, leaving the Antigravity CLI (`agy`) unable to see tracedecay in `/mcp`. New: also writes `~/.gemini/antigravity-cli/plugins/tracedecay.json` with the same `{"mcpServers": {"tracedecay": {...}}}` shape. `uninstall` removes both, `doctor` reports both, `is_detected` triggers on either path. Reported by @ottob. +- **MCP `last synced N ago` warning no longer fires after a no-change sync (#86).** The warning was reading `MAX(files.indexed_at)`, which only advances when a file is actually reindexed. On quiet repos a successful `tracedecay sync` (0 added / 0 modified / 0 removed) left `indexed_at` stuck and the warning fired forever. New: the warning is computed from the `last_sync_at` metadata key, which `sync()` writes unconditionally on every successful invocation. Falls back to `MAX(indexed_at)` only when the metadata key is missing (e.g. a freshly-initialised project that has never been synced). New `TraceDecay::last_sync_timestamp()` helper exposes this for embedders. Reported by @uwe-sure. - **`files_by_language` status output now uses real language names instead of bucketing everything as `Other`.** The SQL `CASE` in `Database::get_stats` only recognised four languages (Rust / Go / Java / Scala) and dumped everything else — Python, TypeScript, C, Swift, Kotlin, etc. — into `"Other"`. Replaced with a Rust-side bucketing helper covering 46 extractor languages; Python files in the FastAPI benchmark now correctly report as `Python` instead of `Other`. Includes special-case basename matching for extensionless `Dockerfile` / `Makefile`. -- **Pre-existing breakage in `tests/mcp_server_test.rs` repaired.** The whole `test-transport`-gated integration suite (31 tests) had been silently failing to compile since `McpServer::new` switched its return type to `Arc` — `setup_server` and `run_server_with_messages` still expected bare `McpServer`. Switched both helpers to `Arc` and bumped the resource-count assertion (`tokensave://status/files/overview/branches`) from 4 to 5 to include the newer `tokensave://schema` resource. All 31 tests now pass. +- **Pre-existing breakage in `tests/mcp_server_test.rs` repaired.** The whole `test-transport`-gated integration suite (31 tests) had been silently failing to compile since `McpServer::new` switched its return type to `Arc` — `setup_server` and `run_server_with_messages` still expected bare `McpServer`. Switched both helpers to `Arc` and bumped the resource-count assertion (`tracedecay://status/files/overview/branches`) from 4 to 5 to include the newer `tracedecay://schema` resource. All 31 tests now pass. ### Changed -- **Seven hand-rolled CLI subcommands replaced by the unified `tokensave tool ` dispatcher.** `query`, `context`, `body`, `impact`, `callers`, `files`, `affected` were each ~50–150 LoC of clap glue duplicating what the MCP tool already declares in its schema. All seven are gone; the same operations are reached as `tokensave tool query …`, `tokensave tool body …`, etc. Drops ~600 LoC of dispatch boilerplate from `src/main.rs`. `query` is kept as an alias for the renamed `search` so muscle memory still works. +- **Seven hand-rolled CLI subcommands replaced by the unified `tracedecay tool ` dispatcher.** `query`, `context`, `body`, `impact`, `callers`, `files`, `affected` were each ~50–150 LoC of clap glue duplicating what the MCP tool already declares in its schema. All seven are gone; the same operations are reached as `tracedecay tool query …`, `tracedecay tool body …`, etc. Drops ~600 LoC of dispatch boilerplate from `src/main.rs`. `query` is kept as an alias for the renamed `search` so muscle memory still works. ### Internal -- New methods on `TokenSave`: `get_call_chain`, `get_nodes_by_name`, `replace_symbol`, `insert_at_symbol`, plus a private `resolve_symbol_for_edit` helper that does qname-first resolution and refuses ambiguous matches rather than silently picking the wrong site. +- New methods on `TraceDecay`: `get_call_chain`, `get_nodes_by_name`, `replace_symbol`, `insert_at_symbol`, plus a private `resolve_symbol_for_edit` helper that does qname-first resolution and refuses ambiguous matches rather than silently picking the wrong site. - New method on `GraphTraverser`: `find_path_directed` (BFS that follows outgoing edges only, with `max_depth` bound). - `McpServer` now has a `timings_enabled: AtomicBool` field with `set_timings_enabled` / `timings_enabled` accessors, toggleable per-session. - The dispatcher in `mcp/tools/handlers/mod.rs` wraps each `handle_tool_call` invocation with `Instant::now()` when `timings_enabled` is set and injects `_meta.duration_us` into the JSON-RPC result. @@ -134,16 +134,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [6.0.0] - 2026-05-25 ### Breaking -- **Daemon mode removed.** The `tokensave daemon` subcommand, autostart flags (`--enable-autostart` / `--disable-autostart`), foreground mode, and all `daemon-kit`-backed service registration are gone. ~1,100 lines of platform glue (launchd plists, systemd user units, Windows SCM, PID files, UAC elevation) deleted. File-watching now lives inside the MCP server itself — it spawns a `notify`-backed watcher that runs `sync_if_stale_silent` for the duration of the agent session, and exits when the agent disconnects. Multiple MCP peers on the same project converge through the existing per-project sync lock plus `sync_if_stale_silent`'s peer-coordination check; no new primitive was needed. Users with a registered autostart service should unload it manually — see "Migration" in `docs/TOKEN-SAVE-WHATSNEW.md` §6.0.0. +- **Daemon mode removed.** The `tracedecay daemon` subcommand, autostart flags (`--enable-autostart` / `--disable-autostart`), foreground mode, and all `daemon-kit`-backed service registration are gone. ~1,100 lines of platform glue (launchd plists, systemd user units, Windows SCM, PID files, UAC elevation) deleted. File-watching now lives inside the MCP server itself — it spawns a `notify`-backed watcher that runs `sync_if_stale_silent` for the duration of the agent session, and exits when the agent disconnects. Multiple MCP peers on the same project converge through the existing per-project sync lock plus `sync_if_stale_silent`'s peer-coordination check; no new primitive was needed. Users with a registered autostart service should unload it manually — see "Migration" in `docs/TRACEDECAY-WHATSNEW.md` §6.0.0. - **`UserConfig::daemon_debounce` renamed to `watcher_debounce`.** TOML load is backwards-compatible via `#[serde(alias = "daemon_debounce")]` and any config-mutating command rewrites the file with the new name; Rust struct literals referencing the old name are a compile-time break. - **`McpServer::new` now returns `Arc`.** The embedded watcher task captures a `Weak` so it cannot extend the server's lifetime. Embedders that bound the return value continue to compile; destructuring by value or storing into a non-`Arc` field needs to adapt. -- **`tokensave install --agent claude` writes the modern hook shape `{type, command, args}`.** Legacy single-string `"command": " "` entries are detected by `tokensave doctor` and auto-rewritten using `current_exe()` as the binary path (issue #81). This is a breaking change for any external tooling that introspects `~/.claude/settings.json` and assumed the legacy concatenated form. +- **`tracedecay install --agent claude` writes the modern hook shape `{type, command, args}`.** Legacy single-string `"command": " "` entries are detected by `tracedecay doctor` and auto-rewritten using `current_exe()` as the binary path (issue #81). This is a breaking change for any external tooling that introspects `~/.claude/settings.json` and assumed the legacy concatenated form. - **Beta release channel disabled.** `.github/workflows/release-beta.yml` is gated behind `BETA_CHANNEL_ENABLED=false` and a `workflow_dispatch`-only trigger. The code is preserved for future revival; no `*-beta.*` versions will ship from this commit forward. ### Added -- **`tokensave_redundancy` MCP tool (#83).** AST-level functional-duplicate detector. Computes four signals per function/method body via tree-sitter — AST shape hash, control-flow-graph hash, ordered call-sequence hash, and a 5-gram token-shingle set — then blends them into a `[0, 1]` composite similarity score (weights 0.40 / 0.25 / 0.20 / 0.15). Pairs are bucketed `definite` / `likely` / `naming_only` and ranked by score. Language-agnostic by design: kind walks use raw tree-sitter strings, so the same code path works for every supported grammar. Computation is lazy — fingerprints land in a new `node_fingerprints` table (schema v10) keyed by `(node_id, body source hash)` and persist across MCP sessions. Pairwise comparison is bucketed by body-token count (±25 % window) so it stays sub-quadratic on large repos. -- **`tokensave_runtime` MCP tool + `tokensave status --runtime` flag (#80).** Captures a process + database telemetry snapshot: PID, RSS, virtual size, sustained CPU% sampled over 200 ms, uptime, host CPU count, total system memory, DB / WAL / SHM file sizes, `journal_mode` PRAGMA, total indexed source bytes, node and edge counts, and a derived `db / source` bloat ratio. Lets users hitting unexpected resource pressure attach a structured snapshot to a bug report. Text report mirrors the `tokensave status` layout; JSON output via `--json` for machine consumption. -- **`tokensave_health` `details=true` sub-score breakdown (#82).** Returns per-dimension `{ score, interpretation, raw_count, source }` objects covering acyclicity (with `edges_in_cycles`), depth (`max_chain` / `ideal_chain`), equality (gini + textual interpretation), redundancy (`dead_count` / `total_fns`), modularity (textual label + components-after-hub-removal), and coverage discipline (`skip_test_coverage_count`). The composite `quality_signal` (geometric mean × 10 000) is preserved as the headline figure. +- **`tracedecay_redundancy` MCP tool (#83).** AST-level functional-duplicate detector. Computes four signals per function/method body via tree-sitter — AST shape hash, control-flow-graph hash, ordered call-sequence hash, and a 5-gram token-shingle set — then blends them into a `[0, 1]` composite similarity score (weights 0.40 / 0.25 / 0.20 / 0.15). Pairs are bucketed `definite` / `likely` / `naming_only` and ranked by score. Language-agnostic by design: kind walks use raw tree-sitter strings, so the same code path works for every supported grammar. Computation is lazy — fingerprints land in a new `node_fingerprints` table (schema v10) keyed by `(node_id, body source hash)` and persist across MCP sessions. Pairwise comparison is bucketed by body-token count (±25 % window) so it stays sub-quadratic on large repos. +- **`tracedecay_runtime` MCP tool + `tracedecay status --runtime` flag (#80).** Captures a process + database telemetry snapshot: PID, RSS, virtual size, sustained CPU% sampled over 200 ms, uptime, host CPU count, total system memory, DB / WAL / SHM file sizes, `journal_mode` PRAGMA, total indexed source bytes, node and edge counts, and a derived `db / source` bloat ratio. Lets users hitting unexpected resource pressure attach a structured snapshot to a bug report. Text report mirrors the `tracedecay status` layout; JSON output via `--json` for machine consumption. +- **`tracedecay_health` `details=true` sub-score breakdown (#82).** Returns per-dimension `{ score, interpretation, raw_count, source }` objects covering acyclicity (with `edges_in_cycles`), depth (`max_chain` / `ideal_chain`), equality (gini + textual interpretation), redundancy (`dead_count` / `total_fns`), modularity (textual label + components-after-hub-removal), and coverage discipline (`skip_test_coverage_count`). The composite `quality_signal` (geometric mean × 10 000) is preserved as the headline figure. ### Changed - **File-watcher rewritten around `notify-debouncer-full` 0.8.0-rc.2.** Replaces the DIY tokio debounce timer with the maintained library, which coalesces rename pairs, suppresses redundant modify-after-create, and batches event bursts cross-platform. Drop-in transparent to callers — `ProjectWatcher::new(root, debounce)` signature unchanged. @@ -152,24 +152,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`doctor` auto-repair logic for hook entries.** Modern-shape hooks with the wrong subcommand are fixed in place (preserving the user's bin path); legacy single-string hooks are rewritten using `current_exe()` since the embedded path cannot be parsed unambiguously when it contains spaces. Breaks the doctor → install loop that issue #81 reported on Windows path-with-spaces installs. ### Fixed -- **Hook install on paths with spaces (#81).** Previously, `install --agent claude` wrote `"command": "C:/Path With Spaces/tokensave.exe hook-pre-tool-use"` as a single string. Claude Code whitespace-splits the field, so the kernel executed `C:/Path` with arg `With` and the hook silently never fired. `doctor` correctly diagnosed the mismatch but then re-installed the same broken shape, looping forever. Switching to `{"command": "...", "args": ["hook-..."]}` sidesteps the splitter entirely. Doctor now also recognises the legacy shape as needing repair, not removal. +- **Hook install on paths with spaces (#81).** Previously, `install --agent claude` wrote `"command": "C:/Path With Spaces/tracedecay.exe hook-pre-tool-use"` as a single string. Claude Code whitespace-splits the field, so the kernel executed `C:/Path` with arg `With` and the hook silently never fired. `doctor` correctly diagnosed the mismatch but then re-installed the same broken shape, looping forever. Switching to `{"command": "...", "args": ["hook-..."]}` sidesteps the splitter entirely. Doctor now also recognises the legacy shape as needing repair, not removal. ### Schema -- **Migration v10.** Adds the `node_fingerprints` table backing `tokensave_redundancy`: `node_id PRIMARY KEY`, `ast_hash`, `cfg_hash`, `call_seq_hash`, `shingles` (comma-separated lowercase hex), `body_tokens`, `source_hash`. Indexed on `ast_hash` and `body_tokens` for the redundancy query's bucketed scan. As with prior migrations, opening a v9 database triggers a one-time full re-index on first open. +- **Migration v10.** Adds the `node_fingerprints` table backing `tracedecay_redundancy`: `node_id PRIMARY KEY`, `ast_hash`, `cfg_hash`, `call_seq_hash`, `shingles` (comma-separated lowercase hex), `body_tokens`, `source_hash`. Indexed on `ast_hash` and `body_tokens` for the redundancy query's bucketed scan. As with prior migrations, opening a v9 database triggers a one-time full re-index on first open. ### Internal - **New `src/redundancy.rs` module.** Tree-sitter walk utilities (kind-only AST hash, control-flow filtered hash, ordered call-sequence hash), token shingle generator, Jaccard similarity over sorted shingle sets, composite similarity blender, and the severity bucketing rules. Eight unit tests cover identical-body / different-structure / call-sequence-order / Jaccard self-similarity behaviour. - **New `src/runtime_telemetry.rs` module.** `RuntimeSnapshot { process, database }` collected via `sysinfo` 0.32 + libsql PRAGMA reads. Process side requires a refresh + 200 ms sleep + refresh sequence because sysinfo reports CPU% as a delta between successive readings. -- **`TokenSave::db_path()` accessor.** Recomputes the on-disk DB path via `resolve_db_for_branch` for diagnostics. Stable across the lifetime of an open `TokenSave`. -- **Tool registry grew from 68 → 71 (or 67 → 70 when `ast-grep` is unavailable).** Two new entries: `tokensave_redundancy`, `tokensave_runtime`. The previously shipped `tokensave_health` `details=true` schema knob is documented here for visibility. +- **`TraceDecay::db_path()` accessor.** Recomputes the on-disk DB path via `resolve_db_for_branch` for diagnostics. Stable across the lifetime of an open `TraceDecay`. +- **Tool registry grew from 68 → 71 (or 67 → 70 when `ast-grep` is unavailable).** Two new entries: `tracedecay_redundancy`, `tracedecay_runtime`. The previously shipped `tracedecay_health` `details=true` schema knob is documented here for visibility. ## [5.1.2] - 2026-05-20 ### Added -- **Kiro agent integration.** `tokensave install --agent kiro` now installs the MCP server, global steering, managed Kiro agent config, default CLI agent selection, and Kiro hook mappings for prompt context, delegated tool context, and post-write re-indexing. Doctor and uninstall support are included with coverage for workspace overrides and idempotent cleanup. +- **Kiro agent integration.** `tracedecay install --agent kiro` now installs the MCP server, global steering, managed Kiro agent config, default CLI agent selection, and Kiro hook mappings for prompt context, delegated tool context, and post-write re-indexing. Doctor and uninstall support are included with coverage for workspace overrides and idempotent cleanup. ### Fixed -- **Edit tool UTF-8 failure handling (#78).** `tokensave_multi_str_replace` and `tokensave_insert_at` no longer panic when failure previews or long anchors contain multi-byte UTF-8 characters. +- **Edit tool UTF-8 failure handling (#78).** `tracedecay_multi_str_replace` and `tracedecay_insert_at` no longer panic when failure previews or long anchors contain multi-byte UTF-8 characters. - **GW-BASIC / MSBASIC2 REM stripping.** Comment extraction no longer relies on a byte-length guard; the `REM` keyword is now content-checked (case-insensitive) before slicing, eliminating a latent panic on multi-byte comment text. ### Changed @@ -178,7 +178,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [5.1.1] - 2026-05-16 ### Performance -- **`tokensave_dead_code` no longer times out on chromium-scale repos.** The pre-4.14.8 form ran the leading-wildcard `LIKE '%::test'` chain inside a correlated `NOT EXISTS` on every dead-code candidate row — fast on scirs (0.097 s, 76 K `annotation_usage`) but timed out at the 25 s probe ceiling on chromium, cascade-poisoning every subsequent MCP tool call via JSON-RPC id reuse. 4.14.8's `WITH test_marker_ids AS (...)` CTE attempt regressed scirs from 0.1 s to >60 s because SQLite inlined the single-reference CTE, so the wildcard scan ran per candidate row instead of once; that attempt was reverted in 4.14.9. A first attempt that put marker ids into a single TEMP table and probed via `e2.source IN (SELECT id FROM temp.test_markers)` ALSO failed on chromium: SQLite picked `idx_edges_unique (source, target, kind)` for the correlated subquery and iterated every marker as the outer driver for every candidate (~13 K markers × ~134 K candidates ≈ 1.7 B probes), >60 s. New shape — **three-step resolve + pre-join + probe via TWO TEMP tables**: +- **`tracedecay_dead_code` no longer times out on chromium-scale repos.** The pre-4.14.8 form ran the leading-wildcard `LIKE '%::test'` chain inside a correlated `NOT EXISTS` on every dead-code candidate row — fast on scirs (0.097 s, 76 K `annotation_usage`) but timed out at the 25 s probe ceiling on chromium, cascade-poisoning every subsequent MCP tool call via JSON-RPC id reuse. 4.14.8's `WITH test_marker_ids AS (...)` CTE attempt regressed scirs from 0.1 s to >60 s because SQLite inlined the single-reference CTE, so the wildcard scan ran per candidate row instead of once; that attempt was reverted in 4.14.9. A first attempt that put marker ids into a single TEMP table and probed via `e2.source IN (SELECT id FROM temp.test_markers)` ALSO failed on chromium: SQLite picked `idx_edges_unique (source, target, kind)` for the correlated subquery and iterated every marker as the outer driver for every candidate (~13 K markers × ~134 K candidates ≈ 1.7 B probes), >60 s. New shape — **three-step resolve + pre-join + probe via TWO TEMP tables**: - `Database::collect_test_marker_ids` runs the marker `SELECT` exactly once over the `kind = 'annotation_usage'` partition (indexed via `idx_nodes_kind`). - `Database::populate_test_marker_temp_table` drops + recreates `temp.test_markers` (with `PRIMARY KEY` on `id` so SQLite builds a real B-tree) and bulk-inserts in 500-id chunks. - `Database::populate_test_annotated_targets_temp_table` joins `edges WHERE kind = 'annotates' AND source IN temp.test_markers` once, materialising "which node ids are annotated by any test marker" into `temp.test_annotated_targets` (PK on `target`). ~15 K rows on chromium. @@ -205,26 +205,26 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ### Added -- **`tokensave_read`** — mode-aware file read (`full`, `lines`, `map`, `signatures`) with cross-session cache. `map` and `signatures` are graph-only — no source bytes are touched. A re-call on an unchanged file returns a ~30-token `{"unchanged": true, …}` stub. The cache key folds `last_sync_at` for graph-backed modes so a force-reindex correctly invalidates derived rows. -- **`tokensave_outline`** — flat list of every top-level symbol in a file, with optional kind filter. The cheapest way to orient before zooming into a large file. -- **`tokensave_implementations`** — find every type implementing a given trait, or every body of a given method name. Returns method bodies with signatures. -- **`tokensave_unsafe_patterns`** — surface `.unwrap()` / `.expect()` / `panic!` / `todo!` / `unimplemented!` / `unsafe { }` sites with an `in_test` flag. Word-boundary matching avoids `.unwrap_or` false positives; an `exclude_tests` option skips test-shaped paths. -- **`tokensave_diagnostics`** — runs the project's compile / type checker (cargo / tsc / pyright) and returns structured errors mapped to graph nodes. Replaces the recurring "shell out → parse text → read file" loop with one structured response. Cargo target dir is forced to `.tokensave/target/` so it can't race with the user's interactive cargo runs. -- **`tokensave_config`** — query TOML / JSON config files by dotted key path. Single file (`path`) or glob (`glob`); returns parsed value plus a heuristic line number. DB-free — works on uninitialized projects. -- **`tokensave_signature_search`** — find functions / methods by signature shape: return type, parameter substring, async flag, path filter. All filters AND-compose. -- **`tokensave_constructors`** — locate every literal-instantiation site of a struct (`Foo { … }`) and report which fields each site sets — plus `missing_fields` relative to the struct's current definition. The classic "I added a required field, what breaks?" question. String- / char-literal awareness and `match` / `if let` / `while let` pattern filtering keep the result list clean. -- **`tokensave_field_sites`** — partition every `.` reference into reads and writes. Writes include `=`, compound assignments, and `&mut x.field` borrows; `==` and `=>` correctly count as reads. -- **`tokensave bench` colored console output** — default `tokensave bench` is now a fixed-width colored table instead of a markdown dump. Compact `k` / `M` numeric units; savings percentages colored by tier (green ≥80 %, yellow ≥50 %, red <50 %); aggregate footer in the same tier color. `--json` is unchanged. +- **`tracedecay_read`** — mode-aware file read (`full`, `lines`, `map`, `signatures`) with cross-session cache. `map` and `signatures` are graph-only — no source bytes are touched. A re-call on an unchanged file returns a ~30-token `{"unchanged": true, …}` stub. The cache key folds `last_sync_at` for graph-backed modes so a force-reindex correctly invalidates derived rows. +- **`tracedecay_outline`** — flat list of every top-level symbol in a file, with optional kind filter. The cheapest way to orient before zooming into a large file. +- **`tracedecay_implementations`** — find every type implementing a given trait, or every body of a given method name. Returns method bodies with signatures. +- **`tracedecay_unsafe_patterns`** — surface `.unwrap()` / `.expect()` / `panic!` / `todo!` / `unimplemented!` / `unsafe { }` sites with an `in_test` flag. Word-boundary matching avoids `.unwrap_or` false positives; an `exclude_tests` option skips test-shaped paths. +- **`tracedecay_diagnostics`** — runs the project's compile / type checker (cargo / tsc / pyright) and returns structured errors mapped to graph nodes. Replaces the recurring "shell out → parse text → read file" loop with one structured response. Cargo target dir is forced to `.tracedecay/target/` so it can't race with the user's interactive cargo runs. +- **`tracedecay_config`** — query TOML / JSON config files by dotted key path. Single file (`path`) or glob (`glob`); returns parsed value plus a heuristic line number. DB-free — works on uninitialized projects. +- **`tracedecay_signature_search`** — find functions / methods by signature shape: return type, parameter substring, async flag, path filter. All filters AND-compose. +- **`tracedecay_constructors`** — locate every literal-instantiation site of a struct (`Foo { … }`) and report which fields each site sets — plus `missing_fields` relative to the struct's current definition. The classic "I added a required field, what breaks?" question. String- / char-literal awareness and `match` / `if let` / `while let` pattern filtering keep the result list clean. +- **`tracedecay_field_sites`** — partition every `.` reference into reads and writes. Writes include `=`, compound assignments, and `&mut x.field` borrows; `==` and `=>` correctly count as reads. +- **`tracedecay bench` colored console output** — default `tracedecay bench` is now a fixed-width colored table instead of a markdown dump. Compact `k` / `M` numeric units; savings percentages colored by tier (green ≥80 %, yellow ≥50 %, red <50 %); aggregate footer in the same tier color. `--json` is unchanged. ### Changed -- **Schema v9: cross-session response cache.** New `read_cache` table keyed by `(project_id, session_id, file_path, mode, args_hash)` with `mtime_ns` for freshness. Backs `tokensave_read`. +- **Schema v9: cross-session response cache.** New `read_cache` table keyed by `(project_id, session_id, file_path, mode, args_hash)` with `mtime_ns` for freshness. Backs `tracedecay_read`. - **Schema v9: `Contains` edges denormalized into `nodes.parent_id`.** The same migration folds containment off the edges table and onto a new column. Cleaner queries — `get_children_of(parent_id)` is one indexed lookup — and the read-only SQL layer no longer has to filter by edge kind for every "find members of this container" question. Extractors keep emitting `Contains` edges as before; the storage layer hoists them into `parent_id` at insert time and skips persisting the row. ### Migration notes - **v9 is forward-only.** First sync after upgrade auto-applies the migration, populates `parent_id` from existing `Contains` rows, and deletes those rows. -- **Recovery path: `tokensave sync -f`.** If a downstream consumer still queries `Contains` edges directly (none of the in-repo tools do), force-sync rebuilds the graph from source under the new schema. +- **Recovery path: `tracedecay sync -f`.** If a downstream consumer still queries `Contains` edges directly (none of the in-repo tools do), force-sync rebuilds the graph from source under the new schema. - External SQLite consumers reading the `edges` table should switch from `kind='contains'` filters to `nodes.parent_id` joins. ## [4.14.11] - 2026-05-16 @@ -238,22 +238,22 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [4.14.10] - 2026-05-16 ### Performance -- **`count_complexity` (called from every extractor on every function) no longer hits an O(N²) trap on high-fanout AST nodes.** The body-walk in `src/extraction/complexity.rs` seeded its stack and pushed children with `node.child(i)` inside a `for i in 0..N` loop. Tree-sitter's `node.child(i)` is **O(i)** — it walks the linked sibling chain from the first child — so the seed + per-pop push pair was O(N²) for every node along the way. On `kernel/bpf/verifier.c` (20 K lines, monster switch statements with thousands of cases) a single `tokensave init` showed the progress bar wedged on that one file long enough that users reported it as "stuck"; chromium had files taking ~3 min individually. New `push_children` helper uses a `TreeCursor` (O(1) per sibling step) and reverses the appended slice so LIFO pop order still produces left-to-right traversal. Same fix applied to `extract_call_name`, `extract_macro_name`, and `rightmost_identifier` — all three did the same O(N²) `child(i)` scan over identifier candidates. Measured on `verifier.c` after the fix: 78 ms end-to-end (file read + parse + extract). Includes `examples/bench_extract.rs` so you can re-measure with `cargo run --release --example bench_extract `. +- **`count_complexity` (called from every extractor on every function) no longer hits an O(N²) trap on high-fanout AST nodes.** The body-walk in `src/extraction/complexity.rs` seeded its stack and pushed children with `node.child(i)` inside a `for i in 0..N` loop. Tree-sitter's `node.child(i)` is **O(i)** — it walks the linked sibling chain from the first child — so the seed + per-pop push pair was O(N²) for every node along the way. On `kernel/bpf/verifier.c` (20 K lines, monster switch statements with thousands of cases) a single `tracedecay init` showed the progress bar wedged on that one file long enough that users reported it as "stuck"; chromium had files taking ~3 min individually. New `push_children` helper uses a `TreeCursor` (O(1) per sibling step) and reverses the appended slice so LIFO pop order still produces left-to-right traversal. Same fix applied to `extract_call_name`, `extract_macro_name`, and `rightmost_identifier` — all three did the same O(N²) `child(i)` scan over identifier candidates. Measured on `verifier.c` after the fix: 78 ms end-to-end (file read + parse + extract). Includes `examples/bench_extract.rs` so you can re-measure with `cargo run --release --example bench_extract `. ## [4.14.9] - 2026-05-16 ### Fixed -- **Revert the 4.14.8 `find_dead_code` CTE refactor — it was a massive regression on real repos.** 4.14.8 moved the test-marker name match into `WITH test_marker_ids AS (...)` thinking that would amortise the leading-wildcard `LIKE`. In practice SQLite does not always materialise a single-reference CTE, and `e2.source IN (SELECT id FROM test_marker_ids)` inside a correlated `NOT EXISTS` degenerated into a per-row scan of the full `annotation_usage` table. On scirs (76 K annotation_usage rows, 153 K annotates edges) `tokensave_dead_code` went from **0.097 s** (pre-4.14.8) to **>60 s timeout**, which hung the MCP probe matrix — every subsequent tool then appeared to time out because the late response poisoned the JSON-RPC id matching (the cascade caveat in `scripts/mcp_probe/README.md`). The original `JOIN nodes a ON a.id = e2.source` form works because `idx_edges_target_kind` narrows to the (typically 0-3) annotates edges per candidate first, then joins via the nodes PK, so the LIKE only runs on that small per-candidate slice. A `Do NOT lift this into a CTE` comment is left at the call site so future refactors don't repeat the mistake. Other 4.14.8 perf changes (SCC frame clone, multi-source BFS, lines cache, dedup'd FTS terms, file-content cache, cycle-path borrowing, inheritance-depth CTE shape, has_bare_call fast path, placeholder builder) are kept as-is. +- **Revert the 4.14.8 `find_dead_code` CTE refactor — it was a massive regression on real repos.** 4.14.8 moved the test-marker name match into `WITH test_marker_ids AS (...)` thinking that would amortise the leading-wildcard `LIKE`. In practice SQLite does not always materialise a single-reference CTE, and `e2.source IN (SELECT id FROM test_marker_ids)` inside a correlated `NOT EXISTS` degenerated into a per-row scan of the full `annotation_usage` table. On scirs (76 K annotation_usage rows, 153 K annotates edges) `tracedecay_dead_code` went from **0.097 s** (pre-4.14.8) to **>60 s timeout**, which hung the MCP probe matrix — every subsequent tool then appeared to time out because the late response poisoned the JSON-RPC id matching (the cascade caveat in `scripts/mcp_probe/README.md`). The original `JOIN nodes a ON a.id = e2.source` form works because `idx_edges_target_kind` narrows to the (typically 0-3) annotates edges per candidate first, then joins via the nodes PK, so the LIKE only runs on that small per-candidate slice. A `Do NOT lift this into a CTE` comment is left at the call site so future refactors don't repeat the mistake. Other 4.14.8 perf changes (SCC frame clone, multi-source BFS, lines cache, dedup'd FTS terms, file-content cache, cycle-path borrowing, inheritance-depth CTE shape, has_bare_call fast path, placeholder builder) are kept as-is. ## [4.14.8] - 2026-05-16 ### Performance -- **Tarjan SCC no longer clones the entire neighbor list per edge visited.** The iterative DFS in `graph/scc.rs` used `work.last_mut().cloned()` on each loop iteration — that deep-copies the top tuple `(node, neighbors, idx)` including the whole `Vec` of neighbors, once per neighbor visited (so ~`out-degree × visits` full Vec clones during a node's life). Rewrote the loop to peek the top frame with `work.last_mut()` and clone only the two values actually needed (`node` and `next`) before any `work.push(...)`. Every SCC consumer benefits: `tokensave_circular`, `tokensave_port_order`, and (since 4.14.7) `tokensave_recursion`. -- **`tokensave_diff_context` walks the impact radius once for the union of modified symbols, not once per symbol.** The old loop called `get_impact_radius(node.id, depth)` for every modified node — each call ran an independent BFS from scratch, so any downstream node reachable from K modified ancestors got re-traversed K times. New `GraphTraverser::get_impact_radius_multi(seed_ids, max_depth)` does one BFS seeded with all modified node IDs and a single shared `visited` set; the result has every reachable node visited at most once. Surfaces as `TokenSave::get_impact_radius_multi`. Particularly impactful on diamond-dependency hotspots (shared utility files reachable from every changed module). -- **`tokensave_recursion` caches source-file lines instead of re-splitting on every self-edge check.** `is_direct_self_call` was caching the raw `String` source but then doing `let lines: Vec<&str> = source.lines().collect();` on each call — for a 10 k-line file with N self-edges, that's N × 10 k allocations purely to throw away. Cache changed to `HashMap>>` so the line vector is built once per file. -- **`tokensave_recursion` cycle-path DFS uses borrowed `&str` and stops once the limit is hit.** `cycle_path_for_scc` / `dfs_cycle_path` previously used `Vec` / `HashSet` on hot paths, allocating a `String` per neighbor visit even though every id already lived in `scc_set`. Switched to `&str` borrows over the SCC's existing storage. The outer loop also sorts SCCs by length first and short-circuits as soon as `cycles.len() == limit`, so we no longer enumerate every cycle in a giant mutually-recursive graph before truncating. -- **`tokensave_inheritance_depth` CTE collapses the hierarchy before joining `nodes`.** The recursive CTE produced one row per (leaf, depth) pair across the full hierarchy; the outer SELECT then ran the `file_path LIKE ?` filter over all of them. Wrapped the hierarchy in a `leaf_depths` CTE that `GROUP BY leaf_id` first, so the path filter and node join only see distinct leaves. -- **`tokensave_dead_code` resolves the test-marker annotation set in a single CTE pass.** Each candidate dead-code row previously re-evaluated `a.name LIKE '%::test'` (and three more leading-wildcard `LIKE`s) — none of those can use an index, so the cost scaled with `dead-candidates × annotation_usage`. New `WITH test_marker_ids AS (…)` resolves the marker ids once; the dead-code subquery then checks `e2.source IN (SELECT id FROM test_marker_ids)`. +- **Tarjan SCC no longer clones the entire neighbor list per edge visited.** The iterative DFS in `graph/scc.rs` used `work.last_mut().cloned()` on each loop iteration — that deep-copies the top tuple `(node, neighbors, idx)` including the whole `Vec` of neighbors, once per neighbor visited (so ~`out-degree × visits` full Vec clones during a node's life). Rewrote the loop to peek the top frame with `work.last_mut()` and clone only the two values actually needed (`node` and `next`) before any `work.push(...)`. Every SCC consumer benefits: `tracedecay_circular`, `tracedecay_port_order`, and (since 4.14.7) `tracedecay_recursion`. +- **`tracedecay_diff_context` walks the impact radius once for the union of modified symbols, not once per symbol.** The old loop called `get_impact_radius(node.id, depth)` for every modified node — each call ran an independent BFS from scratch, so any downstream node reachable from K modified ancestors got re-traversed K times. New `GraphTraverser::get_impact_radius_multi(seed_ids, max_depth)` does one BFS seeded with all modified node IDs and a single shared `visited` set; the result has every reachable node visited at most once. Surfaces as `TraceDecay::get_impact_radius_multi`. Particularly impactful on diamond-dependency hotspots (shared utility files reachable from every changed module). +- **`tracedecay_recursion` caches source-file lines instead of re-splitting on every self-edge check.** `is_direct_self_call` was caching the raw `String` source but then doing `let lines: Vec<&str> = source.lines().collect();` on each call — for a 10 k-line file with N self-edges, that's N × 10 k allocations purely to throw away. Cache changed to `HashMap>>` so the line vector is built once per file. +- **`tracedecay_recursion` cycle-path DFS uses borrowed `&str` and stops once the limit is hit.** `cycle_path_for_scc` / `dfs_cycle_path` previously used `Vec` / `HashSet` on hot paths, allocating a `String` per neighbor visit even though every id already lived in `scc_set`. Switched to `&str` borrows over the SCC's existing storage. The outer loop also sorts SCCs by length first and short-circuits as soon as `cycles.len() == limit`, so we no longer enumerate every cycle in a giant mutually-recursive graph before truncating. +- **`tracedecay_inheritance_depth` CTE collapses the hierarchy before joining `nodes`.** The recursive CTE produced one row per (leaf, depth) pair across the full hierarchy; the outer SELECT then ran the `file_path LIKE ?` filter over all of them. Wrapped the hierarchy in a `leaf_depths` CTE that `GROUP BY leaf_id` first, so the path filter and node join only see distinct leaves. +- **`tracedecay_dead_code` resolves the test-marker annotation set in a single CTE pass.** Each candidate dead-code row previously re-evaluated `a.name LIKE '%::test'` (and three more leading-wildcard `LIKE`s) — none of those can use an index, so the cost scaled with `dead-candidates × annotation_usage`. New `WITH test_marker_ids AS (…)` resolves the marker ids once; the dead-code subquery then checks `e2.source IN (SELECT id FROM test_marker_ids)`. - **`ContextBuilder::find_entry_points` deduplicates FTS terms across the five search rounds.** Full query, extracted symbols, stem variants, and agent-provided extra keywords overlap heavily (e.g. `symbol "foo"` and `keyword "foo"` produce identical FTS results); each duplicate term cost a full DB roundtrip on the single-connection libsql. Terms are now collected into one ordered, deduplicated list before any `search_nodes` calls — original priority preserved (full query → symbols → stems → keywords) so the `cap`-based early exit still favours higher-signal terms first. - **`ContextBuilder` reads each source file at most once per `build_context`.** Both `extract_code_blocks` and `merge_adjacent_blocks` previously called `get_code(node)` which did its own `fs::read_to_string` per call; merging K adjacent blocks meant K disk reads of the same file. Introduced `get_code_cached(node, file_cache)` that consults a shared `HashMap>`; `build_context` allocates one cache for the request and threads it through both phases. - **`has_bare_call` short-circuits lines with no `(` and rejects substring matches on both identifier boundaries.** Common short names like `new` / `get` / `len` triggered `line.match_indices(name)` over the full line and then filtered post-hoc — pathological on comment/docstring lines that mention the name without calling it. Added a `line.contains('(')` fast path plus an after-byte identifier-boundary check (so `new` no longer pre-matches inside `newer`). @@ -262,126 +262,126 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [4.14.5] - 2026-05-16 ### Fixed -- **`tokensave_inheritance_depth` no longer explodes on cyclic / near-cyclic trait-bound graphs.** The recursive CTE in `get_inheritance_depth` had a depth bound of 50 but no cycle detection, so any cycle in the `extends` graph (common in Rust workspaces where generic trait bounds form indirect cycles) made the CTE traverse the cycle up to the depth limit from every entry point. On polkadot-sdk (959 `extends` edges) the query took >60 s and timed out; smaller workspaces (sotf 89, scirs 90, sonium 5) were fine. Fix tracks visited node IDs in a path column and skips recursion when the next target is already in the path — query completes in 0.55 s on polkadot end-to-end through MCP. Regression test `test_get_inheritance_depth_terminates_on_cycle` constructs a 3-node graph with an A↔B cycle and a C→A edge, then asserts the query returns in <2 s with all three nodes reported at finite, bounded depth. +- **`tracedecay_inheritance_depth` no longer explodes on cyclic / near-cyclic trait-bound graphs.** The recursive CTE in `get_inheritance_depth` had a depth bound of 50 but no cycle detection, so any cycle in the `extends` graph (common in Rust workspaces where generic trait bounds form indirect cycles) made the CTE traverse the cycle up to the depth limit from every entry point. On polkadot-sdk (959 `extends` edges) the query took >60 s and timed out; smaller workspaces (sotf 89, scirs 90, sonium 5) were fine. Fix tracks visited node IDs in a path column and skips recursion when the next target is already in the path — query completes in 0.55 s on polkadot end-to-end through MCP. Regression test `test_get_inheritance_depth_terminates_on_cycle` constructs a 3-node graph with an A↔B cycle and a C→A edge, then asserts the query returns in <2 s with all three nodes reported at finite, bounded depth. ### Added -- **`scripts/mcp_probe/` — MCP test-matrix harness.** Drives a fresh `tokensave serve` MCP server over stdio against a configurable set of real repos and exercises every read-only tool with 5 query variants per language, producing a per-tool / per-repo status table that flags tools needing investigation (errors, timeouts, empty results, perf regressions). Same harness doubles as a benchmark — per-call timings are logged, repos serve as a fixed corpus for cross-version perf comparison. Pluggable per-language probe modules under `tools/`; Rust ships included (`tools/rust.py` exercises all 50 MCP tools). `repos.toml` (overridable via `$TOKENSAVE_PROBE_REPOS`) holds the repo set. JSON-RPC ids are strictly matched in `probe.py::McpClient` so a slow call cannot poison subsequent ones; `isolated.py` adds a fresh-server-per-tool retry loop for tools that already showed a real timeout. `build_matrix.py` renders the log to markdown. Used to find and prove this release's `inheritance_depth` cycle bug; same harness verifies the 4.14.4 fixes stay green across the four real repos (sotf, sonium, scirs, polkadot-sdk). +- **`scripts/mcp_probe/` — MCP test-matrix harness.** Drives a fresh `tracedecay serve` MCP server over stdio against a configurable set of real repos and exercises every read-only tool with 5 query variants per language, producing a per-tool / per-repo status table that flags tools needing investigation (errors, timeouts, empty results, perf regressions). Same harness doubles as a benchmark — per-call timings are logged, repos serve as a fixed corpus for cross-version perf comparison. Pluggable per-language probe modules under `tools/`; Rust ships included (`tools/rust.py` exercises all 50 MCP tools). `repos.toml` (overridable via `$TRACEDECAY_PROBE_REPOS`) holds the repo set. JSON-RPC ids are strictly matched in `probe.py::McpClient` so a slow call cannot poison subsequent ones; `isolated.py` adds a fresh-server-per-tool retry loop for tools that already showed a real timeout. `build_matrix.py` renders the log to markdown. Used to find and prove this release's `inheritance_depth` cycle bug; same harness verifies the 4.14.4 fixes stay green across the four real repos (sotf, sonium, scirs, polkadot-sdk). ## [4.14.4] - 2026-05-16 ### Fixed -- **`tokensave_doc_coverage` reports public fields, enum variants, constants, statics, type aliases, properties, …** The query previously filtered to `kind IN ('function', 'method', 'class', 'interface', 'trait', 'struct', 'enum', 'module')` — so a Rust file full of `pub` undocumented struct fields reported `total_undocumented: 0` even though `tokensave_module_api` listed dozens of public symbols on the same file. Kind allow-list expanded to include `field`, `enum_variant`, `const`, `static`, `type_alias`, `property`, `csharp_property`, `record`, `data_class`, `sealed_class`, `object`, `case_class`, `kotlin_object`, `inner_class`, `abstract_method`, `constructor`, `struct_method`, `val`, `var`, `mixin`, `extension`, `union`, `typedef`. Excludes `namespace` and `package` — those are aggregators that almost never carry their own doc and would just drown out actionable items. Single `const` deduplicates the prefix and no-prefix branches. Verified end-to-end on real DBs: `biquad.rs` in sotf went from 0 → 23 undocumented public symbols; polkadot-sdk reports ~51 K with a sensible per-kind breakdown. Regression test `test_get_undocumented_public_symbols_includes_fields_and_variants`. -- **`tokensave_dead_code` excludes `#[test]`-annotated functions whose name does NOT start with `test`.** The previous filter was name-prefix-only (`name NOT LIKE 'test%'`), so `#[test] fn from_measurement_slope_excludes_lfe()` and similar leaked through. The libtest harness is an implicit caller that never appears as a graph edge, so without this filter most Rust tests with non-`test*` names got misreported as dead. Detection now walks the `annotates` edges and excludes any node whose annotation_usage name is `'test'`, `'…::test'` (covers `tokio::test`, `async_std::test`), `'wasm_bindgen_test'`, or `'…::wasm_bindgen_test'`. The JOIN is constrained to `a.kind = 'annotation_usage'` to avoid accidental matches. Real-DB impact: sotf 1794 → 540 dead functions (-70 %), sonium 778 → 209 (-73 %), scirs 4 839 → 2 469 (-49 %), polkadot-sdk **12 136 → 2 295 (-81 %)**. Manual spot-check on polkadot confirmed every dropped name is unambiguously a `#[test]` function. Regression test `test_find_dead_code_excludes_test_annotated`. -- **`tokensave_ast_grep_rewrite` surfaces a useful message when `ast-grep` exits non-zero with empty stderr.** ast-grep returns exit 1 with completely silent stdout/stderr when its pattern matches 0 nodes or when the file's language can't be inferred from the extension (`.txt`). The previous error string was `"ast-grep failed: "` — empty trailer, no actionable info. New handler falls back through stderr → stdout → an explicit explanation listing likely causes (pattern matched 0 nodes, language not inferred, invalid pattern), plus the exit code and the file + pattern that failed. Regression test `ast_grep_rewrite_surfaces_useful_error_on_empty_stderr`. -- **`tokensave_port_status` no longer cross-matches methods that share a name but belong to different parent types.** The match key was `(name.to_lowercase(), kind_compat_group)`, so `Biquad::new` matched `Adaa::new`, `Biquad::process` matched any other `process`, and so on — useless on Rust workspaces where every type has a `new`, `process`, `fmt`, `reset`, etc. Match key now also includes the parent qualifier (stripped of generics — `Biquad` and `Biquad` resolve identically) for kinds that have one (`method`, `field`, `enum_variant`, `struct_method`, `abstract_method`, `constructor`, `csharp_property`, `property`, `val`, `var`); top-level kinds (struct, function, enum, trait) keep name-only matching since their containing context in `qualified_name` is just a file path. Regression tests `port_status_does_not_match_methods_of_different_parents` (Biquad in dir A, Adaa in dir B — must NOT match) and `port_status_matches_methods_with_same_parent_type` (Biquad in both dirs — must match). -- **`tokensave_branch_diff` returns an empty diff when base == head instead of erroring.** Previous behaviour was `MCP error -32603: base and head are the same branch`, inconsistent with `tokensave_pr_context` which already handled the same case by returning empty arrays. Same-ref now returns the normal JSON shape with `summary: {added:0, removed:0, changed:0}`, empty `added`/`removed`/`changed` arrays, and a `note` field explaining the equality — so callers can rely on a single response shape. Regression test `branch_diff_returns_empty_when_base_equals_head`. +- **`tracedecay_doc_coverage` reports public fields, enum variants, constants, statics, type aliases, properties, …** The query previously filtered to `kind IN ('function', 'method', 'class', 'interface', 'trait', 'struct', 'enum', 'module')` — so a Rust file full of `pub` undocumented struct fields reported `total_undocumented: 0` even though `tracedecay_module_api` listed dozens of public symbols on the same file. Kind allow-list expanded to include `field`, `enum_variant`, `const`, `static`, `type_alias`, `property`, `csharp_property`, `record`, `data_class`, `sealed_class`, `object`, `case_class`, `kotlin_object`, `inner_class`, `abstract_method`, `constructor`, `struct_method`, `val`, `var`, `mixin`, `extension`, `union`, `typedef`. Excludes `namespace` and `package` — those are aggregators that almost never carry their own doc and would just drown out actionable items. Single `const` deduplicates the prefix and no-prefix branches. Verified end-to-end on real DBs: `biquad.rs` in sotf went from 0 → 23 undocumented public symbols; polkadot-sdk reports ~51 K with a sensible per-kind breakdown. Regression test `test_get_undocumented_public_symbols_includes_fields_and_variants`. +- **`tracedecay_dead_code` excludes `#[test]`-annotated functions whose name does NOT start with `test`.** The previous filter was name-prefix-only (`name NOT LIKE 'test%'`), so `#[test] fn from_measurement_slope_excludes_lfe()` and similar leaked through. The libtest harness is an implicit caller that never appears as a graph edge, so without this filter most Rust tests with non-`test*` names got misreported as dead. Detection now walks the `annotates` edges and excludes any node whose annotation_usage name is `'test'`, `'…::test'` (covers `tokio::test`, `async_std::test`), `'wasm_bindgen_test'`, or `'…::wasm_bindgen_test'`. The JOIN is constrained to `a.kind = 'annotation_usage'` to avoid accidental matches. Real-DB impact: sotf 1794 → 540 dead functions (-70 %), sonium 778 → 209 (-73 %), scirs 4 839 → 2 469 (-49 %), polkadot-sdk **12 136 → 2 295 (-81 %)**. Manual spot-check on polkadot confirmed every dropped name is unambiguously a `#[test]` function. Regression test `test_find_dead_code_excludes_test_annotated`. +- **`tracedecay_ast_grep_rewrite` surfaces a useful message when `ast-grep` exits non-zero with empty stderr.** ast-grep returns exit 1 with completely silent stdout/stderr when its pattern matches 0 nodes or when the file's language can't be inferred from the extension (`.txt`). The previous error string was `"ast-grep failed: "` — empty trailer, no actionable info. New handler falls back through stderr → stdout → an explicit explanation listing likely causes (pattern matched 0 nodes, language not inferred, invalid pattern), plus the exit code and the file + pattern that failed. Regression test `ast_grep_rewrite_surfaces_useful_error_on_empty_stderr`. +- **`tracedecay_port_status` no longer cross-matches methods that share a name but belong to different parent types.** The match key was `(name.to_lowercase(), kind_compat_group)`, so `Biquad::new` matched `Adaa::new`, `Biquad::process` matched any other `process`, and so on — useless on Rust workspaces where every type has a `new`, `process`, `fmt`, `reset`, etc. Match key now also includes the parent qualifier (stripped of generics — `Biquad` and `Biquad` resolve identically) for kinds that have one (`method`, `field`, `enum_variant`, `struct_method`, `abstract_method`, `constructor`, `csharp_property`, `property`, `val`, `var`); top-level kinds (struct, function, enum, trait) keep name-only matching since their containing context in `qualified_name` is just a file path. Regression tests `port_status_does_not_match_methods_of_different_parents` (Biquad in dir A, Adaa in dir B — must NOT match) and `port_status_matches_methods_with_same_parent_type` (Biquad in both dirs — must match). +- **`tracedecay_branch_diff` returns an empty diff when base == head instead of erroring.** Previous behaviour was `MCP error -32603: base and head are the same branch`, inconsistent with `tracedecay_pr_context` which already handled the same case by returning empty arrays. Same-ref now returns the normal JSON shape with `summary: {added:0, removed:0, changed:0}`, empty `added`/`removed`/`changed` arrays, and a `note` field explaining the equality — so callers can rely on a single response shape. Regression test `branch_diff_returns_empty_when_base_equals_head`. ## [4.14.1] - 2026-05-15 ### Fixed -- **`tokensave_search` always ranks definitions above `use` re-exports.** BM25 was scoring short `pub use crate::operator::LinearOperator;` rows highly enough that five re-exports outranked the actual `pub trait LinearOperator { … }` definition. Sort now uses a coarse `kind_tier` as the primary key (defs tier 0, impl tier 1, values/members tier 2, modules tier 3, `use`/`include`/annotation usage tier 4); BM25 score is secondary within a tier. Added a per-row exact-name match boost (+10) so a trait named exactly `Foo` beats a `Method` whose qualified name happens to contain `Foo`. Regression test `search_ranks_trait_definition_above_use_reexports` constructs a trait plus five `pub use` re-exports across sibling modules and asserts the trait is hit #1. -- **`tokensave_dead_code` no longer treats `annotates` / `derives_macro` / `contains` edges as "this function is alive" evidence.** Real-world Rust pervasively uses `#[inline]`, `#[derive(Debug)]`, and similar attributes — every annotation_usage node creates an `annotates` edge pointing at the function, which the previous `kind != 'contains'` filter accepted as a live reference. Result on the sonium codebase: 0 dead functions across 5,715. Narrowed the SQL filter to an explicit allowlist of real-use edges: `calls`, `implements`, `extends`, `type_of`, `returns`, `receives`, `uses`. Regression test `dead_code_flags_unreferenced_fn_with_attribute` exercises the `#[inline]` case. -- **`tokensave_unused_imports` handles grouped imports (`use std::collections::{HashMap, HashSet};`).** The previous parser treated the literal string `{HashMap, HashSet}` as one identifier and never matched it against the file body, so every grouped import was effectively ignored — explaining the user's "0 unused across 3,404 use nodes" report. A new `identifiers_from_use_path` helper splits grouped/aliased/nested forms (`foo::bar`, `foo::bar as baz`, `foo::{a, b as c}`, `foo::{a, nested::b}`, `foo::{self, bar}`), and the handler now reports one entry per truly-unused identifier with an `unused: ` field. Regression test `unused_imports_handles_grouped_use` verifies the unused half of a grouped use is flagged while the used half is not. -- **`tokensave_changelog` filters deleted-subtree directory entries.** When an entire subtree was removed in a diff, gix yielded a directory-mode deletion entry whose path was gone from disk by the time the post-hoc `is_dir()` check ran — so directories like `crates/sonium-bem` slipped through as `removed_or_not_indexed`. `git_diff_files` now inspects `entry_mode.is_tree()` on each gix `Change` record (addition/modification/deletion/rewrite) and never pushes a tree entry into the changed-files list. The disk-based `is_dir()` filter is kept as belt-and-suspenders for additions/modifications. Regression test `changelog_filters_deleted_directory_entries` synthesises a `git rm -r crates/` commit and asserts no non-`.rs` paths appear in `changed_files`. -- **`tokensave_diff_context.modified_symbols` dedupes by node id and dedupes the input `files` array.** Callers that synthesised the file list from upstream tooling (directory walks, multi-source mergers) sometimes passed the same path multiple times — `hmatrix.rs` was reported up to 7× in a row. Added a `modified_seen: HashSet` to guard pushes and an early `files` dedup pass. Regression test `diff_context_dedupes_modified_symbols_on_duplicate_input` passes the same path three times and asserts unique node ids in the output. -- **`tokensave_pr_context` collapses Cargo.toml into a single `config_summary` entry.** Behaviour was already present; added the regression test `pr_context_collapses_cargo_toml_keys` which synthesises a real git history with a 50-dependency Cargo.toml bump and asserts at most one Cargo.toml entry surfaces (kind = `config_summary`). -- **`tokensave_circular` SCC disjointness stress test.** Added `circular_emits_disjoint_sccs_under_load` — five 3-file cycles connected by non-cyclic DAG-style tails — to guard against any future SCC implementation drift that might let a file leak into more than one cycle entry. +- **`tracedecay_search` always ranks definitions above `use` re-exports.** BM25 was scoring short `pub use crate::operator::LinearOperator;` rows highly enough that five re-exports outranked the actual `pub trait LinearOperator { … }` definition. Sort now uses a coarse `kind_tier` as the primary key (defs tier 0, impl tier 1, values/members tier 2, modules tier 3, `use`/`include`/annotation usage tier 4); BM25 score is secondary within a tier. Added a per-row exact-name match boost (+10) so a trait named exactly `Foo` beats a `Method` whose qualified name happens to contain `Foo`. Regression test `search_ranks_trait_definition_above_use_reexports` constructs a trait plus five `pub use` re-exports across sibling modules and asserts the trait is hit #1. +- **`tracedecay_dead_code` no longer treats `annotates` / `derives_macro` / `contains` edges as "this function is alive" evidence.** Real-world Rust pervasively uses `#[inline]`, `#[derive(Debug)]`, and similar attributes — every annotation_usage node creates an `annotates` edge pointing at the function, which the previous `kind != 'contains'` filter accepted as a live reference. Result on the sonium codebase: 0 dead functions across 5,715. Narrowed the SQL filter to an explicit allowlist of real-use edges: `calls`, `implements`, `extends`, `type_of`, `returns`, `receives`, `uses`. Regression test `dead_code_flags_unreferenced_fn_with_attribute` exercises the `#[inline]` case. +- **`tracedecay_unused_imports` handles grouped imports (`use std::collections::{HashMap, HashSet};`).** The previous parser treated the literal string `{HashMap, HashSet}` as one identifier and never matched it against the file body, so every grouped import was effectively ignored — explaining the user's "0 unused across 3,404 use nodes" report. A new `identifiers_from_use_path` helper splits grouped/aliased/nested forms (`foo::bar`, `foo::bar as baz`, `foo::{a, b as c}`, `foo::{a, nested::b}`, `foo::{self, bar}`), and the handler now reports one entry per truly-unused identifier with an `unused: ` field. Regression test `unused_imports_handles_grouped_use` verifies the unused half of a grouped use is flagged while the used half is not. +- **`tracedecay_changelog` filters deleted-subtree directory entries.** When an entire subtree was removed in a diff, gix yielded a directory-mode deletion entry whose path was gone from disk by the time the post-hoc `is_dir()` check ran — so directories like `crates/sonium-bem` slipped through as `removed_or_not_indexed`. `git_diff_files` now inspects `entry_mode.is_tree()` on each gix `Change` record (addition/modification/deletion/rewrite) and never pushes a tree entry into the changed-files list. The disk-based `is_dir()` filter is kept as belt-and-suspenders for additions/modifications. Regression test `changelog_filters_deleted_directory_entries` synthesises a `git rm -r crates/` commit and asserts no non-`.rs` paths appear in `changed_files`. +- **`tracedecay_diff_context.modified_symbols` dedupes by node id and dedupes the input `files` array.** Callers that synthesised the file list from upstream tooling (directory walks, multi-source mergers) sometimes passed the same path multiple times — `hmatrix.rs` was reported up to 7× in a row. Added a `modified_seen: HashSet` to guard pushes and an early `files` dedup pass. Regression test `diff_context_dedupes_modified_symbols_on_duplicate_input` passes the same path three times and asserts unique node ids in the output. +- **`tracedecay_pr_context` collapses Cargo.toml into a single `config_summary` entry.** Behaviour was already present; added the regression test `pr_context_collapses_cargo_toml_keys` which synthesises a real git history with a 50-dependency Cargo.toml bump and asserts at most one Cargo.toml entry surfaces (kind = `config_summary`). +- **`tracedecay_circular` SCC disjointness stress test.** Added `circular_emits_disjoint_sccs_under_load` — five 3-file cycles connected by non-cyclic DAG-style tails — to guard against any future SCC implementation drift that might let a file leak into more than one cycle entry. ### Added -- **`tokensave_port_order` surfaces intra-cycle ordering signals.** Each cycle entry now reports per-symbol `in_cycle_in_degree` and `in_cycle_out_degree`, a file-level `members_in_cycle` breakdown ranked by member count, an explicit `entry_point` (the SCC member with the smallest in-cycle out-degree — leaf-most, the natural starting point), and a `break_point_candidate` (the highest in-cycle in-degree node, the hub whose call sites are the most-effective refactor target). Replaces the previous flat blob of 200+ symbols with no guidance on where to start. Regression test `port_order_provides_intra_cycle_ordering` wires a 4-node SCC with one obvious hub and asserts `break_point_candidate.name == "h"`. +- **`tracedecay_port_order` surfaces intra-cycle ordering signals.** Each cycle entry now reports per-symbol `in_cycle_in_degree` and `in_cycle_out_degree`, a file-level `members_in_cycle` breakdown ranked by member count, an explicit `entry_point` (the SCC member with the smallest in-cycle out-degree — leaf-most, the natural starting point), and a `break_point_candidate` (the highest in-cycle in-degree node, the hub whose call sites are the most-effective refactor target). Replaces the previous flat blob of 200+ symbols with no guidance on where to start. Regression test `port_order_provides_intra_cycle_ordering` wires a 4-node SCC with one obvious hub and asserts `break_point_candidate.name == "h"`. ### Changed -- **`tokensave_ast_grep_rewrite` is conditionally registered.** The tool is only advertised via `tools/list` when the external `ast-grep` binary is on PATH at server-startup time (cached via `OnceLock` so we don't fork on every `tools/list` request). When the binary is missing, models never see a tool that would immediately return "ast-grep is not installed" on first call. `tokensave::mcp::tools::ast_grep_available()` is now public; tests in `mcp_handler_test::test_tool_definitions_complete` and `mcp_test::test_tool_definitions_count` branch on it so they pass on hosts with or without the binary installed. +- **`tracedecay_ast_grep_rewrite` is conditionally registered.** The tool is only advertised via `tools/list` when the external `ast-grep` binary is on PATH at server-startup time (cached via `OnceLock` so we don't fork on every `tools/list` request). When the binary is missing, models never see a tool that would immediately return "ast-grep is not installed" on first call. `tracedecay::mcp::tools::ast_grep_available()` is now public; tests in `mcp_handler_test::test_tool_definitions_complete` and `mcp_test::test_tool_definitions_count` branch on it so they pass on hosts with or without the binary installed. ## [4.14.0] - 2026-05-15 ### Fixed -- **`tokensave_run_affected_tests` dispatches directly-changed test files.** Previously the handler only walked callers of every node in `changed_paths` — `#[test]` functions are leaves with no callers, so a PR that only touched `tests/foo.rs` returned "no tests cover the changed paths" and skipped running anything. The handler now also dispatches test functions whose file is itself in `changed_paths` (either via `is_test_file` path heuristic or `#[test]` annotation), with the test recorded as covering itself in `covers_source_ids`. +- **`tracedecay_run_affected_tests` dispatches directly-changed test files.** Previously the handler only walked callers of every node in `changed_paths` — `#[test]` functions are leaves with no callers, so a PR that only touched `tests/foo.rs` returned "no tests cover the changed paths" and skipped running anything. The handler now also dispatches test functions whose file is itself in `changed_paths` (either via `is_test_file` path heuristic or `#[test]` annotation), with the test recorded as covering itself in `covers_source_ids`. - **`parse_derives_in_attr_block` handles rustfmt's multi-line derive blocks.** The previous line-bounded scanner only matched `#[derive(...)]` when the closing `)` was on the same line, so rustfmt's split form (`#[derive(\n Debug,\n Clone,\n)]`) dropped every derive. The parser now joins the attribute-block lines and scans for `#[derive(` ... `)` across the whole region. Two new unit tests (`parses_multiline_derive_attribute`, `parses_multiline_derive_mixed_with_single_line`) cover the split form. -- **`tokensave_diagnose` normalises absolute and backslash paths.** Cargo emits absolute spans when `--manifest-path` points outside cwd, and Windows cargo emits backslash-separated paths; neither matches the indexed forward-slash, project-relative form. `node_at_location` now calls a new `normalize_lookup_path` helper that (1) replaces `\` with `/`, (2) strips the canonicalised project-root prefix for absolutes, and (3) falls back to a raw prefix strip when canonicalisation fails. A diagnostic spanning either form now maps to the correct node. +- **`tracedecay_diagnose` normalises absolute and backslash paths.** Cargo emits absolute spans when `--manifest-path` points outside cwd, and Windows cargo emits backslash-separated paths; neither matches the indexed forward-slash, project-relative form. `node_at_location` now calls a new `normalize_lookup_path` helper that (1) replaces `\` with `/`, (2) strips the canonicalised project-root prefix for absolutes, and (3) falls back to a raw prefix strip when canonicalisation fails. A diagnostic spanning either form now maps to the correct node. - **Resolver kind-compatibility filter now applies to the same-file blocklist branches (bug #11 follow-up).** PR8's filter was wired into the main `try_exact_name_match` / `try_qualified_match` paths but not the two `CROSS_FILE_BLOCKLIST` branches in `try_exact_name_match` and `try_exact_name_match_simple`. Common blocklisted names (`new`, `default`, `clone`, …) could still bind a `Calls` reference to a same-file non-callable — a struct or const sharing the name. Both branches now filter candidates through `kind_compatible` before declaring a same-file match. Regression test `resolver_blocklist_branch_respects_kind_filter` reproduces the case (`struct new` + `caller() { let _ = new(); }`) and asserts callees only include callable kinds. ## [4.13.0] - 2026-05-15 ### Fixed -- **Resolver kind-compatibility filter (bug #11)** — `tokensave_rank --edge-kind implements` (and every downstream tool: `tokensave_impls`, `tokensave_type_hierarchy`, `tokensave_callees`'s trait dispatch, …) was poisoned by the resolver fuzzy-binding `impl Default for X` to whatever local node happened to share the name `Default`. The sonium codebase had a parser `Token` enum with a `Default` variant; 150 manual `impl Default for X` blocks all bound to that one `enum_variant`, swamping the rank tool with junk. +- **Resolver kind-compatibility filter (bug #11)** — `tracedecay_rank --edge-kind implements` (and every downstream tool: `tracedecay_impls`, `tracedecay_type_hierarchy`, `tracedecay_callees`'s trait dispatch, …) was poisoned by the resolver fuzzy-binding `impl Default for X` to whatever local node happened to share the name `Default`. The sonium codebase had a parser `Token` enum with a `Default` variant; 150 manual `impl Default for X` blocks all bound to that one `enum_variant`, swamping the rank tool with junk. - New `kind_compatible(ref_kind, target_kind)` helper in `src/resolution/resolver.rs` enforces a structural matrix: - `Implements` / `Extends` / `DerivesMacro` → must target trait/interface/class/abstract-method/sealed-class/annotation/type-alias kinds - `Calls` → must target a callable (function/method/struct-method/constructor/abstract-method/arrow-function/procedure/macro) - `Annotates` → must target annotation/decorator kinds - `Uses` / `TypeOf` / `Returns` / `Contains` / `Receives` → permissive (any kind) - Both `try_qualified_match` and `try_exact_name_match` now apply the filter; when filtering shrinks the candidate list, a `resolve_from_filtered` helper picks the same-file candidate first then falls back to the first overall, with confidence reduced to reflect the partial match. This prevents the previous "any-name-wins" behaviour without dropping legitimate resolutions. -- Regression test `implements_refs_dont_resolve_to_enum_variants` constructs the exact sonium-style scenario (`enum Token { Default, Plus }` plus two manual `impl Default for X` blocks) and asserts that `tokensave_rank --edge-kind implements` does NOT list an `enum_variant` or `field` target. Existing DBs need `tokensave sync --force` to re-resolve refs under the new constraints. +- Regression test `implements_refs_dont_resolve_to_enum_variants` constructs the exact sonium-style scenario (`enum Token { Default, Plus }` plus two manual `impl Default for X` blocks) and asserts that `tracedecay_rank --edge-kind implements` does NOT list an `enum_variant` or `field` target. Existing DBs need `tracedecay sync --force` to re-resolve refs under the new constraints. ## [4.12.0] - 2026-05-15 ### Added -- **`src/graph/scc.rs` — Tarjan's strongly-connected-components algorithm.** Iterative (no recursion, no stack-blow risk on deep graphs), generic over node-id type, returns components in reverse-topological order matching what port ranking needs. Used by both `tokensave_circular` and `tokensave_port_order`. Five unit tests cover DAGs, two-node cycles, three-cycle-plus-tail, self-loops, and reverse-topo emission order. +- **`src/graph/scc.rs` — Tarjan's strongly-connected-components algorithm.** Iterative (no recursion, no stack-blow risk on deep graphs), generic over node-id type, returns components in reverse-topological order matching what port ranking needs. Used by both `tracedecay_circular` and `tracedecay_port_order`. Five unit tests cover DAGs, two-node cycles, three-cycle-plus-tail, self-loops, and reverse-topo emission order. ### Fixed -- **`tokensave_circular` reports one entry per SCC, not per DFS walk (bug #10)** — the previous implementation emitted every distinct DFS path through a cycle, producing 73 overlapping cycle entries on the sonium codebase that all shared a long common tail. `find_circular_dependencies` now computes SCCs via Tarjan and emits one entry per genuine mutually-recursive group, filtering out trivial single-node components that don't have self-loops. The legacy `dfs_cycle_detect` helper and `_legacy_walk_cycles` shim were removed. -- **`tokensave_port_order` exposes per-SCC cycle groups (bug #12)** — previously, every unsorted node after Kahn's topological sort was lumped into a single "Mutual dependency — port together" entry, so two disjoint mutually-recursive pairs `(a,b)` and `(c,d)` would render as one mega-cycle and lose all signal. The handler now runs Tarjan on the subgraph of unsorted nodes and emits one cycle entry per non-trivial SCC, with the `files` set of each cycle surfaced so the user has a concrete "break this edge" target. Each entry carries `symbols`, `files`, `size`, and a refined `note`. +- **`tracedecay_circular` reports one entry per SCC, not per DFS walk (bug #10)** — the previous implementation emitted every distinct DFS path through a cycle, producing 73 overlapping cycle entries on the sonium codebase that all shared a long common tail. `find_circular_dependencies` now computes SCCs via Tarjan and emits one entry per genuine mutually-recursive group, filtering out trivial single-node components that don't have self-loops. The legacy `dfs_cycle_detect` helper and `_legacy_walk_cycles` shim were removed. +- **`tracedecay_port_order` exposes per-SCC cycle groups (bug #12)** — previously, every unsorted node after Kahn's topological sort was lumped into a single "Mutual dependency — port together" entry, so two disjoint mutually-recursive pairs `(a,b)` and `(c,d)` would render as one mega-cycle and lose all signal. The handler now runs Tarjan on the subgraph of unsorted nodes and emits one cycle entry per non-trivial SCC, with the `files` set of each cycle surfaced so the user has a concrete "break this edge" target. Each entry carries `symbols`, `files`, `size`, and a refined `note`. ## [4.11.0] - 2026-05-15 ### Fixed -- **`tokensave_dependency_depth` no longer follows `implements`/`extends` edges (bug #7)** — the resolver fuzzy-binds `impl Debug for T` and similar across unrelated files, producing chains of spurious file-to-file deps (the report observed a 19-level chain spanning 17 unrelated files terminating in a foreign crate). `build_file_adjacency` now follows only `calls` and `uses` edges. Existing `tokensave_health` and `tokensave_circular` callers benefit too — they share the same adjacency builder. -- **`tokensave_dead_code` no longer reports 0 on `pub`-heavy codebases (bug #8a)** — two fixes: (1) the `NOT EXISTS` subquery now excludes `Contains` edges, which previously masked every node behind its parent's bookkeeping edge; (2) new `include_public: true` argument opts into auditing pub items with no indexed callers, useful for workspace-internal cleanup. Default behaviour (no flag) still excludes pub items as before. -- **`tokensave_unused_imports` no longer returns 0 on real codebases (bug #8b)** — the previous graph-only check tested `incoming.is_empty()`, but every Use node has at least one Contains edge from its parent, so the predicate never fired. New heuristic reads the source file once (cached per file) and checks whether the imported identifier appears as a whole-word token outside the use statement itself; matches what `cargo`'s own unused-import lint does. `pub use` re-exports, glob imports, and `use self::...` are skipped (intentional aliases / out-of-scope for textual heuristics). Three regression tests cover unused-detection, the dead-code Contains-edge bug, and the new `include_public` opt-in. +- **`tracedecay_dependency_depth` no longer follows `implements`/`extends` edges (bug #7)** — the resolver fuzzy-binds `impl Debug for T` and similar across unrelated files, producing chains of spurious file-to-file deps (the report observed a 19-level chain spanning 17 unrelated files terminating in a foreign crate). `build_file_adjacency` now follows only `calls` and `uses` edges. Existing `tracedecay_health` and `tracedecay_circular` callers benefit too — they share the same adjacency builder. +- **`tracedecay_dead_code` no longer reports 0 on `pub`-heavy codebases (bug #8a)** — two fixes: (1) the `NOT EXISTS` subquery now excludes `Contains` edges, which previously masked every node behind its parent's bookkeeping edge; (2) new `include_public: true` argument opts into auditing pub items with no indexed callers, useful for workspace-internal cleanup. Default behaviour (no flag) still excludes pub items as before. +- **`tracedecay_unused_imports` no longer returns 0 on real codebases (bug #8b)** — the previous graph-only check tested `incoming.is_empty()`, but every Use node has at least one Contains edge from its parent, so the predicate never fired. New heuristic reads the source file once (cached per file) and checks whether the imported identifier appears as a whole-word token outside the use statement itself; matches what `cargo`'s own unused-import lint does. `pub use` re-exports, glob imports, and `use self::...` are skipped (intentional aliases / out-of-scope for textual heuristics). Three regression tests cover unused-detection, the dead-code Contains-edge bug, and the new `include_public` opt-in. ### Changed -- **`TokenSave::find_dead_code` signature** — gained an `include_public: bool` parameter. Existing callers (`tokensave_health`, internal tests) updated to pass `false` to preserve previous semantics. +- **`TraceDecay::find_dead_code` signature** — gained an `include_public: bool` parameter. Existing callers (`tracedecay_health`, internal tests) updated to pass `false` to preserve previous semantics. ## [4.10.0] - 2026-05-15 ### Fixed -- **`tokensave_body` prefers callable kinds over same-named fields (bug #1)** — sonium hit a case where querying `gmres` returned only a struct field literally named `gmres` and missed the obvious `pub fn gmres(...)`. The handler now does an exact-name DB lookup first (via the PR1 suffix-fallback path) so the function isn't buried under BM25 noise, then sorts matches by `body_kind_preference()`: callable (0) > type def (1) > impl (2) > value (3) > field/variant (4) > use (5). -- **`tokensave_changelog` / `commit_context` / `pr_context` no longer list directories (bug #4)** — gix's `for_each_to_obtain_tree` yields directory-level entries when an entire subtree changes. `git_diff_files` now filters out any path that resolves to a directory on disk, so callers see only file paths. -- **`tokensave_diff_context.impacted_symbols` dedupes by node id (bug #5)** — diamond dependencies caused the same downstream node to appear 6+ times consecutively. `impacted_seen: HashSet` now guards inserts. -- **`tokensave_recursion` drops length-1 self-cycles (bug #6)** — single-node cycles are almost always either resolver fuzzy-binding (`self.push()` cross-bound across distinct impls of the same name) or trivial self-recursion. Cycles with `< 2` distinct nodes are now filtered out before being added to the result set. -- **`tokensave_commit_context` / `tokensave_pr_context` collapse config-file symbols (bug #3)** — Cargo.toml's 50+ dependency keys used to each enumerate as a separate "modified symbol", blowing past 50K tokens on a real diff. Both handlers now emit a single `{kind: "config_summary", file, config_keys: N}` entry per file with role `config` (`*.toml` / `*.yaml` / `*.json` / `*.ini` / `*.cfg` / `*.lock`). +- **`tracedecay_body` prefers callable kinds over same-named fields (bug #1)** — sonium hit a case where querying `gmres` returned only a struct field literally named `gmres` and missed the obvious `pub fn gmres(...)`. The handler now does an exact-name DB lookup first (via the PR1 suffix-fallback path) so the function isn't buried under BM25 noise, then sorts matches by `body_kind_preference()`: callable (0) > type def (1) > impl (2) > value (3) > field/variant (4) > use (5). +- **`tracedecay_changelog` / `commit_context` / `pr_context` no longer list directories (bug #4)** — gix's `for_each_to_obtain_tree` yields directory-level entries when an entire subtree changes. `git_diff_files` now filters out any path that resolves to a directory on disk, so callers see only file paths. +- **`tracedecay_diff_context.impacted_symbols` dedupes by node id (bug #5)** — diamond dependencies caused the same downstream node to appear 6+ times consecutively. `impacted_seen: HashSet` now guards inserts. +- **`tracedecay_recursion` drops length-1 self-cycles (bug #6)** — single-node cycles are almost always either resolver fuzzy-binding (`self.push()` cross-bound across distinct impls of the same name) or trivial self-recursion. Cycles with `< 2` distinct nodes are now filtered out before being added to the result set. +- **`tracedecay_commit_context` / `tracedecay_pr_context` collapse config-file symbols (bug #3)** — Cargo.toml's 50+ dependency keys used to each enumerate as a separate "modified symbol", blowing past 50K tokens on a real diff. Both handlers now emit a single `{kind: "config_summary", file, config_keys: N}` entry per file with role `config` (`*.toml` / `*.yaml` / `*.json` / `*.ini` / `*.cfg` / `*.lock`). - **`classify_file_role` no longer flags source files with inline tests as "test" (bug #3 follow-up)** — a `src/foo.rs` with `#[cfg(test)] mod tests` at the bottom keeps role `source`. The "test" bucket is reserved for files that exist purely to host tests (path-based check via `is_test_file`). Three unit tests in `mcp::tools::handlers::git::tests` cover the classification matrix. -- **Rust extractor emits `Extends` edges for supertrait bounds (bug #9)** — `trait Leaf: Middle + Base` now produces unresolved refs with `EdgeKind::Extends` for each bound, so `tokensave_inheritance_depth`'s recursive CTE walks Rust supertrait chains correctly. Bound extraction handles `type_identifier`, `scoped_type_identifier`, `generic_type`, and `higher_ranked_trait_bound`. Existing DBs need a re-index (`tokensave sync --force`) to pick up the new edges. +- **Rust extractor emits `Extends` edges for supertrait bounds (bug #9)** — `trait Leaf: Middle + Base` now produces unresolved refs with `EdgeKind::Extends` for each bound, so `tracedecay_inheritance_depth`'s recursive CTE walks Rust supertrait chains correctly. Bound extraction handles `type_identifier`, `scoped_type_identifier`, `generic_type`, and `higher_ranked_trait_bound`. Existing DBs need a re-index (`tracedecay sync --force`) to pick up the new edges. ## [4.9.0] - 2026-05-15 ### Added -- **`tokensave_derives` tool** — surfaces the `#[derive(...)]` macros attached to a type plus the trait + method names each one synthesizes. Closes the dead-end-search gap where calls like `.clone()`, `format!("{:?}", x)`, or `serde_json::to_string(&x)` resolve to methods that never appear in the graph (the impl is generated by the proc-macro at compile time). Accepts either `qualified_name` or `node_id`. Well-known derives carry full info (trait path, method list, source crate); unknown / proc-macro derives surface with `well_known: false` and just the derive name. +- **`tracedecay_derives` tool** — surfaces the `#[derive(...)]` macros attached to a type plus the trait + method names each one synthesizes. Closes the dead-end-search gap where calls like `.clone()`, `format!("{:?}", x)`, or `serde_json::to_string(&x)` resolve to methods that never appear in the graph (the impl is generated by the proc-macro at compile time). Accepts either `qualified_name` or `node_id`. Well-known derives carry full info (trait path, method list, source crate); unknown / proc-macro derives surface with `well_known: false` and just the derive name. - **`derive_table` module** (`src/derive_table.rs`) — static knowledge of well-known derives (`Debug`, `Clone`, `Copy`, `Default`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`, `Serialize`, `Deserialize`, `Display`, `Error`), each mapped to its canonical trait path and method names. Five unit tests cover known + unknown derives and the `enrich` wrapper. -- **`derives` field on `tokensave_node` output for type nodes** — when the queried node is a `Struct` / `Enum` / `Union` / `Record` / `CaseClass` / `DataClass` / `PascalRecord`, the response now includes a `derives` array so callers don't need a second roundtrip just to learn what derives are present. -- **`TokenSave::get_derives_for_node(node_id)`** — public helper that re-reads the node's source-file attribute block and parses `#[derive(...)]` directly. The graph's `DerivesMacro` edges are unreliable: the resolver fuzzy-binds std-trait names like `Debug` to nonsense targets (e.g. a `Debug` enum variant in an unrelated test fixture), and the unique constraint on `(source, target, kind, line)` then collapses multiple derives on the same type onto a single edge. Re-parsing from source costs one `fs::read` per node lookup (cheap at typical Rust source sizes) and recovers the full derive list. Five unit tests in `derive_parse_tests` cover single/multi-block derives, qualified paths, mixed attribute kinds, and dedup. +- **`derives` field on `tracedecay_node` output for type nodes** — when the queried node is a `Struct` / `Enum` / `Union` / `Record` / `CaseClass` / `DataClass` / `PascalRecord`, the response now includes a `derives` array so callers don't need a second roundtrip just to learn what derives are present. +- **`TraceDecay::get_derives_for_node(node_id)`** — public helper that re-reads the node's source-file attribute block and parses `#[derive(...)]` directly. The graph's `DerivesMacro` edges are unreliable: the resolver fuzzy-binds std-trait names like `Debug` to nonsense targets (e.g. a `Debug` enum variant in an unrelated test fixture), and the unique constraint on `(source, target, kind, line)` then collapses multiple derives on the same type onto a single edge. Re-parsing from source costs one `fs::read` per node lookup (cheap at typical Rust source sizes) and recovers the full derive list. Five unit tests in `derive_parse_tests` cover single/multi-block derives, qualified paths, mixed attribute kinds, and dedup. ### Changed -- **Total MCP tools: 59 → 60** — `tokensave_derives` added. +- **Total MCP tools: 59 → 60** — `tracedecay_derives` added. ## [4.8.0] - 2026-05-15 ### Added -- **`tokensave_diagnose` tool** — parses raw `cargo check` / `cargo clippy` / `rustc` stderr into structured diagnostics, then maps each one to the smallest containing graph node and (by default) pre-attaches up to 5 callers. Closes the today-an-agent-hand-parses gap: the response includes severity, optional error code (`E0308`, clippy lint name), message, file/line/column, the owning node (id, kind, qualified_name, span), and the call sites the broken code is reachable from. Diagnostics without a `--> file:line:col` span are dropped — they cannot be located. Accepts a `severity` filter (`error` / `warning` / `all`) and a `max_diagnostics` cap (default 50, hard cap 500). -- **`tokensave_run_affected_tests` tool** — closes the loop opened by `tokensave_test_map` / `tokensave_test_risk`. Given `changed_paths` (or, by default, `git diff --name-only HEAD`), the handler walks the graph to find every test that covers a function/method in those files, then runs `cargo test --no-fail-fast -- ` with `kill_on_drop` and a configurable `timeout_secs` (default 300). Parses libtest stdout into JSON `{ test, passed, covers_source_ids[] }` entries plus pass/fail counts and the cargo exit code; trailing stdout/stderr are tailed at 2 KB each so the response stays in budget. `max_tests` defaults to 100 (hard cap 500) so a refactor touching everything doesn't dispatch an unbounded list. +- **`tracedecay_diagnose` tool** — parses raw `cargo check` / `cargo clippy` / `rustc` stderr into structured diagnostics, then maps each one to the smallest containing graph node and (by default) pre-attaches up to 5 callers. Closes the today-an-agent-hand-parses gap: the response includes severity, optional error code (`E0308`, clippy lint name), message, file/line/column, the owning node (id, kind, qualified_name, span), and the call sites the broken code is reachable from. Diagnostics without a `--> file:line:col` span are dropped — they cannot be located. Accepts a `severity` filter (`error` / `warning` / `all`) and a `max_diagnostics` cap (default 50, hard cap 500). +- **`tracedecay_run_affected_tests` tool** — closes the loop opened by `tracedecay_test_map` / `tracedecay_test_risk`. Given `changed_paths` (or, by default, `git diff --name-only HEAD`), the handler walks the graph to find every test that covers a function/method in those files, then runs `cargo test --no-fail-fast -- ` with `kill_on_drop` and a configurable `timeout_secs` (default 300). Parses libtest stdout into JSON `{ test, passed, covers_source_ids[] }` entries plus pass/fail counts and the cargo exit code; trailing stdout/stderr are tailed at 2 KB each so the response stays in budget. `max_tests` defaults to 100 (hard cap 500) so a refactor touching everything doesn't dispatch an unbounded list. - **`src/diagnose.rs`** — standalone parser module. Five unit tests cover typed errors (`error[E0308]`), clippy-style headers without codes, summary lines without spans (correctly dropped), multi-diagnostic blocks, and ANSI-prefixed lines. -- **`TokenSave::node_at_location(file, line_1based)`** — public helper that returns the smallest-span node containing a 1-based source location. Used by `tokensave_diagnose`; converts to the internal 0-based representation transparently. +- **`TraceDecay::node_at_location(file, line_1based)`** — public helper that returns the smallest-span node containing a 1-based source location. Used by `tracedecay_diagnose`; converts to the internal 0-based representation transparently. ### Changed -- **Total MCP tools: 57 → 59** — `tokensave_diagnose` and `tokensave_run_affected_tests` added. +- **Total MCP tools: 57 → 59** — `tracedecay_diagnose` and `tracedecay_run_affected_tests` added. - **New handler module `src/mcp/tools/handlers/workflow.rs`** — keeps cargo/libtest plumbing out of `graph.rs`, which is for code-graph queries. ## [4.7.0] - 2026-05-15 ### Added -- **`tokensave_impls` tool** — index of `impl Trait for Type` blocks. Accepts optional `trait` and `type` filters (both short and qualified names). With neither, returns every impl in the graph. Surfaces information that was previously buried behind the second-class `Implements` edge: which types satisfy a given trait, which traits a type implements, and the impl blocks themselves with their files and signatures. -- **Trait dispatch resolution on `tokensave_callees`** — when a callee resolves to a method whose enclosing scope is a trait, the handler walks back via `Implements` edges to surface the concrete impl methods reachable through that trait. New entries are tagged `dispatch_via_trait: true` and carry a `dispatch_from` pointer to the trait method. Pass `resolve_dispatch: false` to opt out and get only direct call edges. -- **`TokenSave::get_impls(trait, type)`** — public helper backing the new tool. -- **`TokenSave::get_trait_dispatch_targets(method)`** — public helper that returns every impl-method satisfying a given trait method, used by `handle_callees` to surface dispatch targets. +- **`tracedecay_impls` tool** — index of `impl Trait for Type` blocks. Accepts optional `trait` and `type` filters (both short and qualified names). With neither, returns every impl in the graph. Surfaces information that was previously buried behind the second-class `Implements` edge: which types satisfy a given trait, which traits a type implements, and the impl blocks themselves with their files and signatures. +- **Trait dispatch resolution on `tracedecay_callees`** — when a callee resolves to a method whose enclosing scope is a trait, the handler walks back via `Implements` edges to surface the concrete impl methods reachable through that trait. New entries are tagged `dispatch_via_trait: true` and carry a `dispatch_from` pointer to the trait method. Pass `resolve_dispatch: false` to opt out and get only direct call edges. +- **`TraceDecay::get_impls(trait, type)`** — public helper backing the new tool. +- **`TraceDecay::get_trait_dispatch_targets(method)`** — public helper that returns every impl-method satisfying a given trait method, used by `handle_callees` to surface dispatch targets. ### Changed -- **Total MCP tools: 56 → 57** — `tokensave_impls` added. -- **`tokensave_callees` description and schema** updated to advertise dispatch resolution and the new `resolve_dispatch` argument. +- **Total MCP tools: 56 → 57** — `tracedecay_impls` added. +- **`tracedecay_callees` description and schema** updated to advertise dispatch resolution and the new `resolve_dispatch` argument. ### Fixed -- **`tokensave_search` ranks definitions above references (PR1 follow-up)** — BM25 alone was placing `use foo` statements ahead of the actual `pub fn foo()` definition because both score similarly when the symbol name matches. `TokenSave::search` now over-fetches and re-ranks: every `NodeKind` carries an explicit bonus (callable defs +3.0, type defs / proto defs +2.5, impl blocks +2.0, values / macros / enum variants +1.0, members +0.5, neutral 0.0, container modules -1.5, annotation usages -2.0, `use` / `include` -3.0). The match is exhaustive so adding a new `NodeKind` forces a re-tune here. Result: searching for `gmres` returns the function before its imports. -- **`get_nodes_by_qualified_name` falls back to suffix or bare-name match (PR1 follow-up + user feedback)** — strict equality match remains primary. On empty results: queries with `::` retry as `qualified_name LIKE '%::'` (full scan, `LIMIT 50`); queries without `::` retry as `name = ?` using `idx_nodes_name`. Both forms now resolve, e.g. `get_impls`, `TokenSave::get_impls`, and the full doubled path all return the same row. `tokensave_signature` and `tokensave_by_qualified_name` share the lookup so they agree. -- **Rust extractor no longer doubles the file path in `qualified_name`** — `qualified_prefix()` prepended `self.file_path` even though the file root was already pushed onto `node_stack` at extraction start, producing qnames like `src/foo.rs::src/foo.rs::Type::method`. Now iterates the stack only, yielding `src/foo.rs::Type::method`. Existing DBs will keep the old form until re-indexed (`tokensave sync --force`). +- **`tracedecay_search` ranks definitions above references (PR1 follow-up)** — BM25 alone was placing `use foo` statements ahead of the actual `pub fn foo()` definition because both score similarly when the symbol name matches. `TraceDecay::search` now over-fetches and re-ranks: every `NodeKind` carries an explicit bonus (callable defs +3.0, type defs / proto defs +2.5, impl blocks +2.0, values / macros / enum variants +1.0, members +0.5, neutral 0.0, container modules -1.5, annotation usages -2.0, `use` / `include` -3.0). The match is exhaustive so adding a new `NodeKind` forces a re-tune here. Result: searching for `gmres` returns the function before its imports. +- **`get_nodes_by_qualified_name` falls back to suffix or bare-name match (PR1 follow-up + user feedback)** — strict equality match remains primary. On empty results: queries with `::` retry as `qualified_name LIKE '%::'` (full scan, `LIMIT 50`); queries without `::` retry as `name = ?` using `idx_nodes_name`. Both forms now resolve, e.g. `get_impls`, `TraceDecay::get_impls`, and the full doubled path all return the same row. `tracedecay_signature` and `tracedecay_by_qualified_name` share the lookup so they agree. +- **Rust extractor no longer doubles the file path in `qualified_name`** — `qualified_prefix()` prepended `self.file_path` even though the file root was already pushed onto `node_stack` at extraction start, producing qnames like `src/foo.rs::src/foo.rs::Type::method`. Now iterates the stack only, yielding `src/foo.rs::Type::method`. Existing DBs will keep the old form until re-indexed (`tracedecay sync --force`). - **`get_impls` batches the trait lookup (PR2 review follow-up)** — previously one `get_node_by_id` per impl block (N+1). Now collects every Implements-edge target then issues a single `get_nodes_by_ids` to populate the trait map. - **`graph_stale` insertion asserts on non-object results (PR1 review follow-up)** — `handle_tools_call` now `debug_assert!`s that the wrapped tool result is a JSON object before attaching the `graph_stale` field, matching the "crash hard on unknown value" convention so a future handler returning a non-object is caught immediately instead of silently dropping the structured staleness signal. - **`cost_to_expand` body heuristic documented as Rust-tuned (PR1 review follow-up)** — the `20 tokens/line` rate over-estimates Haskell/Python by ~2-3x; the doc comment now explicitly says so and notes the single-line floor of 20 tokens, since this number is part of the public tool contract. @@ -389,14 +389,14 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [4.6.0] - 2026-05-15 ### Added -- **`tokensave_signature` tool** — signature-only lookup by `qualified_name` or `node_id`. Returns visibility, signature string (generics, params, return type, where clauses), docstring, kind, and async flag for matching nodes. No body content. Replaces most agent `Read` calls when only the public-API surface of a symbol is needed. -- **`graph_stale` field on tool results** — when files referenced by a tool result remain stale after the post-call sync attempt, the JSON-RPC response now carries a top-level `graph_stale: ["path", …]` array plus a machine-parseable `tokensave_graph_stale: [...]` text marker. The existing human-readable WARNING is preserved. Closes the silent-drift gap where renamed/deleted symbols could return phantom callers/callees without a programmatic signal. -- **`cost_to_expand` annotation on node results** — `tokensave_node` and `tokensave_signature` responses now include `cost_to_expand: { body, full_file }` (approximate tokens) so callers can decide whether to set `include_code=true` before re-querying. Body estimate uses ~20 tokens/line; `full_file` uses indexed `files.size / 4`. -- **`tokensave://schema` MCP resource** — markdown resource documenting the on-disk `.tokensave/tokensave.db` schema: tables, columns, indexes, FKs, common query recipes (impl-of-trait, top callers, largest functions), and gotchas (content-hashed IDs, trait dispatch, derive macros). Makes the SQLite escape hatch usable without trial-and-error. -- **`TokenSave::get_file_size_bytes(path)`** — public helper that returns the indexed byte size of a file (0 when unknown). Backs the `cost_to_expand` full-file estimate. +- **`tracedecay_signature` tool** — signature-only lookup by `qualified_name` or `node_id`. Returns visibility, signature string (generics, params, return type, where clauses), docstring, kind, and async flag for matching nodes. No body content. Replaces most agent `Read` calls when only the public-API surface of a symbol is needed. +- **`graph_stale` field on tool results** — when files referenced by a tool result remain stale after the post-call sync attempt, the JSON-RPC response now carries a top-level `graph_stale: ["path", …]` array plus a machine-parseable `tracedecay_graph_stale: [...]` text marker. The existing human-readable WARNING is preserved. Closes the silent-drift gap where renamed/deleted symbols could return phantom callers/callees without a programmatic signal. +- **`cost_to_expand` annotation on node results** — `tracedecay_node` and `tracedecay_signature` responses now include `cost_to_expand: { body, full_file }` (approximate tokens) so callers can decide whether to set `include_code=true` before re-querying. Body estimate uses ~20 tokens/line; `full_file` uses indexed `files.size / 4`. +- **`tracedecay://schema` MCP resource** — markdown resource documenting the on-disk `.tracedecay/tracedecay.db` schema: tables, columns, indexes, FKs, common query recipes (impl-of-trait, top callers, largest functions), and gotchas (content-hashed IDs, trait dispatch, derive macros). Makes the SQLite escape hatch usable without trial-and-error. +- **`TraceDecay::get_file_size_bytes(path)`** — public helper that returns the indexed byte size of a file (0 when unknown). Backs the `cost_to_expand` full-file estimate. ### Changed -- **Total MCP tools: 55 → 56** — `tokensave_signature` added; all existing tools unchanged. +- **Total MCP tools: 55 → 56** — `tracedecay_signature` added; all existing tools unchanged. ### Fixed - **Clippy: project-wide cleanup to restore `-D warnings`** — 43 pre-existing lib errors and 3 bin errors resolved without behavioral change: module doc comments wrap snake_case tool names in backticks; `bench.rs` uses `write!` instead of `format!(..).push_str`; `extraction_worker.rs` converted to `let…else`; redundant closures in `agents/copilot.rs`, `extraction/haskell_extractor.rs`, `mcp/tools/handlers/memory.rs` replaced with method references; `resolution/resolver.rs` merges identical match arms; `serve.rs` uses `sort_by_key`; `upgrade.rs` uses `is_ok_and`; `main.rs` drops a useless `.into()`. @@ -404,8 +404,8 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [4.5.1] - 2026-05-15 ### Added -- **`tokensave monitor` highlights the last 3 updates** — the most recently active (project, tool) pair renders green, second-to-last orange, third-to-last yellow. Re-firing the same tool moves it to the front rather than duplicating. Cleared on Ctrl+R. -- **Welcome banner on fresh installs** — when `tokensave` is invoked with no subcommand and the global DB has zero registered projects, print a cyan welcome that explicitly suggests `tokensave init` before the existing "Create one now?" prompt. Returning users see no change. +- **`tracedecay monitor` highlights the last 3 updates** — the most recently active (project, tool) pair renders green, second-to-last orange, third-to-last yellow. Re-firing the same tool moves it to the front rather than duplicating. Cleared on Ctrl+R. +- **Welcome banner on fresh installs** — when `tracedecay` is invoked with no subcommand and the global DB has zero registered projects, print a cyan welcome that explicitly suggests `tracedecay init` before the existing "Create one now?" prompt. Returning users see no change. ### Fixed - **CI: `clippy::items_after_test_module` denied under Rust 1.95.0** — two test modules from the v4.5.0 work (`gain_tests` in `src/commands.rs`, `gain_format_tests` in `src/display.rs`) were inserted mid-file. Rust 1.95.0 promoted this lint into `clippy::all`, which the project denies project-wide. Both moved to file end. @@ -414,9 +414,9 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [4.5.0] - 2026-05-15 ### Added -- **Cross-session memory primitives (3 new MCP tools)** — `tokensave_record_decision`, `tokensave_record_code_area`, and `tokensave_session_recall` persist agent decisions and worked-on paths in the per-project DB so they survive across sessions. `session_recall` uses FTS5 for fuzzy retrieval. Backed by two new tables and an FTS mirror added in schema migration v8. -- **`tokensave gain` CLI for the savings ledger** — every MCP tool call now writes an append-only row to a new `savings_ledger` table in the global DB. `tokensave gain [--all] [--history] [--range 7d] [--json]` reports tokens saved + dollar estimates (Sonnet input pricing, refreshed daily via LiteLLM). -- **`tokensave bench` reproducible retrieval benchmark** — runs a fixed query set through `tokensave_context` and reports retrieval savings vs a full-file baseline (CCE-style methodology). Ships with a 10-query generic default set embedded into the binary (no external file dependency); `--queries ` accepts a custom set. Measured **93% mean retrieval savings on tokensave's own repo** (180K → 3.4K tokens across 10 generic queries). +- **Cross-session memory primitives (3 new MCP tools)** — `tracedecay_record_decision`, `tracedecay_record_code_area`, and `tracedecay_session_recall` persist agent decisions and worked-on paths in the per-project DB so they survive across sessions. `session_recall` uses FTS5 for fuzzy retrieval. Backed by two new tables and an FTS mirror added in schema migration v8. +- **`tracedecay gain` CLI for the savings ledger** — every MCP tool call now writes an append-only row to a new `savings_ledger` table in the global DB. `tracedecay gain [--all] [--history] [--range 7d] [--json]` reports tokens saved + dollar estimates (Sonnet input pricing, refreshed daily via LiteLLM). +- **`tracedecay bench` reproducible retrieval benchmark** — runs a fixed query set through `tracedecay_context` and reports retrieval savings vs a full-file baseline (CCE-style methodology). Ships with a 10-query generic default set embedded into the binary (no external file dependency); `--queries ` accepts a custom set. Measured **93% mean retrieval savings on tracedecay's own repo** (180K → 3.4K tokens across 10 generic queries). ### Changed - **Schema bumped from v7 to v8** — adds `memory_decisions`, `memory_code_areas`, and the `memory_decisions_fts` virtual table. Existing user DBs upgrade idempotently via `migrate_v8`; fresh installs use the mirrored DDL in `create_schema`. No breaking changes; existing tools and queries continue to work. @@ -435,19 +435,19 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ### Added - **`/// skip-test-coverage` doc comment convention (issue #75)** — mark genuinely untestable functions to exclude them from `test_risk` coverage calculations. The `skipped` count appears in the summary. A `coverage_discipline` health dimension penalises overuse (up to 10% quality signal reduction). -- **VS Code Insiders support for the Copilot installer (issue #69)** — `tokensave install --agent copilot` now also configures `Code - Insiders/User/settings.json` alongside the regular VS Code path. -- **Copilot prompt instructions (issue #70)** — the Copilot installer now writes `copilot-instructions.md` with tokensave MCP tool guidance to VS Code (`User/prompts/`), VS Code Insiders, and Copilot CLI (`~/.copilot/`). +- **VS Code Insiders support for the Copilot installer (issue #69)** — `tracedecay install --agent copilot` now also configures `Code - Insiders/User/settings.json` alongside the regular VS Code path. +- **Copilot prompt instructions (issue #70)** — the Copilot installer now writes `copilot-instructions.md` with tracedecay MCP tool guidance to VS Code (`User/prompts/`), VS Code Insiders, and Copilot CLI (`~/.copilot/`). ## [4.3.18] - 2026-05-14 ### Fixed -- **Inline `#[cfg(test)]` test modules are now recognized as test coverage** — `test_map`, `health`, `test_risk`, `affected`, `impact`, and `commit_context` previously only detected tests by file path patterns (`tests/`, `_test.`, etc.). Functions annotated with `#[test]` inside inline `#[cfg(test)] mod tests { ... }` blocks in source files (226 such functions in tokensave's own codebase) were invisible to coverage analysis. The Rust extractor now emits `Annotates` edges from `#[cfg(test)]` to modules, and all test-detection handlers query `#[test]` annotations via the graph in addition to checking file paths. -- **`tokensave serve` resolves the correct project in multi-folder workspaces (issue #66 reopened)** — when multiple projects are registered in the global DB, the `serve` fallback now picks the project closest to cwd (ancestor match first, then descendant match) instead of failing with an ambiguity error. As a last resort, the server peeks at the MCP `initialize` request's `roots` array to discover the workspace folder the client is working in. +- **Inline `#[cfg(test)]` test modules are now recognized as test coverage** — `test_map`, `health`, `test_risk`, `affected`, `impact`, and `commit_context` previously only detected tests by file path patterns (`tests/`, `_test.`, etc.). Functions annotated with `#[test]` inside inline `#[cfg(test)] mod tests { ... }` blocks in source files (226 such functions in tracedecay's own codebase) were invisible to coverage analysis. The Rust extractor now emits `Annotates` edges from `#[cfg(test)]` to modules, and all test-detection handlers query `#[test]` annotations via the graph in addition to checking file paths. +- **`tracedecay serve` resolves the correct project in multi-folder workspaces (issue #66 reopened)** — when multiple projects are registered in the global DB, the `serve` fallback now picks the project closest to cwd (ancestor match first, then descendant match) instead of failing with an ambiguity error. As a last resort, the server peeks at the MCP `initialize` request's `roots` array to discover the workspace folder the client is working in. ## [4.3.17] - 2026-05-14 ### Fixed -- **`tokensave upgrade` no longer breaks Homebrew installs (issue #67)** — previously, self-upgrading a Homebrew-managed install mutated the Cellar directly, leaving Homebrew's recorded keg state inconsistent and causing later `brew upgrade` to fail. `tokensave upgrade` now detects Homebrew installs and delegates to `brew update && brew upgrade tokensave`. (PR #68, thanks @lesbass) +- **`tracedecay upgrade` no longer breaks Homebrew installs (issue #67)** — previously, self-upgrading a Homebrew-managed install mutated the Cellar directly, leaving Homebrew's recorded keg state inconsistent and causing later `brew upgrade` to fail. `tracedecay upgrade` now detects Homebrew installs and delegates to `brew update && brew upgrade tracedecay`. (PR #68, thanks @lesbass) - **Exclude globs now match nested directories (issue #64)** — the default `node_modules/**` pattern only excluded top-level `node_modules/`, not nested ones like `projectA/node_modules/`. Changed default to `**/node_modules/**`. Also added `is_excluded_dir()` so bare patterns like `**/dist` correctly prune directories during scanning without requiring a trailing `/**`. - **VS Code multi-folder workspaces can now start the Copilot MCP server (issue #66)** — the Copilot config used `${workspaceFolder}` which VS Code cannot resolve in multi-folder workspaces. Dropped in favour of the serve command's built-in project discovery, matching every other agent integration. @@ -462,7 +462,7 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [4.3.15] - 2026-05-11 ### Fixed -- **Installing the Codex integration no longer wipes `~/.codex/config.toml` (issue #63)** — `load_toml_file` used `contents.parse::()`, which in the `toml = "1"` crate parses a single TOML *value* rather than a *document*. Any well-formed `config.toml` therefore parsed as an error and silently fell back to an empty table; `install_mcp_server` then serialized that empty-plus-tokensave table back over the file, erasing every other key the user had set (model, approval_policy, other `[mcp_servers.*]` entries, comments). `load_toml_file` now uses `toml::from_str::` so real documents round-trip, returns `Result` instead of swallowing errors, and refuses to overwrite when an existing file cannot be parsed (so a typo or partial edit leaves the original intact for the user to fix). `doctor_check_config`, `install_mcp_server`, `uninstall_mcp_server`, and `CodexIntegration::has_tokensave` were updated to handle the `Result` shape — the doctor now reports parse errors as a failed check, and `has_tokensave` returns `false` on parse error rather than panicking. +- **Installing the Codex integration no longer wipes `~/.codex/config.toml` (issue #63)** — `load_toml_file` used `contents.parse::()`, which in the `toml = "1"` crate parses a single TOML *value* rather than a *document*. Any well-formed `config.toml` therefore parsed as an error and silently fell back to an empty table; `install_mcp_server` then serialized that empty-plus-tracedecay table back over the file, erasing every other key the user had set (model, approval_policy, other `[mcp_servers.*]` entries, comments). `load_toml_file` now uses `toml::from_str::` so real documents round-trip, returns `Result` instead of swallowing errors, and refuses to overwrite when an existing file cannot be parsed (so a typo or partial edit leaves the original intact for the user to fix). `doctor_check_config`, `install_mcp_server`, `uninstall_mcp_server`, and `CodexIntegration::has_tracedecay` were updated to handle the `Result` shape — the doctor now reports parse errors as a failed check, and `has_tracedecay` returns `false` on parse error rather than panicking. ### Changed - **Every config-file write across all agent integrations now leaves a `.bak` copy first.** Previously only install paths went through `backup_config_file`; uninstall paths and `doctor` auto-repair paths called `std::fs::write` directly, so a corrupted serialization or a bug in the rewrite logic could destroy the user's settings with no recovery. A new shared `backup_and_write_json` helper (in `src/agents/mod.rs`) wraps `backup_config_file` + `safe_write_json_file` with best-effort error handling suited to uninstall flows. Every agent's uninstall path (claude, cursor, copilot, cline, zed, kilo, roo-code, opencode, gemini) now goes through this helper, as do the claude `doctor` auto-repair and local-settings-cleanup paths. The Codex TOML write path (`write_toml_file`) also creates a `.bak` before writing for the same reason. Eight per-agent install-side regression tests plus a cursor uninstall-side regression test were added to `tests/agent_test.rs` to guard the new invariant. @@ -470,10 +470,10 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [4.3.14] - 2026-05-11 ### Fixed -- **`tokensave_body` no longer drops the function's outer closing brace (issue #62)** — `handle_body` returned the source spanning `start_line..end_line`, but stored line fields are tree-sitter rows (0-based) while `extract_lines` was written assuming 1-based inclusive inputs. The mismatch meant `lines[start..end_line]` exclusive — one short, lopping off the trailing `}` (or any language's outer block closer sitting on its own line). Inner braces were unaffected because they were never on the boundary. `extract_lines` now treats inputs as 0-based row indices and slices inclusively, so the returned body is byte-exact usable as an `Edit` tool `old_string`. Regression added in `test_body_returns_full_function_source` (`tests/mcp_handler_test.rs`) — verified failing pre-fix with `body: "\nfn format_greeting(name: &str) -> String {\n format!(\"Hello, {}!\", name)"` (closing `}` missing). +- **`tracedecay_body` no longer drops the function's outer closing brace (issue #62)** — `handle_body` returned the source spanning `start_line..end_line`, but stored line fields are tree-sitter rows (0-based) while `extract_lines` was written assuming 1-based inclusive inputs. The mismatch meant `lines[start..end_line]` exclusive — one short, lopping off the trailing `}` (or any language's outer block closer sitting on its own line). Inner braces were unaffected because they were never on the boundary. `extract_lines` now treats inputs as 0-based row indices and slices inclusively, so the returned body is byte-exact usable as an `Edit` tool `old_string`. Regression added in `test_body_returns_full_function_source` (`tests/mcp_handler_test.rs`) — verified failing pre-fix with `body: "\nfn format_greeting(name: &str) -> String {\n format!(\"Hello, {}!\", name)"` (closing `}` missing). ### Changed -- **`tokensave_body` now exposes `start_line` / `end_line` as 1-based file line numbers** — they were previously the raw 0-based tree-sitter row indices, which read as "off by one" against the line numbers any editor or `Edit`-style tool displays. The values now match what users see when they open the file, so the reported `end_line` is the line containing the function's closing brace. The shift is local to `handle_body`; other handlers still expose `node.start_line` as-is. +- **`tracedecay_body` now exposes `start_line` / `end_line` as 1-based file line numbers** — they were previously the raw 0-based tree-sitter row indices, which read as "off by one" against the line numbers any editor or `Edit`-style tool displays. The values now match what users see when they open the file, so the reported `end_line` is the line containing the function's closing brace. The shift is local to `handle_body`; other handlers still expose `node.start_line` as-is. ## [4.3.13] - 2026-05-10 @@ -481,52 +481,52 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo - **Switched to `tree-sitter-grammars/tree-sitter-markdown` (block + inline split parsers)** — the previously-vendored `ikatyang/tree-sitter-markdown` (last updated 2023, GLR-heavy without native frontmatter handling) hung the indexer on otherwise-fine markdown files containing YAML frontmatter. Specifically, the old grammar parsed `---\n…\n---` content as ordinary markdown, where 6/8/10-space-indented YAML lines were simultaneously valid as both deeply-nested list-item continuations and as indented code blocks; tree-sitter's GLR explored all alternatives in parallel, with the surviving-versions count growing exponentially per line. A real-world 18 KB resume.md hung the worker indefinitely; a 4.4 KB minimal reproducer was bisected and is now a regression fixture (`tests/fixtures/markdown_yaml_frontmatter_hang.md`). The new grammar emits an opaque `(minus_metadata)` / `(plus_metadata)` node for frontmatter, so the markdown rules never see the YAML — the same 4.4 KB reproducer parses in ~7 ms, the full 18 KB file in ~16 ms. The markdown extractor was rewritten for the new AST (block parser produces `(atx_heading … heading_content: (inline …))`, headings still become `Module` nodes; the inline parser is run over each `(inline)` byte range via `set_included_ranges` to extract `(inline_link)` for `Uses` edges). All 16 existing markdown extraction tests still pass; 3 new regression tests guard the migration. ### Added -- **Per-file extraction timeout** — every extractor round trip is now wrapped in a watchdog (configurable via `extraction_timeout_secs` in `~/.tokensave/config.toml`, default 60 s). A file whose extractor doesn't respond in time has its worker subprocess killed via `Child::kill()` and is recorded in `SyncResult.skipped_paths` with reason `"extractor timed out (>Ns)"`. Worker crashes (the existing failure path) are now also recorded with reason `"extractor crashed (...)"` instead of disappearing silently. This bounds the worst case for any future grammar pathology — `tokensave sync` can no longer hang forever on a single malformed file. +- **Per-file extraction timeout** — every extractor round trip is now wrapped in a watchdog (configurable via `extraction_timeout_secs` in `~/.tracedecay/config.toml`, default 60 s). A file whose extractor doesn't respond in time has its worker subprocess killed via `Child::kill()` and is recorded in `SyncResult.skipped_paths` with reason `"extractor timed out (>Ns)"`. Worker crashes (the existing failure path) are now also recorded with reason `"extractor crashed (...)"` instead of disappearing silently. This bounds the worst case for any future grammar pathology — `tracedecay sync` can no longer hang forever on a single malformed file. ## [4.3.12] - 2026-05-09 ### Changed -- **The beta channel is open again** — `tokensave channel beta` was hard-gated to `"the beta channel is not available at this time"` while the prior 4.5.x beta line was being merged into stable. With v5.0.0-beta.1 published on the prerelease channel, the gate is removed: `switch_channel` now resolves `"beta"` through the same path as `"stable"` and downloads the latest GitHub prerelease. The `unknown channel` error message also lists `beta` as a valid target again. -- **Retired the "beta channel has been merged into stable" nudge** in `main.rs`. Beta users (anyone whose binary version contains `-`) used to see the nudge on every invocation; with the channel reopened the nudge is no longer correct. Beta users now stay on beta until they explicitly run `tokensave channel stable`. +- **The beta channel is open again** — `tracedecay channel beta` was hard-gated to `"the beta channel is not available at this time"` while the prior 4.5.x beta line was being merged into stable. With v5.0.0-beta.1 published on the prerelease channel, the gate is removed: `switch_channel` now resolves `"beta"` through the same path as `"stable"` and downloads the latest GitHub prerelease. The `unknown channel` error message also lists `beta` as a valid target again. +- **Retired the "beta channel has been merged into stable" nudge** in `main.rs`. Beta users (anyone whose binary version contains `-`) used to see the nudge on every invocation; with the channel reopened the nudge is no longer correct. Beta users now stay on beta until they explicitly run `tracedecay channel stable`. ### Fixed -- **`tokensave wipe` no longer leaks the global DB into the wipe set when `$HOME` is symlinked** — the home `.tokensave` skip relied on lexical path equality, so a user whose `$HOME` resolves through a symlink (e.g. macOS `/Users/x` vs the canonical `/private/var/...`) could see `~/.tokensave` show up as a wipe target if the descendant walk reached it via the canonical chain. The skip now canonicalizes both the home path and every candidate before comparing. +- **`tracedecay wipe` no longer leaks the global DB into the wipe set when `$HOME` is symlinked** — the home `.tracedecay` skip relied on lexical path equality, so a user whose `$HOME` resolves through a symlink (e.g. macOS `/Users/x` vs the canonical `/private/var/...`) could see `~/.tracedecay` show up as a wipe target if the descendant walk reached it via the canonical chain. The skip now canonicalizes both the home path and every candidate before comparing. ### Changed (carried forward from the prior unreleased section) -- **Descendant walk for `tokensave wipe` / `tokensave list` is now iterative with cycle protection** — `find_descendant_tokensave` used to recurse, which made deep trees a stack-overflow risk and relied entirely on `file_type()` skipping symlinks for cycle safety. It now uses an explicit worklist plus a canonical-path `visited` set, so the walk is bounded even if a directory cycle slips past the symlink filter (e.g. Windows junctions). -- **`tokensave doctor` purges stale global-DB entries in batched statements** — purging used to issue one `DELETE` per stale row, which meant N serial round-trips against libsql for a stale-store cleanup (the case that prompted this: 216 deletes). A new `GlobalDb::delete_projects(&[String])` issues one `DELETE … WHERE path IN (…)` per chunk of 256, so the same 216-row purge is now one round-trip. +- **Descendant walk for `tracedecay wipe` / `tracedecay list` is now iterative with cycle protection** — `find_descendant_tracedecay` used to recurse, which made deep trees a stack-overflow risk and relied entirely on `file_type()` skipping symlinks for cycle safety. It now uses an explicit worklist plus a canonical-path `visited` set, so the walk is bounded even if a directory cycle slips past the symlink filter (e.g. Windows junctions). +- **`tracedecay doctor` purges stale global-DB entries in batched statements** — purging used to issue one `DELETE` per stale row, which meant N serial round-trips against libsql for a stale-store cleanup (the case that prompted this: 216 deletes). A new `GlobalDb::delete_projects(&[String])` issues one `DELETE … WHERE path IN (…)` per chunk of 256, so the same 216-row purge is now one round-trip. - **`gather_local_projects_from` is now a separately-exported helper** — extracts the pure discovery logic from the cwd-driven `gather_local_projects` wrapper so the ancestor + descendant walk can be unit-tested without mutating the process's working directory. Backed by 7 new tests covering cwd / ancestor-only / descendant-only / ancestor+descendant dedup / `node_modules` skip / canonical home-skip / empty-dir. - **Cleared `clippy::map_unwrap_or` warning in `display::shuffle_flags`** — the xorshift seed now uses `map_or` instead of `map(...).unwrap_or(...)`. Behavior unchanged. ## [4.3.11] - 2026-05-09 ### Added -- **`tokensave doctor` now reports stale entries in the global DB and offers to purge them** — projects registered in `~/.tokensave/global.db` whose `.tokensave/` directory is gone (deleted, moved, or scratch dirs cleaned up by the OS) are listed under the "Global database" section. Up to 10 paths are shown with an "… and N more" tail. When run interactively, the doctor prompts `Purge N stale row(s) from the global DB? [Y/n]`; on confirmation each stale row is deleted via `GlobalDb::delete_project`. When stdin is not a terminal (CI, piped invocation), the stale list is shown as a warning with a hint to re-run interactively. +- **`tracedecay doctor` now reports stale entries in the global DB and offers to purge them** — projects registered in `~/.tracedecay/global.db` whose `.tracedecay/` directory is gone (deleted, moved, or scratch dirs cleaned up by the OS) are listed under the "Global database" section. Up to 10 paths are shown with an "… and N more" tail. When run interactively, the doctor prompts `Purge N stale row(s) from the global DB? [Y/n]`; on confirmation each stale row is deleted via `GlobalDb::delete_project`. When stdin is not a terminal (CI, piped invocation), the stale list is shown as a warning with a hint to re-run interactively. ### Fixed -- **`tokensave reinstall` now refreshes every detected agent, not just the first one ever installed** — `migrate_installed_agents` previously returned early as soon as `installed_agents` was non-empty. A user who installed agent A and later configured agent B (e.g. installed Copilot first, then Claude) would have only A in the list, so `reinstall` silently skipped B and its tool permissions never got refreshed when new tools shipped. The migration now scans every agent on each call and additively appends any whose tokensave config exists on disk but is missing from the tracked list. Side effect: a stale `tokensave install` warning ("N new tokensave tool(s) not yet permitted") could persist across reinstalls — that no longer happens. The detection logic is also extracted into a pure `detect_missing_installed_agents` helper covered by a regression test that reproduces the original "claude missing when copilot is tracked" scenario. -- **`tokensave wipe` warning banner now reaches full width** — the colored title row was 49 visual columns while the `═` rules above and below were 64, producing a short red strip floating between long horizontal lines. The title is now centered and padded with red-background spaces, sandwiched between two blank red rows so the warning reads as a single fixed-width block. +- **`tracedecay reinstall` now refreshes every detected agent, not just the first one ever installed** — `migrate_installed_agents` previously returned early as soon as `installed_agents` was non-empty. A user who installed agent A and later configured agent B (e.g. installed Copilot first, then Claude) would have only A in the list, so `reinstall` silently skipped B and its tool permissions never got refreshed when new tools shipped. The migration now scans every agent on each call and additively appends any whose tracedecay config exists on disk but is missing from the tracked list. Side effect: a stale `tracedecay install` warning ("N new tracedecay tool(s) not yet permitted") could persist across reinstalls — that no longer happens. The detection logic is also extracted into a pure `detect_missing_installed_agents` helper covered by a regression test that reproduces the original "claude missing when copilot is tracked" scenario. +- **`tracedecay wipe` warning banner now reaches full width** — the colored title row was 49 visual columns while the `═` rules above and below were 64, producing a short red strip floating between long horizontal lines. The title is now centered and padded with red-background spaces, sandwiched between two blank red rows so the warning reads as a single fixed-width block. ## [4.3.10] - 2026-05-09 ### Added -- **`tokensave list` command for inspecting tracked projects** — `list` shows the same projects `wipe` would target (current folder, ancestors, and descendants), with on-disk `.tokensave/` size and tokens-saved per row, sorted by tokens-saved descending. `tokensave list --all` (or `-a`) lists every project tracked in `~/.tokensave/global.db`, marking entries whose `.tokensave/` directory has been removed as `(stale)`. +- **`tracedecay list` command for inspecting tracked projects** — `list` shows the same projects `wipe` would target (current folder, ancestors, and descendants), with on-disk `.tracedecay/` size and tokens-saved per row, sorted by tokens-saved descending. `tracedecay list --all` (or `-a`) lists every project tracked in `~/.tracedecay/global.db`, marking entries whose `.tracedecay/` directory has been removed as `(stale)`. ### Changed -- **Country flags in `tokensave status` are now shuffled on every render** — when more flags are tracked than fit on the line, the row used to always show the same prefix and `…` truncate the rest. Each `status` invocation now applies a Fisher-Yates shuffle (xorshift64 seeded from time + PID) before truncation, so a different sample of contributing countries is shown each time. +- **Country flags in `tracedecay status` are now shuffled on every render** — when more flags are tracked than fit on the line, the row used to always show the same prefix and `…` truncate the rest. Each `status` invocation now applies a Fisher-Yates shuffle (xorshift64 seeded from time + PID) before truncation, so a different sample of contributing countries is shown each time. ### Fixed -- **Tool-permission warning now points at `tokensave reinstall`** — when new tokensave tools are detected that aren't yet permitted in the agent config, the warning previously said "Run `tokensave install` to update", which would re-do the full install. The warning now reads "Run `tokensave reinstall` to update permissions", which is the right command for refreshing permissions on already-installed agents. +- **Tool-permission warning now points at `tracedecay reinstall`** — when new tracedecay tools are detected that aren't yet permitted in the agent config, the warning previously said "Run `tracedecay install` to update", which would re-do the full install. The warning now reads "Run `tracedecay reinstall` to update permissions", which is the right command for refreshing permissions on already-installed agents. ## [4.3.9] - 2026-05-09 ### Added -- **`tokensave wipe` command for clearing local DBs** — `wipe` finds every `.tokensave/tokensave.db` project in the current folder, all its ancestors, and all its descendants (skipping `node_modules`, `target`, `.git`, `vendor`, `dist`, `build`, `.next`, `.venv`, `__pycache__`, and the user-level `~/.tokensave/`), then prompts for a `go!` confirmation before removing each `.tokensave/` directory and its row in the global DB. `tokensave wipe --all` (or `-a`) instead wipes every project tracked in `~/.tokensave/global.db` and then deletes the global DB itself, leaving it empty. Both flows display a bordered, blinking warning that lists every target before asking for confirmation. +- **`tracedecay wipe` command for clearing local DBs** — `wipe` finds every `.tracedecay/tracedecay.db` project in the current folder, all its ancestors, and all its descendants (skipping `node_modules`, `target`, `.git`, `vendor`, `dist`, `build`, `.next`, `.venv`, `__pycache__`, and the user-level `~/.tracedecay/`), then prompts for a `go!` confirmation before removing each `.tracedecay/` directory and its row in the global DB. `tracedecay wipe --all` (or `-a`) instead wipes every project tracked in `~/.tracedecay/global.db` and then deletes the global DB itself, leaving it empty. Both flows display a bordered, blinking warning that lists every target before asking for confirmation. ## [4.3.8] - 2026-05-06 ### Added -- **`DISABLE_TOKENSAVE=true` environment variable to opt out per-project (#19)** — when set in the MCP server configuration, the `serve` command exits cleanly without initializing. This lets users selectively disable tokensave for large projects that consume too much RAM, without removing it from their global agent config. +- **`DISABLE_TRACEDECAY=true` environment variable to opt out per-project (#19)** — when set in the MCP server configuration, the `serve` command exits cleanly without initializing. This lets users selectively disable tracedecay for large projects that consume too much RAM, without removing it from their global agent config. ## [4.3.7] - 2026-05-06 @@ -541,32 +541,32 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [4.3.5] - 2026-05-06 ### Changed -- **Copilot MCP server now passes the workspace folder to `serve`** — both the VS Code (`mcp.servers.tokensave`) and the Copilot CLI (`mcpServers.tokensave`) registrations now launch the daemon as `tokensave serve -p ${workspaceFolder}` instead of plain `tokensave serve`. This lets the MCP server scope its index to the active workspace automatically without requiring a manual `-p` flag. +- **Copilot MCP server now passes the workspace folder to `serve`** — both the VS Code (`mcp.servers.tracedecay`) and the Copilot CLI (`mcpServers.tracedecay`) registrations now launch the daemon as `tracedecay serve -p ${workspaceFolder}` instead of plain `tracedecay serve`. This lets the MCP server scope its index to the active workspace automatically without requiring a manual `-p` flag. - **Copilot agent args validation tightened** — tests for `CopilotIntegration` now verify that `"serve"` is strictly the first argument and that all remaining args are limited to `-p` / `${workspaceFolder}`. This prevents silent regressions where extra or reordered flags could be injected into the MCP server launch command. ### Fixed -- **`serve` now falls back to the global project database when CWD discovery fails (#55)** — when VS Code Copilot (or another host) launches `tokensave serve` with the working directory set to the user's home folder and `${workspaceFolder}` fails to resolve, the server now checks `~/.tokensave/global.db` for registered projects. If exactly one project is found, it is used automatically; if multiple are found, they are listed on stderr with guidance to pass `-p `. +- **`serve` now falls back to the global project database when CWD discovery fails (#55)** — when VS Code Copilot (or another host) launches `tracedecay serve` with the working directory set to the user's home folder and `${workspaceFolder}` fails to resolve, the server now checks `~/.tracedecay/global.db` for registered projects. If exactly one project is found, it is used automatically; if multiple are found, they are listed on stderr with guidance to pass `-p `. - **`insert_at` no longer strips the trailing newline from edited files (#57)** — `str::lines()` discards the final `\n`, so the file was silently rewritten without its POSIX-required trailing newline. The join result now re-appends `\n` when the original file ended with one. - **Clippy CI failures resolved** — fixed 6 `deny`-level clippy errors across extractors (identical `if`/`else` blocks in clojure, redundant `trim()` before `split_whitespace` in haskell, `map_or` → `is_some_and`, `Iterator::last` → `next_back` in SQL, `too_many_arguments` allow in haskell `emit`). -- **Foreign-key violations during incremental sync now point at the recovery path** — when an extractor produces an edge whose source or target is not in the same file's node set, `tokensave sync` would die with `failed to insert edge: SQLite failure: FOREIGN KEY constraint failed` and no guidance. Full re-index masks this because bulk load disables FK enforcement, so the top-level error handler now detects this specific failure and suggests `tokensave sync -f`. +- **Foreign-key violations during incremental sync now point at the recovery path** — when an extractor produces an edge whose source or target is not in the same file's node set, `tracedecay sync` would die with `failed to insert edge: SQLite failure: FOREIGN KEY constraint failed` and no guidance. Full re-index masks this because bulk load disables FK enforcement, so the top-level error handler now detects this specific failure and suggests `tracedecay sync -f`. - **Spinner no longer leaks on early exit** — added `Drop` for `Spinner` so when `?` propagates an error mid-sync the worker thread is joined, the line is cleared, and the cursor is restored. Previously the cursor stayed hidden after a failed sync. ## [4.3.4] - 2026-05-02 ### Fixed -- **`tokensave sync` no longer hangs on large monorepos with `node_modules` symlinks** — the directory walker now prunes excluded directories (e.g. `node_modules`, `vendor`, `build`) at the `filter_entry` level before descending into them. Previously, exclusions were only checked per-file after the walker had already entered the directory, so monorepo setups where a package manager creates symlinks inside `node_modules` pointing back into source directories (e.g. `../../api`) could cause the scanner to spin indefinitely. Closes #36. +- **`tracedecay sync` no longer hangs on large monorepos with `node_modules` symlinks** — the directory walker now prunes excluded directories (e.g. `node_modules`, `vendor`, `build`) at the `filter_entry` level before descending into them. Previously, exclusions were only checked per-file after the walker had already entered the directory, so monorepo setups where a package manager creates symlinks inside `node_modules` pointing back into source directories (e.g. `../../api`) could cause the scanner to spin indefinitely. Closes #36. ## [4.3.3] - 2026-05-02 ### Added -- **`tokensave_body`** — new MCP tool that returns the full source body of a symbol by name (function, struct, const, etc.). Collapses search + node lookup + file read into a single call; returns multiple ranked matches when the name is ambiguous. -- **`tokensave_todos`** — new MCP tool that finds TODO, FIXME, XXX, HACK, WIP, NOTE, and UNIMPLEMENTED markers across the project. Each result includes the marker kind, file, line, the comment text, and the enclosing symbol name. Filterable by marker kind and path prefix. +- **`tracedecay_body`** — new MCP tool that returns the full source body of a symbol by name (function, struct, const, etc.). Collapses search + node lookup + file read into a single call; returns multiple ranked matches when the name is ambiguous. +- **`tracedecay_todos`** — new MCP tool that finds TODO, FIXME, XXX, HACK, WIP, NOTE, and UNIMPLEMENTED markers across the project. Each result includes the marker kind, file, line, the comment text, and the enclosing symbol name. Filterable by marker kind and path prefix. ### Fixed -- **SQL (and 8 other new-language) files no longer panic during sync** — `tokensave-large-treesitters 0.4.0` is now published to crates.io and `Cargo.toml` references the registry version instead of a local path. Users who built 4.3.2 via `cargo install` received the old 0.3.2 grammar bundle (no SQL), causing a panic per `.sql` file. Closes #53. +- **SQL (and 8 other new-language) files no longer panic during sync** — `tracedecay-large-treesitters 0.4.0` is now published to crates.io and `Cargo.toml` references the registry version instead of a local path. Users who built 4.3.2 via `cargo install` received the old 0.3.2 grammar bundle (no SQL), causing a panic per `.sql` file. Closes #53. ### Changed -- **`tokensave-large-treesitters` dependency pinned to published 0.4.0** — switched from a local path dependency to `"0.4.0"` so `cargo install tokensave` picks up the full grammar set including SQL, R, Julia, Haskell, OCaml, Clojure, Erlang, Elixir, and F#. +- **`tracedecay-large-treesitters` dependency pinned to published 0.4.0** — switched from a local path dependency to `"0.4.0"` so `cargo install tracedecay` picks up the full grammar set including SQL, R, Julia, Haskell, OCaml, Clojure, Erlang, Elixir, and F#. ### Internal - **Grammar completeness test** — `ts_provider::tests::all_extractor_keys_are_registered` verifies every language key an extractor passes to `ts_provider::language()` is present in the bundled grammar table. CI will catch mismatches before a release ships. @@ -574,8 +574,8 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [4.3.2] - 2026-05-01 ### Added -- **9 new language extractors — R, SQL, Julia, Haskell, OCaml, Clojure, Erlang, Elixir, F#** — closes the gap between tokensave and sentrux for functional and data-science languages. Each extractor handles the language's primary top-level constructs and is gated behind its own `lang-*` feature flag, all included in `full`: - - **R** (`.r`, `.R`) — function assignments (`foo <- function(...)`), call sites, roxygen2 docstrings. Requires `tokensave-large-treesitters` ≥ 0.4.0. +- **9 new language extractors — R, SQL, Julia, Haskell, OCaml, Clojure, Erlang, Elixir, F#** — closes the gap between tracedecay and sentrux for functional and data-science languages. Each extractor handles the language's primary top-level constructs and is gated behind its own `lang-*` feature flag, all included in `full`: + - **R** (`.r`, `.R`) — function assignments (`foo <- function(...)`), call sites, roxygen2 docstrings. Requires `tracedecay-large-treesitters` ≥ 0.4.0. - **SQL** (`.sql`) — `CREATE TABLE`, `CREATE VIEW`, `CREATE FUNCTION`, `CREATE PROCEDURE` via `tree-sitter-sequel`. - **Julia** (`.jl`) — `function`, `macro`, `struct`, `abstract_definition`, `module` definitions; import/using nodes. - **Haskell** (`.hs`, `.lhs`) — `function`/`bind` declarations, `data_type`/`newtype`, `class`, `instance`, `import` nodes. @@ -585,18 +585,18 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo - **Elixir** (`.ex`, `.exs`) — `def`/`defp`, `defmodule`, `defmacro`/`defmacrop`, `defstruct` via `call`-node dispatch on the function head. - **F#** (`.fs`, `.fsi`, `.fsx`) — `function_or_value_defn`, `type_definition`, `module_defn`, `namespace`, `open_decl` nodes. - **Complexity configs for all 9 new languages** — `R_COMPLEXITY`, `SQL_COMPLEXITY`, `JULIA_COMPLEXITY`, `HASKELL_COMPLEXITY`, `OCAML_COMPLEXITY`, `CLOJURE_COMPLEXITY`, `ERLANG_COMPLEXITY`, `ELIXIR_COMPLEXITY`, `FSHARP_COMPLEXITY` added to `src/extraction/complexity.rs`. -- **`tokensave-large-treesitters` 0.4.0** — bundles the 9 new tree-sitter grammars: `tree-sitter-r`, `tree-sitter-sequel`, `tree-sitter-julia`, `tree-sitter-haskell`, `tree-sitter-ocaml`, `tree-sitter-clojure-orchard`, `tree-sitter-erlang`, `tree-sitter-elixir`, `tree-sitter-fsharp`. +- **`tracedecay-large-treesitters` 0.4.0** — bundles the 9 new tree-sitter grammars: `tree-sitter-r`, `tree-sitter-sequel`, `tree-sitter-julia`, `tree-sitter-haskell`, `tree-sitter-ocaml`, `tree-sitter-clojure-orchard`, `tree-sitter-erlang`, `tree-sitter-elixir`, `tree-sitter-fsharp`. ### Fixed -- **`tokensave monitor` displayed temp directories as projects** — MCP clients that create per-request temp directories (names matching `.tmp…`) were appearing as project entries in the monitor. These are now filtered out at render time; the TOTAL line reflects only real projects. +- **`tracedecay monitor` displayed temp directories as projects** — MCP clients that create per-request temp directories (names matching `.tmp…`) were appearing as project entries in the monitor. These are now filtered out at render time; the TOTAL line reflects only real projects. ### Changed -- **`tokensave monitor` now supports scrolling** — Up/Down arrows scroll one line at a time; PageUp/PageDown scroll one screen. Scroll offset is clamped to the available content and resets to zero on Ctrl+R. Footer hint updated accordingly. +- **`tracedecay monitor` now supports scrolling** — Up/Down arrows scroll one line at a time; PageUp/PageDown scroll one screen. Scroll offset is clamped to the available content and resets to zero on Ctrl+R. Footer hint updated accordingly. ## [4.3.1] - 2026-05-01 ### Fixed -- **`tokensave_str_replace`, `tokensave_multi_str_replace`, and `tokensave_insert_at` silently mutated files for unsupported types (issue #51)** — all three tools write the file to disk and then call `reindex_file` to update the graph. For file types without a registered extractor (e.g. `.css`, `.html`), `reindex_file` returned `Err("unsupported file type: …")`; the `?` propagated that error to the caller, which reported tool failure — but the write had already been committed. The fix changes `reindex_file` to return `Ok(())` early when no extractor is found, so edits to unsupported file types succeed and the graph simply skips reindexing for those files. +- **`tracedecay_str_replace`, `tracedecay_multi_str_replace`, and `tracedecay_insert_at` silently mutated files for unsupported types (issue #51)** — all three tools write the file to disk and then call `reindex_file` to update the graph. For file types without a registered extractor (e.g. `.css`, `.html`), `reindex_file` returned `Err("unsupported file type: …")`; the `?` propagated that error to the caller, which reported tool failure — but the write had already been committed. The fix changes `reindex_file` to return `Ok(())` early when no extractor is found, so edits to unsupported file types succeed and the graph simply skips reindexing for those files. ### Changed - **Sync duration is now tracked and displayed** — `GraphStats` gains a `last_sync_duration_ms` field persisted to the metadata store. All three sync paths (full index, `sync_single_files`, `sync_with_progress_verbose`) write this value. The status table's sync row now shows the duration inline: `Last sync 2m ago (1.2s) Full sync 1d ago`. Duration is omitted when the value is unknown (existing databases before this change). @@ -605,18 +605,18 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ### Added - **Subprocess-isolated extraction** — every file is now parsed inside a short-lived worker process rather than in the sync process itself. If a tree-sitter grammar segfaults, calls `abort()`, or otherwise terminates by a path Rust cannot intercept, only the worker dies; the pool respawns it, the offending file is logged and skipped, and sync continues. This is a stronger guarantee than the v4.2.1 `catch_unwind` defense, which could only catch Rust panics. - - The worker is exposed via a hidden subcommand (`tokensave extract-worker`) that authenticates against the parent through a 256-bit per-spawn token: required as both an env var and as the first 32 bytes on stdin. A user invoking the binary directly hits the missing-env check and exits non-zero. The subcommand is also hidden from `--help`. - - When `current_exe()` does not point at a real `tokensave` binary (e.g. under `cargo test`, where the test harness is the running binary), extraction transparently falls back to the in-process path. Tests therefore continue to exercise extractors directly without needing to spawn subprocesses. - - Defaults to `available_parallelism()` workers; opt out via `TOKENSAVE_DISABLE_SUBPROCESS=1` if needed. + - The worker is exposed via a hidden subcommand (`tracedecay extract-worker`) that authenticates against the parent through a 256-bit per-spawn token: required as both an env var and as the first 32 bytes on stdin. A user invoking the binary directly hits the missing-env check and exits non-zero. The subcommand is also hidden from `--help`. + - When `current_exe()` does not point at a real `tracedecay` binary (e.g. under `cargo test`, where the test harness is the running binary), extraction transparently falls back to the in-process path. Tests therefore continue to exercise extractors directly without needing to spawn subprocesses. + - Defaults to `available_parallelism()` workers; opt out via `TRACEDECAY_DISABLE_SUBPROCESS=1` if needed. ### Changed -- Single-file extraction (used by the `tokensave_str_replace`, `tokensave_insert_at`, etc. edit tools) still runs in-process — the subprocess overhead is unjustified for one-shot operations and these tools are interactive enough that an extractor crash is immediately visible. +- Single-file extraction (used by the `tracedecay_str_replace`, `tracedecay_insert_at`, etc. edit tools) still runs in-process — the subprocess overhead is unjustified for one-shot operations and these tools are interactive enough that an extractor crash is immediately visible. ## [4.2.1] - 2026-04-30 ### Fixed -- **Sync no longer aborts when a tree-sitter grammar hits an internal assertion (issue #49)** — the vendored `tree-sitter-markdown` C++ scanner contains `assert()` calls that, on certain autolink constructs, called `abort()` and killed the entire `tokensave sync` process (core-dumped on Linux). Two layers of defense: - - Added `.cargo/config.toml` with `CFLAGS=-DNDEBUG` and `CXXFLAGS=-DNDEBUG`. `cc-rs` reads these env vars when compiling vendored grammars in `tokensave-large-treesitters`'s build script, disabling C/C++ assertions in release builds. A failed assertion now degrades to a malformed parse tree (which the extractor handles gracefully) instead of `SIGABRT`. +- **Sync no longer aborts when a tree-sitter grammar hits an internal assertion (issue #49)** — the vendored `tree-sitter-markdown` C++ scanner contains `assert()` calls that, on certain autolink constructs, called `abort()` and killed the entire `tracedecay sync` process (core-dumped on Linux). Two layers of defense: + - Added `.cargo/config.toml` with `CFLAGS=-DNDEBUG` and `CXXFLAGS=-DNDEBUG`. `cc-rs` reads these env vars when compiling vendored grammars in `tracedecay-large-treesitters`'s build script, disabling C/C++ assertions in release builds. A failed assertion now degrades to a malformed parse tree (which the extractor handles gracefully) instead of `SIGABRT`. - Added a `safe_extract` helper that wraps every `extractor.extract()` call site with `std::panic::catch_unwind`. A Rust panic from any extractor (malformed input, future bugs) now logs the file path and skips it instead of bringing down the whole sync. - See issue #50 for the broader follow-up: migrating to pure-Rust generated parsers via the `--rust` fork of tree-sitter to eliminate this class of failure entirely. @@ -624,20 +624,20 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ### Added - **Health & structural analysis tools** — seven new MCP tools that expose quality insights from the existing code graph: - - `tokensave_health` — composite quality signal (0–10000) from five independent dimensions: acyclicity, depth, equality, redundancy, and modularity. Uses geometric mean so no single dimension can be gamed. Supports `details: true` for per-dimension breakdown. - - `tokensave_gini` — Gini inequality coefficient for any metric (complexity, lines, fan_in, fan_out, members) across files or symbols. Identifies god files and uneven complexity distribution with interpretive labels and ranked outliers. - - `tokensave_dependency_depth` — longest file-level dependency chains (Lakos levelization). Shows transitive fragility that direct coupling metrics miss, with full chain reconstruction after cycle-breaking via Tarjan's SCC. - - `tokensave_dsm` — Design Structure Matrix in three output formats: `stats` (density, cluster count), `clusters` (per-directory edge analysis), and `matrix` (NxN grid with short filenames). Reveals hidden coupling patterns and layering violations. - - `tokensave_test_risk` — risk-weighted test gap analysis combining complexity, fan-in, test coverage, and git churn (90-day window) into a single score. Answers "where should the next test go?" with `include_tested` option for finding weak-test candidates. - - `tokensave_session_start` — saves current health metrics as a JSON baseline for later comparison. Call before starting an AI coding session. - - `tokensave_session_end` — re-computes health and diffs against the session baseline. Reports per-dimension deltas with improved/degraded/unchanged labels, overall pass/fail, and cleans up the baseline file. -- **Git churn integration** — new `src/graph/git.rs` module shells out to `git log` at runtime to compute per-file commit frequency. Used by `tokensave_test_risk` as a risk multiplier (log2-scaled) without persisting any data to the tokensave DB. + - `tracedecay_health` — composite quality signal (0–10000) from five independent dimensions: acyclicity, depth, equality, redundancy, and modularity. Uses geometric mean so no single dimension can be gamed. Supports `details: true` for per-dimension breakdown. + - `tracedecay_gini` — Gini inequality coefficient for any metric (complexity, lines, fan_in, fan_out, members) across files or symbols. Identifies god files and uneven complexity distribution with interpretive labels and ranked outliers. + - `tracedecay_dependency_depth` — longest file-level dependency chains (Lakos levelization). Shows transitive fragility that direct coupling metrics miss, with full chain reconstruction after cycle-breaking via Tarjan's SCC. + - `tracedecay_dsm` — Design Structure Matrix in three output formats: `stats` (density, cluster count), `clusters` (per-directory edge analysis), and `matrix` (NxN grid with short filenames). Reveals hidden coupling patterns and layering violations. + - `tracedecay_test_risk` — risk-weighted test gap analysis combining complexity, fan-in, test coverage, and git churn (90-day window) into a single score. Answers "where should the next test go?" with `include_tested` option for finding weak-test candidates. + - `tracedecay_session_start` — saves current health metrics as a JSON baseline for later comparison. Call before starting an AI coding session. + - `tracedecay_session_end` — re-computes health and diffs against the session baseline. Reports per-dimension deltas with improved/degraded/unchanged labels, overall pass/fail, and cleans up the baseline file. +- **Git churn integration** — new `src/graph/git.rs` module shells out to `git log` at runtime to compute per-file commit frequency. Used by `tracedecay_test_risk` as a risk multiplier (log2-scaled) without persisting any data to the tracedecay DB. - **File-level DAG builder** — new `build_file_adjacency` method on `GraphQueryManager` constructs a directed file dependency graph from the existing edge data in a single SQL query. Shared foundation for health, depth, DSM, and modularity computations. ## [4.1.8] - 2026-04-30 ### Added -- **`include` config glob** — new `include` field in `.tokensave/config.json` lets users whitelist hidden (dot-prefixed) paths for indexing. By default, all dot-directories are skipped during sync; paths matching an `include` glob (e.g. `[".github/**"]`) are now walked and indexed. The `exclude` list still applies after inclusion, so `.git/**` and `.tokensave/**` remain filtered even with broad include patterns. +- **`include` config glob** — new `include` field in `.tracedecay/config.json` lets users whitelist hidden (dot-prefixed) paths for indexing. By default, all dot-directories are skipped during sync; paths matching an `include` glob (e.g. `[".github/**"]`) are now walked and indexed. The `exclude` list still applies after inclusion, so `.git/**` and `.tracedecay/**` remain filtered even with broad include patterns. - **Markdown extraction** — tree-sitter based markdown parser that extracts headers as `Module` nodes with hierarchical `Contains` edges, and code links as `Uses` edges for cross-reference tracking (PR #47) ## [4.1.7] - 2026-04-29 @@ -655,10 +655,10 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ### Added - **Edit primitives for code modification** — four new MCP tools enable Claude and friends to edit files without regex or shell quoting hazards (PR #43 by @pierreaubert): - - `tokensave_str_replace` — replaces a unique `old_str` with `new_str`; fails if 0 or >1 matches, protecting against multi-edit bugs - - `tokensave_multi_str_replace` — applies N `(old, new)` replacements atomically; all-or-nothing transaction - - `tokensave_insert_at` — inserts content before or after a unique anchor string or line number - - `tokensave_ast_grep_rewrite` — structural code rewrite via ast-grep CLI (`--rewrite` mode) + - `tracedecay_str_replace` — replaces a unique `old_str` with `new_str`; fails if 0 or >1 matches, protecting against multi-edit bugs + - `tracedecay_multi_str_replace` — applies N `(old, new)` replacements atomically; all-or-nothing transaction + - `tracedecay_insert_at` — inserts content before or after a unique anchor string or line number + - `tracedecay_ast_grep_rewrite` — structural code rewrite via ast-grep CLI (`--rewrite` mode) - **Auto re-indexing** — all four edit tools automatically re-index the modified file in the code graph after writing, keeping the graph in sync without manual steps (PR #43 by @pierreaubert) ### Performance @@ -666,36 +666,36 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ### Fixed - **`find_dead_code` hit SQLite variable limit on large codebases** — the query used `IN (?, ?, …)` binds which SQLite caps at 999 variables; replaced with `NOT EXISTS (SELECT 1 FROM edges WHERE …)` to avoid the limit entirely. (PR #43 by @pierreaubert) -- **`tokensave_test_map` failed to resolve cross-crate qualified calls** — when a reference contained `::` (e.g. `crate_name::func`), a failed qualified-name match returned `None` without falling back to a simple-name lookup, breaking test coverage queries for integration tests that call across crate boundaries. Fixed by removing the early return and adding a simple-name fallback that strips the qualifier before matching. (PR #43 by @pierreaubert) -- **Sync frequency reduced and stale-warning auto-sync added** — sync interval dropped from its previous default to 2 s (configurable); the MCP server now automatically triggers a live sync when an agent receives a stale-graph warning, avoiding a manual `tokensave sync` round-trip. (PR #43 by @pierreaubert) +- **`tracedecay_test_map` failed to resolve cross-crate qualified calls** — when a reference contained `::` (e.g. `crate_name::func`), a failed qualified-name match returned `None` without falling back to a simple-name lookup, breaking test coverage queries for integration tests that call across crate boundaries. Fixed by removing the early return and adding a simple-name fallback that strips the qualifier before matching. (PR #43 by @pierreaubert) +- **Sync frequency reduced and stale-warning auto-sync added** — sync interval dropped from its previous default to 2 s (configurable); the MCP server now automatically triggers a live sync when an agent receives a stale-graph warning, avoiding a manual `tracedecay sync` round-trip. (PR #43 by @pierreaubert) - **`TOOL_NAMES` and `EXPECTED_TOOL_PERMS` were static** — `doctor` and `install` would not detect or register newly-introduced MCP tools. Both lists are now built dynamically so adding a tool automatically propagates to health checks and permission installation. (PR #43 by @pierreaubert) -- **`tokensave monitor` now groups output per project then per tool** — previously all tool calls were listed in a flat stream; entries are now grouped by project path first, then by tool name, making it easier to see which project is driving activity. (PR #43 by @pierreaubert) +- **`tracedecay monitor` now groups output per project then per tool** — previously all tool calls were listed in a flat stream; entries are now grouped by project path first, then by tool name, making it easier to see which project is driving activity. (PR #43 by @pierreaubert) ## [4.1.4] - 2026-04-25 ### Fixed -- **`tokensave monitor` panicked on macOS/Linux with "Cannot start a runtime from within a runtime" (issue #39)** — the previous fix for the Windows panic kept a Unix-only branch that built a new `tokio::runtime` and called `block_on` from inside `#[tokio::main]`, which panics on every platform, not just Windows. `refresh_cost_cache` now uses `block_in_place + Handle::current().block_on` unconditionally, since `monitor::run()` is always invoked from the existing multi-threaded runtime. +- **`tracedecay monitor` panicked on macOS/Linux with "Cannot start a runtime from within a runtime" (issue #39)** — the previous fix for the Windows panic kept a Unix-only branch that built a new `tokio::runtime` and called `block_on` from inside `#[tokio::main]`, which panics on every platform, not just Windows. `refresh_cost_cache` now uses `block_in_place + Handle::current().block_on` unconditionally, since `monitor::run()` is always invoked from the existing multi-threaded runtime. ## [4.1.3] - 2026-04-24 ### Fixed -- **Backslashed Windows hook paths never self-healed (issue #38)** — the v4.0.2 fix for #20 normalized `which_tokensave()` output but could not rewrite existing settings. `install_single_hook` is idempotent by presence, so when a tokensave hook already existed with a backslashed path, the silent backfill in `check_install_stale` left it untouched. Additionally, the backfill only scanned `~/.claude/settings.json` — project-level `.claude/settings.json` and `.claude/settings.local.json` were never touched, so opening a previously-configured project could still trigger `bash: C:Usersalkamscoopappstokensavecurrenttokensave.exe: command not found`. Fixed with a new `normalize_hook_command_paths` pass that rewrites any backslash-containing tokensave hook command to forward slashes, and by extending the backfill to the current project's `.claude` directory. +- **Backslashed Windows hook paths never self-healed (issue #38)** — the v4.0.2 fix for #20 normalized `which_tracedecay()` output but could not rewrite existing settings. `install_single_hook` is idempotent by presence, so when a tracedecay hook already existed with a backslashed path, the silent backfill in `check_install_stale` left it untouched. Additionally, the backfill only scanned `~/.claude/settings.json` — project-level `.claude/settings.json` and `.claude/settings.local.json` were never touched, so opening a previously-configured project could still trigger `bash: C:Usersalkamscoopappstracedecaycurrenttracedecay.exe: command not found`. Fixed with a new `normalize_hook_command_paths` pass that rewrites any backslash-containing tracedecay hook command to forward slashes, and by extending the backfill to the current project's `.claude` directory. ## [4.1.2] - 2026-04-22 ### Added -- **Mistral Vibe agent integration** — `tokensave install --agent vibe` registers the tokensave MCP server in Vibe's `~/.vibe/config.toml` as a `[[mcp_servers]]` stdio entry, and appends prompt rules to `~/.vibe/prompts/cli.md`. Supports install, uninstall, and healthcheck. Respects the `VIBE_HOME` environment variable. Closes #37. +- **Mistral Vibe agent integration** — `tracedecay install --agent vibe` registers the tracedecay MCP server in Vibe's `~/.vibe/config.toml` as a `[[mcp_servers]]` stdio entry, and appends prompt rules to `~/.vibe/prompts/cli.md`. Supports install, uninstall, and healthcheck. Respects the `VIBE_HOME` environment variable. Closes #37. ## [4.1.1] - 2026-04-22 ### Added -- **`tokensave sync --verbose` (`-v`)** — prints per-phase diagnostic lines during sync to help diagnose slow or stuck syncs on large repos. Shows file counts, change breakdowns, and timings for each phase (scan, stat-check, hash, content check, index, resolve, DB write). Also works with `--force` full re-index. Addresses #36. +- **`tracedecay sync --verbose` (`-v`)** — prints per-phase diagnostic lines during sync to help diagnose slow or stuck syncs on large repos. Shows file counts, change breakdowns, and timings for each phase (scan, stat-check, hash, content check, index, resolve, DB write). Also works with `--force` full re-index. Addresses #36. ## [4.1.0] - 2026-04-20 ### Added -- **Walk-up project discovery** — `tokensave serve`, `tokensave sync`, and `tokensave status` now walk up the directory tree to find the nearest `.tokensave/` database when no `--path` is given. This means you can launch an AI agent from a subdirectory of your project and tokensave will find the index automatically — similar to how git finds `.git/`. `tokensave init` is unchanged and always creates a new project at the target directory. -- **Subdirectory scope filtering** — when the MCP server is started from a subdirectory, listing and discovery tools (`tokensave_files`, `tokensave_search`, `tokensave_context`, `tokensave_dead_code`, `tokensave_rank`, `tokensave_largest`, `tokensave_coupling`, `tokensave_complexity`, `tokensave_doc_coverage`, `tokensave_god_class`, `tokensave_unused_imports`, `tokensave_hotspots`, and others) automatically scope results to that subdirectory. Graph traversal tools (`tokensave_callers`, `tokensave_callees`, `tokensave_impact`, `tokensave_affected`, `tokensave_type_hierarchy`) remain unscoped so cross-directory relationships are preserved. The user can always override the scope by providing an explicit `path` parameter. `tokensave_status` reports the active scope prefix when one is in effect. +- **Walk-up project discovery** — `tracedecay serve`, `tracedecay sync`, and `tracedecay status` now walk up the directory tree to find the nearest `.tracedecay/` database when no `--path` is given. This means you can launch an AI agent from a subdirectory of your project and tracedecay will find the index automatically — similar to how git finds `.git/`. `tracedecay init` is unchanged and always creates a new project at the target directory. +- **Subdirectory scope filtering** — when the MCP server is started from a subdirectory, listing and discovery tools (`tracedecay_files`, `tracedecay_search`, `tracedecay_context`, `tracedecay_dead_code`, `tracedecay_rank`, `tracedecay_largest`, `tracedecay_coupling`, `tracedecay_complexity`, `tracedecay_doc_coverage`, `tracedecay_god_class`, `tracedecay_unused_imports`, `tracedecay_hotspots`, and others) automatically scope results to that subdirectory. Graph traversal tools (`tracedecay_callers`, `tracedecay_callees`, `tracedecay_impact`, `tracedecay_affected`, `tracedecay_type_hierarchy`) remain unscoped so cross-directory relationships are preserved. The user can always override the scope by providing an explicit `path` parameter. `tracedecay_status` reports the active scope prefix when one is in effect. ## [4.0.7] - 2026-04-18 @@ -705,79 +705,79 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [4.0.6] - 2026-04-18 ### Added -- **GLSL language support** — new tree-sitter-based extractor for OpenGL shading language files (`.glsl`, `.vert`, `.frag`, `.geom`, `.comp`, `.tesc`, `.tese`). Extracts functions, structs with fields, uniform/in/out/varying declarations, preprocessor defines, call sites, and complexity metrics. Requires `tokensave-large-treesitters` 0.3.0. Feature-gated as `lang-glsl` in the Full tier. Closes #35. +- **GLSL language support** — new tree-sitter-based extractor for OpenGL shading language files (`.glsl`, `.vert`, `.frag`, `.geom`, `.comp`, `.tesc`, `.tese`). Extracts functions, structs with fields, uniform/in/out/varying declarations, preprocessor defines, call sites, and complexity metrics. Requires `tracedecay-large-treesitters` 0.3.0. Feature-gated as `lang-glsl` in the Full tier. Closes #35. ### Fixed -- **`tokensave upgrade` fails on Homebrew installs** — `self_replace` failed with `ENOENT` on Homebrew symlinks because it resolved relative symlink targets from CWD instead of the symlink's parent. Now dispatches to install-method-aware replacement: Homebrew bypasses `self_replace` and atomically replaces the binary at the canonical Cellar path, renames the version directory, and updates the symlink + `INSTALL_RECEIPT.json` so `brew` reports the correct version. Scoop updates the version directory, junction, and `manifest.json`. Other symlinked installs get a canonicalization fallback. Supersedes PR #33. +- **`tracedecay upgrade` fails on Homebrew installs** — `self_replace` failed with `ENOENT` on Homebrew symlinks because it resolved relative symlink targets from CWD instead of the symlink's parent. Now dispatches to install-method-aware replacement: Homebrew bypasses `self_replace` and atomically replaces the binary at the canonical Cellar path, renames the version directory, and updates the symlink + `INSTALL_RECEIPT.json` so `brew` reports the correct version. Scoop updates the version directory, junction, and `manifest.json`. Other symlinked installs get a canonicalization fallback. Supersedes PR #33. ## [4.0.5] - 2026-04-17 ### Changed -- **Separate `tokensave init` from `tokensave sync`** — previously, `tokensave sync` silently created a new database if none existed. This was a problem because the global git post-commit hook runs `tokensave sync` in every repo after each commit, causing phantom `.tokensave/` databases to appear in projects that never opted in. Now `tokensave init` handles first-time project setup (creates DB + full index) and errors if already initialized, while `tokensave sync` only performs incremental updates and errors if the project was never initialized. The git hook (`tokensave sync >/dev/null 2>&1 &`) now safely exits with an error in non-enrolled repos — no database created. All agent setup messages and documentation updated to reference `tokensave init` for first-time use. +- **Separate `tracedecay init` from `tracedecay sync`** — previously, `tracedecay sync` silently created a new database if none existed. This was a problem because the global git post-commit hook runs `tracedecay sync` in every repo after each commit, causing phantom `.tracedecay/` databases to appear in projects that never opted in. Now `tracedecay init` handles first-time project setup (creates DB + full index) and errors if already initialized, while `tracedecay sync` only performs incremental updates and errors if the project was never initialized. The git hook (`tracedecay sync >/dev/null 2>&1 &`) now safely exits with an error in non-enrolled repos — no database created. All agent setup messages and documentation updated to reference `tracedecay init` for first-time use. ## [4.0.4] - 2026-04-17 ### Added -- **Google Antigravity support** — new `tokensave install --agent antigravity` registers the MCP server in `~/.gemini/antigravity/mcp_config.json`. Includes install, uninstall, healthcheck, and auto-detection. Closes #24. -- **Kilo CLI support** — new `tokensave install --agent kilo` registers the MCP server in `~/.config/kilo/kilo.jsonc` using Kilo's `mcp` key with `type: "local"` format. Includes install, uninstall, healthcheck, and auto-detection. Closes #31. +- **Google Antigravity support** — new `tracedecay install --agent antigravity` registers the MCP server in `~/.gemini/antigravity/mcp_config.json`. Includes install, uninstall, healthcheck, and auto-detection. Closes #24. +- **Kilo CLI support** — new `tracedecay install --agent kilo` registers the MCP server in `~/.config/kilo/kilo.jsonc` using Kilo's `mcp` key with `type: "local"` format. Includes install, uninstall, healthcheck, and auto-detection. Closes #31. ### Changed -- **Simpler install prompts** — `tokensave install` now asks a Y/n question per detected agent instead of showing a multi-select dialog box. Prints a +/- summary of changes at the end. Removed `dialoguer` dependency. -- **No-op upgrade is no longer an error** — `tokensave upgrade` when already on the latest version now exits successfully instead of printing a misleading error. Same for `tokensave channel` when already on the requested channel. (PR #30 by @lesbass) +- **Simpler install prompts** — `tracedecay install` now asks a Y/n question per detected agent instead of showing a multi-select dialog box. Prints a +/- summary of changes at the end. Removed `dialoguer` dependency. +- **No-op upgrade is no longer an error** — `tracedecay upgrade` when already on the latest version now exits successfully instead of printing a misleading error. Same for `tracedecay channel` when already on the requested channel. (PR #30 by @lesbass) ### Fixed - **Default branch detection wrote `"HEAD"` instead of actual branch name** — `detect_default_branch()` used `reference.name()` on the `refs/remotes/origin/HEAD` symbolic ref, which returns the ref's own name. Now resolves through `reference.follow()` to get the target (e.g. `refs/remotes/origin/master`), then strips the prefix correctly. (PR #26 by @LucioPg) - **Branch detection in git worktrees** — `current_branch()` read `.git/HEAD` directly as a plain file, which fails in git worktrees where `.git` is a pointer file (not a directory). Fixed with a two-tier approach: `gix::open()` first, then `git symbolic-ref -q HEAD` subprocess fallback. (PR #28 by @LucioPg) -- **Windows monitor nested runtime panic** — `tokensave monitor` cost cache refresh panicked on Windows due to nested tokio runtimes. Now uses `block_in_place` + `Handle::current()` on Windows. (PR #29 by @LucioPg) +- **Windows monitor nested runtime panic** — `tracedecay monitor` cost cache refresh panicked on Windows due to nested tokio runtimes. Now uses `block_in_place` + `Handle::current()` on Windows. (PR #29 by @LucioPg) - **Clippy clean** — resolved all clippy errors across the codebase; CI clippy step now passes. ## [4.0.3] - 2026-04-16 ### Fixed -- **Windows daemon nested runtime panic** — `tokensave daemon` panicked on Windows because `daemon-kit` runs the closure inline (no fork), creating a nested tokio runtime. Now uses `block_in_place` + `Handle::current()` on Windows while keeping `Runtime::new()` on Unix where the forked child genuinely has no runtime. +- **Windows daemon nested runtime panic** — `tracedecay daemon` panicked on Windows because `daemon-kit` runs the closure inline (no fork), creating a nested tokio runtime. Now uses `block_in_place` + `Handle::current()` on Windows while keeping `Runtime::new()` on Unix where the forked child genuinely has no runtime. ## [4.0.2] - 2026-04-14 ### Added -- **Token cost observability** — new `tokensave cost` command parses Claude Code session transcripts (`~/.claude/projects/**/*.jsonl`), classifies each API turn into 13 task categories (coding, debugging, exploration, ...), and computes dollar cost per model. Supports `--by-model`, `--by-task`, `--export json|csv`, and time ranges (`today`, `7d`, `30d`, `all`). Model pricing is refreshed from LiteLLM every 24 hours and cached at `~/.tokensave/pricing.json`. Cost data is stored in the existing `~/.tokensave/global.db`. The `tokensave status` header now shows today's cost, 7-day cost, and efficiency ratio. The `tokensave monitor` TUI includes a cost panel. The `hook_stop` handler prints a session cost receipt. Task classification adapted from [AgentSeal/codeburn](https://github.com/AgentSeal/codeburn). -- **`tokensave status --details`** — the node-kind breakdown table is now opt-in via the `--details` flag. Default status output is more compact. -- **Per-file diversity caps** — `tokensave_context` now limits how many symbols from a single file appear in results (default: `max_nodes/3`, minimum 3), preventing one large file from dominating context output. Configurable via the new `max_per_file` parameter. +- **Token cost observability** — new `tracedecay cost` command parses Claude Code session transcripts (`~/.claude/projects/**/*.jsonl`), classifies each API turn into 13 task categories (coding, debugging, exploration, ...), and computes dollar cost per model. Supports `--by-model`, `--by-task`, `--export json|csv`, and time ranges (`today`, `7d`, `30d`, `all`). Model pricing is refreshed from LiteLLM every 24 hours and cached at `~/.tracedecay/pricing.json`. Cost data is stored in the existing `~/.tracedecay/global.db`. The `tracedecay status` header now shows today's cost, 7-day cost, and efficiency ratio. The `tracedecay monitor` TUI includes a cost panel. The `hook_stop` handler prints a session cost receipt. Task classification adapted from [AgentSeal/codeburn](https://github.com/AgentSeal/codeburn). +- **`tracedecay status --details`** — the node-kind breakdown table is now opt-in via the `--details` flag. Default status output is more compact. +- **Per-file diversity caps** — `tracedecay_context` now limits how many symbols from a single file appear in results (default: `max_nodes/3`, minimum 3), preventing one large file from dominating context output. Configurable via the new `max_per_file` parameter. - **Exact name match supplementing** — context search now supplements FTS5 results with exact case-insensitive name lookups, so perfect symbol name matches are never buried by BM25 noise. - **Stem variant search expansion** — search terms are expanded with suffix-based stem variants (e.g. "authenticate" also finds "authentication", "authenticator") via 13 derivational suffix rules, improving recall for conceptual queries. - **Co-occurrence boosting** — when a query has multiple terms, symbols where 2+ terms co-locate in name, qualified name, or file path get a multiplicative score boost, improving precision on multi-word searches. - **Edge recovery after node trimming** — when BFS subgraph expansion trims nodes to fit `max_nodes`, edges are now filtered to retain only those connecting surviving nodes, keeping the returned subgraph consistent. - **Adaptive SQLite pragmas** — `cache_size` and `mmap_size` now scale to the DB file size instead of using fixed 64 MB / 256 MB values. Small projects (5 MB DB) drop from ~320 MB baseline to ~12 MB; large projects keep the same performance. -- **`tokensave reinstall` command** — re-runs install for all already-configured agents, refreshing MCP server registration, hooks, permissions, and prompt rules without the interactive picker. +- **`tracedecay reinstall` command** — re-runs install for all already-configured agents, refreshing MCP server registration, hooks, permissions, and prompt rules without the interactive picker. ### Removed -- **Graph visualizer** — `tokensave visualize` command, `src/visualizer.rs`, and the embedded HTML file have been removed. The upstream CodeGraph project also removed its visualizer in the same period. +- **Graph visualizer** — `tracedecay visualize` command, `src/visualizer.rs`, and the embedded HTML file have been removed. The upstream CodeGraph project also removed its visualizer in the same period. ### Fixed -- **Windows path separators in hooks and MCP config** — `which_tokensave()` now normalizes backslash paths to forward slashes, fixing broken hook command execution on Windows (e.g. Scoop installs). Existing settings with backslash paths are also normalized when read back. +- **Windows path separators in hooks and MCP config** — `which_tracedecay()` now normalizes backslash paths to forward slashes, fixing broken hook command execution on Windows (e.g. Scoop installs). Existing settings with backslash paths are also normalized when read back. ## [4.0.0] - 2026-04-13 ### Added -- **Multi-branch indexing** — opt-in per-branch databases so switching branches never gives stale results. `tokensave branch add` tracks a branch by copying the nearest ancestor DB and syncing only changed files. `tokensave branch list`, `tokensave branch remove`, `tokensave branch removeall`, and `tokensave branch gc` manage tracked branches. -- **`tokensave branch removeall`** — remove all tracked branches except the default in one command, deleting their DB files. -- **`tokensave_branch_search`** MCP tool — search symbols in another branch's code graph without switching your checkout. -- **`tokensave_branch_diff`** MCP tool — compare code graphs between two branches: shows symbols added, removed, and changed (signature differs). Supports file and kind filters. -- **`tokensave_branch_list`** MCP tool and **`tokensave://branches`** MCP resource — list tracked branches with DB sizes, parent branch, sync times. -- **Branch fallback warnings** — when the MCP server serves from an ancestor branch DB (current branch not tracked), every tool response warns to `tokensave branch add`. -- **`keywords` parameter for `tokensave_context`** — agent-driven synonym expansion. Pass extra search terms (e.g. `["login", "session", "token"]` for "authentication") and the context builder searches each keyword independently, bridging conceptual queries to lexically-unrelated symbol names without embedding models. -- **`tokensave monitor` CLI command** — global live TUI showing MCP tool calls from all projects in real time via a shared memory-mapped ring buffer at `~/.tokensave/monitor.mmap`. Entries show `prefix - project - tool_name` so multiple tool suites and projects are distinguishable. Uses `memmap2` with file locking for concurrent writer safety. -- **`path` filter on 7 analytics MCP tools** — `tokensave_god_class`, `tokensave_largest`, `tokensave_complexity`, `tokensave_rank`, `tokensave_coupling`, `tokensave_inheritance_depth`, and `tokensave_recursion` now accept an optional `path` parameter to scope results to a directory (e.g. `"path": "src/main/java"`), preventing large languages from dominating global rankings. +- **Multi-branch indexing** — opt-in per-branch databases so switching branches never gives stale results. `tracedecay branch add` tracks a branch by copying the nearest ancestor DB and syncing only changed files. `tracedecay branch list`, `tracedecay branch remove`, `tracedecay branch removeall`, and `tracedecay branch gc` manage tracked branches. +- **`tracedecay branch removeall`** — remove all tracked branches except the default in one command, deleting their DB files. +- **`tracedecay_branch_search`** MCP tool — search symbols in another branch's code graph without switching your checkout. +- **`tracedecay_branch_diff`** MCP tool — compare code graphs between two branches: shows symbols added, removed, and changed (signature differs). Supports file and kind filters. +- **`tracedecay_branch_list`** MCP tool and **`tracedecay://branches`** MCP resource — list tracked branches with DB sizes, parent branch, sync times. +- **Branch fallback warnings** — when the MCP server serves from an ancestor branch DB (current branch not tracked), every tool response warns to `tracedecay branch add`. +- **`keywords` parameter for `tracedecay_context`** — agent-driven synonym expansion. Pass extra search terms (e.g. `["login", "session", "token"]` for "authentication") and the context builder searches each keyword independently, bridging conceptual queries to lexically-unrelated symbol names without embedding models. +- **`tracedecay monitor` CLI command** — global live TUI showing MCP tool calls from all projects in real time via a shared memory-mapped ring buffer at `~/.tracedecay/monitor.mmap`. Entries show `prefix - project - tool_name` so multiple tool suites and projects are distinguishable. Uses `memmap2` with file locking for concurrent writer safety. +- **`path` filter on 7 analytics MCP tools** — `tracedecay_god_class`, `tracedecay_largest`, `tracedecay_complexity`, `tracedecay_rank`, `tracedecay_coupling`, `tracedecay_inheritance_depth`, and `tracedecay_recursion` now accept an optional `path` parameter to scope results to a directory (e.g. `"path": "src/main/java"`), preventing large languages from dominating global rankings. - **Right-click context menu in graph visualizer** — callers, callees, call graph, and impact actions on node right-click. - **Type annotation references** — TypeScript, Java, and Kotlin type annotation references now tracked as edges in the graph. -- **Graph visualizer** — interactive Cytoscape.js-based code graph visualization served via `tokensave visualize`. -- **Daemon version mismatch detection** — `tokensave daemon --status` warns when the daemon version differs from the CLI with a corrective restart command. -- **Parent branch in status output** — `tokensave status` and `tokensave_status` now show which branch a tracked branch was seeded from. +- **Graph visualizer** — interactive Cytoscape.js-based code graph visualization served via `tracedecay visualize`. +- **Daemon version mismatch detection** — `tracedecay daemon --status` warns when the daemon version differs from the CLI with a corrective restart command. +- **Parent branch in status output** — `tracedecay status` and `tracedecay_status` now show which branch a tracked branch was seeded from. ### Removed -- **Vector/embedding module** — removed `src/vectors/`, `enable_embeddings` config field, and `Vector` error variant. The `keywords` parameter on `tokensave_context` replaces the need for local embedding models. The `vectors` DB table is retained (empty, harmless) to avoid migration issues. +- **Vector/embedding module** — removed `src/vectors/`, `enable_embeddings` config field, and `Vector` error variant. The `keywords` parameter on `tracedecay_context` replaces the need for local embedding models. The `vectors` DB table is retained (empty, harmless) to avoid migration issues. ### Changed -- **Monitor is now global** — moved from per-project (`/.tokensave/monitor.mmap`) to machine-level (`~/.tokensave/monitor.mmap`). `tokensave monitor` no longer takes a `--path` flag. +- **Monitor is now global** — moved from per-project (`/.tracedecay/monitor.mmap`) to machine-level (`~/.tracedecay/monitor.mmap`). `tracedecay monitor` no longer takes a `--path` flag. - Quality improvements to resolution, search, and traversal. - Tool count increased from 34 to 37. @@ -787,8 +787,8 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [3.5.1] - 2026-04-13 ### Fixed -- **Doctor validates hook subcommands** — `tokensave doctor` now checks that each hook event uses the correct tokensave subcommand (e.g. `hook-prompt-submit` for `UserPromptSubmit`, not an invalid or mismatched command). -- **Doctor auto-repairs broken hooks** — when a hook has a wrong subcommand or is missing entirely, `tokensave doctor` replaces it with the correct command automatically. +- **Doctor validates hook subcommands** — `tracedecay doctor` now checks that each hook event uses the correct tracedecay subcommand (e.g. `hook-prompt-submit` for `UserPromptSubmit`, not an invalid or mismatched command). +- **Doctor auto-repairs broken hooks** — when a hook has a wrong subcommand or is missing entirely, `tracedecay doctor` replaces it with the correct command automatically. ### Added - **18 unit tests for Claude hook lifecycle** — install, uninstall, doctor detection, and doctor auto-repair for all three hook events. @@ -796,10 +796,10 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [3.5.0] - 2026-04-13 ### Added -- **Per-call token savings reported inline** — every MCP tool response now appends a `tokensave_metrics: before=N after=M` line showing how many raw-file tokens were avoided. -- **`UserPromptSubmit` and `Stop` hooks** — `tokensave install` now registers three hooks (PreToolUse, UserPromptSubmit, Stop) instead of just PreToolUse. Existing installs are silently backfilled on startup. -- **`tokensave current-counter` / `reset-counter` commands** — expose and reset a per-project local token counter, separate from the lifetime total. -- **Respect global gitignore** for `.tokensave` warning. +- **Per-call token savings reported inline** — every MCP tool response now appends a `tracedecay_metrics: before=N after=M` line showing how many raw-file tokens were avoided. +- **`UserPromptSubmit` and `Stop` hooks** — `tracedecay install` now registers three hooks (PreToolUse, UserPromptSubmit, Stop) instead of just PreToolUse. Existing installs are silently backfilled on startup. +- **`tracedecay current-counter` / `reset-counter` commands** — expose and reset a per-project local token counter, separate from the lifetime total. +- **Respect global gitignore** for `.tracedecay` warning. ### Changed - **Hook install/uninstall generalized** — `install_hook` and `uninstall_hook` now iterate over all three hook events. @@ -822,7 +822,7 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [3.4.4] - 2026-04-07 ### Fixed -- Fix `tokensave upgrade` ENOENT error on Homebrew symlink installs. +- Fix `tracedecay upgrade` ENOENT error on Homebrew symlink installs. ## [3.4.3] - 2026-04-07 @@ -832,7 +832,7 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [3.4.2] - 2026-04-07 ### Added -- **`tokensave channel` command** — show or switch the update channel (stable/beta). +- **`tracedecay channel` command** — show or switch the update channel (stable/beta). ### Fixed - Cross-workflow Homebrew/Scoop failures on wrong release type. @@ -842,12 +842,12 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ### Fixed - Beta Homebrew bottle 404 — fix bottle archive naming. -- Update notices now suggest `tokensave upgrade` instead of platform-specific commands. +- Update notices now suggest `tracedecay upgrade` instead of platform-specific commands. ## [3.4.0] - 2026-04-07 ### Added -- **`tokensave upgrade` command** — self-update the binary directly from GitHub releases. Detects the current channel, downloads the correct platform-specific archive, and replaces the running binary. +- **`tracedecay upgrade` command** — self-update the binary directly from GitHub releases. Detects the current channel, downloads the correct platform-specific archive, and replaces the running binary. - **Annotation/attribute extraction for 7 languages** — Rust, Swift, Dart, Scala, PHP, C++, and VB.NET. All create `AnnotationUsage` nodes with `Annotates` edges. Brings annotation support to 12 of 31 languages. - **McpTransport trait** — zero-cost abstraction for MCP server I/O, enabling in-memory test transports. - **370+ new tests** — line coverage 71% → 84%. @@ -855,20 +855,20 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [3.3.3] - 2026-04-05 ### Added -- `tokensave sync --doctor` lists added/modified/removed files. +- `tracedecay sync --doctor` lists added/modified/removed files. ## [3.3.2] - 2026-04-05 ### Fixed -- **Windows build failure blocking Homebrew/Scoop updates** — `SHELLEXECUTEINFOW` in `windows-sys` 0.59 requires the `Win32_System_Registry` feature flag, which was missing. This caused Windows CI builds to fail since v3.2.0, and because the release workflow used `fail-fast: true`, the failure cascaded to skip the Homebrew tap and Scoop bucket update jobs entirely. Users on Homebrew were stuck on v3.1.0. ([#12](https://github.com/ScriptedAlchemy/tokensave/issues/12)) +- **Windows build failure blocking Homebrew/Scoop updates** — `SHELLEXECUTEINFOW` in `windows-sys` 0.59 requires the `Win32_System_Registry` feature flag, which was missing. This caused Windows CI builds to fail since v3.2.0, and because the release workflow used `fail-fast: true`, the failure cascaded to skip the Homebrew tap and Scoop bucket update jobs entirely. Users on Homebrew were stuck on v3.1.0. ([#12](https://github.com/ScriptedAlchemy/tracedecay/issues/12)) - **`HANDLE` type mismatch on Windows** — `windows-sys` 0.59 changed `HANDLE` from `usize` to `*mut c_void`. The UAC elevation code now uses `std::ptr::null_mut()` and `.is_null()` instead of literal `0`. - **Release workflow resilience** — changed build matrix to `fail-fast: false` and downstream jobs (`update-homebrew`, `update-scoop`) to `if: !cancelled()`, so a single platform build failure no longer blocks formula/manifest updates for platforms that succeeded. ## [3.3.1] - 2026-04-05 ### Fixed -- **Windows `is_installed()` always returned `false`** — the daemon autostart check via `daemon-kit` used a file-path probe that returns `None` on Windows, so `is_service_installed()` never detected an existing service. This caused `tokensave install` to re-offer autostart every time. Now dispatches to the Windows SCM query that was already implemented but never wired up. (daemon-kit 0.1.4) -- **Windows `--enable-autostart` failed on reinstall** — running `tokensave daemon --enable-autostart` twice would error with "service already exists". The installer now stops and removes the old service before re-creating, making the operation idempotent. (daemon-kit 0.1.4) +- **Windows `is_installed()` always returned `false`** — the daemon autostart check via `daemon-kit` used a file-path probe that returns `None` on Windows, so `is_service_installed()` never detected an existing service. This caused `tracedecay install` to re-offer autostart every time. Now dispatches to the Windows SCM query that was already implemented but never wired up. (daemon-kit 0.1.4) +- **Windows `--enable-autostart` failed on reinstall** — running `tracedecay daemon --enable-autostart` twice would error with "service already exists". The installer now stops and removes the old service before re-creating, making the operation idempotent. (daemon-kit 0.1.4) ### Added - **Upgrade-aware daemon restart** — the background daemon now snapshots its own binary's mtime and size at startup and checks every 60 seconds. When an upgrade is detected (via `brew upgrade`, `cargo install`, `scoop update`, or any package manager), the daemon flushes pending syncs, logs the event, and exits. The service manager (launchd `KeepAlive`, systemd `Restart=on-failure`, Windows SCM failure actions) automatically relaunches with the new binary. Previously the old version ran until the next reboot or manual restart. @@ -881,53 +881,53 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [3.3.0] - 2026-04-05 ### Changed -- **Sync progress now matches full-index display** — `tokensave sync` now shows `[current/total] syncing file (ETA: Ns)` with the braille spinner and path truncation, matching the progress display used during initial indexing. Previously sync only showed phase names without file counters or ETA. +- **Sync progress now matches full-index display** — `tracedecay sync` now shows `[current/total] syncing file (ETA: Ns)` with the braille spinner and path truncation, matching the progress display used during initial indexing. Previously sync only showed phase names without file counters or ETA. ### Added -- **MCP tool annotations** — all 34 tools now include `readOnlyHint: true` and a human-friendly `title` in their MCP annotations. Clients that support annotations can run all tokensave tools concurrently without permission prompts and display cleaner tool names. -- **`_meta["anthropic/alwaysLoad"]`** on core tools — `tokensave_context`, `tokensave_search`, and `tokensave_status` are marked for immediate loading, bypassing the client's tool-search round-trip on first use. -- **Server instructions** — the MCP `initialize` response now includes an `instructions` field guiding the model to start with `tokensave_context` and noting all tools are read-only and safe to call in parallel. +- **MCP tool annotations** — all 34 tools now include `readOnlyHint: true` and a human-friendly `title` in their MCP annotations. Clients that support annotations can run all tracedecay tools concurrently without permission prompts and display cleaner tool names. +- **`_meta["anthropic/alwaysLoad"]`** on core tools — `tracedecay_context`, `tracedecay_search`, and `tracedecay_status` are marked for immediate loading, bypassing the client's tool-search round-trip on first use. +- **Server instructions** — the MCP `initialize` response now includes an `instructions` field guiding the model to start with `tracedecay_context` and noting all tools are read-only and safe to call in parallel. - **MCP resources** — three resources exposed via `resources/list` and `resources/read`: - - `tokensave://status` — graph statistics as JSON - - `tokensave://files` — indexed file tree grouped by directory - - `tokensave://overview` — project summary with language distribution and symbol kinds -- **`tokensave_commit_context`** — semantic summary of uncommitted changes for commit message drafting. Returns changed symbols grouped by file role (source/test/config/docs), a suggested commit category, and recent commit subjects for style matching. -- **`tokensave_pr_context`** — semantic diff between two git refs for pull request descriptions. Returns commit log, symbols added/modified, affected tests, and impacted modules. -- **`tokensave_simplify_scan`** — quality analysis of changed files: detects symbol duplications, dead code introductions, complexity hotspots, and high-coupling files. -- **`tokensave_test_map`** — source-to-test mapping at the symbol level. Shows which test functions call which source functions and identifies uncovered symbols. -- **`tokensave_type_hierarchy`** — recursive type hierarchy tree for traits, interfaces, and classes showing all implementors and extenders with file locations. -- **`tokensave_context` extended** — new `include_code` parameter includes source code snippets for key symbols (wires through to the existing context builder). New `mode: "plan"` parameter appends extension points (public traits/interfaces with implementor counts) and test coverage for related modules. + - `tracedecay://status` — graph statistics as JSON + - `tracedecay://files` — indexed file tree grouped by directory + - `tracedecay://overview` — project summary with language distribution and symbol kinds +- **`tracedecay_commit_context`** — semantic summary of uncommitted changes for commit message drafting. Returns changed symbols grouped by file role (source/test/config/docs), a suggested commit category, and recent commit subjects for style matching. +- **`tracedecay_pr_context`** — semantic diff between two git refs for pull request descriptions. Returns commit log, symbols added/modified, affected tests, and impacted modules. +- **`tracedecay_simplify_scan`** — quality analysis of changed files: detects symbol duplications, dead code introductions, complexity hotspots, and high-coupling files. +- **`tracedecay_test_map`** — source-to-test mapping at the symbol level. Shows which test functions call which source functions and identifies uncovered symbols. +- **`tracedecay_type_hierarchy`** — recursive type hierarchy tree for traits, interfaces, and classes showing all implementors and extenders with file locations. +- **`tracedecay_context` extended** — new `include_code` parameter includes source code snippets for key symbols (wires through to the existing context builder). New `mode: "plan"` parameter appends extension points (public traits/interfaces with implementor counts) and test coverage for related modules. ### Changed - Tool count increased from 29 to 34. -- Trimmed verbose tool descriptions for lower token overhead in deferred tool lists (`tokensave_rank`, `tokensave_coupling`, `tokensave_port_status`, `tokensave_port_order`, `tokensave_affected`, `tokensave_complexity`, `tokensave_doc_coverage`, `tokensave_god_class`, `tokensave_recursion`, `tokensave_inheritance_depth`, `tokensave_distribution`). +- Trimmed verbose tool descriptions for lower token overhead in deferred tool lists (`tracedecay_rank`, `tracedecay_coupling`, `tracedecay_port_status`, `tracedecay_port_order`, `tracedecay_affected`, `tracedecay_complexity`, `tracedecay_doc_coverage`, `tracedecay_god_class`, `tracedecay_recursion`, `tracedecay_inheritance_depth`, `tracedecay_distribution`). ## [3.2.2] - 2026-04-05 ### Fixed -- **MCP tools no longer warn on patch-only updates** — the `tokensave_status` MCP tool now uses `is_newer_minor_version` instead of `is_newer_version`, so patch-level releases (e.g. 3.2.0 → 3.2.1) no longer trigger update warnings in MCP tool output. The CLI status command continues to show all available updates. +- **MCP tools no longer warn on patch-only updates** — the `tracedecay_status` MCP tool now uses `is_newer_minor_version` instead of `is_newer_version`, so patch-level releases (e.g. 3.2.0 → 3.2.1) no longer trigger update warnings in MCP tool output. The CLI status command continues to show all available updates. - **Separate beta/stable update channels** — `is_newer_version` now returns `false` for cross-channel comparisons (beta vs stable). Previously a beta user could be told to upgrade to a stable release, or vice versa. Each channel now only sees updates from its own channel. ## [3.1.1] - 2026-04-02 ### Fixed -- **Windows daemon service installation** — `tokensave install` and `tokensave daemon --enable-autostart` no longer fail on non-elevated Windows terminals. When administrator privileges are required to register the Windows Service, the process now automatically requests UAC elevation for just the service installation step; everything else continues non-elevated. ([#7](https://github.com/ScriptedAlchemy/tokensave/issues/7)) -- **Quieter version update warnings** — the CLI no longer warns about patch-only releases (e.g. 3.2.0 → 3.2.1); warnings now appear only for minor or major version bumps. The status page (`tokensave_status` MCP tool) continues to show all available updates. +- **Windows daemon service installation** — `tracedecay install` and `tracedecay daemon --enable-autostart` no longer fail on non-elevated Windows terminals. When administrator privileges are required to register the Windows Service, the process now automatically requests UAC elevation for just the service installation step; everything else continues non-elevated. ([#7](https://github.com/ScriptedAlchemy/tracedecay/issues/7)) +- **Quieter version update warnings** — the CLI no longer warns about patch-only releases (e.g. 3.2.0 → 3.2.1); warnings now appear only for minor or major version bumps. The status page (`tracedecay_status` MCP tool) continues to show all available updates. ## [3.1.0] - 2026-04-01 ### Fixed -- **Edge duplication during incremental sync** — reference resolution was re-resolving ALL unresolved refs on every sync (not just from changed files) and inserting duplicate edges with no deduplication. Over many syncs this caused unbounded DB growth (e.g. 5.1 GB for a 108 MB codebase). A unique index on edges and `INSERT OR IGNORE` now prevent duplicates entirely. A V5 migration automatically deduplicates existing databases on upgrade. ([#5](https://github.com/ScriptedAlchemy/tokensave/issues/5)) +- **Edge duplication during incremental sync** — reference resolution was re-resolving ALL unresolved refs on every sync (not just from changed files) and inserting duplicate edges with no deduplication. Over many syncs this caused unbounded DB growth (e.g. 5.1 GB for a 108 MB codebase). A unique index on edges and `INSERT OR IGNORE` now prevent duplicates entirely. A V5 migration automatically deduplicates existing databases on upgrade. ([#5](https://github.com/ScriptedAlchemy/tracedecay/issues/5)) ### Added -- **Concurrent sync prevention** — a PID-based lockfile (`.tokensave/sync.lock`) prevents the CLI and the background daemon from running sync simultaneously. If a sync is already in progress, the second attempt fails immediately with a clear error message. Stale locks from crashed processes are reclaimed automatically. -- **`doctor` database compaction** — `tokensave doctor` now opens the project database, reports its size, and runs `VACUUM + ANALYZE` to reclaim space. Particularly useful after upgrading from versions affected by edge duplication. +- **Concurrent sync prevention** — a PID-based lockfile (`.tracedecay/sync.lock`) prevents the CLI and the background daemon from running sync simultaneously. If a sync is already in progress, the second attempt fails immediately with a clear error message. Stale locks from crashed processes are reclaimed automatically. +- **`doctor` database compaction** — `tracedecay doctor` now opens the project database, reports its size, and runs `VACUUM + ANALYZE` to reclaim space. Particularly useful after upgrading from versions affected by edge duplication. - **Index design documentation** — new `docs/INDEX-DESIGN.md` describes the full indexing pipeline, database schema, extraction process, reference resolution, incremental sync, and how `diff_context` uses the graph. ## [3.0.1] - 2026-04-01 ### Fixed -- **Safe JSON config editing** — `tokensave install` no longer silently destroys agent config files (e.g. `opencode.json`, `settings.json`) when they contain invalid or unparseable JSON. Previously, a parse failure caused the file to be silently replaced with an empty object plus the tokensave entry, wiping all existing configuration. +- **Safe JSON config editing** — `tracedecay install` no longer silently destroys agent config files (e.g. `opencode.json`, `settings.json`) when they contain invalid or unparseable JSON. Previously, a parse failure caused the file to be silently replaced with an empty object plus the tracedecay entry, wiping all existing configuration. ### Added - **Atomic backup before config writes** — a `.bak` copy of the original file is created (via atomic staging) before any modification. If the install fails at any point, the original file is untouched and the backup is preserved. @@ -938,30 +938,30 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [3.0.0] - 2026-03-28 ### Changed -- **Bundled tree-sitter grammars** — all 31 language grammars now come from the `tokensave-large-treesitters` crate (which includes `tokensave-medium-treesitters` and `tokensave-lite-treesitters`). Zero individual `tree-sitter-*` crate dependencies remain in tokensave itself. The grammar provider (`ts_provider`) is a single `LazyLock` lookup, replacing 100+ lines of per-crate match arms. -- **Removed vendored C grammars** — the Protobuf and COBOL grammars previously compiled from C source via `build.rs` are now vendored inside the bundled crate. tokensave no longer needs `cc` as a build dependency. +- **Bundled tree-sitter grammars** — all 31 language grammars now come from the `tracedecay-large-treesitters` crate (which includes `tracedecay-medium-treesitters` and `tracedecay-lite-treesitters`). Zero individual `tree-sitter-*` crate dependencies remain in tracedecay itself. The grammar provider (`ts_provider`) is a single `LazyLock` lookup, replacing 100+ lines of per-crate match arms. +- **Removed vendored C grammars** — the Protobuf and COBOL grammars previously compiled from C source via `build.rs` are now vendored inside the bundled crate. tracedecay no longer needs `cc` as a build dependency. - **Simplified feature flags** — the `lang-*` feature flags still control which extractors are compiled, but no longer pull in individual grammar crate dependencies (all grammars are always present via the bundle). The `ts-ffi`/`ts-rust`/`ts-both` grammar source selection flags have been removed. ### Added -- **Daemon install prompt** — `tokensave install` now offers to install the background daemon as an autostart service (launchd on macOS, systemd on Linux) after agent configuration. Skips silently in non-interactive mode or when the service is already installed. +- **Daemon install prompt** — `tracedecay install` now offers to install the background daemon as an autostart service (launchd on macOS, systemd on Linux) after agent configuration. Skips silently in non-interactive mode or when the service is already installed. - **Last sync / Full sync in status** — the status table header now shows a third row with relative timestamps for the most recent incremental sync and the most recent full reindex, stored in the metadata table. ## [2.4.0] - 2026-03-27 ### Added -- **Daemon mode** — `tokensave daemon` watches all tracked projects for file changes and runs incremental syncs automatically; debounce configurable via `daemon_debounce` in `~/.tokensave/config.toml` (default `"15s"`) -- **Daemon management** — `--stop`, `--status`, `--foreground` flags for process control; PID file at `~/.tokensave/daemon.pid` +- **Daemon mode** — `tracedecay daemon` watches all tracked projects for file changes and runs incremental syncs automatically; debounce configurable via `daemon_debounce` in `~/.tracedecay/config.toml` (default `"15s"`) +- **Daemon management** — `--stop`, `--status`, `--foreground` flags for process control; PID file at `~/.tracedecay/daemon.pid` - **Autostart service** — `--enable-autostart` / `--disable-autostart` generates and manages a launchd plist (macOS) or systemd user unit (Linux); cross-platform via `daemon-kit` crate -- **Doctor daemon checks** — `tokensave doctor` now reports daemon running status and autostart configuration +- **Doctor daemon checks** — `tracedecay doctor` now reports daemon running status and autostart configuration - **`daemon-kit` crate** — new standalone cross-platform daemon/service toolkit published to crates.io, using `daemonize2` on Unix and `windows-service` on Windows ## [2.3.2] - 2026-03-27 ### Added -- **5 new agent integrations** — Copilot (VS Code), Cursor, Zed, Cline, and Roo Code now supported via `tokensave install --agent `; each registers the MCP server in the agent's native config format (VS Code `settings.json`, `~/.cursor/mcp.json`, Zed `settings.json`, Cline/Roo Code `cline_mcp_settings.json`) -- **Auto-detect agents** — running `tokensave install` without `--agent` detects which agents are installed by checking their config directories; if one is found it installs directly, if multiple are found an interactive checkbox selector is shown -- **Installed-agent tracking** — `installed_agents` list in `~/.tokensave/config.toml` tracks which integrations are active; on upgrade from older versions the list is backfilled by scanning existing configs -- **Uninstall-all** — `tokensave uninstall` without `--agent` silently removes all tracked integrations +- **5 new agent integrations** — Copilot (VS Code), Cursor, Zed, Cline, and Roo Code now supported via `tracedecay install --agent `; each registers the MCP server in the agent's native config format (VS Code `settings.json`, `~/.cursor/mcp.json`, Zed `settings.json`, Cline/Roo Code `cline_mcp_settings.json`) +- **Auto-detect agents** — running `tracedecay install` without `--agent` detects which agents are installed by checking their config directories; if one is found it installs directly, if multiple are found an interactive checkbox selector is shown +- **Installed-agent tracking** — `installed_agents` list in `~/.tracedecay/config.toml` tracks which integrations are active; on upgrade from older versions the list is backfilled by scanning existing configs +- **Uninstall-all** — `tracedecay uninstall` without `--agent` silently removes all tracked integrations - **JSONC parser** — VS Code and Zed settings files (JSON with comments and trailing commas) are now parsed correctly ### Changed @@ -970,12 +970,12 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ## [2.3.1] - 2026-03-27 ### Changed -- **Version-update warning suppressed for 15 minutes** — the "Update available" notice shown after `sync` and in MCP tool responses is now suppressed for 15 minutes after it was last displayed, reducing noise for frequent users; `tokensave status` always shows the warning regardless of suppression +- **Version-update warning suppressed for 15 minutes** — the "Update available" notice shown after `sync` and in MCP tool responses is now suppressed for 15 minutes after it was last displayed, reducing noise for frequent users; `tracedecay status` always shows the warning regardless of suppression ## [2.3.0] - 2026-03-27 ### Added -- **`--skip-folder` flag for sync** — accepts one or more folder names to exclude during indexing (e.g. `tokensave sync --skip-folder tests benches`); each folder is converted to a `folder/**` glob pattern at runtime +- **`--skip-folder` flag for sync** — accepts one or more folder names to exclude during indexing (e.g. `tracedecay sync --skip-folder tests benches`); each folder is converted to a `folder/**` glob pattern at runtime - **ETA during full index** — the progress spinner now shows `[current/total]` file counts and an estimated time remaining (e.g. `[12/150] indexing src/main.rs (ETA: 8s)`) ### Changed @@ -1012,7 +1012,7 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ### Fixed - **Windows: sync re-adding files** — normalize all relative file paths to forward slashes in the scanner, preventing path mismatch between index and sync on Windows -- **Windows: wrong upgrade command** — detect Scoop installations (`\scoop\` in binary path) and suggest `scoop update tokensave` instead of `cargo install tokensave` +- **Windows: wrong upgrade command** — detect Scoop installations (`\scoop\` in binary path) and suggest `scoop update tracedecay` instead of `cargo install tracedecay` - **Windows: git hook backslashes** — write forward slashes in `core.hooksPath` and the post-commit hook snippet, since Git's shell expects `/` separators - **Scoop bucket structure** — moved manifest to `bucket/` subdirectory for better compatibility with `scoop update` - **Double-counted token savings** — "Global" total no longer includes the current project's count; display now shows "Project" and "All projects" labels @@ -1046,7 +1046,7 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo #### Enhanced Nix extraction - **Derivation field extraction** — `mkDerivation`, `mkShell`, `buildPythonPackage`, `buildGoModule`, `buildRustPackage`, `buildNpmPackage` calls have their attrset arguments extracted as `Field` nodes (`pname`, `version`, `buildInputs`, `nativeBuildInputs`, `src`, `meta`, etc.) -- **Import path resolution** — `import ./path.nix` creates a `Use` node with a `Uses` unresolved ref, enabling cross-file dependency tracking via `tokensave_callers` and `tokensave_impact` +- **Import path resolution** — `import ./path.nix` creates a `Use` node with a `Uses` unresolved ref, enabling cross-file dependency tracking via `tracedecay_callers` and `tracedecay_impact` - **Flake output schema awareness** — in `flake.nix` files, standard output attributes (`packages`, `devShells`, `apps`, `nixosModules`, `nixosConfigurations`, `overlays`, `lib`, `checks`, `formatter`) are force-classified as `Module` nodes with recursive child extraction #### Feature flag tiers @@ -1063,11 +1063,11 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo - `ProtoRpc` — Protobuf RPC method definitions #### Porting assessment tools -- **`tokensave_port_status`** — compare symbols between source and target directories within the same project to track porting progress; matches by name with cross-language kind compatibility (`class` ↔ `struct`, `interface` ↔ `trait`); reports matched/unmatched/target-only counts and coverage percentage -- **`tokensave_port_order`** — topological sort of source symbols for porting; uses Kahn's algorithm on the internal dependency graph to produce levels (port leaves first, then dependents); detects and reports dependency cycles +- **`tracedecay_port_status`** — compare symbols between source and target directories within the same project to track porting progress; matches by name with cross-language kind compatibility (`class` ↔ `struct`, `interface` ↔ `trait`); reports matched/unmatched/target-only counts and coverage percentage +- **`tracedecay_port_order`** — topological sort of source symbols for porting; uses Kahn's algorithm on the internal dependency graph to produce levels (port leaves first, then dependents); detects and reports dependency cycles #### Agent prompt improvements -- **SQLite fallback instruction** — agents are told to query `.tokensave/tokensave.db` directly via SQL when MCP tools can't answer a code analysis question +- **SQLite fallback instruction** — agents are told to query `.tracedecay/tracedecay.db` directly via SQL when MCP tools can't answer a code analysis question - **Improvement feedback loop** — agents propose opening a GitHub issue when they discover an extractor/schema/tool gap, reminding the user to strip sensitive data ### Changed @@ -1076,21 +1076,21 @@ The largest functional jump since 4.0: nine new MCP tools, a cross-session respo ### Breaking - Tree-sitter grammar dependencies for medium/full tier languages are now **optional** behind feature flags. Downstream crates depending on specific extractors must enable the corresponding `lang-*` feature. -- `cargo install tokensave --no-default-features` now builds a **lite** binary (11 languages) instead of the previous 15. To get the old behavior, use `cargo install tokensave` (default = full, 30 languages). +- `cargo install tracedecay --no-default-features` now builds a **lite** binary (11 languages) instead of the previous 15. To get the old behavior, use `cargo install tracedecay` (default = full, 30 languages). - Three new `NodeKind` variants (`ProtoMessage`, `ProtoService`, `ProtoRpc`) added — code matching exhaustively on `NodeKind` will need updating. ### Upgrade guide ```bash -cargo install tokensave # or: brew upgrade tokensave -tokensave install # re-run to get updated prompt rules -tokensave sync --force # re-index to pick up new language extractors +cargo install tracedecay # or: brew upgrade tracedecay +tracedecay install # re-run to get updated prompt rules +tracedecay sync --force # re-index to pick up new language extractors ``` ## [1.10.0] - 2026-03-26 ### Added - **Version update notifications** — the MCP server checks GitHub releases (with a 5-minute cache) and warns users when a newer version is available, via both a `notifications/message` logging notification and a text block prepended to tool responses -- **Global git post-commit hook** — `tokensave install` now offers to install a global `post-commit` hook that auto-runs `tokensave sync` after each commit, keeping the index up to date without manual intervention +- **Global git post-commit hook** — `tracedecay install` now offers to install a global `post-commit` hook that auto-runs `tracedecay sync` after each commit, keeping the index up to date without manual intervention - MCP `logging` capability advertised in `initialize` response - Minimal gitconfig parser for reading `core.hooksPath` from `~/.gitconfig` and `~/.config/git/config` without shelling out to `git` - 12 unit tests for gitconfig parsing, insertion, and tilde expansion @@ -1111,8 +1111,8 @@ tokensave sync --force # re-index to pick up new language extractors ## [1.8.1] - 2026-03-26 ### Added -- **OpenCode agent** (`tokensave install --agent opencode`) — registers MCP server in `.opencode.json`, appends prompt rules to `OPENCODE.md`; healthcheck validates config and prompt file -- **Codex CLI agent** (`tokensave install --agent codex`) — registers MCP server in `~/.codex/config.toml` with auto-approval for all 27 tools, appends prompt rules to `~/.codex/AGENTS.md`; healthcheck validates config, tool approval counts, and prompt file +- **OpenCode agent** (`tracedecay install --agent opencode`) — registers MCP server in `.opencode.json`, appends prompt rules to `OPENCODE.md`; healthcheck validates config and prompt file +- **Codex CLI agent** (`tracedecay install --agent codex`) — registers MCP server in `~/.codex/config.toml` with auto-approval for all 27 tools, appends prompt rules to `~/.codex/AGENTS.md`; healthcheck validates config, tool approval counts, and prompt file - TOML helpers (`load_toml_file`, `write_toml_file`) in agents module for Codex config support - `TOOL_NAMES` constant with bare tool names (without agent-specific prefix) for cross-agent use @@ -1124,17 +1124,17 @@ tokensave sync --force # re-index to pick up new language extractors ### Added - **Multi-agent architecture** with a trait-based `Agent` abstraction (`install`, `uninstall`, `healthcheck`) to support CLI agents beyond Claude Code -- `tokensave install [--agent NAME]` replaces `claude-install` — defaults to `claude` when no agent is specified -- `tokensave uninstall [--agent NAME]` replaces `claude-uninstall` — defaults to `claude` -- `tokensave doctor [--agent NAME]` now checks all registered agents by default; use `--agent` to narrow to one +- `tracedecay install [--agent NAME]` replaces `claude-install` — defaults to `claude` when no agent is specified +- `tracedecay uninstall [--agent NAME]` replaces `claude-uninstall` — defaults to `claude` +- `tracedecay doctor [--agent NAME]` now checks all registered agents by default; use `--agent` to narrow to one - Agent registry with `get_agent()`, `all_agents()`, and `available_agents()` for programmatic access -- `tokensave install --agent unknown` returns a clear error listing available agents +- `tracedecay install --agent unknown` returns a clear error listing available agents ### Changed - Extracted ~600 lines of Claude-specific install/uninstall/doctor logic from `main.rs` into `src/agents/claude.rs` -- Shared helpers (`load_json_file`, `write_json_file`, `which_tokensave`, `home_dir`, `DoctorCounters`, `EXPECTED_TOOL_PERMS`) moved to `src/agents/mod.rs` -- Error messages updated from `tokensave claude-install` to `tokensave install` -- Backward compatibility preserved: `tokensave claude-install` and `tokensave claude-uninstall` still work as aliases +- Shared helpers (`load_json_file`, `write_json_file`, `which_tracedecay`, `home_dir`, `DoctorCounters`, `EXPECTED_TOOL_PERMS`) moved to `src/agents/mod.rs` +- Error messages updated from `tracedecay claude-install` to `tracedecay install` +- Backward compatibility preserved: `tracedecay claude-install` and `tracedecay claude-uninstall` still work as aliases ### New files - `src/agents/mod.rs` — `Agent` trait, `InstallContext`, `HealthcheckContext`, `DoctorCounters`, agent registry, shared helpers @@ -1143,7 +1143,7 @@ tokensave sync --force # re-index to pick up new language extractors ## [1.7.1] - 2026-03-25 ### Fixed -- Database schema migrations now trigger an automatic full re-index instead of printing a warning asking users to run `tokensave sync --full` manually +- Database schema migrations now trigger an automatic full re-index instead of printing a warning asking users to run `tracedecay sync --full` manually ### Changed - Decomposed 6 oversized functions into small orchestrators + helpers for NASA Power of 10 Rule 4 compliance (no function exceeds 47 lines): @@ -1165,13 +1165,13 @@ tokensave sync --force # re-index to pick up new language extractors - Extended `ComplexityConfig` with 6 new fields (`unsafe_types`, `unchecked_types`, `unchecked_methods`, `call_expression_types`, `call_method_field`, `assertion_names`, `macro_invocation_types`) to support cross-language detection - `count_complexity` now accepts source bytes for method-name and macro-name matching in call expressions - DB migration V4 adds `unsafe_blocks`, `unchecked_calls`, and `assertions` columns to the nodes table -- `tokensave_node` and `tokensave_complexity` MCP tools now include the 3 new fields in their responses -- Migration log message advises users to run `tokensave sync --full` to populate new columns for existing data +- `tracedecay_node` and `tracedecay_complexity` MCP tools now include the 3 new fields in their responses +- Migration log message advises users to run `tracedecay sync --full` to populate new columns for existing data ## [1.6.2] - 2026-03-25 ### Fixed -- Suppressed the "new tokensave tool(s) not yet permitted" warning when running `tokensave claude-install`, since that command is about to fix the permissions anyway +- Suppressed the "new tracedecay tool(s) not yet permitted" warning when running `tracedecay claude-install`, since that command is about to fix the permissions anyway ## [1.6.1] - 2026-03-25 @@ -1184,15 +1184,15 @@ tokensave sync --force # re-index to pick up new language extractors ### Added - 9 new MCP tools (27 total) for codebase analytics, code quality, and guideline compliance: - - `tokensave_rank` — rank nodes by relationship count with direction support (incoming/outgoing); answers "most implemented interface", "class that implements the most interfaces", etc. - - `tokensave_largest` — rank nodes by line count; find largest classes, longest methods - - `tokensave_coupling` — rank files by fan-in (most depended-on) or fan-out (most dependencies) - - `tokensave_inheritance_depth` — find deepest class hierarchies via recursive CTE on extends chains - - `tokensave_distribution` — node kind breakdown per file/directory with summary mode - - `tokensave_recursion` — detect recursive/mutually-recursive call cycles (NASA Power of 10, Rule 1) - - `tokensave_complexity` — rank functions by composite complexity score with real cyclomatic complexity from AST - - `tokensave_doc_coverage` — find public symbols missing documentation (Rust guidelines M-CANONICAL-DOCS) - - `tokensave_god_class` — find classes with the most members (methods + fields) + - `tracedecay_rank` — rank nodes by relationship count with direction support (incoming/outgoing); answers "most implemented interface", "class that implements the most interfaces", etc. + - `tracedecay_largest` — rank nodes by line count; find largest classes, longest methods + - `tracedecay_coupling` — rank files by fan-in (most depended-on) or fan-out (most dependencies) + - `tracedecay_inheritance_depth` — find deepest class hierarchies via recursive CTE on extends chains + - `tracedecay_distribution` — node kind breakdown per file/directory with summary mode + - `tracedecay_recursion` — detect recursive/mutually-recursive call cycles (NASA Power of 10, Rule 1) + - `tracedecay_complexity` — rank functions by composite complexity score with real cyclomatic complexity from AST + - `tracedecay_doc_coverage` — find public symbols missing documentation (Rust guidelines M-CANONICAL-DOCS) + - `tracedecay_god_class` — find classes with the most members (methods + fields) - **Complexity metrics on every function/method node** — 4 new columns extracted from the AST during indexing: - `branches` — branching statements (if, match/switch arms, ternary, catch). CC = branches + 1. - `loops` — loop constructs (for, while, loop, do). Enables NASA Rule 2 audits. @@ -1205,36 +1205,36 @@ tokensave sync --force # re-index to pick up new language extractors ## [1.5.4] - 2026-03-25 ### Fixed -- Token counter inflation: `tokensave_files` no longer accumulates tokens saved (listing file names is metadata, not a file-read substitute) +- Token counter inflation: `tracedecay_files` no longer accumulates tokens saved (listing file names is metadata, not a file-read substitute) - Worldwide counter staleness: periodic flush every 30 seconds during MCP sessions instead of only on shutdown - Shutdown flush was effectively a no-op (delta always 0 because `accumulate_tokens_saved` already upserted the current value to global DB); now uses `last_flushed_tokens` to correctly track remaining delta ## [1.5.1] - 2026-03-25 ### Added -- `tokensave doctor` command — comprehensive health check of binary, project index, global DB, user config, Claude Code integration (MCP server, hook, permissions, CLAUDE.md), and network connectivity +- `tracedecay doctor` command — comprehensive health check of binary, project index, global DB, user config, Claude Code integration (MCP server, hook, permissions, CLAUDE.md), and network connectivity - Stale install warning: automatically detects when `claude-install` needs re-running due to new tool permissions and warns on every CLI command ### Added - 9 new MCP tools (18 total): - - `tokensave_dead_code` — find unreachable symbols with no incoming edges - - `tokensave_diff_context` — semantic context for changed files (modified symbols, dependencies, affected tests) - - `tokensave_module_api` — public API surface of a file or directory - - `tokensave_circular` — detect circular file dependencies - - `tokensave_hotspots` — most connected symbols by edge count - - `tokensave_similar` — find symbols with similar names - - `tokensave_rename_preview` — all references to a symbol - - `tokensave_unused_imports` — import statements never referenced - - `tokensave_changelog` — semantic diff between two git refs -- `get_all_edges()`, `get_nodes_by_file()`, `get_all_nodes()`, `get_incoming_edges()`, `get_outgoing_edges()` delegation methods on `TokenSave` + - `tracedecay_dead_code` — find unreachable symbols with no incoming edges + - `tracedecay_diff_context` — semantic context for changed files (modified symbols, dependencies, affected tests) + - `tracedecay_module_api` — public API surface of a file or directory + - `tracedecay_circular` — detect circular file dependencies + - `tracedecay_hotspots` — most connected symbols by edge count + - `tracedecay_similar` — find symbols with similar names + - `tracedecay_rename_preview` — all references to a symbol + - `tracedecay_unused_imports` — import statements never referenced + - `tracedecay_changelog` — semantic diff between two git refs +- `get_all_edges()`, `get_nodes_by_file()`, `get_all_nodes()`, `get_incoming_edges()`, `get_outgoing_edges()` delegation methods on `TraceDecay` - `find_circular_dependencies()` graph query for file-level cycle detection -- `tokensave status` prompts to create index if none exists (Y/n) +- `tracedecay status` prompts to create index if none exists (Y/n) - Country flags in status output via `--show-flags` ## [1.4.3] - 2026-03-25 ### Added -- Country flags row in `tokensave status` — shows emoji flags of countries where tokensave is used, centered below the token counters +- Country flags row in `tracedecay status` — shows emoji flags of countries where tracedecay is used, centered below the token counters - `fetch_country_flags()` in cloud module (500ms timeout, best-effort) - Flags truncated with ellipsis if they exceed the available table width @@ -1248,7 +1248,7 @@ tokensave sync --force # re-index to pick up new language extractors ### Added - Cross-platform release workflow — GitHub Actions builds prebuilt binaries for macOS (ARM), Linux (x86_64, ARM64), and Windows (x86_64) on every release -- Scoop package manager support for Windows (`scoop install tokensave`) +- Scoop package manager support for Windows (`scoop install tracedecay`) - Automated Scoop bucket updates on release - Automated Homebrew formula + bottle updates on release @@ -1258,24 +1258,24 @@ tokensave sync --force # re-index to pick up new language extractors ## [1.4.0] - 2026-03-25 ### Added -- Worldwide token-saved counter — aggregates anonymous token counts across all tokensave users via Cloudflare Worker + Upstash Redis -- `tokensave status` shows three tiers: Local, Global, and Worldwide token counts -- `tokensave disable-upload-counter` / `tokensave enable-upload-counter` commands to opt out of uploading -- All upload state stored transparently in `~/.tokensave/config.toml` +- Worldwide tracedecayd counter — aggregates anonymous token counts across all tracedecay users via Cloudflare Worker + Upstash Redis +- `tracedecay status` shows three tiers: Local, Global, and Worldwide token counts +- `tracedecay disable-upload-counter` / `tracedecay enable-upload-counter` commands to opt out of uploading +- All upload state stored transparently in `~/.tracedecay/config.toml` - Version check on `status` (5-min cache) and `sync` (parallel, no added latency) with auto-detected upgrade command (cargo/brew) - First-run notice informing users about the worldwide counter and how to opt out - Flush cooldown (60s) after failed uploads to prevent sluggish CLI during outages - Network Calls & Privacy section in README documenting all outbound requests ### Changed -- `update_global_db()` now computes token-saved deltas for accurate pending upload accumulation -- Moved Cloudflare Worker source to separate `tokensave-cloud` repository +- `update_global_db()` now computes tracedecayd deltas for accurate pending upload accumulation +- Moved Cloudflare Worker source to separate `tracedecay-cloud` repository ## [1.3.0] - 2026-03-24 ### Added -- User-level global database (`~/.tokensave/global.db`) that tracks all TokenSave projects and their cumulative saved tokens -- `tokensave_status` and CLI `tokensave status` now report both local (project) and global (all projects) tokens saved when the global DB is available +- User-level global database (`~/.tracedecay/global.db`) that tracks all TraceDecay projects and their cumulative saved tokens +- `tracedecay_status` and CLI `tracedecay status` now report both local (project) and global (all projects) tokens saved when the global DB is available - All CLI entry points (`sync`, `status`, `claude-install` init) register the project in the global DB on every run - MCP server updates the global DB on every token accumulation and on shutdown (best-effort, no locking) @@ -1285,11 +1285,11 @@ tokensave sync --force # re-index to pick up new language extractors ## [1.2.1] - 2026-03-24 ### Fixed -- Renamed all remaining `codegraph` references in release workflow, Homebrew formula, setup script, and hook to `tokensave` -- Release workflow now produces `tokensave` binary, bottles, and source tarballs (was still using `codegraph` names) -- Homebrew formula class renamed from `Codegraph` to `Tokensave` with updated URLs -- Setup script variable `CODEGRAPH_BIN` renamed to `TOKENSAVE_BIN` -- CLAUDE.md marker in setup script updated to use `Tokensave` name +- Renamed all remaining `codegraph` references in release workflow, Homebrew formula, setup script, and hook to `tracedecay` +- Release workflow now produces `tracedecay` binary, bottles, and source tarballs (was still using `codegraph` names) +- Homebrew formula class renamed from `Codegraph` to `TraceDecay` with updated URLs +- Setup script variable `CODEGRAPH_BIN` renamed to `TRACEDECAY_BIN` +- CLAUDE.md marker in setup script updated to use `TraceDecay` name ## [1.2.0] - 2026-03-24 @@ -1303,10 +1303,10 @@ tokensave sync --force # re-index to pick up new language extractors ## [1.1.0] - 2026-03-24 ### Added -- `tokensave files` CLI command — list indexed files with `--filter` (directory prefix), `--pattern` (glob), and `--json` output -- `tokensave affected` CLI command — BFS through file dependency graph to find test files impacted by source changes; supports `--stdin` (pipe from `git diff --name-only`), `--depth`, `--filter`, `--json`, `--quiet` -- `tokensave_files` MCP tool — file listing with path/pattern filtering, flat or grouped-by-directory output -- `tokensave_affected` MCP tool — find affected test files via file-level dependency traversal +- `tracedecay files` CLI command — list indexed files with `--filter` (directory prefix), `--pattern` (glob), and `--json` output +- `tracedecay affected` CLI command — BFS through file dependency graph to find test files impacted by source changes; supports `--stdin` (pipe from `git diff --name-only`), `--depth`, `--filter`, `--json`, `--quiet` +- `tracedecay_files` MCP tool — file listing with path/pattern filtering, flat or grouped-by-directory output +- `tracedecay_affected` MCP tool — find affected test files via file-level dependency traversal - Graceful shutdown handler for MCP server — persists tokens-saved counter, checkpoints SQLite WAL, and logs session summary on SIGINT/SIGTERM - `Database::checkpoint()` method for WAL cleanup on shutdown @@ -1318,11 +1318,11 @@ tokensave sync --force # re-index to pick up new language extractors ## [1.0.0] - 2026-03-24 ### Changed -- **Renamed project from `token-codegraph` to `tokensave`** -- Crate name: `tokensave` (was `token-codegraph`) -- Binary name: `tokensave` (was `codegraph`) -- Data directory: `.tokensave/` (was `.codegraph/`) -- MCP tool prefix: `tokensave_*` (was `codegraph_*`) +- **Renamed project from `token-codegraph` to `tracedecay`** +- Crate name: `tracedecay` (was `token-codegraph`) +- Binary name: `tracedecay` (was `codegraph`) +- Data directory: `.tracedecay/` (was `.codegraph/`) +- MCP tool prefix: `tracedecay_*` (was `codegraph_*`) - Version bump to 1.0.0 ### Added @@ -1352,8 +1352,8 @@ tokensave sync --force # re-index to pick up new language extractors ## [0.5.2] ### Changed -- Update repo URLs after GitHub rename to tokensave -- Rename crate to tokensave for crates.io +- Update repo URLs after GitHub rename to tracedecay +- Rename crate to tracedecay for crates.io ## [0.5.1] @@ -1381,9 +1381,9 @@ tokensave sync --force # re-index to pick up new language extractors ## [0.4.1] ### Added -- Show version number in tokensave status +- Show version number in tracedecay status - Persist tokens-saved counter to database -- Show indexed token count in tokensave status +- Show indexed token count in tracedecay status ### Changed - Update dependencies @@ -1426,7 +1426,7 @@ tokensave sync --force # re-index to pick up new language extractors ### Added - MCP server (JSON-RPC 2.0 over stdio) -- CLI interface and TokenSave orchestrator +- CLI interface and TraceDecay orchestrator - Vector embeddings for semantic search - Context builder for AI-ready code graph context - Incremental sync for detecting file changes @@ -1436,6 +1436,6 @@ tokensave sync --force # re-index to pick up new language extractors - libsql database layer with full CRUD operations - Configuration module with glob-based file filtering - Core types and error handling scaffold -[6.1.1]: https://github.com/ScriptedAlchemy/tokensave/releases/tag/v6.1.1 -[--help]: https://github.com/ScriptedAlchemy/tokensave/releases/tag/v--help +[6.1.1]: https://github.com/ScriptedAlchemy/tracedecay/releases/tag/v6.1.1 +[--help]: https://github.com/ScriptedAlchemy/tracedecay/releases/tag/v--help [0.0.2]: https://github.com/ScriptedAlchemy/tracedecay/releases/tag/v0.0.2 diff --git a/README.md b/README.md index 90bcf88c..40645cc5 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ scoop bucket add tracedecay https://github.com/ScriptedAlchemy/scoop-bucket scoop install tracedecay ``` -> **Note:** The Homebrew tap and Scoop bucket are external repositories updated separately from this one — until they pick up the rename, the package may still be published under the legacy `tokensave` name. +> **Note:** The Homebrew tap and Scoop bucket are external repositories updated separately from this one — until they pick up the rename, the package may still be published under the legacy package name. **Cargo (any platform):** @@ -151,12 +151,12 @@ For project-scoped setup, run from the repository root: tracedecay install --local --agent cursor ``` -Local install writes only workspace files such as `.mcp.json`, `.codex/config.toml`, `.vscode/mcp.json`, `.hermes/plugins/tracedecay/`, or the equivalent project config for Claude, Codex, Gemini, Hermes, Kiro, OpenCode, Copilot/VS Code, Zed, Roo Code, Kimi, Kilo, and Vibe. Generated MCP configs and plugin wrappers use the resolved absolute `tracedecay` executable path. Hermes installs into `~/.hermes/plugins/tracedecay/` by default, or into `~/.hermes/profiles//plugins/tracedecay/` with `--profile `; profile names are normalized to lowercase and must match `[a-z0-9][a-z0-9_-]{0,63}`. The generated context engine resolves its project from explicit host kwargs first, then a `project_root` key on the Hermes config object, then the install-time pin, and finally the session cwd. Pin a profile to one project with `tracedecay install --agent hermes [--profile ] --project-root /abs/path` — the pin is written into the generated plugin, applies to every plugin tool call regardless of the Hermes working directory, and survives later unpinned reinstalls. Use `tracedecay uninstall --agent hermes --profile ` to remove a named profile install; `reinstall` and `doctor --agent hermes` currently operate on the default profile. To refresh generated plugin code after an upgrade without any config writes, run `tracedecay update-plugin` (alias `update-plugins`): it rewrites only tracedecay-generated artifacts — the Hermes plugin files and dashboard page for every detected profile (pin re-read from `config.yaml`, never written), the Cursor plugin bundle, the Codex plugin bundle, and the Kiro managed agent — and leaves `config.yaml`, `config.toml`, `mcp.json`, settings, hooks, and prompt rules byte-for-byte intact. Config-managed integrations (Claude, Gemini, …) have no generated artifacts and are reported as untouched; `tracedecay reinstall` remains the command that reconciles their config entries. Hermes wrappers run from Hermes' current working directory, use a 600-second timeout, and include truncated stdout/stderr in error JSON. Hermes local install without `--profile` writes only project plugin files and `.hermes/config.yaml`; `tracedecay install --local --agent hermes --profile ` is a deliberate mixed-scope mode that targets the named profile instead. Project-local Hermes plugins are loaded by launching Hermes with `HERMES_HOME=/.hermes`. For Codex, global install writes a Codex plugin source bundle to `~/plugins/tracedecay`, registers it in `~/.agents/plugins/marketplace.json`, and prints `codex plugin add tracedecay@personal`; hooks and AGENTS.md stay config-managed because current Codex plugin manifests accept MCP and skills but not hook declarations. For Cursor, both global and `--local` install put the plugin in `~/.cursor/plugins/local/tracedecay` and require a Cursor reload. The plugin MCP config runs `tracedecay serve --path ${workspaceFolder}`, so it resolves the active workspace instead of the plugin directory and uses that workspace's active project store rather than the legacy global Cursor MCP registration. Cursor install no longer writes `.cursor/mcp.json`, `.cursor/hooks.json`, `.cursor/rules/tokensave.mdc` (legacy artifact name), or `.cursor/permissions.json`; approvals are left to Cursor approval/run-mode behavior. The plugin hooks are: +Local install writes only workspace files such as `.mcp.json`, `.codex/config.toml`, `.vscode/mcp.json`, `.hermes/plugins/tracedecay/`, or the equivalent project config for Claude, Codex, Gemini, Hermes, Kiro, OpenCode, Copilot/VS Code, Zed, Roo Code, Kimi, Kilo, and Vibe. Generated MCP configs and plugin wrappers use the resolved absolute `tracedecay` executable path. Hermes installs into `~/.hermes/plugins/tracedecay/` by default, or into `~/.hermes/profiles//plugins/tracedecay/` with `--profile `; profile names are normalized to lowercase and must match `[a-z0-9][a-z0-9_-]{0,63}`. The generated context engine resolves its project from explicit host kwargs first, then a `project_root` key on the Hermes config object, then the install-time pin, and finally the session cwd. Pin a profile to one project with `tracedecay install --agent hermes [--profile ] --project-root /abs/path` — the pin is written into the generated plugin, applies to every plugin tool call regardless of the Hermes working directory, and survives later unpinned reinstalls. Use `tracedecay uninstall --agent hermes --profile ` to remove a named profile install; `reinstall` and `doctor --agent hermes` currently operate on the default profile. To refresh generated plugin code after an upgrade without any config writes, run `tracedecay update-plugin` (alias `update-plugins`): it rewrites only tracedecay-generated artifacts — the Hermes plugin files and dashboard page for every detected profile (pin re-read from `config.yaml`, never written), the Cursor plugin bundle, the Codex plugin bundle, and the Kiro managed agent — and leaves `config.yaml`, `config.toml`, `mcp.json`, settings, hooks, and prompt rules byte-for-byte intact. Config-managed integrations (Claude, Gemini, …) have no generated artifacts and are reported as untouched; `tracedecay reinstall` remains the command that reconciles their config entries. Hermes wrappers run from Hermes' current working directory, use a 600-second timeout, and include truncated stdout/stderr in error JSON. Hermes local install without `--profile` writes only project plugin files and `.hermes/config.yaml`; `tracedecay install --local --agent hermes --profile ` is a deliberate mixed-scope mode that targets the named profile instead. Project-local Hermes plugins are loaded by launching Hermes with `HERMES_HOME=/.hermes`. For Codex, global install writes a Codex plugin source bundle to `~/plugins/tracedecay`, registers it in `~/.agents/plugins/marketplace.json`, and prints `codex plugin add tracedecay@personal`; hooks and AGENTS.md stay config-managed because current Codex plugin manifests accept MCP and skills but not hook declarations. For Cursor, both global and `--local` install put the plugin in `~/.cursor/plugins/local/tracedecay` and require a Cursor reload. The plugin MCP config runs `tracedecay serve --path ${workspaceFolder}`, so it resolves the active workspace instead of the plugin directory and uses that workspace's active project store rather than the legacy global Cursor MCP registration. Cursor install no longer writes `.cursor/mcp.json`, `.cursor/hooks.json`, `.cursor/rules/tracedecay.mdc` (legacy artifact name), or `.cursor/permissions.json`; approvals are left to Cursor approval/run-mode behavior. The plugin hooks are: - `sessionStart` — fire-and-forget; injects context steering the Agent toward tracedecay MCP tools and reports index freshness (suggests `tracedecay init` when no initialized project store is found). - `subagentStart` — blocks research/explore subagents until tracedecay MCP tools have been tried; the plugin's own `code-explorer`/`code-health-auditor`/`session-historian` agents are allow-listed. - `postToolUse` (unmatched — Cursor's docs enumerate no matcher value for semantic search) — fail-open; injects a soft `additional_context` hint after broad search/read tools (Grep, Glob, Read, semantic search, shell `rg`) so Cursor switches to `tracedecay_context`, `tracedecay_search`, `tracedecay_outline`, or `tracedecay_files`. Each hint category is emitted at most once per session (persisted in `.tracedecay/tool_hints_seen.json`). -- `beforeSubmitPrompt` — resets the local token counter for the new turn and ingests the current Cursor transcript into `.tracedecay/sessions.db` when `transcript_path` is present. +- `beforeSubmitPrompt` — resets the local token counter for the new turn and ingests the current Cursor transcript into the active project session store when `transcript_path` is present. - `afterFileEdit` (unmatched, so every Agent edit tool counts) — runs a **targeted single-file** sync of just the edited path(s) via `sync_if_stale_silent`, never a full-tree scan (which would scale with repo size, not edit size). - `afterShellExecution` — on Agent-run `git checkout`/`switch`/`worktree add`, bootstraps/maintains tracedecay branch tracking (`branch add`); on other state-changing git commands (pull/merge/rebase/reset/cherry-pick/stash apply|pop), runs a coalesced incremental sync. - `workspaceOpen` — ensures the current branch's DB exists (branch add if missing) and runs a catch-up incremental sync. @@ -195,7 +195,7 @@ tracedecay init **Non-interactive environments** (CI, containers, headless shells): `init`, `status`, and bare invocation skip prompts and use safe defaults. `init` creates the index without modifying `.gitignore` (prints a notice); `status` exits cleanly without creating an index if none exists. -This creates the active project's local TraceDecay store. Repo-local projects create `.tracedecay/` with the graph database; legacy `.tokensave/` directories are still honored. Profile-backed projects keep graph/session artifacts in a private profile shard while the repo may contain only an enrollment marker. `init` is the explicit opt-in; `sync` only updates already-initialized projects, so the global post-commit hook does not create stores in repos you never intended to index. After `init`, use `tracedecay sync` for incremental updates. +This creates the active project's TraceDecay store. Graph/session artifacts live in a user-level profile shard scoped to this project; `.tracedecay/` in the repo is only a lightweight marker/config directory. `init` is the explicit opt-in; `sync` only updates already-initialized projects, so the global post-commit hook does not create stores in repos you never intended to index. After `init`, use `tracedecay sync` for incremental updates.
What install writes for Claude Code @@ -664,15 +664,11 @@ chmod +x .git/hooks/post-commit ### Upgrading from 5.x -The standalone `tokensave daemon` command (5.x-era, pre-rename) and its launchd/systemd/Windows Service autostart were removed in 6.0.0. The embedded OS-level file watcher that replaced the daemon was itself removed in 6.1.0 (it caused runaway CPU and memory on large monorepos with deep `node_modules` or `target` trees). The on-demand staleness model above is the current design. - -If you still have a daemon autostart from 5.x, remove it (the artifacts use the historical `tokensave` name): - -- macOS: `launchctl unload ~/Library/LaunchAgents/com.tokensave.daemon.plist && rm ~/Library/LaunchAgents/com.tokensave.daemon.plist` -- Linux: `systemctl --user disable --now tokensave-daemon && rm ~/.config/systemd/user/tokensave-daemon.service` -- Windows: `sc.exe delete tokensave-daemon` (from an elevated terminal) - -If you don't recall the exact name: `launchctl list | grep tokensave` / `systemctl --user list-units | grep tokensave` / `sc.exe query state= all | findstr -i tokensave`. +The standalone daemon command from 5.x-era installs and its +launchd/systemd/Windows Service autostart were removed in 6.0.0. The embedded +OS-level file watcher that replaced the daemon was itself removed in 6.1.0 (it +caused runaway CPU and memory on large monorepos with deep `node_modules` or +`target` trees). The on-demand staleness model above is the current design. --- @@ -942,7 +938,7 @@ Full-index benchmark on a 1,782-file mixed Rust/Java/Scala codebase (57K nodes, ### "tracedecay not initialized" -TraceDecay could not find an initialized project store. In repo-local mode, create `.tracedecay/` with `tracedecay init`; profile-backed projects also need their enrollment marker and profile shard. +TraceDecay could not find an initialized project store. Run `tracedecay init` from the project root to create the user-level profile store and project enrollment marker. ```bash tracedecay init @@ -974,7 +970,7 @@ Large projects take longer on the first full index. | Variable | Effect | |----------|--------| -| `TRACEDECAY_GLOBAL_DB` | Override the path to the global database (used for LCM session storage selection). When set, the dashboard serves this store instead of the project-local sessions.db. | +| `TRACEDECAY_GLOBAL_DB` | Override the path to the global database or session store for dashboard LCM selection. When unset, the dashboard serves the active project's resolved session store, profile-sharded by default. | | `TRACEDECAY_BIN` | Path to the tracedecay binary (used by Hermes wrapper for spawn mode). | | `TRACEDECAY_DASHBOARD_PROJECT` | Project root path for Hermes dashboard spawn mode (defaults to Hermes' cwd). | | `TRACEDECAY_DASHBOARD_URL` | Full URL to an already-running dashboard (Hermes external URL mode). | @@ -982,7 +978,7 @@ Large projects take longer on the first full index. | `TRACEDECAY_OFFLINE` | Set to `1` to skip network requests for pricing data (Savings & Cost tab uses bundled fallback). | | `DISABLE_TRACEDECAY` | Set to `true` to disable the MCP server entirely (exits cleanly without initializing). | -Legacy `TOKENSAVE_*` environment variables (and `DISABLE_TOKENSAVE`) from before the rename are still honored as a fallback; the `TRACEDECAY_*` names take precedence when both are set. +Use `TRACEDECAY_*` environment variables for TraceDecay configuration. Pre-rename environment variable names are not runtime fallbacks. ### Disabling tracedecay for specific projects diff --git a/SECURITY.md b/SECURITY.md index 22c6e47d..5d846f4b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -26,7 +26,7 @@ When a vulnerability is found, the fix is shipped as a new release — there are ### What tracedecay stores -tracedecay builds a **local** code graph stored in the active project store. Repo-local projects use `.tracedecay/tracedecay.db`; legacy `.tokensave/` data directories are still honored. Profile-backed projects keep graph data in a private user profile shard such as `~/.tracedecay/projects//`, while the repository may contain only an enrollment marker plus project config. The database contains: +tracedecay builds a **local** code graph stored in the active project store. Repo-local projects use `.tracedecay/tracedecay.db`; legacy `.tracedecay/` data directories are still honored. Profile-backed projects keep graph data in a private user profile shard such as `~/.tracedecay/projects//`, while the repository may contain only an enrollment marker plus project config. The database contains: - Symbol names, signatures, and docstrings - File paths, sizes, and content hashes @@ -37,7 +37,7 @@ tracedecay builds a **local** code graph stored in the active project store. Rep Aside from the `read_cache`, the graph itself does **not** persist raw source code — it stores structural metadata only. The active project store is local-only — there is no cloud sync, remote database, or server-side storage. -The user-level `~/.tracedecay/global.db` tracks indexed projects, aggregate token-saved counts, and cost accounting data parsed from Claude Code session transcripts. Cursor transcript search is stored in the active project's session store (`.tracedecay/sessions.db` for repo-local projects), which contains ingested Cursor user/assistant message text plus transcript paths and metadata for that project. Both stores remain local-only and are not synced to a remote service. +The user-level `~/.tracedecay/global.db` tracks indexed projects, aggregate tracedecayd counts, and cost accounting data parsed from Claude Code session transcripts. Cursor transcript search is stored in the active project's session store (`.tracedecay/sessions.db` for repo-local projects), which contains ingested Cursor user/assistant message text plus transcript paths and metadata for that project. Both stores remain local-only and are not synced to a remote service. ### Network access @@ -49,7 +49,7 @@ Outbound connections are limited to: |-------------|---------|------|-------------| | `api.github.com` | Check for new releases | None (public API) | Silently ignored | | `github.com` | Download binary during `tracedecay upgrade` | None (public releases) | Error shown to user | -| `tokensave-counter.enzinol.workers.dev` | Aggregate token-saved counter (endpoint keeps its pre-rename name) | None | Silently ignored | +| `tracedecay-counter.enzinol.workers.dev` | Aggregate tracedecayd counter (endpoint keeps its pre-rename name) | None | Silently ignored | | `raw.githubusercontent.com` | Fetch model pricing from [LiteLLM](https://github.com/BerriAI/litellm) | None (public file) | Falls back to embedded pricing | All best-effort network calls use short timeouts (1-5 seconds) and never block the CLI or MCP server. The pricing fetch (5s timeout) only runs during `tracedecay cost` and is cached for 24 hours at `~/.tracedecay/pricing.json`. @@ -119,7 +119,7 @@ The Windows-elevation `unsafe` documented in earlier versions was removed alongs ## Best Practices -- Add `.tracedecay/` (and, for projects indexed before the rename, `.tokensave/`) to your `.gitignore` to avoid committing local store markers or repo-local databases. +- Add `.tracedecay/` (and, for projects indexed before the rename, `.tracedecay/`) to your `.gitignore` to avoid committing local store markers or repo-local databases. - If your project contains sensitive code, be aware that the database stores symbol names and signatures, and the `read_cache` table can hold rendered source text from `tracedecay_read` responses. Keeping repo-local store directories ignored and treating profile-sharded stores as private user data keeps both out of version control. - Keep tracedecay updated (`tracedecay upgrade`) to receive security fixes. - Review the [CHANGELOG](CHANGELOG.md) before upgrading to understand what changed. diff --git a/benchmarks/comparison-report.md b/benchmarks/comparison-report.md deleted file mode 100644 index 21984ebc..00000000 --- a/benchmarks/comparison-report.md +++ /dev/null @@ -1,43 +0,0 @@ -# TraceDecay vs Token-Savior Benchmark - -Generated: 2026-05-25 12:08:32 UTC - -> Historical run, recorded before the project was renamed from Tokensave to -> TraceDecay. The `tokensave` binary, command, and column names below are the -> names that were measured at run time and are preserved as recorded. - -Side-by-side comparison on real-world Python repositories. Both tools index -the same clone, and the same random sample of symbols (seed=42) is used for -query timing so the find_symbol column is directly comparable. - -**Memory notes.** token-savior's peak memory is measured with `tracemalloc` -(Python heap only). tokensave runs as a subprocess, so its peak is the -`ru_maxrss` delta from `getrusage(RUSAGE_CHILDREN)` (resident set size). -These are *not* identical units — treat them as order-of-magnitude. - -**Query timing.** token-savior is called in-process (pure Python dict -lookups). tokensave is driven over MCP via `tokensave serve --timings` -and the per-query column reports the handler's `_meta.duration_us` — -i.e. the time spent inside the Rust handler, with JSON-RPC / stdio / -Python-parse overhead stripped out. A warm-up call is issued before -each timed loop. `get_change_impact` for tokensave sums the handler -times of `search → impact`, mirroring how an agent must resolve the -symbol to a node_id before querying. - -## fastapi - -Naive `.py` source size: 3.6 MiB - -| Metric | token-savior | tokensave | -|--------|--------------|-----------| -| Cold index time | 6.212s | 2.147s | -| Warm reindex time | 1.575s | 1.363s | -| Peak memory (cold) | 65.4 MiB | 146.5 MiB | -| Cache / DB size | 5.2 MiB | 28.6 MiB | -| Files indexed | 2,715 | 2,677 | -| Symbols / nodes | 2,740 | 29,019 | -| find_symbol avg | 0.004 ms | 0.136 ms | -| get_function_source avg | 0.133 ms | 0.117 ms | -| get_change_impact avg | 24.374 ms | 0.569 ms | - -_tokensave indexed languages:_ Bash=6, JavaScript=4, Other=1548, Python=1118, TOML=1 diff --git a/benchmarks/comparison-results.json b/benchmarks/comparison-results.json deleted file mode 100644 index 6e2cc7cc..00000000 --- a/benchmarks/comparison-results.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "results": [ - { - "repo": "fastapi", - "root": "/tmp/tokensave-bench/fastapi", - "token_savior": { - "cold_index_seconds": 6.212, - "cold_index_peak_memory_bytes": 68604258, - "total_files": 2715, - "total_lines": 351673, - "total_functions": 4322, - "total_classes": 638, - "symbol_table_size": 2740, - "warm_index_seconds": 1.575, - "find_symbol_avg_ms": 0.004, - "get_function_source_avg_ms": 0.133, - "get_change_impact_avg_ms": 24.374, - "cache_size_bytes": 5431616 - }, - "tokensave": { - "cold_index_seconds": 2.147, - "cold_index_peak_memory_bytes": 153567232, - "warm_index_seconds": 1.363, - "node_count": 29019, - "edge_count": 2876, - "file_count": 2677, - "files_by_language": { - "TOML": 1, - "Bash": 6, - "JavaScript": 4, - "Other": 1548, - "Python": 1118 - }, - "find_symbol_avg_ms": 0.136, - "get_function_source_avg_ms": 0.117, - "get_change_impact_avg_ms": 0.569, - "cache_size_bytes": 30040064 - } - } - ], - "naive_source_size_bytes": { - "fastapi": 3788467 - } -} \ No newline at end of file diff --git a/benchmarks/tsbench/SUMMARY.md b/benchmarks/tsbench/SUMMARY.md deleted file mode 100644 index 735fa55a..00000000 --- a/benchmarks/tsbench/SUMMARY.md +++ /dev/null @@ -1,90 +0,0 @@ -# tsbench — TraceDecay results - -> Historical run, recorded before the project was renamed from tokensave to -> TraceDecay. Tool names (`mcp__tokensave__*`, `tokensave_*`), the fork -> filename `bench_tokensave.py`, and result paths below are preserved exactly -> as measured at run time. The live patch to reproduce this setup is now -> [`bench_tracedecay.patch`](bench_tracedecay.patch) (rebranded; see README). - -Run date: 2026-05-25 -Bench: `Mibayy/tsbench` — 96 synthetic tasks across 12 categories -Model: Claude Opus 4.7 (1M context) via Max OAuth -Tool surface: `mcp__tokensave__*` (74 MCP tools) + Read/Grep/Edit/Bash fallback - -## Headline - -| | tokensave | token-savior v4.0 | -|---|---:|---:| -| **Score** | **184 / 192 (95.8%)** | 188 / 192 (97.9%) | -| Score distribution | 90 × 2/2, 4 × 1/2, 2 × 0/2 | 92 × 2/2, 4 × 1/2, 0 × 0/2 | -| Wall time / task | 18.1 s | 18.9 s | -| Active tokens / task | 695 | 3 395 | -| Tool-call mix (TS / total) | 97 / 147 (66%) | n/a | - -tokensave lands **2.1 pp behind token-savior** on a benchmark whose prompt and -scoring rubric were both designed around token-savior's exact tool surface, and -whose synthetic project was *generated by* the team behind token-savior. Doing -this well first-attempt on someone else's home turf is meaningful. - -## What scored - -- **96/96 tasks completed** — no timeouts, no harness errors, no `CANNOT_ANSWER`. -- **90 tasks at 2/2** — full credit. Coverage included every category: navigation, - refactoring, bug_fixing, code_generation, writing_tests, code_review, audit, - git, explanation, documentation, config_infra, data_analysis. -- The four "missing tools" worry was unfounded: - - `add_field_to_model` tasks were solved cleanly with `tokensave_str_replace` - on Prisma / TS interface blocks (TASK-012 → 2/2). - - Config / Docker audits used Read fallback (TASK-024 → 2/2). - -## What didn't score - -| Task | Cat. | Score | Root cause | -|---|---|:-:|---| -| TASK-008 | audit | 0/2 | `tokensave_dead_code` returns a different candidate set than token-savior's `find_dead_code` — semantic mismatch with the ground-truth `DEAD-*` IDs. Tool difference, not a transport problem. | -| TASK-015 | edit | 0/2 | `tokensave_insert_at_symbol` succeeded but placed the new function differently from what the grader expects — token-savior's `insert_near_symbol` and ours differ on insertion-point semantics. | -| TASK-007 | explanation | 1/2 | Missing one required entry-point keyword from STANDARD VOCABULARY. | -| TASK-018 | debug | 1/2 | **Self-inflicted**: `tokensave_redundancy` correctly flagged `bench_tokensave.py` (our fork of `bench.py`) as a duplicate. The grader expected a curated set; including a real-but-unlisted duplicate cost a point. Bench-environment contamination, not a tokensave bug. | -| TASK-086 | documentation | 1/2 | Module README missing some rubric keywords. token-savior also drops points on documentation tasks. | -| TASK-087 | documentation | 1/2 | Same class — token-savior's own BENCHMARK-SUMMARY lists TASK-087 as one of their known 1/2 tasks. | - -## Token efficiency - -| | tokensave | token-savior | -|---|---:|---:| -| Active tokens / task | **695** | 3 395 | -| Cache_creation / task | 18 589 | 2 560 | -| Cache_read / task | 158 336 | n/a | - -tokensave is **~5× more active-token-efficient per task**. The cache_creation -gap reflects our run *without* `--bare` (token-savior's BENCHMARK-SUMMARY -mandates `--bare` for the cited numbers); enabling `--bare` would close that -gap but broke OAuth in our environment. - -## Cost - -List Opus 4.7 pricing on the full run: **$61.22**. Actual out-of-pocket on -Max OAuth: $0 (within quota). Total wall time: 29.0 min. - -## Method - -- `tsbench` cloned to `/tmp/tsbench`, no v1/v2 git tags created (so 6 BREAK-* - tasks rely on `tokensave_changelog` instead of `tokensave_branch_diff`). -- Forked `bench.py` → `bench_tokensave.py` with: tokensave MCP config (binary - + `serve --timings`), tokensave-flavored system prompt mapping each - token-savior tool to its tokensave equivalent (or to a `Read`/`Edit` - fallback for tools without one), `mcp__tokensave__*` prefix matching, and - relaxed `--disallowedTools` (only `Agent` banned). -- `tokensave init` at the project root; long-lived `tokensave serve --timings` - per task via stdio MCP transport. -- `TSBENCH_BARE=0` (Max OAuth required `--bare` to be off). -- Per-task results: `results-tokensave/raw/TASK-NNN-run-B.json`. - -## Notable: this is first-attempt, untuned - -token-savior reports 97.9% as the result of *audit + tuning cycles* between -April and May 2026, including memory-hook fixes, schema thinning, and a -`SYSTEM_PROMPT_TS` rewritten 5+ times with rubric-specific vocabulary. The -tokensave 95.8% number is the *first run* of an unmodified-rubric translation -to a new tool surface. A real tuning pass (especially on TASK-008 dead-code -mapping and TASK-015 insertion-point semantics) should close most of the gap. diff --git a/docs/BRANCHING-USER-GUIDE.md b/docs/BRANCHING-USER-GUIDE.md index 32a2e572..078ace90 100644 --- a/docs/BRANCHING-USER-GUIDE.md +++ b/docs/BRANCHING-USER-GUIDE.md @@ -35,7 +35,7 @@ which branches have their own database. In repo-local mode, the storage layout l release_3_4.db ``` -Projects indexed before the rebrand may still use a legacy `.tokensave/` directory +Projects indexed before the rebrand may still use a legacy `.tracedecay/` directory with the same layout; it is honored as a fallback. Profile-backed projects keep the same logical layout in their profile shard. The repository may contain only an enrollment marker, while `branch-meta.json`, `tracedecay.db`, and `branches/*.db` live under the resolved store root. diff --git a/docs/CARGO-CONTENTION-POLICY.md b/docs/CARGO-CONTENTION-POLICY.md index ea69ba0c..7c154812 100644 --- a/docs/CARGO-CONTENTION-POLICY.md +++ b/docs/CARGO-CONTENTION-POLICY.md @@ -16,23 +16,23 @@ Live evidence observed while authoring this policy: `target/debug/` and `target/ ## Why the MCP tools are not affected -The tracedecay MCP diagnostic tools (`tracedecay_diagnostics`, `tracedecay_run_affected_tests`, `tracedecay_affected`) already pin `--target-dir .tracedecay/target/` in `src/diagnostics/rust.rs` (`target_dir_for()`), deliberately so concurrent IDE / user `cargo check` runs don't race for `target/`'s lockfile. That path is owned by the MCP server. **Workers running their own `cargo` from the terminal are the only unmanaged case** — and they default to the repo's `target/`, which is the user's ~399 GB interactive dir. +The tracedecay MCP diagnostic tools (`tracedecay_diagnostics`, `tracedecay_run_affected_tests`, `tracedecay_affected`) already pin `--target-dir /tmp/tracedecay-target//diagnostics/` in `src/diagnostics/rust.rs` (`target_dir_for()`), deliberately so concurrent IDE / user `cargo check` runs don't race for `target/`'s lockfile and diagnostics never create repo-local TraceDecay folders. **Workers running their own `cargo` from the terminal are the only unmanaged case** — and they default to the repo's `target/`, which is the user's ~399 GB interactive dir. ## Policy 1. **Default: per-task isolated target dir.** Every cargo-heavy card exports, before its first `cargo` command: ```sh - export CARGO_TARGET_DIR="$HERMES_KANBAN_WORKSPACE/.tracedecay/target/$HERMES_KANBAN_TASK" + export CARGO_TARGET_DIR="/tmp/tracedecay-target/$HERMES_KANBAN_TASK" ``` - This is an independent cargo target root (cargo creates its own `debug/` / `release/` / `.cargo-lock` inside it). Different path ⇒ different lock ⇒ **zero contention** with other workers, with the MCP tool's `.tracedecay/target/`, or with the user's `target/`. `.gitignore` already covers `.tracedecay`, so these dirs never pollute `git status`. + This is an independent cargo target root (cargo creates its own `debug/` / `release/` / `.cargo-lock` inside it). Different path ⇒ different lock ⇒ **zero contention** with other workers, with the MCP diagnostics target, or with the user's `target/`. Because the target lives under `/tmp`, it cannot pollute `git status` or create a project-local TraceDecay folder. 2. **Never run bare `cargo` against the repo's `target/`.** That dir is the user's interactive build cache. A worker building there contends with the human *and* with every other default-target worker. Always export `CARGO_TARGET_DIR` first. -3. **Leave `.tracedecay/target/` (no task suffix) to the MCP tools.** Do not `cargo clean` it, do not point a worker at it. It is shared by `tracedecay_diagnostics` / `tracedecay_run_affected_tests`. +3. **Leave `/tmp/tracedecay-target//diagnostics/` to the MCP tools.** Do not `cargo clean` it, do not point a worker at it. It is shared by `tracedecay_diagnostics` / `tracedecay_run_affected_tests`. -4. **Reserved serialized lane for full-workspace integration.** A single shared dir `.tracedecay/target/integration/` is reserved for the one card type that genuinely benefits from a warm, shared, whole-workspace cache: the final "is the whole workspace green?" verification before merge. Only one integration card may use it at a time — the board owner keeps integration cards serial (chain them with dependencies) so they reuse the cache instead of rebuilding it. Because it is a separate dir, an integration build never contends with the per-task dev dirs. +4. **Full-workspace integration uses its own target dir.** Integration checks follow the same per-invocation rule as other cargo-heavy cards. Use a unique target dir for the integration run so full-suite verification can run without blocking unrelated workers. 5. **Cleanup.** Per-task dirs are scratch. Before `kanban_complete`, a worker reclaims its disk (~1.6–4 GB each) with: @@ -43,7 +43,7 @@ The tracedecay MCP diagnostic tools (`tracedecay_diagnostics`, `tracedecay_run_a The owner periodically GCs dirs left behind by crashed / timed-out runs: ```sh - ls "$HERMES_KANBAN_WORKSPACE/.tracedecay/target/" + ls /tmp/tracedecay-target/ # remove entries whose task id is no longer running/ready/todo ``` @@ -51,7 +51,7 @@ The tracedecay MCP diagnostic tools (`tracedecay_diagnostics`, `tracedecay_run_a ## Rejected alternatives -- **Serialize all cargo cards on one shared target (the board's old implicit behavior).** Rejected: it collapses the board to a single effective cargo lane, defeating the point of concurrent multi-model workers. Reserve serialization for the single integration lane only. +- **Serialize cargo cards on one shared target.** Rejected: it collapses the board to a single effective cargo lane, defeating the point of concurrent multi-model workers. - **Set a repo-level `build.target-dir` in `.cargo/config.toml`.** Rejected: it would also hijack the user's interactive builds into one dir, re-creating contention with the human. The override must be per-invocation via the env var, not repo-wide. - **Share compiled deps across per-task dirs.** Not natively supported by cargo without `sccache` / `cargo-chef`. Noted as a future optimization if cold-build time becomes a bottleneck; until then the bounded one-time compile per task is far cheaper than lock contention or serialization. @@ -59,12 +59,12 @@ The tracedecay MCP diagnostic tools (`tracedecay_diagnostics`, `tracedecay_run_a ```text Cargo policy (see docs/CARGO-CONTENTION-POLICY.md): - export CARGO_TARGET_DIR="$HERMES_KANBAN_WORKSPACE/.tracedecay/target/$HERMES_KANBAN_TASK" + export CARGO_TARGET_DIR="/tmp/tracedecay-target/$HERMES_KANBAN_TASK" Run all cargo check/test/build/clippy AFTER that export. Never use the bare repo `target/` (it is the user's interactive dir and is contended). Before kanban_complete, reclaim disk: rm -rf "$CARGO_TARGET_DIR" -Full-workspace integration checks use the serialized -`.tracedecay/target/integration/` lane — only one such card at a time. +Full-workspace integration checks use their own target dir too, so they do not +block unrelated workers. ``` ## Verification checklist for a cargo-heavy card diff --git a/docs/DASHBOARD-API-AUDIT.md b/docs/DASHBOARD-API-AUDIT.md index 8cafd979..dd81cee7 100644 --- a/docs/DASHBOARD-API-AUDIT.md +++ b/docs/DASHBOARD-API-AUDIT.md @@ -35,7 +35,7 @@ MCP can never diverge. Preserve one router builder. - `lcm_conn: Option` — LCM session store (`lcm_raw_messages`, `lcm_summary_nodes`, `lcm_summary_sources` + FTS mirrors). Resolved by `resolve_lcm_store` (`mod.rs:117`): project-local `.tracedecay/sessions.db` - by default; `TRACEDECAY_GLOBAL_DB` override wins (legacy `TOKENSAVE_GLOBAL_DB` + by default; `TRACEDECAY_GLOBAL_DB` override wins (legacy `TRACEDECAY_GLOBAL_DB` still accepted); global DB is the fallback. `lcm_scope` ∈ `{"project_local","global"}` records which. - `savings_db: Option>` — savings ledger (out of scope here). diff --git a/docs/DESIGN-DOC.md b/docs/DESIGN-DOC.md index 82705761..0cd23bd1 100644 --- a/docs/DESIGN-DOC.md +++ b/docs/DESIGN-DOC.md @@ -370,7 +370,7 @@ subtracts the size of the tool's response, and accumulates the difference in the per-project database. A global database at `~/.tracedecay/global.db` aggregates totals across all projects -(an existing legacy `~/.tokensave/` directory is still honored as a fallback). +(an existing legacy `~/.tracedecay/` directory is still honored as a fallback). An opt-in worldwide counter (Cloudflare Worker) lets users contribute their totals anonymously. Upload is best-effort with 2-second timeouts and never blocks the CLI. diff --git a/docs/DOMAIN-EXTRACTORS.md b/docs/DOMAIN-EXTRACTORS.md index d752e989..94664d5f 100644 --- a/docs/DOMAIN-EXTRACTORS.md +++ b/docs/DOMAIN-EXTRACTORS.md @@ -246,7 +246,7 @@ This means two rules in the same layer that extract the same string automaticall ### Project-local rules (primary use case) -`.tracedecay/domain-symbols.toml` at the project root (an existing legacy `.tokensave/` directory is still honored as a fallback). Rules are private to the project; they are not committed alongside the source unless the project author chooses to. +`.tracedecay/domain-symbols.toml` at the project root (an existing legacy `.tracedecay/` directory is still honored as a fallback). Rules are private to the project; they are not committed alongside the source unless the project author chooses to. ### Rule packs (shareable) diff --git a/docs/INDEX-DESIGN.md b/docs/INDEX-DESIGN.md index 1e042e31..06cc905c 100644 --- a/docs/INDEX-DESIGN.md +++ b/docs/INDEX-DESIGN.md @@ -4,7 +4,7 @@ How tracedecay builds and maintains a semantic code graph from source files, and ## Overview -tracedecay indexes a codebase into a directed graph stored in the active project store. Repo-local projects use `.tracedecay/tracedecay.db`; existing `.tokensave/` directories remain a fallback. Profile-backed projects use the same schema through the storage resolver, with the physical DB in a private profile shard. Source files are parsed with tree-sitter to extract **nodes** (code entities) and **edges** (relationships between them). Cross-file references that cannot be resolved during single-file extraction are stored as **unresolved refs** and resolved in a second pass once all files have been processed. +tracedecay indexes a codebase into a directed graph stored in the active project store. Repo-local projects use `.tracedecay/tracedecay.db`; existing `.tracedecay/` directories remain a fallback. Profile-backed projects use the same schema through the storage resolver, with the physical DB in a private profile shard. Source files are parsed with tree-sitter to extract **nodes** (code entities) and **edges** (relationships between them). Cross-file references that cannot be resolved during single-file extraction are stored as **unresolved refs** and resolved in a second pass once all files have been processed. The graph is kept up-to-date through incremental sync: only files whose content hash has changed are re-extracted. A lock in the active store prevents concurrent sync operations from the CLI and the embedded MCP file watcher (and from multiple MCP servers attached to the same project). diff --git a/docs/LCM-PAYLOAD-GC.md b/docs/LCM-PAYLOAD-GC.md index b465f9e0..f646f50e 100644 --- a/docs/LCM-PAYLOAD-GC.md +++ b/docs/LCM-PAYLOAD-GC.md @@ -597,8 +597,8 @@ via `last_gc_at`. The dry-run is cheap (no I/O mutations, no locks beyond reads) and safe to run any time. A run skips apply if `now - last_gc_at < gc_interval_seconds` unless the operator forces it. -**Per-store, not global.** Each store (project-local `/.tracedecay` or -profile-scoped `/.tracedecay`) is GC'd independently against its +**Per-store, not global.** Each resolved project store (user-level shard, +explicit local store, or legacy local store) is GC'd independently against its own DB (contract §14). There is no cross-store coordination. --- diff --git a/docs/LCM-PAYLOAD-LIFECYCLE.md b/docs/LCM-PAYLOAD-LIFECYCLE.md index f945c19b..6d0801a4 100644 --- a/docs/LCM-PAYLOAD-LIFECYCLE.md +++ b/docs/LCM-PAYLOAD-LIFECYCLE.md @@ -46,9 +46,9 @@ has the full map. (`src/sessions/lcm/payload.rs:117`) names each payload `payload_.payload` and writes it under `payload_dir(storage_root)` = `/lcm-payloads` - (`payload.rs:52`). Project-local stores resolve the root to - `/.tracedecay`; profile-scoped stores to `/.tracedecay` (legacy - `.tokensave` fallback). + (`payload.rs:52`). User-level project stores resolve the root to + `~/.tracedecay/projects/`; explicit local/legacy stores resolve to + `/.tracedecay` (or legacy `.tracedecay` fallback). - **Schema.** `lcm_external_payloads` is keyed by `payload_ref` (PK) with `UNIQUE(provider, message_id, payload_ref)` and `FOREIGN KEY(provider, session_id) REFERENCES sessions ON DELETE CASCADE` (`src/sessions/lcm/schema.rs:133-149`). There is @@ -390,8 +390,8 @@ The full algorithm is `t_bbd369f2`'s to design, but it MUST satisfy this contrac - Age- or content-based retention policy (delete payloads older/larger than N). This contract defines the *mechanical* lifetime only; age-based retention can layer on top later and would convert `live` payloads into `unreferenced` via a policy pass. -- Cross-storage-root or cross-profile GC. Each store (project-local or profile-scoped) is GC'd - independently against its own DB. +- Cross-project-store GC. Each resolved project store (user-level shard, explicit local store, + or legacy local store) is GC'd independently against its own DB. - Soft-delete, undo, archive, or restoration of reaped bodies. Reap is hard and permanent, consistent with memory fact deletion being hard-delete. The tombstone is informational only. - Cross-message / cross-owner payload deduplication. The owner-hash includes `message_id`; no diff --git a/docs/LSP-INTEGRATION.md b/docs/LSP-INTEGRATION.md index c348c403..ad753dd7 100644 --- a/docs/LSP-INTEGRATION.md +++ b/docs/LSP-INTEGRATION.md @@ -318,7 +318,7 @@ TRACEDECAY_LSP=0 # disable LSP globally (equivalent to --no-lsp) TRACEDECAY_LSP_TIMEOUT=30 # override timeout ``` -The legacy `TOKENSAVE_*` variable names are still honored as a fallback when the +The legacy `TRACEDECAY_*` variable names are still honored as a fallback when the `TRACEDECAY_*` equivalents are unset. ## Performance Considerations diff --git a/docs/MEMORY-HEALTH-VISIBILITY-GAPS.md b/docs/MEMORY-HEALTH-VISIBILITY-GAPS.md index 0979c08d..67741be7 100644 --- a/docs/MEMORY-HEALTH-VISIBILITY-GAPS.md +++ b/docs/MEMORY-HEALTH-VISIBILITY-GAPS.md @@ -13,7 +13,7 @@ implementation. It builds on two prior audits in this repo: - `docs/MEMORY-STORAGE-GROWTH-AUDIT.md` — per-fact cost, capacity math, unbounded-growth paths. Real numbers below are measured against the live checkout DB -(`.tracedecay/tokensave.db`, 78.5 MB total; **129 facts**, memory subsystem +(`.tracedecay/tracedecay.db`, 78.5 MB total; **129 facts**, memory subsystem **2.43 MiB**) on the `master` working tree at `5ad31c4`. > **Current-status update (post-Q3/Q6):** two gaps this analysis flagged have @@ -301,7 +301,7 @@ story. `src/tracedecay.rs:3386` (`memory_status`), `:3367` (`repair_derived_memory`). - Prior audits: `docs/DASHBOARD-API-AUDIT.md`, `docs/MEMORY-STORAGE-GROWTH-AUDIT.md`, `docs/HOLOGRAPHIC-DASHBOARD-SEAMS.md`. -- Live DB: `.tracedecay/tokensave.db` (129 facts; memory subsystem 2.43 MiB of +- Live DB: `.tracedecay/tracedecay.db` (129 facts; memory subsystem 2.43 MiB of 78.5 MB total; 5 orphan entities; 3 dirty banks; 129/129 facts never recalled). *Generated for Kanban task t_f47ae50b. Source audited at the `master` working diff --git a/docs/MEMORY-STORAGE-GROWTH-AUDIT.md b/docs/MEMORY-STORAGE-GROWTH-AUDIT.md index 526cbb25..e1550f7d 100644 --- a/docs/MEMORY-STORAGE-GROWTH-AUDIT.md +++ b/docs/MEMORY-STORAGE-GROWTH-AUDIT.md @@ -2,18 +2,18 @@ Scope: the holographic (FHRR) memory subsystem that tracedecay exposes to Hermes as the `tracedecay` memory provider. This is the `src/memory/` module plus its -SQLite tables in the per-project `tokensave.db` / `tracedecay.db`. The LCM +SQLite tables in the per-project `tracedecay.db` / `tracedecay.db`. The LCM session store (`sessions.db`) and the code-graph node store are out of scope but referenced where they share infrastructure. All numbers below are measured against the live checkout DB -(`.tracedecay/tokensave.db`, 129 facts) unless stated otherwise. +(`.tracedecay/tracedecay.db`, 129 facts) unless stated otherwise. --- ## 1. Where the data lives -Tables in `tokensave.db` (schema in `src/db/migrations.rs`, runtime access in +Tables in `tracedecay.db` (schema in `src/db/migrations.rs`, runtime access in `src/memory/store.rs`, `src/memory/retrieval.rs`): | Table | Role | Growth | Observed (129 facts) | @@ -28,7 +28,7 @@ Tables in `tokensave.db` (schema in `src/db/migrations.rs`, runtime access in | `memory_bank_dirty` | Dirty-bank rebuild queue | bounded | 3 rows | | `vectors` | Code-graph node embeddings (NOT memory) | per graph node | 0 rows here | -Schema anchors (`sqlite3 .tracedecay/tokensave.db ".schema "`): +Schema anchors (`sqlite3 .tracedecay/tracedecay.db ".schema "`): - `memory_facts(... hrr_vector BLOB, hrr_algebra TEXT DEFAULT 'amari_fhrr', hrr_dim INTEGER DEFAULT 2048, access_count, last_recalled_at)` - `memory_banks(bank_name UNIQUE, vector BLOB, hrr_dim, fact_count, updated_at)` - `memory_entities(entity_id PK, name, normalized_name UNIQUE, entity_type, aliases, created_at)` @@ -230,5 +230,5 @@ facts/category but that only hurts the superposition fast-path, not correctness. - Capacity formula: `src/tracedecay.rs:3472` - Trust/decay: `src/memory/trust.rs` - Hygiene gates: `src/memory/hygiene.rs` -- Schema: `src/db/migrations.rs`; live DB `.tracedecay/tokensave.db` +- Schema: `src/db/migrations.rs`; live DB `.tracedecay/tracedecay.db` - Crate: `amari-holographic = "0.23.0"` (`Cargo.toml:144`) diff --git a/docs/MEMORY-TRIAGE-DECISION.md b/docs/MEMORY-TRIAGE-DECISION.md index 8a029fe9..29673279 100644 --- a/docs/MEMORY-TRIAGE-DECISION.md +++ b/docs/MEMORY-TRIAGE-DECISION.md @@ -21,7 +21,7 @@ authorized Q1/Q2. The current checkout has since adopted Option 1: the former `retrieval.rs::temporal_decay_factor` remains live. Every load-bearing number was re-measured against the live checkout DB -(`.tracedecay/tokensave.db`) and every code anchor re-checked against `master`. +(`.tracedecay/tracedecay.db`) and every code anchor re-checked against `master`. | Claim (from audits) | Re-measured | Match | |---|---|---| diff --git a/docs/MEMORY-TRIAGE-PLAN.md b/docs/MEMORY-TRIAGE-PLAN.md index db94ccc0..edc834b5 100644 --- a/docs/MEMORY-TRIAGE-PLAN.md +++ b/docs/MEMORY-TRIAGE-PLAN.md @@ -20,7 +20,7 @@ the current checkout, only `retrieval.rs::temporal_decay_factor` remains. | Trust-decay | `docs/TRUST-DECAY-SEMANTICS.md` | persisted vs. ranking decay, dead code, explainability gaps | | Visibility | `docs/MEMORY-HEALTH-VISIBILITY-GAPS.md` | dashboard/CLI/doctor/MCP surfacing gaps (G1–G11) | -All four audited the same live checkout (`.tracedecay/tokensave.db`, 129 facts, +All four audited the same live checkout (`.tracedecay/tracedecay.db`, 129 facts, memory subsystem 2.43 MiB of a 78.5 MiB DB) and agree on the headline numbers. Load-bearing anchors were re-verified for this synthesis. The historical `src/memory/trust.rs:46` anchor named the now-removed persisted-aging routine; diff --git a/docs/MORE-LANGUAGES-SUPPORT.md b/docs/MORE-LANGUAGES-SUPPORT.md index 4a4e5822..aa3e56bd 100644 --- a/docs/MORE-LANGUAGES-SUPPORT.md +++ b/docs/MORE-LANGUAGES-SUPPORT.md @@ -14,7 +14,7 @@ Each language needs 4 things: | # | What | Where | Pattern to follow | |---|------|-------|-------------------| -| 1 | Tree-sitter grammar | `tokensave-large-treesitters` crate on crates.io | Add dep + register in `all_languages()` | +| 1 | Tree-sitter grammar | `tracedecay-large-treesitters` crate on crates.io | Add dep + register in `all_languages()` | | 2 | Extractor | `src/extraction/{lang}_extractor.rs` (~400-700 lines) | Implement `LanguageExtractor` trait | | 3 | Wiring | `Cargo.toml` + `src/extraction/mod.rs` | Feature flag, `mod` decl, registry push | | 4 | Tests | `tests/fixtures/sample.{ext}` + `tests/{lang}_extraction_test.rs` | Sample file + extraction assertions | @@ -31,7 +31,7 @@ pub trait LanguageExtractor: Send + Sync { ### Grammar sourcing -- **Crate on crates.io:** Add as a dependency to `tokensave-large-treesitters` and register in `all_languages()`. This is the standard path. +- **Crate on crates.io:** Add as a dependency to `tracedecay-large-treesitters` and register in `all_languages()`. This is the standard path. - **Vendor from C source:** If no Rust crate exists, compile the grammar's C source via `build.rs` (same pattern as `protobuf` and `cobol` in the bundled crate). - **No grammar at all:** Either write a regex-based extractor (skip tree-sitter) or wait for a community grammar. diff --git a/docs/MULTI-BRANCH-INVARIANTS.md b/docs/MULTI-BRANCH-INVARIANTS.md index 41bfade8..543f3bdb 100644 --- a/docs/MULTI-BRANCH-INVARIANTS.md +++ b/docs/MULTI-BRANCH-INVARIANTS.md @@ -18,8 +18,8 @@ All `file:line` references are against the current tree. | Artifact | Path | Owner | Notes | |---|---|---|---| -| Project data dir | `/.tracedecay/` (legacy `/.tokensave/`) | `config::get_tracedecay_dir` (`config.rs:87`) | Prefer `.tracedecay`; fall back to an *existing* legacy dir read+write. Brand-aware everywhere via `db_filename` (`config.rs:115`). | -| Default-branch DB | `/tracedecay.db` (legacy `tokensave.db`) | `init`/`resolve_db_for_branch` | `DB_FILENAME` for new dirs; `LEGACY_DB_FILENAME` inside legacy dirs. | +| Project data dir | `/.tracedecay/` (legacy `/.tracedecay/`) | `config::get_tracedecay_dir` (`config.rs:87`) | Prefer `.tracedecay`; fall back to an *existing* legacy dir read+write. Brand-aware everywhere via `db_filename` (`config.rs:115`). | +| Default-branch DB | `/tracedecay.db` (legacy `tracedecay.db`) | `init`/`resolve_db_for_branch` | `DB_FILENAME` for new dirs; `LEGACY_DB_FILENAME` inside legacy dirs. | | Branch DBs | `/branches/.db` (+ `.db-wal`/`.db-shm`) | `branch_meta::ensure_branches_dir` (`branch_meta.rs:148`), created in `add_branch_tracking`/CLI `branch add` | One SQLite file per non-default tracked branch. | | Branch metadata | `/branch-meta.json` | `branch_meta::{load,save}_branch_meta` (`branch_meta.rs:125,141`) | Source of truth for `default_branch`, tracked branches, `db_file`, `parent`, timestamps. | | In-memory state | `TraceDecay { active_branch, serving_branch, fallback_warning }` | `tracedecay.rs` (struct ~line 298-306) | Resolved at `open()` time; see §3. | diff --git a/docs/MULTI-BRANCH-RECOVERY.md b/docs/MULTI-BRANCH-RECOVERY.md index e0fa9845..ef283e62 100644 --- a/docs/MULTI-BRANCH-RECOVERY.md +++ b/docs/MULTI-BRANCH-RECOVERY.md @@ -26,7 +26,7 @@ after merges. | Artifact | Path | Notes | |---|---|---| -| Data dir | `/.tracedecay/` (legacy `/.tokensave/`) | Brand-aware; legacy dir honored as fallback if it already exists. | +| Data dir | `/.tracedecay/` (legacy `/.tracedecay/`) | Brand-aware; legacy dir honored as fallback if it already exists. | | Default-branch DB | `/tracedecay.db` (+ `.db-wal`/`.db-shm`) | The DB for `main`/`master`. | | Branch DBs | `/branches/.db` (+ sidecars) | One per non-default tracked branch. | | Branch metadata | `/branch-meta.json` | Source of truth: `default_branch`, tracked branches, `db_file`, `parent`, timestamps. | @@ -49,7 +49,7 @@ tracedecay branch list > drift-before.txt 2>&1 tracedecay status --short >> drift-before.txt 2>&1 # 2. Snapshot metadata and every branch DB (including WAL/SHM sidecars). -DATA_DIR="$(git rev-parse --show-toplevel)/.tracedecay" # or .tokensave on legacy projects +DATA_DIR="$(git rev-parse --show-toplevel)/.tracedecay" # or .tracedecay on legacy projects BACKUP="$DATA_DIR/../.tracedecay-recovery-$(date +%Y%m%d-%H%M%S)" mkdir -p "$BACKUP" cp -a "$DATA_DIR/branch-meta.json" "$BACKUP/" 2>/dev/null diff --git a/docs/PLUGINS-DESIGN.md b/docs/PLUGINS-DESIGN.md index c9aa762e..d8d67c87 100644 --- a/docs/PLUGINS-DESIGN.md +++ b/docs/PLUGINS-DESIGN.md @@ -6,9 +6,9 @@ Replace compile-time, feature-gated language extractors with dynamically loaded ## Problem with the current model -Every language extractor is gated on a Cargo feature (`lang-lua`, `lang-zig`, …) and compiled into the binary at build time via `tokensave-large-treesitters`. Adding a language today means: +Every language extractor is gated on a Cargo feature (`lang-lua`, `lang-zig`, …) and compiled into the binary at build time via `tracedecay-large-treesitters`. Adding a language today means: -1. Adding a grammar crate dependency to `tokensave-large-treesitters` and cutting a release. +1. Adding a grammar crate dependency to `tracedecay-large-treesitters` and cutting a release. 2. Writing an extractor in `src/extraction/`, adding `#[cfg(feature = "…")]` gates in `mod.rs`, and updating `Cargo.toml`. 3. Releasing a new version of `tracedecay` itself. @@ -139,8 +139,8 @@ impl LanguageExtractor for ElixirExtractor { … } tracedecay searches the following directories in order, stopping at the first match for a given extension: -1. `$TRACEDECAY_PLUGIN_PATH` (colon-separated, same convention as `PATH`; the legacy `TOKENSAVE_PLUGIN_PATH` is still honored as a fallback) -2. `.tracedecay/plugins/` in the current project root (an existing `.tokensave/plugins/` is still honored as a fallback) +1. `$TRACEDECAY_PLUGIN_PATH` (colon-separated, same convention as `PATH`; the legacy `TRACEDECAY_PLUGIN_PATH` is still honored as a fallback) +2. `.tracedecay/plugins/` in the current project root (an existing `.tracedecay/plugins/` is still honored as a fallback) 3. `~/.tracedecay/plugins/` 4. Platform config dir (`%APPDATA%\tracedecay\plugins` on Windows, `~/Library/Application Support/tracedecay/plugins` on macOS) diff --git a/docs/PROFILE-STORAGE-SUPPORT.md b/docs/PROFILE-STORAGE-SUPPORT.md index 55b0cf9c..618dea84 100644 --- a/docs/PROFILE-STORAGE-SUPPORT.md +++ b/docs/PROFILE-STORAGE-SUPPORT.md @@ -6,13 +6,14 @@ This document captures support, privacy, and test-fixture requirements for profi User-facing docs and generated guidance should describe the **resolved active project store** instead of assuming every project writes graph data to `/.tracedecay/tracedecay.db`. -Repo-local behavior remains the default: +User-level project storage is the default: -- New projects create `/.tracedecay/` with `tracedecay.db`, `sessions.db`, branch metadata, response handles, and dashboard sidecars. -- Legacy projects with `.tokensave/` continue to use that directory in place when `.tracedecay/` is absent. +- New projects resolve to profile-sharded stores such as `~/.tracedecay/projects//`. +- Repo-local `.tracedecay/` is used only for explicit local installs or legacy projects. +- Legacy projects with `.tracedecay/` continue to use that directory in place when `.tracedecay/` is absent. - `~/.tracedecay/global.db` remains user-level accounting/registry state, not the canonical graph DB. -Profile storage adds profile-sharded code-project stores such as `~/.tracedecay/projects//`. A repository may then have only an enrollment marker while graph DBs, sessions, payloads, response handles, branch DBs, and dashboard sidecars live in the profile shard. Hermes profile stores remain separate from code-project shards. +Profile-sharded stores contain graph DBs, sessions, payloads, response handles, branch DBs, and dashboard sidecars. Hermes agents use the same user-level project store as other adapters; unpinned Hermes profiles use the profile home as their project identity. ## Planned Support Bundle Privacy @@ -41,9 +42,9 @@ Migration and storage-status tests should share fixture builders instead of reim Reusable fixtures should cover: - Repo-local `.tracedecay/` stores with graph DB, sessions DB, branch metadata, response handles, and dashboard sidecars. -- Legacy `.tokensave/` stores that remain active in place. +- Legacy `.tracedecay/` stores that remain active in place. - Profile-sharded code-project stores with a repo enrollment marker and private profile shard. -- Hermes profile stores under `/.tracedecay/` that are not code-project shards. +- Hermes profile-home project identities resolved through the user-level store. - Stale or unregistered registry rows, moved repos, worktrees, symlinked roots, dirty sentinels, sync locks, and `.branch-add.lock`. - Seeded `lcm-payloads/`, response handles, curation previews, WAL/SHM sidecars, and `TRACEDECAY_GLOBAL_DB` overrides. diff --git a/docs/REBRAND-COMPATIBILITY-DECISION.md b/docs/REBRAND-COMPATIBILITY-DECISION.md index ca6cab85..6f72a05e 100644 --- a/docs/REBRAND-COMPATIBILITY-DECISION.md +++ b/docs/REBRAND-COMPATIBILITY-DECISION.md @@ -13,7 +13,7 @@ points at them and records what the root reviewer verified and decided. | Artifact | Role | Child task | |---|---|---| -| `docs/TOKENSAVE-COMPATIBILITY-AUDIT.md` | Fact inventory of every legacy surface, with file:line anchors and risk notes. | `t_3482e439` | +| `docs/TRACEDECAY-COMPATIBILITY-AUDIT.md` | Fact inventory of every legacy surface, with file:line anchors and risk notes. | `t_3482e439` | | `docs/TREESITTERS-RENAME-CONSTRAINTS.md` | Deep dive on the one externally constrained name. | `t_4070b0b0` | | `docs/REBRAND-COMPATIBILITY-POLICY.md` | Normative policy: categories A–E, principles, a 37-row surface map, and a review checklist. | `t_da7151b6` | | `docs/REBRAND-COMPATIBILITY-FOLLOW-UP-CHECKLIST.md` | 15 ready-to-ticket follow-ups + 5 explicit non-goals, with a suggested order. | `t_a33b4ad3` | @@ -27,11 +27,11 @@ categories gives the one-line answer: | Triage question | Policy category | Surfaces | Summary | |---|---|---:|---| -| Remains indefinitely | A — retained in place | 11 | Existing `.tokensave/` + `~/.tokensave/` data, `tokensave.db`, branch metadata, discovery, counter endpoint, and all historical docs/changelog/benchmarks. No auto-migration, ever. | +| Remains indefinitely | A — retained in place | 11 | Existing `.tracedecay/` + `~/.tracedecay/` data, `tracedecay.db`, branch metadata, discovery, counter endpoint, and all historical docs/changelog/benchmarks. No auto-migration, ever. | | Migrates automatically | B — installers/refreshers reconcile | 16 | Generated agent/plugin config keys, prompt/rule markers, plugin dirs, marketplace entries, Hermes config aliases, release-asset probing for explicit old versions, primary-doc wording. | -| Warns (accepted as fallback) | C — accepted, new spelling wins | 8 | `TOKENSAVE_*` env fallbacks, `DISABLE_TOKENSAVE`, global-DB toggles, hook/extraction/pricing vars, plugin-path docs claim, Homebrew/Scoop package note. | +| Warns (accepted as fallback) | C — accepted, new spelling wins | 8 | `TRACEDECAY_*` env fallbacks, `DISABLE_TRACEDECAY`, global-DB toggles, hook/extraction/pricing vars, plugin-path docs claim, Homebrew/Scoop package note. | | Never silently accepted | D — reject/fail | 1 | Legacy-only / pre-reset releases in automatic latest-version detection. | -| Never renamed yet (upstream constraint) | E — externally owned | 1 | `tokensave-large-treesitters` (+ `-medium`/`-lite`). Keep until upstream renames all three or a maintained fork is approved as a policy change. | +| Never renamed yet (upstream constraint) | E — externally owned | 1 | `tracedecay-large-treesitters` (+ `-medium`/`-lite`). Keep until upstream renames all three or a maintained fork is approved as a policy change. | Categories A and E are the load-bearing "do not touch" answer; B and C are the "acceptable legacy debt, managed deliberately" answer; D is the supply-chain @@ -41,18 +41,18 @@ guardrail. No surface is left uncategorized. Spot-checked the load-bearing claims against the working tree — all accurate: -- `tokensave-large-treesitters` is declared as an upstream git dep at - `Cargo.toml:108` (`aovestdipaperino/tokensave-large-treesitters`, `0.5.0`), and +- `tracedecay-large-treesitters` is declared as an upstream git dep at + `Cargo.toml:108` (`aovestdipaperino/tracedecay-large-treesitters`, `0.5.0`), and the three import sites match the constraints doc exactly: - `tokensave_large_treesitters::all_languages()` at `src/extraction/ts_provider.rs:27`, + `tracedecay_large_treesitters::all_languages()` at `src/extraction/ts_provider.rs:27`, and `markdown::inline::LANGUAGE` / `markdown::LANGUAGE` at `src/extraction/markdown_extractor.rs:70,139`. - The no-fork / upstream policy the constraints doc relies on is present at `AGENTS.md:20`. - The in-place legacy storage contract is explicit in code: - `src/config.rs:16-24` defines `LEGACY_TOKENSAVE_DIR = ".tokensave"` and - `LEGACY_DB_FILENAME = "tokensave.db"` with a "no auto-migration" comment. -- `DISABLE_TOKENSAVE=true` is honored as an exact-string `true` serve opt-out + `src/config.rs:16-24` defines `LEGACY_TRACEDECAY_DIR = ".tracedecay"` and + `LEGACY_DB_FILENAME = "tracedecay.db"` with a "no auto-migration" comment. +- `DISABLE_TRACEDECAY=true` is honored as an exact-string `true` serve opt-out alongside `DISABLE_TRACEDECAY=true` (`src/main.rs:1020-1024`). *(Minor line drift: the audit cited `main.rs:1150-1157`; in this checkout it is at `1020-1024`. Behavior is exactly as documented — non-material.)* @@ -60,7 +60,7 @@ Spot-checked the load-bearing claims against the working tree — all accurate: One child-flagged open question resolved by the root review (closes M-04/T-03 as a docs-only fix): -- `$TRACEDECAY_PLUGIN_PATH` / `$TOKENSAVE_PLUGIN_PATH` and `.tokensave/plugins/` +- `$TRACEDECAY_PLUGIN_PATH` / `$TRACEDECAY_PLUGIN_PATH` and `.tracedecay/plugins/` discovery is documented at `docs/PLUGINS-DESIGN.md:138-156`, but **no source in `src/` reads these variables**. The whole "Plugin discovery" section there describes an aspirational dynamic-`.so`-plugin loader (env path, project/user @@ -78,7 +78,7 @@ a docs-only fix): Only one item in the four artifacts is a genuine fork that cannot be resolved by a worker and is not pure implementation work: -- **W-04 — `DISABLE_TOKENSAVE` boolean semantics.** Today `DISABLE_TOKENSAVE` +- **W-04 — `DISABLE_TRACEDECAY` boolean semantics.** Today `DISABLE_TRACEDECAY` matches the exact string `true` (`src/main.rs:1020-1021`), while the generic `brand_env()` boolean consumers parse truthy values (`1/true/yes/on`). The policy correctly refuses to pick a side without input. @@ -99,6 +99,6 @@ explicit non-goals (N-01..N-05). Suggested implementation order is in that file (§"Suggested implementation order"); the highest-leverage first slice is W-01 + W-02 + T-01 (shared warning infrastructure that most other Category C behavior depends on). N-03 (never rename/fork the treesitters) and N-01 (no -auto-rename of `.tokensave/` storage) are the two non-goals most likely to be +auto-rename of `.tracedecay/` storage) are the two non-goals most likely to be accidentally violated by a future refactor and should be re-cited in any PR touching `src/config.rs`, `src/global.rs`, `src/branch_meta.rs`, or `Cargo.toml`. diff --git a/docs/REBRAND-COMPATIBILITY-FOLLOW-UP-CHECKLIST.md b/docs/REBRAND-COMPATIBILITY-FOLLOW-UP-CHECKLIST.md index dc83e538..d105b8d9 100644 --- a/docs/REBRAND-COMPATIBILITY-FOLLOW-UP-CHECKLIST.md +++ b/docs/REBRAND-COMPATIBILITY-FOLLOW-UP-CHECKLIST.md @@ -1,7 +1,7 @@ # TraceDecay rebrand compatibility follow-up checklist Date: 2026-06-14 -Sources: `docs/REBRAND-COMPATIBILITY-POLICY.md`, `docs/TOKENSAVE-COMPATIBILITY-AUDIT.md`, `docs/TREESITTERS-RENAME-CONSTRAINTS.md` +Sources: `docs/REBRAND-COMPATIBILITY-POLICY.md`, `docs/TRACEDECAY-COMPATIBILITY-AUDIT.md`, `docs/TREESITTERS-RENAME-CONSTRAINTS.md` Goal: turn the approved compatibility policy into small, implementation-ready follow-up tasks without making code changes in this document. @@ -21,13 +21,13 @@ Goal: turn the approved compatibility policy into small, implementation-ready fo - `src/global_db.rs` - `src/dashboard/savings_pricing.rs` - any shared logging/warning utility chosen by the implementer -- Why: the policy requires concise warnings when legacy `TOKENSAVE_*` spellings are honored, but current fallback helpers (`brand_env`, `env_with_legacy`) silently accept old names. +- Why: the policy requires concise warnings when legacy `TRACEDECAY_*` spellings are honored, but current fallback helpers (`brand_env`, `env_with_legacy`) silently accept old names. - Done when: - there is one reusable helper for "old name honored" and "both old and new set; new wins" - warnings never print secret values - warnings are emitted at most once per key per process or invocation -### W-02: Wire warnings into the generic `TRACEDECAY_*` / `TOKENSAVE_*` fallback path +### W-02: Wire warnings into the generic `TRACEDECAY_*` / `TRACEDECAY_*` fallback path - Area: warning messages - Touchpoints: - `src/config.rs` (`brand_env`) @@ -44,17 +44,17 @@ Goal: turn the approved compatibility policy into small, implementation-ready fo - `src/dashboard/savings_pricing.rs` - Why: pricing uses its own `env_with_legacy` helper instead of `brand_env`, so it will miss any shared warning work unless updated separately. - Done when: - - `TOKENSAVE_OFFLINE` and `TOKENSAVE_MODEL_PRICES_PATH` behave like other Category C fallbacks + - `TRACEDECAY_OFFLINE` and `TRACEDECAY_MODEL_PRICES_PATH` behave like other Category C fallbacks - logs mention keys only, not paths or values beyond what policy permits -### W-04: Decide and codify `DISABLE_TOKENSAVE` boolean semantics +### W-04: Decide and codify `DISABLE_TRACEDECAY` boolean semantics - Area: warning messages / env semantics - Touchpoints: - `src/main.rs` - `README.md` - `docs/dashboard.md` - new tests near current serve opt-out coverage -- Why: `DISABLE_TOKENSAVE` is currently exact-string `true`, while most other boolean fallbacks use truthy parsing. The policy says not to change semantics casually. +- Why: `DISABLE_TRACEDECAY` is currently exact-string `true`, while most other boolean fallbacks use truthy parsing. The policy says not to change semantics casually. - Done when: - the project explicitly chooses either "keep exact-string behavior" or "normalize to shared truthy parsing" - docs and tests match that choice @@ -66,9 +66,9 @@ Goal: turn the approved compatibility policy into small, implementation-ready fo - Area: env/config alias handling - Touchpoints: - `src/agents/hermes/profile_config.rs` - - `src/agents/hermes/tokensave_migration.rs` + - `src/agents/hermes/tracedecay_migration.rs` - `src/agents/hermes/lifecycle.rs` -- Why: the policy requires `plugins.tokensave.project_root` to migrate cleanly to `plugins.tracedecay.project_root` when no current key exists. +- Why: the policy requires `plugins.tracedecay.project_root` to migrate cleanly to `plugins.tracedecay.project_root` when no current key exists. - Done when: - reinstall/refresh preserves a legacy pin when appropriate - a current `plugins.tracedecay.project_root` still wins over the old key @@ -78,7 +78,7 @@ Goal: turn the approved compatibility policy into small, implementation-ready fo - Area: env/config alias handling - Touchpoints: - `src/agents/hermes/profile_config.rs` -- Why: `plugins: ["tokensave"]`, `provider: tokensave`, and `engine: tokensave` are all Category B aliases that should be rewritten to canonical `tracedecay` behavior. +- Why: `plugins: ["tracedecay"]`, `provider: tracedecay`, and `engine: tracedecay` are all Category B aliases that should be rewritten to canonical `tracedecay` behavior. - Done when: - enable/disable flows handle both old and new spellings predictably - conflicting non-tracedecay providers/engines still fail closed instead of being overwritten silently @@ -94,9 +94,9 @@ Goal: turn the approved compatibility policy into small, implementation-ready fo - `src/agents/{cline,gemini,kilo,kimi,kiro,opencode,roo_code,vibe,zed}.rs` - `tests/agent_test.rs` - `tests/claude_agent_test.rs` -- Why: the audit found strong legacy-specific tests for Cursor and Hermes, but weaker or unclear coverage for other integrations that also claim to remove `tokensave` artifacts. +- Why: the audit found strong legacy-specific tests for Cursor and Hermes, but weaker or unclear coverage for other integrations that also claim to remove `tracedecay` artifacts. - Done when: - - each owned integration has at least one focused test proving legacy `tokensave` entries are reconciled or removed + - each owned integration has at least one focused test proving legacy `tracedecay` entries are reconciled or removed - tests distinguish generated artifacts from user-authored files - uninstall and reinstall both stay idempotent @@ -105,7 +105,7 @@ Goal: turn the approved compatibility policy into small, implementation-ready fo - Touchpoints: - `docs/PLUGINS-DESIGN.md` - plugin discovery implementation, if it exists -- Why: docs currently claim `$TOKENSAVE_PLUGIN_PATH` and `.tokensave/plugins/` fallbacks, but the audit did not find implementation. +- Why: docs currently claim `$TRACEDECAY_PLUGIN_PATH` and `.tracedecay/plugins/` fallbacks, but the audit did not find implementation. - Done when: - either implementation is located and covered by tests, or - the docs are downgraded to a compatibility target / follow-up instead of a guaranteed runtime behavior @@ -124,7 +124,7 @@ Goal: turn the approved compatibility policy into small, implementation-ready fo - Why: active docs should use TraceDecay as canonical naming while keeping short compatibility notes where runtime fallback still exists. - Done when: - examples default to `tracedecay`, `.tracedecay`, and `TRACEDECAY_*` - - any remaining `tokensave` mentions are clearly historical, compatibility-related, or external + - any remaining `tracedecay` mentions are clearly historical, compatibility-related, or external - daemon/service cleanup notes for old installs remain intact where still useful ### D-02: Add explicit compatibility-note wording for supported legacy env/path fallbacks @@ -144,7 +144,7 @@ Goal: turn the approved compatibility policy into small, implementation-ready fo - Touchpoints: - `AGENTS.md` - contributor-facing docs or PR templates, if the repo keeps one -- Why: the policy includes a review checklist, but future PRs touching `tokensave` surfaces will drift unless reviewers can find the policy quickly. +- Why: the policy includes a review checklist, but future PRs touching `tracedecay` surfaces will drift unless reviewers can find the policy quickly. - Done when: - contributor guidance points rebrand-related changes to `docs/REBRAND-COMPATIBILITY-POLICY.md` - reviewers have a short reminder to classify each touched surface into policy category A-E @@ -167,7 +167,7 @@ Goal: turn the approved compatibility policy into small, implementation-ready fo - `tests/claude_agent_test.rs` - Why: the audit explicitly called out missing or unclear legacy-specific tests for Codex, Antigravity, Claude, Copilot, Kimi/Kilo/Roo/Cline/Gemini/Zed/OpenCode/Vibe. - Done when: - - there is at least one regression test per integration family with legacy `tokensave` config/artifacts + - there is at least one regression test per integration family with legacy `tracedecay` config/artifacts - owned files are removed/reconciled, unknown user files preserved ### T-03: Add a regression test for plugin-path fallback resolution or remove the docs claim @@ -188,7 +188,7 @@ Goal: turn the approved compatibility policy into small, implementation-ready fo - any maintenance-command tests covering discovery/wipe previews - Why: the policy treats silent storage renames as forbidden. Existing tests cover parts of this, but future follow-up work should keep the no-migration contract visible. - Done when: - - legacy `.tokensave/` and `~/.tokensave/` usage remains in-place when it is the active data root + - legacy `.tracedecay/` and `~/.tracedecay/` usage remains in-place when it is the active data root - `.tracedecay/` still wins when both exist - maintenance/discovery flows continue to surface legacy project roots @@ -196,7 +196,7 @@ Goal: turn the approved compatibility policy into small, implementation-ready fo These are intentional non-goals unless the policy itself changes. -### N-01: Do not auto-rename `.tokensave/`, `~/.tokensave/`, or `tokensave.db` +### N-01: Do not auto-rename `.tracedecay/`, `~/.tracedecay/`, or `tracedecay.db` - Keep existing legacy project/user data roots active in place. - Any future migration must be explicit, backup-first, atomic, and reversible. - Primary touchpoints to leave behaviorally unchanged: @@ -207,25 +207,25 @@ These are intentional non-goals unless the policy itself changes. - `src/diagnostics/rust.rs` ### N-02: Do not remove legacy maintenance discovery -- Keep recognizing `.tokensave/tokensave.db` in list/status/wipe-style maintenance flows. +- Keep recognizing `.tracedecay/tracedecay.db` in list/status/wipe-style maintenance flows. - This is compatibility, not historical fluff. -### N-03: Do not rename or fork `tokensave-large-treesitters` +### N-03: Do not rename or fork `tracedecay-large-treesitters` - Keep the exact upstream dependency names: - - `tokensave-large-treesitters` - - `tokensave-medium-treesitters` - - `tokensave-lite-treesitters` + - `tracedecay-large-treesitters` + - `tracedecay-medium-treesitters` + - `tracedecay-lite-treesitters` - Constraints are documented in `docs/TREESITTERS-RENAME-CONSTRAINTS.md`. - Only revisit if upstream renames all three crates or the project explicitly approves a maintained fork. ### N-04: Do not rename the worldwide counter endpoint yet -- Keep `tokensave-counter` until a replacement endpoint is deployed with preserved continuity/behavior. +- Keep `tracedecay-counter` until a replacement endpoint is deployed with preserved continuity/behavior. - Touchpoints: - `src/cloud.rs` - `SECURITY.md` ### N-05: Do not rewrite historical artifacts just to remove old names -- Leave changelog history, old plans/specs, benchmark outputs, and `docs/TOKEN-SAVE-WHATSNEW.md` intact except for factual corrections. +- Leave changelog history, old plans/specs, benchmark outputs, and `docs/TRACEDECAY-WHATSNEW.md` intact except for factual corrections. - This is archival history, not active product naming. ## Suggested implementation order @@ -241,5 +241,5 @@ These are intentional non-goals unless the policy itself changes. ## Notes for future task authors - Use the policy categories in `docs/REBRAND-COMPATIBILITY-POLICY.md` when scoping each implementation card. -- Keep ordinary schema/config compatibility separate from TokenSave-brand compatibility. +- Keep ordinary schema/config compatibility separate from TraceDecay-brand compatibility. - Treat any task that touches legacy storage paths or deletes old files as higher risk than wording-only doc edits. diff --git a/docs/REBRAND-COMPATIBILITY-POLICY.md b/docs/REBRAND-COMPATIBILITY-POLICY.md index 1dba9658..203646bb 100644 --- a/docs/REBRAND-COMPATIBILITY-POLICY.md +++ b/docs/REBRAND-COMPATIBILITY-POLICY.md @@ -1,10 +1,10 @@ # TraceDecay rebrand compatibility policy Date: 2026-06-14 -Source audits: `docs/TOKENSAVE-COMPATIBILITY-AUDIT.md`, `docs/TREESITTERS-RENAME-CONSTRAINTS.md` +Source audits: `docs/TRACEDECAY-COMPATIBILITY-AUDIT.md`, `docs/TREESITTERS-RENAME-CONSTRAINTS.md` This policy defines how the project treats names and artifacts from the pre-rebrand -TokenSave era. It is normative for new code, docs, tests, release automation, and +TraceDecay era. It is normative for new code, docs, tests, release automation, and agent/plugin integrations. ## Policy principles @@ -20,11 +20,11 @@ agent/plugin integrations. legacy spelling must say whether it is retained indefinitely, auto-migrated, warning-producing, rejected, or externally constrained. Tests should cover the chosen category for runtime behavior. -4. **No silent destructive migration.** A fallback from `.tokensave/` to +4. **No silent destructive migration.** A fallback from `.tracedecay/` to `.tracedecay/` is not a license to rename, delete, or rewrite user data. Any future conversion command must be explicit, backed up, atomic, and reversible. 5. **Historical documents are not product surface.** Changelog entries, old plans, - benchmark outputs, and TokenSave-era narrative may keep old names when the name + benchmark outputs, and TraceDecay-era narrative may keep old names when the name is part of the historical record. ## Category A — retained indefinitely without migration @@ -33,43 +33,44 @@ These surfaces remain valid indefinitely. New code must continue to understand t because removing them would hide existing data, break old installs, or erase useful history. -### A1. Existing `.tokensave/` project data directories +### A1. Existing `.tracedecay/` project data directories -If a project root has `.tokensave/` and no `.tracedecay/`, TraceDecay must continue -using `.tokensave/` as the active data directory **in place**. It must keep reading -and writing `tokensave.db`, branch metadata, curation sidecars, diagnostics target +If a project root has `.tracedecay/` and no `.tracedecay/`, TraceDecay must continue +using `.tracedecay/` as the active data directory **in place**. It must keep reading +and writing `tracedecay.db`, branch metadata, curation sidecars, diagnostics target artifacts, session sidecars, and other data rooted under that directory. It must not auto-rename the directory or create a parallel `.tracedecay/` index that makes the old index look lost. -If both `.tracedecay/` and `.tokensave/` exist in the same project, `.tracedecay/` +If both `.tracedecay/` and `.tracedecay/` exist in the same project, `.tracedecay/` is canonical and wins. The legacy directory remains user-owned data and must not be deleted unless an explicit destructive command asks for it. -### A2. Existing `~/.tokensave/` user data directories +### A2. Existing `~/.tracedecay/` user data directories -If the user has `~/.tokensave/` and no `~/.tracedecay/`, TraceDecay must keep using -`~/.tokensave/` for user-scoped state such as `global.db`, monitor mmap/lock files, +If the user has `~/.tracedecay/` and no `~/.tracedecay/`, TraceDecay must keep using +`~/.tracedecay/` for user-scoped state such as `global.db`, monitor mmap/lock files, and compatible caches. New users should default to `~/.tracedecay/`. If both exist, `~/.tracedecay/` wins. ### A3. Legacy project discovery for maintenance commands Commands that discover local project roots for listing, status, global accounting, -wipe/cleanup previews, or similar maintenance must continue recognizing both -`.tracedecay/tracedecay.db` and `.tokensave/tokensave.db`. Destructive commands must -show the resolved legacy path clearly before deleting anything. +wipe/cleanup previews, or similar maintenance must recognize the canonical +`.tracedecay/tracedecay.db` layout and any explicitly enumerated pre-rename stores +during migration. Destructive commands must show the resolved legacy path clearly +before deleting anything. ### A4. Historical docs, changelog entries, benchmark outputs, and old daemon cleanup docs -Historical TokenSave wording in `CHANGELOG.md`, `docs/TOKEN-SAVE-WHATSNEW.md`, old -plans/specs, benchmark reports, and daemon-removal instructions should remain unless -it is factually wrong. These references should be labeled as historical when helpful, -not mechanically rewritten. +Historical wording in `CHANGELOG.md`, `docs/TRACEDECAY-WHATSNEW.md`, old plans/specs, +benchmark reports, and daemon-removal instructions should remain unless it is +factually wrong. These references should be labeled as historical when helpful, not +mechanically rewritten. ### A5. Worldwide counter endpoint -The existing counter endpoint name may remain `tokensave-counter` while the service +The existing counter endpoint name may remain `tracedecay-counter` while the service continues to be best-effort and documented. A future endpoint rename is allowed only when the replacement worker is deployed and continuity/failure behavior is preserved. @@ -82,16 +83,16 @@ state has a single canonical TraceDecay entry. ### B1. Agent MCP server keys and generated prompt/rule markers Agent integrations must install new entries under `tracedecay`. During install, -refresh, or uninstall they should detect generated `tokensave` server entries, hook +refresh, or uninstall they should detect generated `tracedecay` server entries, hook commands, permissions, prompt headers, rule files, and managed plugin metadata and reconcile them to the canonical `tracedecay` form or remove them when uninstalling. Ownership checks are required. Generated legacy artifacts may be removed; unrelated user-authored files under old directories must be preserved. -Examples: Cursor `rules/tokensave.mdc`, old `## Prefer tokensave MCP tools` prompt -blocks, Codex marketplace entries named `tokensave`, Antigravity -`plugins/tokensave.json`, Claude hook/config legacy shapes, and generated entries in +Examples: Cursor `rules/tracedecay.mdc`, old `## Prefer tracedecay MCP tools` prompt +blocks, Codex marketplace entries named `tracedecay`, Antigravity +`plugins/tracedecay.json`, Claude hook/config legacy shapes, and generated entries in Zed/Cline/Kimi/OpenCode/Gemini/Kiro/Roo/Kilo/Copilot agent configs. ### B2. Hermes plugin/profile configuration aliases @@ -99,24 +100,24 @@ Zed/Cline/Kimi/OpenCode/Gemini/Kiro/Roo/Kilo/Copilot agent configs. Hermes config migration must treat these legacy values as old generated state and move them to canonical TraceDecay configuration: -- `plugins.tokensave.project_root` -> `plugins.tracedecay.project_root` when no +- `plugins.tracedecay.project_root` -> `plugins.tracedecay.project_root` when no current key exists. -- `plugins: ["tokensave"]` -> `plugins: ["tracedecay"]` when enabling/refreshing. -- `provider: tokensave` and context-engine `engine: tokensave` -> the corresponding +- `plugins: ["tracedecay"]` -> `plugins: ["tracedecay"]` when enabling/refreshing. +- `provider: tracedecay` and context-engine `engine: tracedecay` -> the corresponding `tracedecay` provider/engine. -- Legacy `plugins/tokensave` generated plugin directories and - `skills/tokensave/SKILL.md` -> removed or replaced by generated `tracedecay` +- Legacy `plugins/tracedecay` generated plugin directories and + `skills/tracedecay/SKILL.md` -> removed or replaced by generated `tracedecay` artifacts, preserving unknown user files. ### B3. Legacy release archive extraction for explicit versions When a user explicitly requests an old version, upgrade/install code must probe -current `tracedecay-*` assets first and then legacy `tokensave-*` / `tokensave-beta-*` -assets. Archive extraction must prefer `tracedecay` / `tracedecay.exe` and then accept -legacy `tokensave` / `tokensave.exe` binaries for those old archives. +current `tracedecay-*` assets first and then pre-rename release assets. Archive +extraction must prefer `tracedecay` / `tracedecay.exe` and then accept pre-rename +binaries for those old archives. This is compatibility for explicit old-version requests only; it is not permission -to present old TokenSave-only releases as the latest version. +to present old TraceDecay-only releases as the latest version. ### B4. Documentation wording for active compatibility behavior @@ -135,7 +136,7 @@ non-fatal. Recommended warning format: ```text -warning: legacy TokenSave setting is deprecated; use instead. TraceDecay honored for this run. +warning: legacy TraceDecay setting is deprecated; use instead. TraceDecay honored for this run. ``` When both old and new settings are present, the new setting wins. The preferred @@ -150,36 +151,36 @@ locations only. ### C1. Legacy environment variable fallbacks -The following `TOKENSAVE_*` variables remain accepted as fallbacks for their +The following `TRACEDECAY_*` variables remain accepted as fallbacks for their `TRACEDECAY_*` equivalents, with `TRACEDECAY_*` winning when both are set: -- Generic `TOKENSAVE_` values read through the brand env helper. -- `TOKENSAVE_GLOBAL_DB`. -- `TOKENSAVE_ENABLE_GLOBAL_DB` / `TOKENSAVE_DISABLE_GLOBAL_DB`. -- `TOKENSAVE_RESEARCH_BLOCK_REASON`. -- `TOKENSAVE_PLUGIN_SUBAGENTS`. -- `TOKENSAVE_PROJECT_ROOT`. -- `TOKENSAVE_DISABLE_SUBPROCESS`. -- `TOKENSAVE_OFFLINE`. -- `TOKENSAVE_MODEL_PRICES_PATH`. -- `TOKENSAVE_PLUGIN_PATH`, but only where an implementation actually honors it; docs +- Generic `TRACEDECAY_` values read through the brand env helper. +- `TRACEDECAY_GLOBAL_DB`. +- `TRACEDECAY_ENABLE_GLOBAL_DB` / `TRACEDECAY_DISABLE_GLOBAL_DB`. +- `TRACEDECAY_RESEARCH_BLOCK_REASON`. +- `TRACEDECAY_PLUGIN_SUBAGENTS`. +- `TRACEDECAY_PROJECT_ROOT`. +- `TRACEDECAY_DISABLE_SUBPROCESS`. +- `TRACEDECAY_OFFLINE`. +- `TRACEDECAY_MODEL_PRICES_PATH`. +- `TRACEDECAY_PLUGIN_PATH`, but only where an implementation actually honors it; docs must not promise this fallback beyond verified code paths. Boolean parsing should be made consistent before changing semantics. Today, -`DISABLE_TOKENSAVE` is exact-string `true`, while many `brand_env()` consumers use +`DISABLE_TRACEDECAY` is exact-string `true`, while many `brand_env()` consumers use truthy parsing. Treat this as a compatibility constraint until tests document any intentional divergence. ### C2. Legacy disable variable for server opt-out -`DISABLE_TOKENSAVE=true` remains accepted as the legacy spelling for +`DISABLE_TRACEDECAY=true` remains accepted as the legacy spelling for `DISABLE_TRACEDECAY=true` and must continue to make `tracedecay serve` exit cleanly so hosts do not retry. New docs and examples should use `DISABLE_TRACEDECAY=true`. If a warning is emitted, it must not prevent the clean zero-effect exit. ### C3. Legacy docs-only plugin path claims -Docs that mention `$TOKENSAVE_PLUGIN_PATH` or `.tokensave/plugins/` fallback must be +Docs that mention `$TRACEDECAY_PLUGIN_PATH` or `.tracedecay/plugins/` fallback must be kept only where backed by implementation. If implementation is absent or unknown, the doc should be phrased as a compatibility target/follow-up rather than a runtime guarantee. @@ -187,8 +188,7 @@ guarantee. ### C4. Homebrew/Scoop legacy package names Docs may warn that external taps/buckets can lag behind the rename and still expose a -legacy `tokensave` package. This is a user-support warning, not a new canonical -install path. +legacy package name. This is a user-support warning, not a new canonical install path. ## Category D — reject, fail, or do not silently accept @@ -198,30 +198,29 @@ data-loss, or supply-chain risk. ### D1. Legacy-only releases in latest-version detection Automatic latest-version detection must reject releases that only contain legacy -`tokensave-*` assets, including old pre-reset release epochs. Users may still request +`tracedecay-*` assets, including old pre-reset release epochs. Users may still request specific old versions explicitly via Category B behavior, but the automatic updater must not advertise legacy-only releases as current. ### D2. Silent storage renames or implicit data moves -Code must reject any implicit migration that renames `.tokensave/` to `.tracedecay/`, -renames `tokensave.db` to `tracedecay.db`, or moves `~/.tokensave/` to -`~/.tracedecay/` without an explicit migration command. A future migration command -must require clear user intent and must handle backups, branch metadata, session -payloads, curation sidecars, monitor files, caches, rollback, and both project and -user data roots. +Code must reject any implicit migration that renames pre-rename data directories or +databases, or moves user-level data roots without an explicit migration command. A +future migration command must require clear user intent and must handle backups, +branch metadata, session payloads, curation sidecars, monitor files, caches, +rollback, and both project and user data roots. -### D3. New public TokenSave-branded product surface +### D3. New public TraceDecay-branded product surface -New features must not introduce new public `tokensave` names for commands, -configuration, API fields, docs examples, package names, release assets, service -identifiers, or generated plugin artifacts unless the name is categorized here as -legacy retention or an external constraint. +New features must not introduce public pre-rename names for commands, configuration, +API fields, docs examples, package names, release assets, service identifiers, or +generated plugin artifacts unless the name is categorized here as legacy retention or +an external constraint. ### D4. Unowned cleanup Installers and uninstallers must not delete unknown user-authored files merely -because they live under an old `tokensave` path. Generated files can be migrated or +because they live under an old path. Generated files can be migrated or removed; unrecognized files require preservation or explicit user confirmation. ## Category E — externally constrained names that must not change yet @@ -230,10 +229,10 @@ These names are not compatibility shims owned by TraceDecay. They are external upstream identifiers or ecosystem state that the project cannot safely rename by itself. -### E1. `tokensave-large-treesitters` +### E1. `tracedecay-large-treesitters` -`tokensave-large-treesitters` and its `tokensave-medium-treesitters` / -`tokensave-lite-treesitters` siblings must keep their exact legacy names while the +`tracedecay-large-treesitters` and its `tracedecay-medium-treesitters` / +`tracedecay-lite-treesitters` siblings must keep their exact legacy names while the project depends on the external upstream package. The crate name, git repository, crates.io package, build script, vendored grammars, and large -> medium -> lite internal dependency chain are upstream-owned. @@ -248,7 +247,7 @@ crates unless one of these policy-changing events occurs: ongoing language updates. Until then, keep `Cargo.toml` pointed at the upstream dependency, keep Rust imports -such as `tokensave_large_treesitters::*`, and prefer the Rust-parser migration as the +such as `tracedecay_large_treesitters::*`, and prefer the Rust-parser migration as the long-term path to removing the dependency instead of renaming it. ## Compatibility surface map @@ -257,43 +256,43 @@ Every audited surface maps to exactly one policy category below. | Surface | Category | Required behavior | |---|---:|---| -| Project `.tokensave/` data dir and `tokensave.db` | A | Use in place when `.tracedecay/` is absent; no auto-migration. | -| User `~/.tokensave/` data dir, `global.db`, monitor files, cache defaults | A | Use in place when `~/.tracedecay/` is absent; new users default to `~/.tracedecay/`. | -| Project root discovery for `.tokensave/tokensave.db` | A | Continue detecting for list/status/wipe-style maintenance. | -| Branch metadata DB filenames in legacy dirs | A | Keep `tokensave.db` for legacy active data dirs. | -| Dashboard curation preview under legacy active dir | A | Store under the active data dir, including `.tokensave/` when that is active. | +| Project `.tracedecay/` data dir and `tracedecay.db` | A | Use in place when `.tracedecay/` is absent; no auto-migration. | +| User `~/.tracedecay/` data dir, `global.db`, monitor files, cache defaults | A | Use in place when `~/.tracedecay/` is absent; new users default to `~/.tracedecay/`. | +| Project root discovery for `.tracedecay/tracedecay.db` | A | Continue detecting for list/status/wipe-style maintenance. | +| Branch metadata DB filenames in legacy dirs | A | Keep `tracedecay.db` for legacy active data dirs. | +| Dashboard curation preview under legacy active dir | A | Store under the active data dir, including `.tracedecay/` when that is active. | | MCP schema text describing legacy project fallback | B | Keep docs/resources aligned with actual fallback behavior. | -| Diagnostics target dir under active data dir | A | Use active data dir; legacy projects use `.tokensave/target`. | -| Generic `TRACEDECAY_*` / `TOKENSAVE_*` env fallback | C | Accept old as fallback, warn, and let new spelling win. | -| `DISABLE_TOKENSAVE=true` | C | Accept as clean serve opt-out; prefer `DISABLE_TRACEDECAY=true` in docs. | -| `TOKENSAVE_GLOBAL_DB` | C | Accept fallback with warning; `TRACEDECAY_GLOBAL_DB` wins. | -| `TOKENSAVE_ENABLE_GLOBAL_DB` / `TOKENSAVE_DISABLE_GLOBAL_DB` | C | Accept fallback with warning; keep test hermeticity for both names. | -| Hook/extraction env fallbacks (`TOKENSAVE_RESEARCH_BLOCK_REASON`, `TOKENSAVE_PLUGIN_SUBAGENTS`, `TOKENSAVE_PROJECT_ROOT`, `TOKENSAVE_DISABLE_SUBPROCESS`) | C | Accept fallback with warning; new names win. | -| Savings pricing env fallbacks (`TOKENSAVE_OFFLINE`, `TOKENSAVE_MODEL_PRICES_PATH`) | C | Accept fallback with warning; do not leak values in logs. | -| Hermes `plugins.tokensave.project_root` | B | Migrate to/read as `plugins.tracedecay.project_root` when no current pin exists. | -| Hermes plugin list, memory provider, and context-engine aliases named `tokensave` | B | Rewrite/remove to canonical `tracedecay` behavior. | -| `$TOKENSAVE_PLUGIN_PATH` and `.tokensave/plugins/` docs claim | C | Warn/accept only where implemented; otherwise document as pending compatibility. | -| Generic agent config server key `tokensave` | B | Reconcile generated old entries to one canonical `tracedecay` entry. | +| Diagnostics target dir under active data dir | A | Use active data dir; legacy projects use `.tracedecay/target`. | +| Generic `TRACEDECAY_*` / `TRACEDECAY_*` env fallback | C | Accept old as fallback, warn, and let new spelling win. | +| `DISABLE_TRACEDECAY=true` | C | Accept as clean serve opt-out; prefer `DISABLE_TRACEDECAY=true` in docs. | +| `TRACEDECAY_GLOBAL_DB` | C | Accept fallback with warning; `TRACEDECAY_GLOBAL_DB` wins. | +| `TRACEDECAY_ENABLE_GLOBAL_DB` / `TRACEDECAY_DISABLE_GLOBAL_DB` | C | Accept fallback with warning; keep test hermeticity for both names. | +| Hook/extraction env fallbacks (`TRACEDECAY_RESEARCH_BLOCK_REASON`, `TRACEDECAY_PLUGIN_SUBAGENTS`, `TRACEDECAY_PROJECT_ROOT`, `TRACEDECAY_DISABLE_SUBPROCESS`) | C | Accept fallback with warning; new names win. | +| Savings pricing env fallbacks (`TRACEDECAY_OFFLINE`, `TRACEDECAY_MODEL_PRICES_PATH`) | C | Accept fallback with warning; do not leak values in logs. | +| Hermes `plugins.tracedecay.project_root` | B | Migrate to/read as `plugins.tracedecay.project_root` when no current pin exists. | +| Hermes plugin list, memory provider, and context-engine aliases named `tracedecay` | B | Rewrite/remove to canonical `tracedecay` behavior. | +| `$TRACEDECAY_PLUGIN_PATH` and `.tracedecay/plugins/` docs claim | C | Warn/accept only where implemented; otherwise document as pending compatibility. | +| Generic agent config server key `tracedecay` | B | Reconcile generated old entries to one canonical `tracedecay` entry. | | Legacy prompt/rule markers | B | Remove generated old blocks/files during install/uninstall/refresh. | | Cursor legacy plugin directory and skill slugs | B | Remove generated old plugin artifacts; preserve unknown user files. | -| Hermes generated `plugins/tokensave` directories and `skills/tokensave/SKILL.md` | B | Migrate/remove generated artifacts; preserve unknown user files. | +| Hermes generated `plugins/tracedecay` directories and `skills/tracedecay/SKILL.md` | B | Migrate/remove generated artifacts; preserve unknown user files. | | Codex legacy plugin directory and marketplace entry | B | Reconcile to canonical `tracedecay`; remove generated stale hooks/prompts. | -| Antigravity legacy CLI plugin path | B | Remove generated `tokensave.json` on uninstall/reconcile. | +| Antigravity legacy CLI plugin path | B | Remove generated `tracedecay.json` on uninstall/reconcile. | | Claude legacy hook/config migration | B | Repair/remove known generated legacy hook and config shapes. | -| `tokensave-large-treesitters` package/dependency name | E | Keep exact upstream name until upstream rename or explicit maintained-fork policy. | -| Worldwide counter endpoint `tokensave-counter` | A | Retain until replacement endpoint is deployed with preserved behavior. | +| `tracedecay-large-treesitters` package/dependency name | E | Keep exact upstream name until upstream rename or explicit maintained-fork policy. | +| Worldwide counter endpoint `tracedecay-counter` | A | Retain until replacement endpoint is deployed with preserved behavior. | | Legacy release asset names for explicit requested versions | B | Probe after current asset names for explicit old-version installs/upgrades. | | Latest-version filtering of legacy-only releases | D | Reject legacy-only/pre-reset releases as automatic latest candidates. | -| Extracted binary name `tokensave` in old archives | B | Accept only as fallback while extracting explicit old archives. | +| Extracted binary name `tracedecay` in old archives | B | Accept only as fallback while extracting explicit old archives. | | Homebrew/Scoop legacy package note | C | Keep as support warning while external packages lag; not canonical install path. | | Primary user docs mentioning fallback behavior | B | Keep aligned with runtime behavior; canonical examples use TraceDecay names. | | Design docs mentioning path/env/plugin fallback | B | Keep aligned with runtime behavior or mark as compatibility target. | -| `docs/TOKEN-SAVE-WHATSNEW.md` historical narrative | A | Preserve as history. | -| Historical plans/specs with TokenSave names | A | Preserve as history unless factually wrong. | -| Benchmark reports with TokenSave tool/path names | A | Preserve measured historical names. | -| Changelog historical TokenSave references | A | Preserve history; summarize rename in current entries only. | +| `docs/TRACEDECAY-WHATSNEW.md` historical narrative | A | Preserve as history. | +| Historical plans/specs with TraceDecay names | A | Preserve as history unless factually wrong. | +| Benchmark reports with TraceDecay tool/path names | A | Preserve measured historical names. | +| Changelog historical TraceDecay references | A | Preserve history; summarize rename in current entries only. | | Legacy config field `include` unrelated to brand rename | B | Keep ordinary config compatibility/migration separate from brand policy. | -| LCM/session schema legacy data carry-forward unrelated to brand names | B | Treat as ordinary schema migration, not TokenSave-brand compatibility. | +| LCM/session schema legacy data carry-forward unrelated to brand names | B | Treat as ordinary schema migration, not TraceDecay-brand compatibility. | ## Warning and migration implementation guidance @@ -312,8 +311,8 @@ Every audited surface maps to exactly one policy category below. ## Review checklist for future rebrand-related changes -Before merging a change that touches `tokensave`, `.tokensave`, `TOKENSAVE_*`, or -TokenSave-era docs, answer all of the following in the PR description or review: +Before merging a change that touches `tracedecay`, `.tracedecay`, `TRACEDECAY_*`, or +TraceDecay-era docs, answer all of the following in the PR description or review: 1. Which policy category owns the surface? 2. Does the change preserve existing user data and installed agent state? @@ -321,4 +320,4 @@ TokenSave-era docs, answer all of the following in the PR description or review: 4. If it migrates/removes anything, is ownership proven and are unknown user files preserved? 5. If it rejects something, is the failure mode clear and covered by tests? -6. If it touches `tokensave-large-treesitters`, has the upstream/fork policy changed? +6. If it touches `tracedecay-large-treesitters`, has the upstream/fork policy changed? diff --git a/docs/RETRIEVAL-QUALITY-EVAL.md b/docs/RETRIEVAL-QUALITY-EVAL.md index 718bcf7d..b33dafd2 100644 --- a/docs/RETRIEVAL-QUALITY-EVAL.md +++ b/docs/RETRIEVAL-QUALITY-EVAL.md @@ -56,7 +56,7 @@ ranking function: `extract_entities` (`entities.rs:13-39`) captures five shapes: - quoted spans (`"..."` / `'...'`), - `aka`/`also known as` aliases (`entities.rs:64-105`), -- code tokens that look like file paths, `::` Rust symbols, or `tracedecay_`/`tokensave_` tool +- code tokens that look like file paths, `::` Rust symbols, or `tracedecay_`/`tracedecay_` tool names (`entities.rs:107-119, 217-238`), - **multi-word capitalized sequences** — requires **≥2 consecutive capitalized words** (`extract_capitalized_names` / `push_capitalized_sequence`, `entities.rs:121-151`), diff --git a/docs/RUST-PARSER-MIGRATION.md b/docs/RUST-PARSER-MIGRATION.md index 64811fa6..3f0517cd 100644 --- a/docs/RUST-PARSER-MIGRATION.md +++ b/docs/RUST-PARSER-MIGRATION.md @@ -66,7 +66,7 @@ Counting the work: | Small scanner (one or two tokens) | Bash, Ruby, Lua, Dart, PHP | ~100–300 lines to port | | Medium scanner | Rust, Python, C++, C#, Scala, Kotlin, Swift | ~300–800 lines to port | | Large scanner | **Markdown** | ~1500 lines C++, the urgent one | -| Vendored | Cobol, Dockerfile | Live in `tokensave-large-treesitters/vendor/`; we already control these | +| Vendored | Cobol, Dockerfile | Live in `tracedecay-large-treesitters/vendor/`; we already control these | The migration value is highest for grammars where `--rust` is a clean win (top row) and for markdown (the bug source). The middle rows are real engineering work that has to be amortized across each grammar. @@ -216,7 +216,7 @@ The reason this whole project exists. The C++ scanner is a few thousand lines bu ### Phase 6: cleanup -- Drop `tokensave-large-treesitters` / `-medium` / `-lite` dependencies. +- Drop `tracedecay-large-treesitters` / `-medium` / `-lite` dependencies. - Drop the `[env]` block in `.cargo/config.toml`. - Drop `safe_extract`'s `catch_unwind` if the parity suite has been clean for a release cycle. (Or keep it; the cost is one closure per file and the safety value is real.) - Issue #49 stays closed but the comment is updated to point at the Rust port. diff --git a/docs/TOKENSAVE-COMPATIBILITY-AUDIT.md b/docs/TOKENSAVE-COMPATIBILITY-AUDIT.md deleted file mode 100644 index da769dcb..00000000 --- a/docs/TOKENSAVE-COMPATIBILITY-AUDIT.md +++ /dev/null @@ -1,100 +0,0 @@ -# Legacy TokenSave compatibility audit - -Date: 2026-06-14 -Task: `t_3482e439` - -Scope: repository surfaces that still intentionally mention or honor the pre-rebrand `TokenSave` / `tokensave` / `TOKENSAVE` names. I attempted to start with the TraceDecay MCP graph (`tracedecay_context`), but the configured tool binary path `/new/bin/tracedecay` was missing in this worker. I then used repository text searches plus targeted reads of the matching source, test, docs, and manifest files. - -## Executive summary - -The remaining legacy surfaces fall into six buckets: - -1. Filesystem/data compatibility: existing `.tokensave/` project and user directories remain active storage, not one-time import inputs. When `.tracedecay/` is absent and `.tokensave/` exists, TraceDecay reads and writes the legacy directory in place. -2. Environment variable fallback: most `TRACEDECAY_*` knobs accept `TOKENSAVE_*` fallbacks; `DISABLE_TOKENSAVE=true` is still a direct serve opt-out. -3. Agent/plugin cleanup and migration: installers and uninstallers detect/remove legacy config keys, plugin directories, prompt markers, hooks, and generated skills under the `tokensave` name. -4. Release/upgrade compatibility: explicit upgrades probe legacy `tokensave-v*` archive assets and extracted `tokensave` binaries, but latest-version detection deliberately ignores legacy-only releases. -5. Documentation/tests/history: public docs still tell users that fallback behavior exists and how to remove old daemon/service artifacts; historical benchmark/planning docs intentionally preserve old names. -6. Package/dependency naming: the tree-sitter grammar bundle remains `tokensave-large-treesitters` from the old upstream; this is a live dependency, not just documentation. - -Policy implication: removing the storage/env/installer/upgrade surfaces risks data loss, broken existing installs, or stranded plugin files. Historical docs and benchmarks are lower runtime risk but user-facing. The grammar dependency name is build-facing and cannot be renamed inside this repo alone. - -## Filesystem paths and database defaults - -| Surface | Location | Current behavior | User-facing? | Risk if changed | Existing warnings/tests | -|---|---|---|---|---|---| -| Project data dir `.tokensave/` | `src/config.rs:13-24`, `src/config.rs:81-133` | `get_tracedecay_dir()` prefers `.tracedecay/`; if absent and `.tokensave/` exists, it returns `.tokensave/`. `db_filename()` then keeps `tokensave.db` for that data dir, so legacy projects continue read/write in place. New projects default to `.tracedecay/tracedecay.db`. | Yes: affects every CLI/MCP/dashboard path that resolves project storage. | High. Removing or auto-renaming could make existing indexes invisible; writing a new `.tracedecay/` beside `.tokensave/` could look like data loss and split branch/session sidecars. | Explicit comments say read/write as-is, no auto-migration. Unit tests in `src/config.rs:457-505` cover default, legacy fallback, preference when both dirs exist, and DB filename choice. | -| User data dir `~/.tokensave/` | `src/config.rs:135-149`, `src/global_db.rs:105-112`, `src/monitor.rs:1-44`, `src/dashboard/savings_pricing.rs:91-100` | `user_data_dir()` prefers existing `~/.tracedecay`, falls back to existing `~/.tokensave`, otherwise defaults to `~/.tracedecay`. This controls `global.db`, monitor mmap/lock files, and some cache defaults. Pricing cache separately prefers `~/.tracedecay/model-prices.json` but reads legacy `~/.tokensave/model-prices.json` if the new cache does not exist and the old one does. | Yes: users with pre-rebrand global accounting/cache data. | High for `global.db` and monitor files; medium for model-prices cache. Removing fallback hides historical savings/session accounting and cache state. | `.cargo/config.toml:9-17` comments warn cargo-launched processes would otherwise write to `~/.tracedecay/global.db` or legacy `~/.tokensave/global.db`. | -| Project root discovery / wipe/list targets | `src/global.rs:175-249`, `src/global.rs:280-299` | Local project discovery recognizes both `.tracedecay/tracedecay.db` and `.tokensave/tokensave.db` in cwd, ancestors, and descendants. | Yes: `list` / `wipe` style maintenance commands. | High for destructive commands: if legacy roots are not found, they will not be shown or cleaned when requested; if mis-detected, wipe/list behavior changes. | `src/global.rs:398-403` has a helper that plants a legacy `.tokensave/tokensave.db` marker for tests. | -| Branch metadata DB file names | `src/branch_meta.rs:1-50`, `src/tracedecay.rs:291-295`, `src/tracedecay.rs:438-495` | `BranchMeta::new_for_dir()` records `tokensave.db` as the default branch DB file when the active data dir is `.tokensave/`; branch DB resolution uses the active data dir. | Indirectly user-facing via multi-branch indexing. | High: changing default DB filenames for legacy dirs can orphan branch metadata or point branch operations at a non-existent `tracedecay.db`. | Unit test `src/branch_meta.rs:197-202` checks legacy `.tokensave` maps to `tokensave.db`. | -| Dashboard curation sidecar | `src/dashboard/curate_preview_store.rs:1-25` | Persisted dry-run curation preview lives under the active data dir: `.tracedecay/dashboard/curation_preview.json`, or legacy `.tokensave/dashboard/curation_preview.json` when only the old dir exists. | Yes for dashboard users who expect previews to survive restarts. | Medium: old previews disappear but primary data remains. | Comments call this best-effort and backward-compatible; failures log warnings and do not fail API requests. | -| MCP schema documentation resource | `src/mcp/server.rs:48-55` | The `tracedecay://schema` resource tells clients that legacy projects fall back to `.tokensave/tokensave.db`. | User-facing to MCP clients. | Low runtime risk, but misleading if behavior changes without this text. | Hand-maintained schema doc warns to update with schema. | -| Diagnostics target dir | `src/diagnostics/rust.rs:117-119` | Cargo diagnostics target dir is under the active data dir (`get_tracedecay_dir(project_root).join("target")`), therefore legacy projects use `.tokensave/target`. | Indirect. | Medium: switching only this path could mix or duplicate build artifacts for legacy projects. | No direct legacy-specific test found. | - -## Environment variables and config keys - -| Surface | Location | Current behavior | User-facing? | Risk if changed | Existing warnings/tests | -|---|---|---|---|---|---| -| Generic `TRACEDECAY_` / `TOKENSAVE_` fallback | `src/config.rs:151-157` | `brand_env(suffix)` reads the new variable first, then legacy `TOKENSAVE_`. Used by multiple runtime knobs. | Yes. | Medium/high depending on suffix. Existing agent configs and shell profiles using old names stop working. | README and dashboard docs document precedence. | -| `DISABLE_TOKENSAVE=true` | `src/main.rs:1150-1157`, `README.md:978-980`, `docs/dashboard.md:92-96` | `tracedecay serve` exits cleanly if either `DISABLE_TRACEDECAY=true` or `DISABLE_TOKENSAVE=true`. This is checked directly rather than through `brand_env`. | Yes: per-project MCP opt-out. | Medium: old opt-out stops suppressing server startup, causing hosts to launch it again. | Comments explain host retry avoidance. Docs state legacy opt-out is honored. | -| `TOKENSAVE_GLOBAL_DB` | `src/global_db.rs:94-119`, `src/dashboard/mod.rs:112-135`, `docs/DASHBOARD-API-AUDIT.md:38` | Explicit path override for the global/session dashboard store. New `TRACEDECAY_GLOBAL_DB` wins because the override checks it first; legacy remains fallback. Dashboard treats override as operator decision and scopes LCM to `global`. | Yes, especially tests/smoke/Hermes wrapper deployments. | High if existing operators pin the DB with the old name; dashboard may silently serve project-local DB instead. | Comments in dashboard and global DB modules document precedence. | -| `TOKENSAVE_ENABLE_GLOBAL_DB` / `TOKENSAVE_DISABLE_GLOBAL_DB` | `src/global_db.rs:167-194`, `.cargo/config.toml:9-17`, `tests/mcp_server_test.rs:1635-1719` | Global accounting is enabled by default. `brand_env("ENABLE_GLOBAL_DB")` decides first; truthy enables, falsy disables. If no enable var, truthy disable var disables. Cargo config sets both new and legacy disable vars so cargo-launched tests/runs stay hermetic. | Yes: savings ledger/worldwide counter behavior and test hermeticity. | High: old env users lose opt-in/opt-out, and cargo test may write to a real legacy `~/.tokensave/global.db` unless both names stay set. | Tests clear legacy vars to prove default-on and mention legacy fallback; override test covers new vars directly. | -| `TOKENSAVE_RESEARCH_BLOCK_REASON`, `TOKENSAVE_PLUGIN_SUBAGENTS`, `TOKENSAVE_PROJECT_ROOT`, `TOKENSAVE_DISABLE_SUBPROCESS` | `src/hooks.rs:38-40`, `src/hooks.rs:348-350`, `src/hooks.rs:501-502`, `src/tracedecay.rs:197-200` | All are accepted through `brand_env()` as fallbacks for their `TRACEDECAY_*` equivalents. They control hook block messaging, plugin-subagent allow-list, hook project-root discovery, and subprocess extraction disablement. | Yes for users/agents customizing hooks or extraction. | Medium: behavior reverts to defaults if old env names are removed. | No focused tests found for each suffix in this audit. | -| `TOKENSAVE_OFFLINE` / `TOKENSAVE_MODEL_PRICES_PATH` | `src/dashboard/savings_pricing.rs:46-104` | Savings pricing fetch honors legacy offline flag and cache-path override via `env_with_legacy()`. Any non-empty non-`0` offline value disables network access. | Yes for offline/privacy-sensitive users and tests. | Medium: old env names stop suppressing OpenRouter fetches or stop pointing at test/operator cache files. | No focused test found in this audit. | -| Hermes `plugins.tokensave.project_root` | `src/agents/hermes/profile_config.rs:14-27`, `src/agents/hermes/tokensave_migration.rs:66-77`, `src/agents/hermes/lifecycle.rs:96-107` | Hermes profile config reads a project pin from `plugins.tracedecay.project_root`, falling back to `plugins.tokensave.project_root`. Refresh uses that legacy pin if no current pin exists; dashboard-deployed state can also be inferred from the legacy plugin. | Yes for Hermes users with pre-rebrand plugin config. | High: plugin refresh/reinstall could lose pinned project roots, changing storage scope and tool results. | Multiple helper tests in `profile_config.rs` and `tokensave_migration.rs` exercise legacy pin/detection behavior. | -| Hermes plugin enabled/disabled/memory/context aliases | `src/agents/hermes/profile_config.rs:124-190`, `src/agents/hermes/profile_config.rs:217-279`, `src/agents/hermes/profile_config.rs:313-347` | Enabling removes `tokensave` from plugin lists and adds `tracedecay`; disabling removes either name. Memory provider `provider: tokensave` is upgraded/removed like `tracedecay`; context-engine `engine: tokensave` is treated as the old alias. | Yes for Hermes config migration. | High: old configs remain enabled under the wrong name or fail to migrate to new provider/engine. | Tests in the same file cover config rewrite shapes. | -| Plugin path discovery var | `docs/PLUGINS-DESIGN.md:142-143` | Docs state `$TRACEDECAY_PLUGIN_PATH` honors legacy `$TOKENSAVE_PLUGIN_PATH`, and `.tracedecay/plugins/` honors `.tokensave/plugins/`. I did not find the implementing source in this pass. | User-facing docs. | Unknown/runtime risk depends on implementation; policy worker should verify before changing docs. | No source/test located by this audit. | - -## Agent/plugin/package compatibility surfaces - -| Surface | Location | Current behavior | User-facing? | Risk if changed | Existing warnings/tests | -|---|---|---|---|---|---| -| Generic agent config server key `tokensave` | `src/agents/{zed,cline,kimi,opencode,gemini,kiro,roo_code,kilo,copilot,claude,codex,antigravity}.rs` | Detection, uninstall, and replacement code generally treats both `tracedecay` and `tokensave` config entries as owned by this integration. Installers write only `tracedecay` but remove stale `tokensave` entries when reconciling. | Yes for users upgrading agents. | High: uninstall/reinstall may leave old MCP servers/hooks behind, causing duplicate tools or stale binaries. | Broad integration tests in `tests/agent_test.rs`; specific Cursor legacy plugin test at `tests/agent_test.rs:2991-3023`. | -| Legacy prompt/rule markers | `src/agents/{vibe,opencode,codex,copilot,gemini,kimi,kiro}.rs`, `src/agents/cursor.rs:498-501` | Prompt cleanup recognizes old headers like `## Prefer tokensave MCP tools` and Cursor legacy `rules/tokensave.mdc`; current installs write tracedecay-branded rules. | Yes: prompt files in users' repos/homes. | Medium/high: stale instructions remain after uninstall or duplicate after reinstall. | Cursor sweep test checks `rules/tokensave.mdc` removal. | -| Cursor plugin directory and legacy skill slugs | `src/agents/cursor.rs:448-501`, `tests/agent_test.rs:2991-3023` | Current local plugin lives at `~/.cursor/plugins/local/tracedecay`; installer removes old `~/.cursor/plugins/local/tokensave`, `commands/`, `skills/tokensave-*`, old `skills/tracedecay-*`, and `rules/tokensave.mdc` if the directory is tracedecay-owned. | Yes. | High: old Cursor plugin can shadow/duplicate current plugin; removing too aggressively risks user files, so ownership checks matter. | Dedicated test simulates pre-rebrand manifest and generated files, then asserts legacy dir is removed. | -| Hermes generated plugin directories and skills | `src/agents/hermes/tokensave_migration.rs:1-93`, `src/agents/hermes.rs:286-334`, `src/agents/hermes/lifecycle.rs:25-59`, `src/agents/hermes/lifecycle.rs:126-131` | Current plugin dir is `plugins/tracedecay`; legacy `plugins/tokensave` is detected, mapped to the current dir for refresh, and generated legacy artifacts are removed while preserving unrelated user files. Uninstall removes both generated current and legacy files including `skills/tokensave/SKILL.md`. | Yes for Hermes plugin users. | High: bad migration can delete user files or strand a broken plugin. Current helper deliberately preserves unknown files. | `tokensave_migration.rs:102-164` tests current/legacy locations, legacy manifest detection, and preserving `user-note.txt` while removing generated files. | -| Codex plugin directory and marketplace entry | `src/agents/codex.rs:195-224`, `src/agents/codex.rs:293-310`, `src/agents/codex.rs:365-424`, `src/agents/codex.rs:732-775` | Current plugin source lives at `plugins/tracedecay`; install removes legacy `plugins/tokensave`. Marketplace entries with either name are reconciled to a single `tracedecay` entry. Hooks/prompt cleanup accepts `tokensave`. | Yes. | Medium/high: duplicate Codex plugins or stale hooks. | No single legacy-specific test found beyond integration coverage. | -| Antigravity CLI plugin path | `src/agents/antigravity.rs:32-40`, `src/agents/antigravity.rs:108-113`, `src/agents/antigravity.rs:184-215` | Current CLI plugin is `.gemini/antigravity-cli/plugins/tracedecay.json`; uninstall also removes `.gemini/antigravity-cli/plugins/tokensave.json`; IDE config removes both server keys. | Yes. | Medium: stale CLI plugin persists after uninstall/reinstall. | Changelog notes issue #85; no focused test found here. | -| Claude hook/config migration | `src/agents/claude.rs:160-172`, `src/agents/claude.rs:290-303`, `src/agents/claude.rs:455-617` | Detects hook command strings containing either `tracedecay` or `tokensave`, rewrites broken legacy single-string hook shape, and removes `tokensave` server/permission entries from several Claude config locations. | Yes. | High: stale hooks can keep executing old binary names; wrong cleanup can alter user Claude settings. | Changelog describes hook legacy-shape repair; tests exist in `agent_test.rs` but were not exhaustively traced here. | -| Package/dependency name `tokensave-large-treesitters` | `Cargo.toml:56`, `Cargo.toml:108`, `Cargo.lock:4473-4531`, `docs/PLUGINS-DESIGN.md:9-11`, `docs/MORE-LANGUAGES-SUPPORT.md:17-34`, `docs/RUST-PARSER-MIGRATION.md:69,219` | Live Rust dependency on the legacy-named grammar bundle from `https://github.com/aovestdipaperino/tokensave-large-treesitters`; lockfile also includes `tokensave-medium-treesitters` and `tokensave-lite-treesitters`. | Build-facing and contributor-facing. | High if renamed locally without publishing/repointing dependency; build will fail. User memory says this dependency intentionally stays pointed at upstream. | Cargo lock pins the dependency. Docs mention future parser migration might drop these packages. | - -## Release, upgrade, and network compatibility - -| Surface | Location | Current behavior | User-facing? | Risk if changed | Existing warnings/tests | -|---|---|---|---|---|---| -| Worldwide counter endpoint | `src/cloud.rs:9-10`, `SECURITY.md:50-52` | Counter URL remains `https://tokensave-counter.enzinol.workers.dev`; operations are best-effort and silently ignored on failure. | User-facing via network/security docs and global metrics. | Low/medium: renaming endpoint requires operating/deploying new worker or loses aggregate counter continuity. | SECURITY.md explicitly says endpoint keeps pre-rename name. | -| Legacy release asset names | `src/cloud.rs:140-165`, `src/upgrade.rs:525-530`, `src/upgrade.rs:743-755` | Explicit install/upgrade for a requested version probes `tracedecay-v*` first, then legacy `tokensave-v*` / `tokensave-beta-v*`. | Yes for users upgrading from/to old releases. | High: old releases become non-installable by the built-in upgrader. | Tests assert legacy names and candidate order. | -| Latest-version filtering | `src/cloud.rs:168-200`, `src/cloud.rs:400-430` | Latest-version detection is stricter than explicit install: it requires post-rebrand `tracedecay-*` assets and rejects legacy-only `tokensave-*` releases plus pre-reset 4.x-6.x epoch releases. | Yes, avoids downgrade/bad upgrade prompts. | High if relaxed accidentally: old releases could be offered as latest. | Tests `skips_release_with_only_legacy_tokensave_asset` and beta variant. | -| Extracted binary name during upgrade | `src/upgrade.rs:553-563` | Archive extraction probes `tracedecay`/`tracedecay.exe` first, then `tokensave`/`tokensave.exe`. | Yes for legacy archives. | High for legacy explicit upgrades; low for current releases. | Indirectly covered by upgrade asset tests. | -| Homebrew/Scoop legacy package note | `README.md:90-101` | README warns external tap/bucket may still publish under legacy `tokensave` until they pick up the rename. | Yes. | Low runtime risk; removing too early may confuse package-manager users. | No test. | - -## Docs, historical artifacts, and public messaging - -These references are user-facing but mostly descriptive. They should be kept aligned with runtime policy rather than removed mechanically. - -| Area | Files | Current content / behavior | Risk | -|---|---|---|---| -| Primary user docs | `README.md:198`, `README.md:662-670`, `README.md:940`, `README.md:978-980`; `docs/USER-GUIDE.md:453-468`, `docs/USER-GUIDE.md:788`; `docs/dashboard.md:92-96`; `docs/LSP-INTEGRATION.md:321-322`; `SECURITY.md:29`, `SECURITY.md:116` | Explain `.tokensave` fallback, legacy env fallback, old daemon removal commands (`tokensave-daemon`, `com.tokensave.daemon.plist`), and gitignore guidance. | Medium: stale docs produce support issues; daemon-removal docs are historical but actionable for old installs. | -| Design/docs references to path fallback | `docs/BRANCHING-USER-GUIDE.md:38`, `docs/DESIGN-DOC.md:373`, `docs/DOMAIN-EXTRACTORS.md:249`, `docs/INDEX-DESIGN.md:7`, `docs/LCM-PAYLOAD-LIFECYCLE.md:51`, `docs/PLUGINS-DESIGN.md:142-143` | Describe legacy `.tokensave`/env/plugin path fallbacks. | Low/medium. Must track policy decisions. | -| Historical TokenSave narrative | `docs/TOKEN-SAVE-WHATSNEW.md` | Explicit historical narrative retaining TokenSave-era names. | Low. Removing would erase history; header says names are intentional. | -| Historical plans/specs | `docs/superpowers/plans/*tokensave*`, `docs/superpowers/specs/*tokensave*`, daemon design docs | Dated plans/specs preserve TokenSave names with rebrand notes. Some mention old paths like `~/.tokensave/daemon.pid` and `daemon_debounce`. | Low runtime risk; high archival value. | -| Benchmarks | `benchmarks/tsbench/SUMMARY.md`, `benchmarks/comparison-report.md`, `benchmarks/comparison-results.json` | Historical benchmark results preserve old binary/tool prefixes and paths; summary explicitly says old names are preserved as measured. | Low. Do not treat these as active compatibility code. | -| Changelog | `CHANGELOG.md` | Large number of historical TokenSave references. Top unreleased note at `CHANGELOG.md:11` summarizes rename and says older entries intentionally retain old names. | Low. Changelog history should remain stable. | - -## Tests found for legacy compatibility - -- `src/config.rs:457-505`: data dir defaults/fallback/both-present and DB filename behavior. -- `src/branch_meta.rs:197-202`: legacy data dir keeps `tokensave.db` in branch metadata. -- `src/cloud.rs:400-430`: latest-version detection skips legacy-only assets. -- `src/cloud.rs:469-471` and `src/upgrade.rs:743-755`: asset candidate and legacy asset naming. -- `src/agents/hermes/tokensave_migration.rs:102-164`: Hermes current/legacy plugin dir detection and safe generated-file cleanup preserving user files. -- `tests/agent_test.rs:2991-3023`: Cursor install sweeps legacy `~/.cursor/plugins/local/tokensave` generated artifacts. -- `tests/mcp_server_test.rs:1635-1719`: global accounting default/override behavior accounts for legacy env spellings in setup/comments. -- `tests/config_test.rs:47-64`: old config files with a now-legacy `include` field still load. This is legacy config compatibility, not TokenSave-brand compatibility. -- LCM/session schema tests (`tests/session_lcm_schema_test.rs`, `tests/hermes_transcript_ingest_test.rs`) cover legacy schema/data carry-forward, but not the TokenSave brand name specifically. - -## Gaps / follow-up questions for policy workers - -- `docs/PLUGINS-DESIGN.md` documents `TOKENSAVE_PLUGIN_PATH` and `.tokensave/plugins/` fallback, but this audit did not locate the implementation. Verify before promising or removing it. -- Several agent integrations accept or remove `tokensave` keys, but only Cursor and Hermes have obvious legacy-specific tests. If policy is to keep those surfaces long-term, add focused tests for Codex, Antigravity, Claude, Copilot, Kimi/Kilo/Roo/Cline/Gemini/Zed/OpenCode/Vibe cleanup behavior. -- `DISABLE_TOKENSAVE` is handled directly and only recognizes exact string `true`, while `brand_env()` boolean consumers generally use truthy parsing (`1/true/yes/on`). Decide whether that divergence is intentional before changing either side. -- Storage fallback is not migration: existing `.tokensave` directories remain the active write target. Any future policy that renames/migrates must include backup, atomicity, branch metadata, sessions sidecars, curation sidecars, global DB, monitor files, and rollback behavior. diff --git a/docs/TRACEDECAY-COMPATIBILITY-AUDIT.md b/docs/TRACEDECAY-COMPATIBILITY-AUDIT.md new file mode 100644 index 00000000..48182bbc --- /dev/null +++ b/docs/TRACEDECAY-COMPATIBILITY-AUDIT.md @@ -0,0 +1,36 @@ +# TraceDecay storage and rename compatibility audit + +Date: 2026-06-21 + +This document records the current policy after the storage unification and rename cleanup work. It is not a compatibility promise for branch-only experiments. + +## Current policy + +TraceDecay defaults to user/profile-level storage. Project data is registered under the user profile and scoped to the current project for normal MCP, CLI, dashboard, memory, session, and LCM operations. Tools may search or list other projects only when explicitly instructed to do so. + +Project-local storage is reserved for explicit local installs and maintenance paths. Normal operation must not create project-local databases just because the current working directory is a repository. + +Hermes uses TraceDecay as its memory and context provider. Unpinned Hermes profiles use profile-level TraceDecay storage; profiles with an explicit project root stay scoped to that project. + +Codex, Cursor, Hermes, and other agent installers should remove generated project-local artifacts they own while preserving user-authored config entries. Cleanup must not follow symlinked project directories outside the repository. + +Windows CI and local test runs should use normal test concurrency. If a test is flaky under concurrency, fix the underlying race instead of serializing the whole suite or adding artificial caps. + +## Intentional exceptions + +The tree-sitter grammar bundle can keep using the upstream package and repository name that it already has. Do not vendor or rename it locally unless the dependency is published under a new canonical name first. + +Old daemon and service names may appear in user-facing cleanup commands when the command is specifically about removing an already-installed old service. Those references are migration instructions, not runtime fallbacks. + +Hermes config migration may still recognize an old memory-provider spelling and rewrite it to `tracedecay`. That is a one-way config upgrade path, not a second supported provider name. + +Historical changelog entries, benchmark output, and dated design notes may preserve names that existed when they were written. Do not treat those references as current runtime support. + +## Review checklist + +- No new project-local database path is introduced for default operation. +- New session, memory, dashboard, curation, and LCM data resolves through the user/profile store by default. +- MCP tools scope to the current project by default and only cross project boundaries on explicit request. +- Agent installers write current plugin/config names and remove only generated legacy project-local artifacts. +- CI commands do not set global serialization or test-thread caps. +- Remaining old-name references are limited to the intentional exceptions above. diff --git a/docs/TOKEN-SAVE-WHATSNEW.md b/docs/TRACEDECAY-WHATSNEW.md similarity index 60% rename from docs/TOKEN-SAVE-WHATSNEW.md rename to docs/TRACEDECAY-WHATSNEW.md index e3f56fd0..877ef544 100644 --- a/docs/TOKEN-SAVE-WHATSNEW.md +++ b/docs/TRACEDECAY-WHATSNEW.md @@ -1,10 +1,10 @@ -# TokenSave: From 1.0 to 6.1, the Story So Far +# TraceDecay: From 1.0 to 6.1, the Story So Far -> **Note:** The project described here has since been renamed **TraceDecay**. This page is a historical narrative of the TokenSave era (1.0 through 6.1) and intentionally keeps the original names for the product, commands, tools, and releases as they existed at the time. +> **Note:** The project described here has since been renamed **TraceDecay**. This page is a historical narrative of the TraceDecay era (1.0 through 6.1) and intentionally keeps the original names for the product, commands, tools, and releases as they existed at the time. [Illustration: generate a landscape oriented image of a glowing semantic knowledge graph floating above a laptop screen, with nodes and edges made of light connecting code symbols, a small orange crab perched on the laptop corner observing the graph, dark workspace background with subtle warm lighting, photographic style with shallow depth of field] -TokenSave reached 1.0.0 on March 24, 2026. It could index Rust projects, serve a handful of MCP tools over stdio, and save Claude Code from burning tokens on redundant file scans. Useful, but narrow. A few months and ninety-plus releases later, it speaks more than fifty programming languages, keeps its index fresh on demand with no background process, installs itself into more than a dozen different AI coding agents, maintains optional per-branch code graphs, tracks token savings down to the individual tool call, and edits files through atomic, anchor-based primitives. All in a single ~25 MB binary with zero runtime dependencies. +TraceDecay reached 1.0.0 on March 24, 2026. It could index Rust projects, serve a handful of MCP tools over stdio, and save Claude Code from burning tokens on redundant file scans. Useful, but narrow. A few months and ninety-plus releases later, it speaks more than fifty programming languages, keeps its index fresh on demand with no background process, installs itself into more than a dozen different AI coding agents, maintains optional per-branch code graphs, tracks token savings down to the individual tool call, and edits files through atomic, anchor-based primitives. All in a single ~25 MB binary with zero runtime dependencies. This is the story of how it got there. Not a changelog transcription, but the arc of decisions, the problems that surfaced, and the features they demanded. @@ -42,21 +42,21 @@ flowchart TB style C fill:#6b3d3d,color:#fff,stroke:#6b3d3d ``` -That many grammars create a binary-size problem. Not everyone needs COBOL parsing. Version 2.0.0 introduced feature-flag tiers: `lite` compiles eleven core languages, `medium` adds nine more, and `full` (the default) includes all thirty-one. Individual `lang-*` flags let you cherry-pick. A `cargo install tokensave --no-default-features --features lang-nix,lang-bash` gives you exactly what you need and nothing else. +That many grammars create a binary-size problem. Not everyone needs COBOL parsing. Version 2.0.0 introduced feature-flag tiers: `lite` compiles eleven core languages, `medium` adds nine more, and `full` (the default) includes all thirty-one. Individual `lang-*` flags let you cherry-pick. A `cargo install tracedecay --no-default-features --features lang-nix,lang-bash` gives you exactly what you need and nothing else. ## From Nine Tools to Thirty-Seven The MCP tool surface grew in four distinct waves, each responding to a different class of question that AI agents kept asking. -The first wave (1.0 through 1.1) covered navigation. `tokensave_search`, `tokensave_context`, `tokensave_callers`, `tokensave_callees`, `tokensave_node`. Then `tokensave_files` and `tokensave_affected` for understanding which files matter and which tests break when you touch a given module. These tools replaced the bulk of what Explore agents were doing with grep and glob. +The first wave (1.0 through 1.1) covered navigation. `tracedecay_search`, `tracedecay_context`, `tracedecay_callers`, `tracedecay_callees`, `tracedecay_node`. Then `tracedecay_files` and `tracedecay_affected` for understanding which files matter and which tests break when you touch a given module. These tools replaced the bulk of what Explore agents were doing with grep and glob. The second wave (1.5.1) added structural analysis. Dead code detection. Circular dependency finding. Module API surfaces. Unused imports. Semantic changelogs between git refs. Rename previews that show every reference to a symbol. Nine new tools, each backed by graph queries over the indexed codebase rather than file-by-file scanning. -The third wave (1.6.0 through 2.0.0) shifted toward code quality and compliance. `tokensave_complexity` ranks functions by cyclomatic complexity computed from the AST during indexing, not approximated at query time. `tokensave_recursion` detects recursive and mutually-recursive call cycles. `tokensave_god_class` finds classes with too many members. `tokensave_doc_coverage` identifies public symbols missing documentation. The porting tools (`tokensave_port_status` and `tokensave_port_order`) arrived for teams migrating codebases across languages. +The third wave (1.6.0 through 2.0.0) shifted toward code quality and compliance. `tracedecay_complexity` ranks functions by cyclomatic complexity computed from the AST during indexing, not approximated at query time. `tracedecay_recursion` detects recursive and mutually-recursive call cycles. `tracedecay_god_class` finds classes with too many members. `tracedecay_doc_coverage` identifies public symbols missing documentation. The porting tools (`tracedecay_port_status` and `tracedecay_port_order`) arrived for teams migrating codebases across languages. -The fourth wave (3.3 through 4.0) brought workflow integration and multi-branch awareness. `tokensave_commit_context` generates semantic summaries of uncommitted changes for commit message drafting. `tokensave_pr_context` produces semantic diffs between git refs for pull request descriptions. `tokensave_test_map` maps source functions to their test functions at the symbol level, flagging uncovered symbols. `tokensave_simplify_scan` runs quality analysis on changed files: duplications, dead code introductions, complexity hotspots. And `tokensave_type_hierarchy` generates recursive inheritance trees for traits, interfaces, and classes. +The fourth wave (3.3 through 4.0) brought workflow integration and multi-branch awareness. `tracedecay_commit_context` generates semantic summaries of uncommitted changes for commit message drafting. `tracedecay_pr_context` produces semantic diffs between git refs for pull request descriptions. `tracedecay_test_map` maps source functions to their test functions at the symbol level, flagging uncovered symbols. `tracedecay_simplify_scan` runs quality analysis on changed files: duplications, dead code introductions, complexity hotspots. And `tracedecay_type_hierarchy` generates recursive inheritance trees for traits, interfaces, and classes. -Then came the branch tools. `tokensave_branch_search` lets you search symbols in another branch's graph without switching your checkout. `tokensave_branch_diff` compares code graphs between branches, showing symbols added, removed, and changed. `tokensave_branch_list` exposes tracked branches with their DB sizes and parent info. Thirty-seven tools total, each read-only, safe to call in parallel, and annotated with MCP metadata so clients know what they're getting. +Then came the branch tools. `tracedecay_branch_search` lets you search symbols in another branch's graph without switching your checkout. `tracedecay_branch_diff` compares code graphs between branches, showing symbols added, removed, and changed. `tracedecay_branch_list` exposes tracked branches with their DB sizes and parent info. Thirty-seven tools total, each read-only, safe to call in parallel, and annotated with MCP metadata so clients know what they're getting. ```mermaid %%{init: {'theme': 'base', 'themeVariables': {'fontSize': '16px'}}}%% @@ -101,23 +101,23 @@ flowchart TB style W4 fill:#8b4513,color:#fff ``` -Version 1.7.0 embedded three safety metrics directly into every function node during indexing: `unsafe_blocks`, `unchecked_calls` (`.unwrap()`, `!!`, forced optionals), and `assertions`. These numbers are available on every `tokensave_node` response and power the complexity ranking tool. The goal was NASA Power of 10 compliance auditing without a single grep invocation. +Version 1.7.0 embedded three safety metrics directly into every function node during indexing: `unsafe_blocks`, `unchecked_calls` (`.unwrap()`, `!!`, forced optionals), and `assertions`. These numbers are available on every `tracedecay_node` response and power the complexity ranking tool. The goal was NASA Power of 10 compliance auditing without a single grep invocation. ## The Agent Problem -TokenSave started as a Claude Code plugin. You ran `tokensave claude-install`, it configured your MCP server and permissions, injected prompt rules into `CLAUDE.md`, and set up a PreToolUse hook to block wasteful Explore agents. That worked fine for exactly one agent. +TraceDecay started as a Claude Code plugin. You ran `tracedecay claude-install`, it configured your MCP server and permissions, injected prompt rules into `CLAUDE.md`, and set up a PreToolUse hook to block wasteful Explore agents. That worked fine for exactly one agent. Then the ecosystem diversified. Codex CLI, OpenCode, Gemini CLI, Copilot, Cursor, Zed, Cline, Roo Code. Each with its own configuration format, its own prompt file location, its own way of registering MCP servers. Some use JSON. Some use TOML. Zed and VS Code use JSON with comments and trailing commas, which isn't actually JSON at all. Version 1.8.0 introduced a trait-based `AgentIntegration` abstraction with three methods: `install`, `uninstall`, and `healthcheck`. The Claude-specific logic (600 lines of it) moved into `src/agents/claude.rs`. OpenCode and Codex CLI followed in 1.8.1. Gemini CLI in 2.1.0. Then 2.3.2 added five more in a single release: Copilot, Cursor, Zed, Cline, and Roo Code. Nine agents total. -The install experience evolved accordingly. Running `tokensave install` without specifying an agent now auto-detects which ones are present by checking their config directories. If it finds exactly one, it installs directly. If it finds several, it presents an interactive checkbox selector. An `installed_agents` list in `~/.tokensave/config.toml` tracks which integrations are active, and upgrading from older versions backfills the list by scanning existing configs. +The install experience evolved accordingly. Running `tracedecay install` without specifying an agent now auto-detects which ones are present by checking their config directories. If it finds exactly one, it installs directly. If it finds several, it presents an interactive checkbox selector. An `installed_agents` list in `~/.tracedecay/config.toml` tracks which integrations are active, and upgrading from older versions backfills the list by scanning existing configs. -The Claude Code integration deepened further in v3.5.0 with two additional hooks. Beyond the original PreToolUse hook that blocks Explore agents, tokensave now registers a UserPromptSubmit hook (runs at prompt submission for lifecycle tracking) and a Stop hook (flushes token counters when the session ends). Three hooks, each implemented as a native Rust subcommand, no bash or jq required. The doctor command validates that each hook uses the correct subcommand and auto-repairs broken ones. +The Claude Code integration deepened further in v3.5.0 with two additional hooks. Beyond the original PreToolUse hook that blocks Explore agents, tracedecay now registers a UserPromptSubmit hook (runs at prompt submission for lifecycle tracking) and a Stop hook (flushes token counters when the session ends). Three hooks, each implemented as a native Rust subcommand, no bash or jq required. The doctor command validates that each hook uses the correct subcommand and auto-repairs broken ones. ## The Daemon Gets Smarter -Version 2.4.0 introduced daemon mode: `tokensave daemon` watches all tracked projects for file changes and runs incremental syncs automatically. The debounce interval is configurable (default 15 seconds). On macOS, `--enable-autostart` generates a launchd plist. On Linux, it creates a systemd user unit. On Windows, it registers a Windows Service with SCM failure recovery actions. +Version 2.4.0 introduced daemon mode: `tracedecay daemon` watches all tracked projects for file changes and runs incremental syncs automatically. The debounce interval is configurable (default 15 seconds). On macOS, `--enable-autostart` generates a launchd plist. On Linux, it creates a systemd user unit. On Windows, it registers a Windows Service with SCM failure recovery actions. But shipping the daemon created an upgrade problem. When `brew upgrade` or `cargo install` replaces the binary while the daemon is running, the old process keeps serving stale code. Version 3.3.1 solved this: the daemon now snapshots its own binary's mtime and size at startup and checks every 60 seconds. When an upgrade is detected, the daemon flushes pending syncs, logs the event, and exits. The service manager (launchd `KeepAlive`, systemd `Restart=on-failure`, Windows SCM failure actions) automatically relaunches with the new version. @@ -125,7 +125,7 @@ The daemon work produced a side effect worth mentioning: `daemon-kit`, a standal ## Bundled Grammars and the 26x Speedup -Version 3.0 consolidated all 31 tree-sitter grammars into a single bundled dependency (`tokensave-large-treesitters`), eliminating dozens of individual grammar crate dependencies. The grammar provider became a single `LazyLock` lookup, replacing 100+ lines of per-crate match arms. +Version 3.0 consolidated all 31 tree-sitter grammars into a single bundled dependency (`tracedecay-large-treesitters`), eliminating dozens of individual grammar crate dependencies. The grammar provider became a single `LazyLock` lookup, replacing 100+ lines of per-crate match arms. Version 3.2 tackled indexing performance head-on. rayon parallel extraction, prepared-statement DB writes, suffix-indexed reference resolution, and bulk-load mode with deferred index creation. The results were dramatic: a 1,782-file codebase went from 14.8s to 1.2s. A 28K-file monorepo went from 565s (over nine minutes) to 22s. That's a 13-26x speedup depending on the codebase. @@ -133,27 +133,27 @@ Version 3.2 tackled indexing performance head-on. rayon parallel extraction, pre Version 4.0.0 brought optional multi-branch indexing, which turned out to be more architecturally interesting than expected. The basic idea is simple: keep a separate code graph per git branch so switching branches never gives stale results. The implementation is where it gets clever. -When you run `tokensave branch add`, tokensave doesn't build a new graph from scratch. It finds the nearest ancestor branch that already has a database (typically `main`), copies that DB, then runs an incremental sync to update only the files that differ between branches. For a feature branch with 20 changed files off a 5,000-file main branch, this takes seconds instead of the full index time. +When you run `tracedecay branch add`, tracedecay doesn't build a new graph from scratch. It finds the nearest ancestor branch that already has a database (typically `main`), copies that DB, then runs an incremental sync to update only the files that differ between branches. For a feature branch with 20 changed files off a 5,000-file main branch, this takes seconds instead of the full index time. -Three new MCP tools make cross-branch work possible without switching your checkout. `tokensave_branch_search` lets the agent search symbols in another branch's graph. `tokensave_branch_diff` compares two branches structurally, showing symbols added, removed, and changed (with signature diffs). `tokensave_branch_list` exposes tracked branches with metadata. +Three new MCP tools make cross-branch work possible without switching your checkout. `tracedecay_branch_search` lets the agent search symbols in another branch's graph. `tracedecay_branch_diff` compares two branches structurally, showing symbols added, removed, and changed (with signature diffs). `tracedecay_branch_list` exposes tracked branches with metadata. -The fallback behavior handles the case where a branch isn't tracked gracefully: the MCP server serves from the nearest ancestor branch's database and appends a warning to every tool response suggesting the user run `tokensave branch add`. This means multi-branch is genuinely opt-in -- everything works without it, just with a single database for all branches. +The fallback behavior handles the case where a branch isn't tracked gracefully: the MCP server serves from the nearest ancestor branch's database and appends a warning to every tool response suggesting the user run `tracedecay branch add`. This means multi-branch is genuinely opt-in -- everything works without it, just with a single database for all branches. -Managing branch databases could easily become a chore, so there's CLI support for the full lifecycle: `tokensave branch list` shows tracked branches with DB sizes and parent info, `tokensave branch remove` drops a single branch, `tokensave branch removeall` clears everything except the default, and `tokensave branch gc` cleans up databases for branches that have been deleted from git. +Managing branch databases could easily become a chore, so there's CLI support for the full lifecycle: `tracedecay branch list` shows tracked branches with DB sizes and parent info, `tracedecay branch remove` drops a single branch, `tracedecay branch removeall` clears everything except the default, and `tracedecay branch gc` cleans up databases for branches that have been deleted from git. ## Token Tracking Gets Serious -The worldwide counter was always there, but v3.5.0 made token tracking granular. Every MCP tool response now appends a `tokensave_metrics: before=N after=M` line showing how many raw-file tokens that specific call avoided. Not a session estimate, not a heuristic -- the actual before-and-after token count for each query. +The worldwide counter was always there, but v3.5.0 made token tracking granular. Every MCP tool response now appends a `tracedecay_metrics: before=N after=M` line showing how many raw-file tokens that specific call avoided. Not a session estimate, not a heuristic -- the actual before-and-after token count for each query. -Two new CLI commands (`tokensave current-counter` and `tokensave reset-counter`) expose a per-project local counter, separate from the lifetime total. The worldwide counter continues to aggregate anonymous totals across all users via a Cloudflare Worker. +Two new CLI commands (`tracedecay current-counter` and `tracedecay reset-counter`) expose a per-project local counter, separate from the lifetime total. The worldwide counter continues to aggregate anonymous totals across all users via a Cloudflare Worker. -The real showpiece is `tokensave monitor`: a global live TUI that shows MCP tool calls from all projects in real time. The implementation uses a shared memory-mapped ring buffer at `~/.tokensave/monitor.mmap`. Multiple MCP server instances (one per project) write concurrently using file locking via `fs2`, and the monitor TUI reads the ring in a `crossterm`-powered terminal UI. Each entry shows the tool suite prefix, the project name, and the tool called, so you can watch activity across multiple projects and multiple tool suites simultaneously. +The real showpiece is `tracedecay monitor`: a global live TUI that shows MCP tool calls from all projects in real time. The implementation uses a shared memory-mapped ring buffer at `~/.tracedecay/monitor.mmap`. Multiple MCP server instances (one per project) write concurrently using file locking via `fs2`, and the monitor TUI reads the ring in a `crossterm`-powered terminal UI. Each entry shows the tool suite prefix, the project name, and the tool called, so you can watch activity across multiple projects and multiple tool suites simultaneously. ## The Embedding Decision Version 4.0.0 also made a deliberate subtraction: the vector/embedding module was removed entirely. The `src/vectors/` directory, the `enable_embeddings` config field, the ONNX model dependency -- all gone. -The replacement is the `keywords` parameter on `tokensave_context`. Instead of embedding symbols during indexing and matching by cosine similarity at query time, the calling agent simply provides synonyms. When you ask "how does authentication work?", the agent passes `keywords: ["login", "session", "credential", "token"]` and the context builder runs an FTS5 search for each keyword independently. +The replacement is the `keywords` parameter on `tracedecay_context`. Instead of embedding symbols during indexing and matching by cosine similarity at query time, the calling agent simply provides synonyms. When you ask "how does authentication work?", the agent passes `keywords: ["login", "session", "credential", "token"]` and the context builder runs an FTS5 search for each keyword independently. The trade-offs are real. Embeddings catch conceptual matches with zero lexical overlap -- "authentication" would find a function called `guardianGateway` because the embedding model learned that association from training data. Agent-driven keywords can't do that if the agent doesn't know the codebase's naming conventions. @@ -165,29 +165,29 @@ Version 3.4.0 added decorator and annotation extraction across 12 languages. Rus ## The Visualizer -`tokensave visualize` serves an interactive code graph in the browser via Cytoscape.js. Right-clicking any node opens a context menu with callers, callees, call graph, and impact analysis actions. It's useful for getting a spatial sense of how a codebase is structured, especially for onboarding or architecture reviews. The implementation is a single embedded HTML file served by a lightweight HTTP handler in the tokensave binary -- no external dependencies. +`tracedecay visualize` serves an interactive code graph in the browser via Cytoscape.js. Right-clicking any node opens a context menu with callers, callees, call graph, and impact analysis actions. It's useful for getting a spatial sense of how a codebase is structured, especially for onboarding or architecture reviews. The implementation is a single embedded HTML file served by a lightweight HTTP handler in the tracedecay binary -- no external dependencies. ## Self-Upgrade -Version 3.4.0 introduced `tokensave upgrade`, which downloads the correct platform binary from GitHub releases, stops the daemon, replaces the running executable via `self-replace`, and restarts the daemon. Stable and beta channels are tracked independently: a beta build only sees beta releases and vice versa. `tokensave channel` shows the current channel, and `tokensave channel beta` or `tokensave channel stable` switches between them. +Version 3.4.0 introduced `tracedecay upgrade`, which downloads the correct platform binary from GitHub releases, stops the daemon, replaces the running executable via `self-replace`, and restarts the daemon. Stable and beta channels are tracked independently: a beta build only sees beta releases and vice versa. `tracedecay channel` shows the current channel, and `tracedecay channel beta` or `tracedecay channel stable` switches between them. -The upgrade path had its own set of platform bugs. Homebrew installs use relative symlinks (e.g., `../Cellar/tokensave/4.1.1/bin/tokensave`), and `self-replace` resolves them via `fs::read_link`, which returns the raw relative target. Subsequent operations resolve that relative path from CWD instead of the symlink's parent, causing ENOENT. The fix: canonicalize the exe path before passing it to self-replace. Twelve regression tests cover every symlink layout we've seen in the wild. +The upgrade path had its own set of platform bugs. Homebrew installs use relative symlinks (e.g., `../Cellar/tracedecay/4.1.1/bin/tracedecay`), and `self-replace` resolves them via `fs::read_link`, which returns the raw relative target. Subsequent operations resolve that relative path from CWD instead of the symlink's parent, causing ENOENT. The fix: canonicalize the exe path before passing it to self-replace. Twelve regression tests cover every symlink layout we've seen in the wild. ## Windows: Still Harder Than It Looks -The Windows saga continued through the 3.x series. The daemon autostart check via `daemon-kit` used a file-path probe that returns `None` on Windows, so `is_service_installed()` never detected an existing service. Running `tokensave daemon --enable-autostart` twice would error with "service already exists." Non-elevated terminals couldn't register the Windows Service at all. Each of these required its own fix: dispatching to the SCM query, idempotent stop-and-remove before re-creating, and automatic UAC elevation for just the service installation step. +The Windows saga continued through the 3.x series. The daemon autostart check via `daemon-kit` used a file-path probe that returns `None` on Windows, so `is_service_installed()` never detected an existing service. Running `tracedecay daemon --enable-autostart` twice would error with "service already exists." Non-elevated terminals couldn't register the Windows Service at all. Each of these required its own fix: dispatching to the SCM query, idempotent stop-and-remove before re-creating, and automatic UAC elevation for just the service installation step. -The zip handling in `tokensave upgrade` hit a memorable bug. Users on v3.4.4 couldn't self-upgrade because the `self_update` crate had pulled in the `zip` crate without deflate support -- meaning it could only open uncompressed zip entries. But the release archives use Deflate compression. The upgrade tool was guaranteed to fail on the archives it was trying to download. Version 3.4.5 replaced `self_update` entirely with a direct implementation using `ureq` for downloads and `zip` v8 with explicit deflate support. +The zip handling in `tracedecay upgrade` hit a memorable bug. Users on v3.4.4 couldn't self-upgrade because the `self_update` crate had pulled in the `zip` crate without deflate support -- meaning it could only open uncompressed zip entries. But the release archives use Deflate compression. The upgrade tool was guaranteed to fail on the archives it was trying to download. Version 3.4.5 replaced `self_update` entirely with a direct implementation using `ureq` for downloads and `zip` v8 with explicit deflate support. ## The Infrastructure That Grew Around It Some features don't fit neatly into "languages" or "tools." They're the connective tissue. -**The doctor command** (1.5.1, expanded continuously) runs a comprehensive health check: binary version, project index integrity, global database, user config, daemon status, agent integrations, MCP server registration, hook configuration (now validating that each hook uses the correct subcommand), permissions, prompt rules, and network connectivity. When it finds a broken hook, it auto-repairs it. As TokenSave grew more complex, this became essential. +**The doctor command** (1.5.1, expanded continuously) runs a comprehensive health check: binary version, project index integrity, global database, user config, daemon status, agent integrations, MCP server registration, hook configuration (now validating that each hook uses the correct subcommand), permissions, prompt rules, and network connectivity. When it finds a broken hook, it auto-repairs it. As TraceDecay grew more complex, this became essential. -**The worldwide counter** (1.4.0) aggregates anonymous token-savings counts across all TokenSave users via a Cloudflare Worker. `tokensave status` shows three tiers: project, all local projects, and worldwide. The counter is opt-out. Getting the accounting right took a few iterations: 1.5.4 fixed an inflation bug, and the periodic flush interval moved from shutdown-only to every 30 seconds during MCP sessions. +**The worldwide counter** (1.4.0) aggregates anonymous token-savings counts across all TraceDecay users via a Cloudflare Worker. `tracedecay status` shows three tiers: project, all local projects, and worldwide. The counter is opt-out. Getting the accounting right took a few iterations: 1.5.4 fixed an inflation bug, and the periodic flush interval moved from shutdown-only to every 30 seconds during MCP sessions. -**MCP resources** (3.3.0) expose four read-only resources via the standard `resources/list` and `resources/read` protocol: `tokensave://status`, `tokensave://files`, `tokensave://overview`, and `tokensave://branches`. MCP annotations (`readOnlyHint`, `title`) distinguish read-only tools from edit and session-memory tools, and `anthropic/alwaysLoad` on the three core tools lets clients load the essentials without a tool-search round-trip. +**MCP resources** (3.3.0) expose four read-only resources via the standard `resources/list` and `resources/read` protocol: `tracedecay://status`, `tracedecay://files`, `tracedecay://overview`, and `tracedecay://branches`. MCP annotations (`readOnlyHint`, `title`) distinguish read-only tools from edit and session-memory tools, and `anthropic/alwaysLoad` on the three core tools lets clients load the essentials without a tool-search round-trip. **Atomic config writes** (3.0.1) protect agent config files from corruption. A `.bak` copy is created before any modification, new content is written to a `.new` sibling file, and an atomic `rename(2)` moves it into place. A crash during install can't leave a half-written config. Twenty regression tests cover the full lifecycle. @@ -195,9 +195,9 @@ Some features don't fit neatly into "languages" or "tools." They're the connecti ## Search Quality: Five Ported Improvements -The upstream CodeGraph project spent 55 commits refining its search and context-building pipeline. Five of those ideas were worth porting to tokensave, each targeting a different failure mode. +The upstream CodeGraph project spent 55 commits refining its search and context-building pipeline. Five of those ideas were worth porting to tracedecay, each targeting a different failure mode. -The first was per-file diversity caps. Without them, a single large file with many FTS matches could monopolize the entire context result. A 2,000-line `utils.rs` with 40 functions would place a dozen of them in the top 20 results, crowding out the three functions from `auth.rs` that actually mattered. The fix is a simple post-ranking pass: each file gets at most `max_nodes/3` slots (minimum 3), and excess results go to a spillover list that fills remaining slots. The cap is configurable via the `max_per_file` parameter on `tokensave_context`. +The first was per-file diversity caps. Without them, a single large file with many FTS matches could monopolize the entire context result. A 2,000-line `utils.rs` with 40 functions would place a dozen of them in the top 20 results, crowding out the three functions from `auth.rs` that actually mattered. The fix is a simple post-ranking pass: each file gets at most `max_nodes/3` slots (minimum 3), and excess results go to a spillover list that fills remaining slots. The cap is configurable via the `max_per_file` parameter on `tracedecay_context`. The second was exact name match supplementing. FTS5's BM25 ranking optimizes for term frequency and inverse document frequency, which is exactly wrong when a user searches for "Parser" and the function named `Parser` gets buried below "HTMLParserFactory" and "parseStreamConfig" because they have more matching terms in their docstrings. A supplementary `WHERE LOWER(name) IN (...)` query now injects perfect name matches with a high base score, guaranteeing they compete in the final ranking. @@ -243,35 +243,27 @@ Meanwhile, the MCP server was always running anyway. Whenever an agent is attach Multiple agents on the same project converge correctly through the existing per-project sync lock plus a `sync_if_stale_silent` peer-coordination call. When two MCP servers see a file change at nearly the same moment, only one of them holds the lock and performs the sync; the other detects that the index is already fresh and skips. No new coordination primitive was needed -- the locking that already protected concurrent writes turned out to be sufficient for concurrent watchers, too. -The config field renames accordingly: `daemon_debounce` becomes `watcher_debounce`. The rename is friendly in both directions. Existing `~/.tokensave/config.toml` files load fine because the new field is annotated with `#[serde(alias = "daemon_debounce")]`, and the next command that mutates the config (anything that writes it back -- adding an agent, switching channels, etc.) silently rewrites the file using the new name. There is no manual migration step; users who never edit their config will see the new name appear naturally the first time the file is touched. +The config field renames accordingly: `daemon_debounce` becomes `watcher_debounce`. The rename is friendly in both directions. Existing `~/.tracedecay/config.toml` files load fine because the new field is annotated with `#[serde(alias = "daemon_debounce")]`, and the next command that mutates the config (anything that writes it back -- adding an agent, switching channels, etc.) silently rewrites the file using the new name. There is no manual migration step; users who never edit their config will see the new name appear naturally the first time the file is touched. ### Breaking -- The `tokensave daemon` subcommand is removed. The autostart flags (`--enable-autostart`, `--disable-autostart`), the foreground mode, and all `daemon-kit`-backed service registration are gone. +- The pre-rename daemon subcommand is removed. The autostart flags (`--enable-autostart`, `--disable-autostart`), the foreground mode, and all `daemon-kit`-backed service registration are gone. - `UserConfig::daemon_debounce` is now `UserConfig::watcher_debounce`. This is a programmatic-API break. The `user_config` module is `pub`, so any external crate that constructs a `UserConfig` literal referencing the old field name will fail to compile. The serde alias only covers deserialization from TOML, not Rust struct literals. - `McpServer::new` now returns `Arc` instead of `Self`. The embedded watcher task captures a `Weak` so it cannot extend the server's lifetime, which forces the constructor to own the strong `Arc`. External embedders that previously bound the return value (e.g. `let server = McpServer::new(cg, None).await;`) keep compiling, but anything that destructured by value or stored the server in a non-`Arc` field needs to adapt. ### Migration -Users who previously ran `tokensave daemon --enable-autostart` need to remove the orphaned service. The command depends on platform: - -- **macOS:** `launchctl unload ~/Library/LaunchAgents/com.tokensave.daemon.plist`, then `rm ~/Library/LaunchAgents/com.tokensave.daemon.plist`. -- **Linux:** `systemctl --user disable --now tokensave-daemon`, then remove the unit file from `~/.config/systemd/user/`. -- **Windows:** `sc.exe delete tokensave-daemon` from an elevated terminal. - -If you don't remember whether you registered autostart, these discovery commands will tell you: - -- **macOS:** `launchctl list | grep tokensave` -- **Linux:** `systemctl --user list-units | grep tokensave` -- **Windows:** `sc.exe query state= all | findstr -i tokensave` - -CLI-only users (anyone who runs `tokensave` commands without an attached agent) lose automatic background syncing. The recommended replacement is a git post-commit hook -- a starter script lives at `scripts/post-commit` in the TraceDecay repo. Drop it into `.git/hooks/post-commit` in each project where you want the index to update on commit. +CLI-only users (anyone who runs `tracedecay` commands without an attached agent) +lose automatic background syncing. The recommended replacement is a git +post-commit hook -- a starter script lives at `scripts/post-commit` in the +TraceDecay repo. Drop it into `.git/hooks/post-commit` in each project where you +want the index to update on commit. ### What this means for you -If you use TokenSave through an AI coding agent (Claude Code, Codex CLI, OpenCode, Gemini, Cursor, Zed, Cline, Roo Code, Copilot, Antigravity, Kilo, Kiro, Kimi, or Vibe), nothing changes from your perspective except that you no longer have a background process to manage. The MCP server keeps your index fresh while you work, just without a separate lifecycle to debug. +If you use TraceDecay through an AI coding agent (Claude Code, Codex CLI, OpenCode, Gemini, Cursor, Zed, Cline, Roo Code, Copilot, Antigravity, Kilo, Kiro, Kimi, or Vibe), nothing changes from your perspective except that you no longer have a background process to manage. The MCP server keeps your index fresh while you work, just without a separate lifecycle to debug. -If you use TokenSave purely from the CLI without an attached agent, this is a real regression: the index won't update on its own anymore. Install the post-commit hook, or run `tokensave sync` manually before queries. +If you use TraceDecay purely from the CLI without an attached agent, this is a real regression: the index won't update on its own anymore. Install the post-commit hook, or run `tracedecay sync` manually before queries. **A 6.1.0 postscript.** The embedded watcher introduced here did not survive long. On large monorepos it registered OS-level watches on nested `node_modules`/`target`/`dist` trees that the top-level ignore filter missed, producing event storms and unbounded memory growth (one report reached 19 GB). 6.1.0 removed the watcher entirely — along with the `notify-debouncer-full` dependency — and replaced it with an on-demand staleness check at the top of every MCP tool call (30-second cooldown) plus a catch-up sync when the server connects. The reaction is no longer instant-on-save, but resource use is bounded, and concurrent multi-agent work is expected to use git worktrees rather than a shared watched directory. @@ -285,7 +277,7 @@ The language coverage is broad — the functional family (Haskell, Elixir, OCaml The agent ecosystem keeps growing too. Every month brings a new AI coding tool with its own configuration format. The trait-based architecture handles this well, but the real challenge is keeping more than a dozen integration paths tested and working across three operating systems. -TokenSave started as a way to make Claude Code stop reading the same files over and over. It's become something broader: a semantic index that any AI coding agent can query, refreshing its index on demand, understanding more than fifty languages across optional per-branch databases, tracking its own impact down to the individual tool call, and shipping as a single native binary that upgrades itself. The core insight hasn't changed. Give the AI a graph instead of making it grep. Everything else followed from that. +TraceDecay started as a way to make Claude Code stop reading the same files over and over. It's become something broader: a semantic index that any AI coding agent can query, refreshing its index on demand, understanding more than fifty languages across optional per-branch databases, tracking its own impact down to the individual tool call, and shipping as a single native binary that upgrades itself. The core insight hasn't changed. Give the AI a graph instead of making it grep. Everything else followed from that. --- diff --git a/docs/TREESITTERS-RENAME-CONSTRAINTS.md b/docs/TREESITTERS-RENAME-CONSTRAINTS.md index 0bf5ea77..63abd529 100644 --- a/docs/TREESITTERS-RENAME-CONSTRAINTS.md +++ b/docs/TREESITTERS-RENAME-CONSTRAINTS.md @@ -1,13 +1,13 @@ -# `tokensave-large-treesitters` — Rename Constraints +# `tracedecay-large-treesitters` — Rename Constraints > Finding for Kanban task `t_4070b0b0`: why the grammar bundle dependency must keep -> its legacy `tokensave-*` name after the tracedecay rebrand, what would have to +> its legacy `tracedecay-*` name after the tracedecay rebrand, what would have to > change before a rename is safe, and the wording to record in the compatibility > policy. Every claim below is backed by a `file:line` reference. ## TL;DR -**Do not rename `tokensave-large-treesitters` (or its `-medium` / `-lite` siblings) +**Do not rename `tracedecay-large-treesitters` (or its `-medium` / `-lite` siblings) for the foreseeable future.** The name is owned and published by an **external upstream** (`aovestdipaperino`), the three crates form a hard internal dependency chain whose names are baked into the upstream's *own* manifests, and the package is @@ -21,45 +21,45 @@ migration (`docs/RUST-PARSER-MIGRATION.md`), not renaming them. | Aspect | Value | Source | |---|---|---| -| Declared in tracedecay | `tokensave-large-treesitters = { version = "0.5.0", git = "https://github.com/aovestdipaperino/tokensave-large-treesitters" }` | `Cargo.toml:108` | +| Declared in tracedecay | `tracedecay-large-treesitters = { version = "0.5.0", git = "https://github.com/aovestdipaperino/tracedecay-large-treesitters" }` | `Cargo.toml:108` | | Source | **git** (upstream), pinned to rev `0fc87352…` | `Cargo.lock:4472-4475` | -| Rust crate / lib name | `tokensave_large_treesitters` | imports at `src/extraction/ts_provider.rs:27`, `src/extraction/markdown_extractor.rs:70,139` | -| Internal dep chain | `large` → depends on `tokensave-medium-treesitters` → depends on `tokensave-lite-treesitters` | `Cargo.lock:4480`, `Cargo.lock:4531` | +| Rust crate / lib name | `tracedecay_large_treesitters` | imports at `src/extraction/ts_provider.rs:27`, `src/extraction/markdown_extractor.rs:70,139` | +| Internal dep chain | `large` → depends on `tracedecay-medium-treesitters` → depends on `tracedecay-lite-treesitters` | `Cargo.lock:4480`, `Cargo.lock:4531` | | Source of `medium` / `lite` | **crates.io registry** (0.2.0 each), not git | `Cargo.lock:4509-4512`, `Cargo.lock:4525-4529` | | Public registry history | Published as `0.3.2`, `0.4.0` on crates.io before the switch to a git pin | `CHANGELOG.md:530,552` | | Build-script coupling | Upstream's build script compiles vendored C/C++ grammars under `.cargo/config.toml` `CFLAGS=-DNDEBUG` | `CHANGELOG.md:582-583` | | API surface tracedecay consumes | `all_languages()`, `markdown::LANGUAGE`, `markdown::inline::LANGUAGE` | `ts_provider.rs:27`, `markdown_extractor.rs:70,139` | -The project *itself* rebranded tokensave → tracedecay (lib/bin name is `tracedecay`, +The project *itself* rebranded tracedecay → tracedecay (lib/bin name is `tracedecay`, `Cargo.toml:97,100`; version reset at `CHANGELOG.md:12`) and carries extensive -pre-rebrand compatibility shims (legacy `.tokensave/` dir at `src/config.rs:16`, -legacy archive names at `src/cloud.rs:140`, legacy `TOKENSAVE_*` env at +pre-rebrand compatibility shims (legacy `.tracedecay/` dir at `src/config.rs:16`, +legacy archive names at `src/cloud.rs:140`, legacy `TRACEDECAY_*` env at `src/config.rs:152`). **Only the grammar-bundle dependency retains the legacy -`tokensave-` prefix — and that is intentional** (`AGENTS.md:20`). +`tracedecay-` prefix — and that is intentional** (`AGENTS.md:20`). ## Why it must keep the name (constraints) 1. **External upstream ownership.** The crate name, the git repo URL, the crates.io package, the build script, and the vendored grammars are all owned by `aovestdipaperino`. `AGENTS.md:20` is explicit: *"never push or open PRs to the - aovestdipaperino upstream; only the tokensave-large-treesitters dependency + aovestdipaperino upstream; only the tracedecay-large-treesitters dependency intentionally stays pointed at upstream."* We cannot unilaterally change the crate's published name. 2. **Three-tier name coupling in the upstream's own manifests.** The `large` crate - declares a dependency named `tokensave-medium-treesitters` (`Cargo.lock:4480`), - which in turn declares `tokensave-lite-treesitters` (`Cargo.lock:4531`). You + declares a dependency named `tracedecay-medium-treesitters` (`Cargo.lock:4480`), + which in turn declares `tracedecay-lite-treesitters` (`Cargo.lock:4531`). You cannot rename `large` in isolation — the names are part of the upstream's internal `Cargo.toml`, not ours. -3. **Public registry package with external consumers.** `tokensave-large-treesitters` +3. **Public registry package with external consumers.** `tracedecay-large-treesitters` is a public crates.io crate (`CHANGELOG.md:530,552`). Other downstream users depend on the exact name; a tracedecay-published rename would be a *different* package. 4. **`package = "..."` aliasing does not solve this.** Cargo's dependency-rename field only changes how *tracedecay* refers to the crate locally. The crate - identity on crates.io and the git repo remain `tokensave-large-treesitters`, and + identity on crates.io and the git repo remain `tracedecay-large-treesitters`, and the `large → medium → lite` registry names are still fixed. It is cosmetic, not a rename. @@ -92,10 +92,10 @@ In **either** path, the mechanical changes in tracedecay itself are the same: update `Cargo.toml:108` (and any sibling `-medium`/`-lite` references that may appear if we ever declare them directly), the `Cargo.lock` pin, the three import sites (`ts_provider.rs:27`, `markdown_extractor.rs:70,139`), and any CI/release -scripts that hardcode the `aovestdipaperino/tokensave-large-treesitters` git URL. +scripts that hardcode the `aovestdipaperino/tracedecay-large-treesitters` git URL. **Real long-term exit:** `docs/RUST-PARSER-MIGRATION.md:219` plans to *drop* the -`tokensave-large/medium/lite` dependencies entirely in favour of in-tree Rust +`tracedecay-large/medium/lite` dependencies entirely in favour of in-tree Rust parsers. Eliminating the dependency removes the naming question outright and is the recommended end state rather than a rename. @@ -104,26 +104,26 @@ recommended end state rather than a rename. Add the following (verbatim or adapted) to the compatibility policy / `AGENTS.md` `Learned Workspace Facts`, expanding on the existing one-liner at `AGENTS.md:20`: -> **`tokensave-large-treesitters` is an external upstream dependency and must not be +> **`tracedecay-large-treesitters` is an external upstream dependency and must not be > renamed, vendored, or forked.** The crate name, git repo -> (`aovestdipaperino/tokensave-large-treesitters`), crates.io publication, build +> (`aovestdipaperino/tracedecay-large-treesitters`), crates.io publication, build > script, and vendored grammars are owned by the upstream and intentionally kept on -> the legacy `tokensave-*` name despite the tracedecay rebrand. The `large → medium +> the legacy `tracedecay-*` name despite the tracedecay rebrand. The `large → medium > → lite` tier names are coupled inside the upstream's own manifests, so the three > crates rise and fall together. Renaming is only safe if (a) the upstream renames > all three crates and republishes, or (b) the project explicitly adopts a > maintained fork as a policy change. Until then, keep the exact name in -> `Cargo.toml`, keep import sites as `tokensave_large_treesitters::*`, and prefer the +> `Cargo.toml`, keep import sites as `tracedecay_large_treesitters::*`, and prefer the > Rust-parser migration (`docs/RUST-PARSER-MIGRATION.md`) as the path to eventually > removing the dependency rather than renaming it. ## Verification trail - `Cargo.toml:108` — git dep declaration (current, `0.5.0`, aovestdipaperino). -- `Cargo.lock:4472-4506` — `large` 0.5.0 git source + deps incl. `tokensave-medium-treesitters`. +- `Cargo.lock:4472-4506` — `large` 0.5.0 git source + deps incl. `tracedecay-medium-treesitters`. - `Cargo.lock:4509-4523` — `lite` 0.2.0 from crates.io. - `Cargo.lock:4525-4531` — `medium` 0.2.0 from crates.io, depends on `lite`. -- `src/extraction/ts_provider.rs:27` — `tokensave_large_treesitters::all_languages()`. +- `src/extraction/ts_provider.rs:27` — `tracedecay_large_treesitters::all_languages()`. - `src/extraction/markdown_extractor.rs:70,139` — `markdown::inline::LANGUAGE` / `markdown::LANGUAGE`. - `CHANGELOG.md:530,533,552` — prior crates.io publication history (0.3.2 / 0.4.0). - `CHANGELOG.md:905` — confirms `large` includes `medium` + `lite`. diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index c2fb396c..c4a87c0e 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -72,7 +72,7 @@ cd /path/to/your/project tracedecay init ``` -TraceDecay will scan every supported source file, extract symbols (functions, classes, methods, imports, type relationships, complexity metrics), and store everything in the active project store. Repo-local projects use `.tracedecay/tracedecay.db`; legacy `.tokensave/` directories are still honored. Profile-backed projects keep graph/session artifacts in a private profile shard. You'll see a spinner with file-by-file progress and an ETA. +TraceDecay will scan every supported source file, extract symbols (functions, classes, methods, imports, type relationships, complexity metrics), and store everything in the active project store. Graph/session artifacts live in a private user-level profile shard scoped to this project; the repo `.tracedecay/` directory is only a lightweight marker/config directory. You'll see a spinner with file-by-file progress and an ETA. Once it finishes, run `tracedecay status` to see what was indexed: @@ -159,7 +159,7 @@ tracedecay gitignore on # enable (default) tracedecay gitignore off # disable — index everything ``` -Don't forget to add `.tracedecay` to your `.gitignore` so repo-local databases or enrollment markers do not get committed: +Don't forget to add `.tracedecay` to your `.gitignore` so enrollment markers do not get committed: ```bash echo .tracedecay >> .gitignore @@ -471,29 +471,6 @@ chmod +x .git/hooks/post-commit Or run `tracedecay sync` manually when you need a fresh index. -### Upgrading from 5.x - -The standalone `tokensave daemon` command and its system-service autostart -were removed in 6.0.0 (when the project was still named TokenSave). If you had -a daemon autostart installed under 5.x, remove it manually. Note the service -names below use the old `tokensave` branding because that is what 5.x installed. - -If you don't remember the exact service/plist name, list them first: - -- macOS: `launchctl list | grep tokensave` -- Linux: `systemctl --user list-units | grep tokensave` -- Windows: `sc.exe query state= all | findstr -i tokensave` - -Then remove the entry matching your install: - -- macOS: `launchctl unload ~/Library/LaunchAgents/com.tokensave.daemon.plist && rm ~/Library/LaunchAgents/com.tokensave.daemon.plist` -- Linux: `systemctl --user disable --now tokensave-daemon && rm ~/.config/systemd/user/tokensave-daemon.service` -- Windows: `sc.exe delete tokensave-daemon` (from an elevated terminal) - -Once your agent is attached, MCP tool calls keep the index fresh on demand. - ---- - ## Checking Your Setup with Doctor The `doctor` command runs a comprehensive health check: @@ -807,9 +784,9 @@ Repo-local projects create `.tracedecay/` inside each project you index. Profile - `tracedecay.db` — the libSQL database with all symbols, edges, files, and vector embeddings - `sessions.db` and sidecar directories such as response handles, LCM payloads, branch metadata, and dashboard previews when those features are used -Add `.tracedecay` to your `.gitignore` so repo-local databases or enrollment markers are not committed. +Add `.tracedecay` to your `.gitignore` so enrollment markers are not committed. -Projects indexed before the TraceDecay rename may still have a `.tokensave/` directory with a `tokensave.db` inside — it is still honored as a fallback, so you don't need to re-index. +Projects indexed before the TraceDecay rename should be migrated into the user-level profile store; runtime storage no longer falls back to `.tracedecay/`. ### Per-user: `~/.tracedecay/` diff --git a/docs/dashboard-port-handoff.md b/docs/dashboard-port-handoff.md index f2e3b7ba..84716d12 100644 --- a/docs/dashboard-port-handoff.md +++ b/docs/dashboard-port-handoff.md @@ -3,7 +3,7 @@ Status: integration gate passed (2026-06-10) — see "Final integration gate" at the end of this doc. Phase 3 complete (curation implemented; hard-delete semantics, no archive). Nothing committed; everything is in the working trees -of `/home/zack/projects/tokensave` and `/home/zack/hermes-agent`. +of `/home/zack/projects/tracedecay` and `/home/zack/hermes-agent`. Hermes integration (2026-06-10): boot failure fixed, the tracedecay-backed dashboard is render-verified inside a live `hermes dashboard`, and the old @@ -128,7 +128,7 @@ tracedecay dashboard [--path ] [--host 127.0.0.1] [--port 7341] ### Holographic memory (`/api/plugins/holographic`, `src/dashboard/memory_api.rs`) Backed by the **project DB** (`.tracedecay/tracedecay.db`; a legacy -`.tokensave/` directory is still honored as a fallback). Old backend was +`.tracedecay/` directory is still honored as a fallback). Old backend was Hermes `~/.hermes/memory_store.db` (`facts`/`entities`/`memory_banks`). | Route | Old source | New source | Status | @@ -152,7 +152,7 @@ used `cat:`); `dim` ← `hrr_dim`; timestamps are unix epoch seconds ### LCM (`/api/plugins/hermes-lcm`, `src/dashboard/lcm_api.rs`) Backed by the **global DB** (`~/.tracedecay/global.db`; overridable via -`TRACEDECAY_GLOBAL_DB` — legacy `TOKENSAVE_*` variable names are still honored +`TRACEDECAY_GLOBAL_DB` — legacy `TRACEDECAY_*` variable names are still honored as fallbacks). Old backend was `$HERMES_HOME/lcm.db` (`messages`/`summary_nodes` + FTS). @@ -343,16 +343,16 @@ tracedecay-side curation/archive Phase 3 work.) All work here is confined to ### How the dashboard was launched Commands below are reproduced verbatim as run on 2026-06-10, pre-rebrand -(old `tokensave` binary and `TOKENSAVE_*` variable names; both are still +(old `tracedecay` binary and `TRACEDECAY_*` variable names; both are still honored as legacy fallbacks): ```bash -# Build the working tokensave binary used by the wrapper (see note below). -# (target/debug/tokensave already had the dashboard subcommand.) +# Build the working tracedecay binary used by the wrapper (see note below). +# (target/debug/tracedecay already had the dashboard subcommand.) cd /home/zack/hermes-agent -TOKENSAVE_BIN=/home/zack/projects/tokensave/target/debug/tokensave \ -TOKENSAVE_DASHBOARD_PROJECT=/home/zack/projects/tokensave \ +TRACEDECAY_BIN=/home/zack/projects/tracedecay/target/debug/tracedecay \ +TRACEDECAY_DASHBOARD_PROJECT=/home/zack/projects/tracedecay \ HERMES_HOME=/home/zack/.hermes \ uv run --no-sync hermes dashboard --port 9215 --host 127.0.0.1 --no-open --skip-build ``` @@ -375,22 +375,22 @@ uv run --no-sync hermes dashboard --port 9215 --host 127.0.0.1 --no-open --skip- project 10 / decision 7), HRR coverage rings (100%), trust distribution, facts/day chart, and fact/entity/bank lists. - **LCM** internal tab renders the first-class empty state ("No LCM sessions - indexed yet") for the local `~/.tokensave/global.db` (zero LCM rows at the + indexed yet") for the local `~/.tracedecay/global.db` (zero LCM rows at the time; pre-rebrand path), with the `Database detected` badge. -- Spawn confirmed: with no `TOKENSAVE_DASHBOARD_URL` set (pre-rebrand variable +- Spawn confirmed: with no `TRACEDECAY_DASHBOARD_URL` set (pre-rebrand variable name), the wrapper spawned - `tokensave dashboard --host 127.0.0.1 --port 0 --path - /home/zack/projects/tokensave` as a **child of the hermes python process** + `tracedecay dashboard --host 127.0.0.1 --port 0 --path + /home/zack/projects/tracedecay` as a **child of the hermes python process** (verified via PPID), and `/api/plugins/hermes-intelligence/capabilities` returned `mode: "hermes"`. -Note on the binary (then named `tokensave`): a concurrent effort in this repo +Note on the binary (then named `tracedecay`): a concurrent effort in this repo was mid-flight on a curation/archive **v13 schema migration**. After it rebuilt -`target/debug/tokensave` (09:03), fresh spawns failed while opening the v12 +`target/debug/tracedecay` (09:03), fresh spawns failed while opening the v12 project DB (`v13: failed to add archive columns to memory_facts: SQLite failure: near "EXISTS": syntax error`) — its WIP, not the wrapper's. The live post-retirement screenshots were therefore taken with the wrapper pointed at a -healthy standalone server via `TOKENSAVE_DASHBOARD_URL=http://127.0.0.1:7350` +healthy standalone server via `TRACEDECAY_DASHBOARD_URL=http://127.0.0.1:7350` (the task-sanctioned external-mode fallback). Spawn mode itself was verified working earlier with the pre-rebuild binary. Once the v13 migration lands cleanly, spawn mode needs no wrapper changes. @@ -533,9 +533,9 @@ conservatism backstop, and callers can pass a higher `threshold` / ## Remaining stubbed / known gaps (post Phase 3) 1. LCM data scope: MOSTLY RESOLVED — the standalone dashboard now serves the - project-local `.tracedecay/sessions.db` (where transcript ingest writes), - with `TRACEDECAY_GLOBAL_DB` pinning an explicit store (also the path for - hermes-profile stores: point it at `/.tracedecay/sessions.db`). + resolved project session store (user-level profile shard by default, local + only for explicit/legacy stores), with `TRACEDECAY_GLOBAL_DB` pinning an + explicit test/smoke store. Remaining: an in-UI store *switcher* for browsing multiple stores. 2. The wrapper picks the project root from `TRACEDECAY_DASHBOARD_PROJECT` or Hermes' cwd — no per-request/workspace project selection. @@ -569,17 +569,15 @@ conservatism backstop, and callers can pass a higher `threshold` / the holographic_plus **memory provider** (tools, curator, retrieval, `on_session_end`) is fully intact. See the Hermes live-render section below. -- [ ] LCM: surface provider/profile selection (project-local vs global vs - hermes-profile stores); consider storing real `token_estimate`, +- [ ] LCM: surface provider/profile selection (resolved project stores vs + global override stores); consider storing real `token_estimate`, `tool_name`, `pinned` in `lcm_raw_messages` so the session drawer stops approximating. - 2026-06-10 PARTIAL — project-local vs global selection shipped. The - dashboard now serves the project's `.tracedecay/sessions.db` (where - Cursor hooks + the hookless-agent catch-up sweep actually ingest) - instead of the always-empty `~/.tracedecay/global.db`; a - `TRACEDECAY_GLOBAL_DB` override still pins the store (smoke harness / - Hermes wrapper contract, which is also the path for hermes-profile - stores: point the override at `/.tracedecay/sessions.db`). + dashboard now serves the resolved project session store (where Cursor + hooks + the hookless-agent catch-up sweep actually ingest) instead of + the always-empty `~/.tracedecay/global.db`; a `TRACEDECAY_GLOBAL_DB` + override still pins the store for smoke harnesses. Additive `storage_scope` field on every LCM payload + `lcm_scope` in capabilities; LCM header shows "Project store"/"Global store". `tracedecay dashboard` startup now spawns the same detached catch-up @@ -629,7 +627,7 @@ conservatism backstop, and callers can pass a higher `threshold` / ```bash # 1. Build UI assets (required before cargo build when UI changed) -cd /home/zack/projects/tokensave/dashboard && npm install && npm run build +cd /home/zack/projects/tracedecay/dashboard && npm install && npm run build # Optional smoke checks (browser automation) # Empty-state LCM: @@ -638,13 +636,13 @@ TRACEDECAY_GLOBAL_DB=/tmp/tracedecay-dashboard-lcm-empty.db npm run smoke -- --e TRACEDECAY_GLOBAL_DB=/tmp/tracedecay-dashboard-lcm-nonempty.db npm run smoke -- --expect-lcm=non-empty # 2. Build + run standalone -cd /home/zack/projects/tokensave +cd /home/zack/projects/tracedecay cargo build --bin tracedecay ./target/debug/tracedecay dashboard # http://127.0.0.1:7341/ # 3. Hermes-hosted (wrapper spawns the server automatically) -TRACEDECAY_BIN=/home/zack/projects/tokensave/target/debug/tracedecay \ -TRACEDECAY_DASHBOARD_PROJECT=/home/zack/projects/tokensave \ +TRACEDECAY_BIN=/home/zack/projects/tracedecay/target/debug/tracedecay \ +TRACEDECAY_DASHBOARD_PROJECT=/home/zack/projects/tracedecay \ hermes dashboard # → "TraceDecay" tab (named "Hermes Intelligence" at port time) ``` diff --git a/docs/dashboard.md b/docs/dashboard.md index 7745ed23..212555d7 100644 --- a/docs/dashboard.md +++ b/docs/dashboard.md @@ -80,7 +80,7 @@ This format is stable and used by wrapper tools (like the Hermes plugin) to disc | Variable | Description | |----------|-------------| -| `TRACEDECAY_GLOBAL_DB` | Pin the LCM session store to an explicit database path. When set, it wins over project-local store selection (`storage_scope` becomes `"global"`); when unset, the dashboard serves the project's `.tracedecay/sessions.db` and only falls back to `~/.tracedecay/global.db` if the project store cannot be opened | +| `TRACEDECAY_GLOBAL_DB` | Pin the LCM session store to an explicit database path. When set, it wins over resolved project-store selection (`storage_scope` becomes `"global"`); when unset, the dashboard serves the active project's resolved user-level profile session store. | | `TRACEDECAY_BIN` | Path to the tracedecay binary (used by Hermes wrapper for spawn mode) | | `TRACEDECAY_DASHBOARD_PROJECT` | Project root path for Hermes dashboard spawn mode (defaults to Hermes' cwd) | | `TRACEDECAY_DASHBOARD_URL` | Full URL to an already-running dashboard (Hermes external URL mode) | @@ -89,11 +89,8 @@ This format is stable and used by wrapper tools (like the Hermes plugin) to disc | `TRACEDECAY_MODEL_PRICES_PATH` | Override the on-disk model-price cache location (default `~/.tracedecay/model-prices.json`; mainly for tests) | | `DISABLE_TRACEDECAY` | Set to `true` to disable the MCP server entirely (exits cleanly without initializing) | -All `TRACEDECAY_*` variables (and `DISABLE_TRACEDECAY`) also accept their -legacy `TOKENSAVE_*` / `DISABLE_TOKENSAVE` spellings as fallbacks; the -`TRACEDECAY_*` name wins when both are set. Likewise, projects indexed before -the rebrand with a `.tokensave/` data directory are still honored as a -fallback wherever `.tracedecay/` paths are mentioned below. +Use `TRACEDECAY_*` variables (and `DISABLE_TRACEDECAY`) for dashboard runtime +configuration. Pre-rename `TRACEDECAY_*` spellings are not runtime fallbacks. --- @@ -254,13 +251,12 @@ Transcript ingest is **per project**, not global: | Store | Path | Written by | `storage_scope` | |-------|------|------------|-----------------| -| Project-local (default) | `/.tracedecay/sessions.db` | All transcript ingest for sessions belonging to that project root | `"project_local"` | -| Hermes profile | `/.tracedecay/sessions.db` | Hermes-side ingest | `"hermes_profile"` | +| User project store (default) | `~/.tracedecay/projects//sessions.db` | All transcript ingest for sessions belonging to that project root | `"profile_sharded"` | | Global | `~/.tracedecay/global.db` | Cross-project registry (project paths, savings ledger) — **no session messages are ingested here** | `"global"` | -The dashboard serves the **project-local store** by default (where Cursor hooks and hookless-agent catch-up sweeps actually ingest). The LCM header shows a **"Project store"** or **"Global store"** badge. Every LCM API payload reports the active store via the additive `path` + `storage_scope` fields. +The dashboard serves the active project's resolved profile store by default. The LCM header shows a **"User project store"** or **"Global store"** badge. Every LCM API payload reports the active store via the additive `path` + `storage_scope` fields. -Setting `TRACEDECAY_GLOBAL_DB` pins the dashboard to an explicit store instead (used by tests, the smoke harness, and the Hermes wrapper, which points it at a Hermes profile's `sessions.db`). When this override is active, `storage_scope` becomes `"global"`. +Setting `TRACEDECAY_GLOBAL_DB` pins the dashboard to an explicit store instead (used by tests and the smoke harness). When this override is active, `storage_scope` becomes `"global"`. #### How Ingest Works Per Tool @@ -268,7 +264,7 @@ Setting `TRACEDECAY_GLOBAL_DB` pins the dashboard to an explicit store instead ( |------|---------| | Cursor | Cursor hooks ingest incrementally at end of turn / stop / session start (subagent transcripts included); no sweep needed | | Claude Code, Codex, Vibe, Cline / Roo / Kilo | No hooks — discovered by a catch-up sweep that scans each tool's home transcript directory (e.g. `~/.codex/sessions`) and ingests sessions whose recorded `cwd`/project matches the served project root | -| Hermes | Hermes-side ingest into the Hermes profile store (not the project store) | +| Hermes | Hermes-side ingest into the same resolved user-level project store as the generated TraceDecay adapter; unpinned profiles use the Hermes profile directory as their project identity | The catch-up sweep runs automatically when the MCP server starts (`tracedecay serve`) and when `tracedecay dashboard` starts with project-local @@ -498,9 +494,9 @@ Returns feature flags and server configuration. Used by the UI and wrappers to d "version": "0.0.2", "mode": "standalone", "project_root": "/home/user/my-project", - "memory_db": "/home/user/my-project/.tracedecay/tracedecay.db", - "lcm_db": "/home/user/my-project/.tracedecay/sessions.db", - "lcm_scope": "project_local", + "memory_db": "/home/user/.tracedecay/projects/proj_1234/tracedecay.db", + "lcm_db": "/home/user/.tracedecay/projects/proj_1234/sessions.db", + "lcm_scope": "profile_sharded", "features": { "memory": true, "lcm": true, @@ -514,7 +510,7 @@ Returns feature flags and server configuration. Used by the UI and wrappers to d **Fields:** - `mode`: `"standalone"` for direct use, `"hermes"` when wrapped by Hermes -- `lcm_db` / `lcm_scope`: The LCM session store being served and its scope (`"project_local"` or `"global"`; see [Storage Scopes](#storage-scopes--where-messages-live)) +- `lcm_db` / `lcm_scope`: The LCM session store being served and its scope (`"profile_sharded"`, `"project_local"`, or `"global"`; see [Storage Scopes](#storage-scopes--where-messages-live)) - `features.memory`: Whether the project database is available - `features.lcm`: Whether the LCM session store is available - `features.curation`: Whether similarity-dedup curation tools are enabled @@ -861,8 +857,8 @@ Summary statistics and recent sessions/nodes. **Response Structure:** ```json { - "path": "/home/user/my-project/.tracedecay/sessions.db", - "storage_scope": "project_local", + "path": "/home/user/.tracedecay/projects/proj_1234/sessions.db", + "storage_scope": "profile_sharded", "exists": true, "overview": { "messages_total": 1500, @@ -904,8 +900,8 @@ Full-text search with facets. **Response:** ```json { - "path": "/home/user/my-project/.tracedecay/sessions.db", - "storage_scope": "project_local", + "path": "/home/user/.tracedecay/projects/proj_1234/sessions.db", + "storage_scope": "profile_sharded", "exists": true, "query": "authentication", "limit": 25, @@ -951,8 +947,8 @@ Get a summary node with its source items. **Response:** ```json { - "path": "/home/user/my-project/.tracedecay/sessions.db", - "storage_scope": "project_local", + "path": "/home/user/.tracedecay/projects/proj_abc/sessions.db", + "storage_scope": "profile_sharded", "exists": true, "node_id": "node-abc", "node": { /* node details */ }, @@ -1311,9 +1307,8 @@ tracedecay dashboard # when `tracedecay serve` or `tracedecay dashboard` starts # - Explicit LCM tool calls -# Check the project session store for rows -ls -la .tracedecay/sessions.db -sqlite3 .tracedecay/sessions.db 'SELECT COUNT(*) FROM lcm_raw_messages' +# Check which project session store is active +tracedecay status --json # The LCM header shows which store is being served ("Project store" / # "Global store") and its path. If it shows the global DB unexpectedly, diff --git a/docs/plans/2026-06-06-holographic-memory-replacement-plan.md b/docs/plans/2026-06-06-holographic-memory-replacement-plan.md index 4e68af18..b0b109d1 100644 --- a/docs/plans/2026-06-06-holographic-memory-replacement-plan.md +++ b/docs/plans/2026-06-06-holographic-memory-replacement-plan.md @@ -2,19 +2,19 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Replace the current `tokensave` cross-session memory implementation with a Rust-native holographic fact memory backed by `amari-holographic`, SQLite, FTS5, trust scoring, and MCP tools inspired by Hermes Holographic. +**Goal:** Replace the current `tracedecay` cross-session memory implementation with a Rust-native holographic fact memory backed by `amari-holographic`, SQLite, FTS5, trust scoring, and MCP tools inspired by Hermes Holographic. -**Architecture:** Keep memory local to `.tokensave/tokensave.db`, but replace `memory_decisions` / `memory_code_areas` as the active model with a fact/entity store. Use `amari-holographic` for the VSA/HRR algebra, SQLite/FTS5 for durable metadata and lexical recall, and a Rust retrieval layer that blends FTS5, token overlap, holographic similarity, trust, and optional temporal decay. Existing memory tools become compatibility wrappers over the new fact store, while new first-class MCP tools expose add/search/probe/related/reason/contradict/update/remove/list/feedback/status behavior. +**Architecture:** Keep memory local to `.tracedecay/tracedecay.db`, but replace `memory_decisions` / `memory_code_areas` as the active model with a fact/entity store. Use `amari-holographic` for the VSA/HRR algebra, SQLite/FTS5 for durable metadata and lexical recall, and a Rust retrieval layer that blends FTS5, token overlap, holographic similarity, trust, and optional temporal decay. Existing memory tools become compatibility wrappers over the new fact store, while new first-class MCP tools expose add/search/probe/related/reason/contradict/update/remove/list/feedback/status behavior. -**Tech Stack:** Rust 2021, `libsql`, `serde`, `serde_json`, `sha2`, `amari-holographic`, `tokio`, FTS5, existing `tokensave` MCP handler patterns. Future Python interop can use PyO3/maturin for Rust-to-Python exports and `pyo3_bindgen` only when Rust needs generated bindings to existing Python modules. +**Tech Stack:** Rust 2021, `libsql`, `serde`, `serde_json`, `sha2`, `amari-holographic`, `tokio`, FTS5, existing `tracedecay` MCP handler patterns. Future Python interop can use PyO3/maturin for Rust-to-Python exports and `pyo3_bindgen` only when Rust needs generated bindings to existing Python modules. --- ## Deep-Dive Findings -The current memory system is intentionally small: `DecisionRecord` and `CodeAreaRecord` live in `src/tokensave.rs`; migrations create `memory_decisions`, `memory_code_areas`, and `memory_decisions_fts`; MCP definitions expose `tokensave_record_decision`, `tokensave_record_code_area`, and `tokensave_session_recall`; tests live in `tests/memory_test.rs` and `tests/mcp_handler_test.rs`. +The current memory system is intentionally small: `DecisionRecord` and `CodeAreaRecord` live in `src/tracedecay.rs`; migrations create `memory_decisions`, `memory_code_areas`, and `memory_decisions_fts`; MCP definitions expose `tracedecay_record_decision`, `tracedecay_record_code_area`, and `tracedecay_session_recall`; tests live in `tests/memory_test.rs` and `tests/mcp_handler_test.rs`. -Hermes Holographic is broader than the current `tokensave` memory model. It stores facts, entities, fact-entity links, trust scores, retrieval counters, optional HRR vectors, category memory banks, and FTS5 indexes. Its tool surface is `fact_store` with actions `add`, `search`, `probe`, `related`, `reason`, `contradict`, `update`, `remove`, and `list`, plus `fact_feedback` for helpful/unhelpful scoring. +Hermes Holographic is broader than the current `tracedecay` memory model. It stores facts, entities, fact-entity links, trust scores, retrieval counters, optional HRR vectors, category memory banks, and FTS5 indexes. Its tool surface is `fact_store` with actions `add`, `search`, `probe`, `related`, `reason`, `contradict`, `update`, `remove`, and `list`, plus `fact_feedback` for helpful/unhelpful scoring. Hermes' Python HRR implementation uses deterministic SHA-256 phase atoms, phase-add binding, phase-subtract unbinding, circular-mean bundling, and phase cosine similarity. `amari-holographic` gives Rust-native binding, bundling, unbinding, similarity, `HolographicMemory`, `RetrievalResult`, `CapacityInfo`, and resonator cleanup, so the math should be adopted rather than ported. @@ -34,7 +34,7 @@ The plan deliberately avoids a feature flag. Holographic memory becomes the only - Create `src/memory/trust.rs`: trust clamping, feedback deltas, retrieval counters, and temporal decay math. - Modify `src/lib.rs`: export `memory`. - Modify `src/db/migrations.rs`: add v11 schema and fresh-schema tables/triggers. -- Modify `src/tokensave.rs`: remove active decision/code-area memory methods, add fact memory facade methods, and keep compatibility wrappers. +- Modify `src/tracedecay.rs`: remove active decision/code-area memory methods, add fact memory facade methods, and keep compatibility wrappers. - Modify `src/mcp/tools/definitions.rs`: add new holographic memory tool schemas and update old memory tool descriptions. - Modify `src/mcp/tools/handlers/memory.rs`: replace current handlers with fact-store handlers and wrappers. - Modify `src/mcp/tools/handlers/mod.rs`: dispatch new tool names. @@ -124,13 +124,13 @@ relevance = (0.40 * fts_score) + (0.30 * jaccard_score) + (0.30 * holographic_sc score = relevance * trust_score * temporal_decay ``` -`retrieval_count` and `last_retrieved_at` update only for facts returned to the MCP caller. `min_trust` defaults to `0.3`, matching Hermes. `tokensave_memory_status` must report trust distribution buckets (`0.0-0.25`, `0.25-0.5`, `0.5-0.75`, `0.75-1.0`), feedback counts, and facts below the default recall threshold so later Hermes integration can decide when to prune, ask for confirmation, or mark memories stale. +`retrieval_count` and `last_retrieved_at` update only for facts returned to the MCP caller. `min_trust` defaults to `0.3`, matching Hermes. `tracedecay_memory_status` must report trust distribution buckets (`0.0-0.25`, `0.25-0.5`, `0.5-0.75`, `0.75-1.0`), feedback counts, and facts below the default recall threshold so later Hermes integration can decide when to prune, ask for confirmation, or mark memories stale. --- ## Rust API Contract -The implementation should expose these typed request/response structs from `src/memory/types.rs` and facade methods on `TokenSave`: +The implementation should expose these typed request/response structs from `src/memory/types.rs` and facade methods on `TraceDecay`: ```rust pub struct AddFactRequest { @@ -157,7 +157,7 @@ pub struct FeedbackRequest { } ``` -Required `TokenSave` facade methods: +Required `TraceDecay` facade methods: ```rust add_fact(request) -> FactRecord @@ -181,11 +181,11 @@ These APIs are intentionally close to Hermes' provider methods, but use typed Ru Add these first-class tools: -- `tokensave_fact_store`: action-based tool matching Hermes semantics with actions `add`, `search`, `probe`, `related`, `reason`, `contradict`, `update`, `remove`, and `list`. -- `tokensave_fact_feedback`: rate a fact as `helpful` or `unhelpful`. -- `tokensave_memory_status`: report fact counts, entity counts, bank status, algebra name, dimension, estimated capacity, missing-vector count, and migration/backfill status. +- `tracedecay_fact_store`: action-based tool matching Hermes semantics with actions `add`, `search`, `probe`, `related`, `reason`, `contradict`, `update`, `remove`, and `list`. +- `tracedecay_fact_feedback`: rate a fact as `helpful` or `unhelpful`. +- `tracedecay_memory_status`: report fact counts, entity counts, bank status, algebra name, dimension, estimated capacity, missing-vector count, and migration/backfill status. -`tokensave_fact_store` input must accept the Hermes-compatible fields below. The tool name remains `tokensave_`-prefixed because the current tool registry asserts that all exposed MCP tools use that prefix. +`tracedecay_fact_store` input must accept the Hermes-compatible fields below. The tool name remains `tracedecay_`-prefixed because the current tool registry asserts that all exposed MCP tools use that prefix. ```json { @@ -206,9 +206,9 @@ Add these first-class tools: } ``` -`tokensave_fact_store` output must include `results` or `fact`, `count`, and, for ranked retrieval actions, per-result `score`, `trust_score`, `fts_score`, `jaccard_score`, `holographic_score`, and `why`. This is required for later Hermes integration and for debugging trust/ranking behavior. +`tracedecay_fact_store` output must include `results` or `fact`, `count`, and, for ranked retrieval actions, per-result `score`, `trust_score`, `fts_score`, `jaccard_score`, `holographic_score`, and `why`. This is required for later Hermes integration and for debugging trust/ranking behavior. -`tokensave_fact_feedback` input must accept: +`tracedecay_fact_feedback` input must accept: ```json { @@ -219,29 +219,29 @@ Add these first-class tools: } ``` -`tokensave_fact_feedback` output must include `fact_id`, `action`, `old_trust`, `new_trust`, `trust_delta`, `helpful_count`, `unhelpful_count`, and `event_id`. +`tracedecay_fact_feedback` output must include `fact_id`, `action`, `old_trust`, `new_trust`, `trust_delta`, `helpful_count`, `unhelpful_count`, and `event_id`. Keep these compatibility wrappers: -- `tokensave_record_decision`: writes a `decision` category fact. -- `tokensave_record_code_area`: writes or updates a `code_area` category fact. -- `tokensave_session_recall`: delegates to hybrid search/list and can include code-area facts. +- `tracedecay_record_decision`: writes a `decision` category fact. +- `tracedecay_record_code_area`: writes or updates a `code_area` category fact. +- `tracedecay_session_recall`: delegates to hybrid search/list and can include code-area facts. --- ## Hermes Integration Contract -Future Hermes integration should not read `.tokensave/tokensave.db` directly. It should call MCP tools or the typed Rust facade. The mapping is: +Future Hermes integration should not read `.tracedecay/tracedecay.db` directly. It should call MCP tools or the typed Rust facade. The mapping is: -- Hermes `fact_store(action="add")` -> `tokensave_fact_store { "action": "add", "source": "hermes", ... }` -- Hermes `fact_store(action="search")` / provider `prefetch(query)` -> `tokensave_fact_store { "action": "search", "query": query, "min_trust": 0.3, "limit": 5 }` -- Hermes `fact_store(action="probe")` -> `tokensave_fact_store { "action": "probe", "entity": entity }` -- Hermes `fact_store(action="related")` -> `tokensave_fact_store { "action": "related", "entity": entity }` -- Hermes `fact_store(action="reason")` -> `tokensave_fact_store { "action": "reason", "entities": entities }` -- Hermes `fact_store(action="contradict")` -> `tokensave_fact_store { "action": "contradict", "threshold": 0.3 }` -- Hermes `fact_feedback(action="helpful"|"unhelpful")` -> `tokensave_fact_feedback` -- Hermes `system_prompt_block()` -> `tokensave_memory_status` summarized by the integration layer. -- Hermes `on_memory_write(action="add", target, content)` -> `tokensave_fact_store { "action": "add", "category": "user_pref"|"general", "source": "hermes" }` +- Hermes `fact_store(action="add")` -> `tracedecay_fact_store { "action": "add", "source": "hermes", ... }` +- Hermes `fact_store(action="search")` / provider `prefetch(query)` -> `tracedecay_fact_store { "action": "search", "query": query, "min_trust": 0.3, "limit": 5 }` +- Hermes `fact_store(action="probe")` -> `tracedecay_fact_store { "action": "probe", "entity": entity }` +- Hermes `fact_store(action="related")` -> `tracedecay_fact_store { "action": "related", "entity": entity }` +- Hermes `fact_store(action="reason")` -> `tracedecay_fact_store { "action": "reason", "entities": entities }` +- Hermes `fact_store(action="contradict")` -> `tracedecay_fact_store { "action": "contradict", "threshold": 0.3 }` +- Hermes `fact_feedback(action="helpful"|"unhelpful")` -> `tracedecay_fact_feedback` +- Hermes `system_prompt_block()` -> `tracedecay_memory_status` summarized by the integration layer. +- Hermes `on_memory_write(action="add", target, content)` -> `tracedecay_fact_store { "action": "add", "category": "user_pref"|"general", "source": "hermes" }` The MCP outputs must be JSON-serializable, must not expose raw vector bytes, and must include stable `fact_id` values so Hermes can call feedback after using a memory. This is the key integration invariant: every recalled fact can be rated later. @@ -249,19 +249,19 @@ The MCP outputs must be JSON-serializable, must not expose raw vector bytes, and ## Future Python Interop Contract -Python interoperability should remain outside the core memory replacement path so `tokensave` does not require a Python runtime to build or run. If future Hermes integration needs Python packaging, add a separate wrapper crate or package rather than putting PyO3 into the main binary. +Python interoperability should remain outside the core memory replacement path so `tracedecay` does not require a Python runtime to build or run. If future Hermes integration needs Python packaging, add a separate wrapper crate or package rather than putting PyO3 into the main binary. There are two distinct directions: -- Rust exposed to Python: use PyO3 plus maturin in a future `tokensave-py` wrapper that calls the typed Rust memory facade (`add_fact`, `search_facts`, `record_fact_feedback`, `memory_status`). This is the right path if Hermes wants to import `tokensave` as a Python module. +- Rust exposed to Python: use PyO3 plus maturin in a future `tracedecay-py` wrapper that calls the typed Rust memory facade (`add_fact`, `search_facts`, `record_fact_feedback`, `memory_status`). This is the right path if Hermes wants to import `tracedecay` as a Python module. - Python exposed to Rust: use `pyo3_bindgen` as a build dependency only in a bridge/test crate when Rust needs generated bindings to an existing Python module, such as a Hermes provider shim or compatibility harness. `pyo3_bindgen` generates Rust bindings to Python modules; it does not by itself package Rust APIs for Python callers. Future wrapper APIs must preserve the same contract as MCP: stable `fact_id`, ranked score components, trust fields, feedback event IDs, and no raw vector bytes in normal responses. Potential future files: -- `crates/tokensave-py/Cargo.toml`: PyO3/maturin wrapper crate. -- `crates/tokensave-py/src/lib.rs`: Python module exports for fact store, feedback, and status. +- `crates/tracedecay-py/Cargo.toml`: PyO3/maturin wrapper crate. +- `crates/tracedecay-py/src/lib.rs`: Python module exports for fact store, feedback, and status. - `crates/hermes-bridge/build.rs`: optional `pyo3_bindgen` generation for importing Hermes Python contracts during compatibility tests. - `tests/hermes_bridge_contract_test.rs`: verifies Python-facing and MCP-facing payloads stay equivalent. @@ -299,7 +299,7 @@ Potential future files: - [ ] 18. Create `src/memory/entities.rs` with `normalize_entity_name` that lowercases, trims whitespace, collapses repeated spaces, and preserves code identifiers. - [ ] 19. Implement Hermes-style entity extraction for capitalized multi-word names, double-quoted strings, single-quoted strings, and `aka` / `also known as` patterns. -- [ ] 20. Add code-aware extraction for project memory: file paths like `src/foo.rs`, symbol-like tokens like `TokenSave::init`, and tool names like `tokensave_context`. +- [ ] 20. Add code-aware extraction for project memory: file paths like `src/foo.rs`, symbol-like tokens like `TraceDecay::init`, and tool names like `tracedecay_context`. - [ ] 21. Deduplicate extracted entities by normalized name while preserving first-seen display names. - [ ] 22. Add tests for quoted entities, aliases, capitalized names, file paths, Rust symbols, MCP tool names, and deduplication order. @@ -315,7 +315,7 @@ Potential future files: ### Phase 6: SQLite Store Layer -- [ ] 30. Create `src/memory/store.rs` with `MemoryStore<'a>` or a lightweight facade around `libsql::Connection` obtained from `TokenSave`. +- [ ] 30. Create `src/memory/store.rs` with `MemoryStore<'a>` or a lightweight facade around `libsql::Connection` obtained from `TraceDecay`. - [ ] 31. Implement `add_fact(AddFactRequest, default_trust)` with duplicate detection by `content`, explicit `source`, JSON `metadata`, explicit entities, extracted entities, and initial `trust_score`. - [ ] 32. Implement entity resolution by `normalized_name`, alias JSON matching, and insertion into `memory_fact_entities`. - [ ] 33. Implement `update_fact(UpdateFactRequest)` for content, category, tags, source, metadata, trust delta, and entity relinking when content changes. @@ -341,9 +341,9 @@ Potential future files: - [ ] 47. Implement `contradict(category, threshold, limit)` using entity overlap plus low holographic/content similarity, capped at a safe comparison limit. - [ ] 48. Add retrieval tests for FTS operator sanitization, Jaccard reranking, holographic probe, multi-entity reason, related, contradiction, min-trust filtering, trust-weighted ordering, score-component output, and retrieval counters. -### Phase 9: TokenSave Facade +### Phase 9: TraceDecay Facade -- [ ] 49. Add new typed `TokenSave` methods: `add_fact(AddFactRequest)`, `search_facts(SearchFactsRequest)`, `probe_entity`, `related_facts`, `reason_facts`, `contradict_facts`, `update_fact(UpdateFactRequest)`, `remove_fact`, `list_facts`, `record_fact_feedback(FeedbackRequest)`, and `memory_status`. +- [ ] 49. Add new typed `TraceDecay` methods: `add_fact(AddFactRequest)`, `search_facts(SearchFactsRequest)`, `probe_entity`, `related_facts`, `reason_facts`, `contradict_facts`, `update_fact(UpdateFactRequest)`, `remove_fact`, `list_facts`, `record_fact_feedback(FeedbackRequest)`, and `memory_status`. - [ ] 50. Replace `record_decision` so it writes a `decision` fact, with `reason`, `files`, and `tags` incorporated into tags/entities/content in a predictable way. - [ ] 51. Replace `record_code_area` so it writes or updates a `code_area` fact keyed by path and preserves touch-count semantics through tags or metadata if needed for old callers. - [ ] 52. Replace `session_recall` so it delegates to `search_facts` when `query` exists and `list_facts` when omitted, preserving `since` and `limit` behavior where practical. @@ -351,14 +351,14 @@ Potential future files: ### Phase 10: MCP Tooling -- [ ] 54. Add `tokensave_fact_store`, `tokensave_fact_feedback`, and `tokensave_memory_status` definitions in `src/mcp/tools/definitions.rs` with exact JSON schemas matching the MCP Surface section, Hermes-compatible field names, and clear guidance to use `probe` or `reason` before answering user/project-memory questions. -- [ ] 55. Rewrite `src/mcp/tools/handlers/memory.rs` to parse the action enum, validate required fields per action, call the new `TokenSave` facade, and format JSON output through the existing `ToolResult` envelope. +- [ ] 54. Add `tracedecay_fact_store`, `tracedecay_fact_feedback`, and `tracedecay_memory_status` definitions in `src/mcp/tools/definitions.rs` with exact JSON schemas matching the MCP Surface section, Hermes-compatible field names, and clear guidance to use `probe` or `reason` before answering user/project-memory questions. +- [ ] 55. Rewrite `src/mcp/tools/handlers/memory.rs` to parse the action enum, validate required fields per action, call the new `TraceDecay` facade, and format JSON output through the existing `ToolResult` envelope. - [ ] 56. Update `src/mcp/tools/handlers/mod.rs` dispatch and `src/tool_command.rs` grouping for all new memory tools. -- [ ] 57. Add MCP handler tests for every `tokensave_fact_store` action, `tokensave_fact_feedback` helpful/unhelpful paths, feedback event fields, `tokensave_memory_status` trust distribution fields, malformed input, Hermes-compatible payload names, and the three compatibility wrappers. +- [ ] 57. Add MCP handler tests for every `tracedecay_fact_store` action, `tracedecay_fact_feedback` helpful/unhelpful paths, feedback event fields, `tracedecay_memory_status` trust distribution fields, malformed input, Hermes-compatible payload names, and the three compatibility wrappers. ### Phase 11: Documentation And Validation -- [ ] 58. Update `README.md`, `docs/USER-GUIDE.md`, and `src/mcp/server.rs` schema notes to describe the holographic fact store, trust scoring, entity recall, compositional reasoning, contradiction detection, compatibility wrappers, the Hermes integration mapping from `fact_store` / `fact_feedback` to `tokensave_` MCP tools, and the future Python interop split between PyO3/maturin and `pyo3_bindgen`. +- [ ] 58. Update `README.md`, `docs/USER-GUIDE.md`, and `src/mcp/server.rs` schema notes to describe the holographic fact store, trust scoring, entity recall, compositional reasoning, contradiction detection, compatibility wrappers, the Hermes integration mapping from `fact_store` / `fact_feedback` to `tracedecay_` MCP tools, and the future Python interop split between PyO3/maturin and `pyo3_bindgen`. - [ ] 59. Run focused validation: `cargo test memory_test`, `cargo test mcp_handler_test`, `cargo test migration_test`, and `cargo check`. - [ ] 60. Run full validation with `cargo test`, review output for performance or flaky timing issues, then prepare a follow-up implementation summary and migration notes for users. diff --git a/docs/superpowers/plans/2026-03-27-daemon-mode.md b/docs/superpowers/plans/2026-03-27-daemon-mode.md index 5e3d57e8..9faf247d 100644 --- a/docs/superpowers/plans/2026-03-27-daemon-mode.md +++ b/docs/superpowers/plans/2026-03-27-daemon-mode.md @@ -1,14 +1,14 @@ # Daemon Mode Implementation Plan -> **Rebrand note:** The project has since been renamed **TraceDecay** (binary/crate `tracedecay`, MCP tools `tracedecay_*`). This dated planning artifact keeps the TokenSave-era names it was written with. +> **Rebrand note:** The project has since been renamed **TraceDecay** (binary/crate `tracedecay`, MCP tools `tracedecay_*`). This dated planning artifact keeps the TraceDecay-era names it was written with. > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** A background daemon that watches all tracked tokensave projects for file changes and automatically runs incremental syncs. +**Goal:** A background daemon that watches all tracked tracedecay projects for file changes and automatically runs incremental syncs. -**Architecture:** A new `tokensave daemon` subcommand backed by `src/daemon.rs`. Uses `notify` for filesystem watching, tokio timers for per-project debounce, and `daemon-kit` for cross-platform daemonization (fork on Unix via daemonize2, Windows Service via windows-service), PID file management, and service installation (launchd/systemd/Windows Service). Discovers projects from the global DB, re-polls every 60s for new ones. +**Architecture:** A new `tracedecay daemon` subcommand backed by `src/daemon.rs`. Uses `notify` for filesystem watching, tokio timers for per-project debounce, and `daemon-kit` for cross-platform daemonization (fork on Unix via daemonize2, Windows Service via windows-service), PID file management, and service installation (launchd/systemd/Windows Service). Discovers projects from the global DB, re-polls every 60s for new ones. -**Tech Stack:** Rust, `daemon-kit` (cross-platform daemon/service), `notify` v7 (file watcher), tokio (async runtime, timers), existing `TokenSave::sync()`, existing `GlobalDb`. +**Tech Stack:** Rust, `daemon-kit` (cross-platform daemon/service), `notify` v7 (file watcher), tokio (async runtime, timers), existing `TraceDecay::sync()`, existing `GlobalDb`. --- @@ -132,7 +132,7 @@ feat: add GlobalDb::list_project_paths() - [ ] **Step 1: Create daemon.rs with duration parser and daemon-kit Daemon instance** ```rust -//! Background daemon that watches all tracked tokensave projects for file +//! Background daemon that watches all tracked tracedecay projects for file //! changes and runs incremental syncs automatically. use std::path::PathBuf; @@ -140,7 +140,7 @@ use std::time::Duration; use daemon_kit::{Daemon, DaemonConfig}; -use crate::errors::{Result, TokenSaveError}; +use crate::errors::{Result, TraceDecayError}; /// Parse a human-readable duration string like "15s" or "1m" into a Duration. /// Returns None if the format is unrecognized. @@ -155,20 +155,20 @@ pub fn parse_duration(s: &str) -> Option { } } -/// Build the daemon-kit Daemon instance with tokensave paths. -fn build_daemon() -> std::result::Result { - let home = dirs::home_dir().ok_or_else(|| TokenSaveError::Config { +/// Build the daemon-kit Daemon instance with tracedecay paths. +fn build_daemon() -> std::result::Result { + let home = dirs::home_dir().ok_or_else(|| TraceDecayError::Config { message: "cannot determine home directory".to_string(), })?; - let ts_dir = home.join(".tokensave"); - let bin = crate::agents::which_tokensave().unwrap_or_else(|| "tokensave".to_string()); + let ts_dir = home.join(".tracedecay"); + let bin = crate::agents::which_tracedecay().unwrap_or_else(|| "tracedecay".to_string()); - let config = DaemonConfig::new("tokensave-daemon") + let config = DaemonConfig::new("tracedecay-daemon") .pid_dir(&ts_dir) .log_file(ts_dir.join("daemon.log")) .executable(PathBuf::from(bin)) .service_args(vec!["daemon".to_string(), "--foreground".to_string()]) - .description("tokensave file watcher daemon"); + .description("tracedecay file watcher daemon"); Ok(Daemon::new(config)) } @@ -254,7 +254,7 @@ use notify::{RecommendedWatcher, RecursiveMode, Watcher, Event}; /// Directories to ignore inside watched projects. const IGNORED_DIRS: &[&str] = &[ - ".tokensave", ".git", "node_modules", "target", ".build", + ".tracedecay", ".git", "node_modules", "target", ".build", "__pycache__", ".next", "dist", "build", ".cache", ]; @@ -262,7 +262,7 @@ const IGNORED_DIRS: &[&str] = &[ /// a shutdown signal is received. pub async fn run(foreground: bool) -> Result<()> { if let Some(pid) = running_daemon_pid() { - return Err(TokenSaveError::Config { + return Err(TraceDecayError::Config { message: format!("daemon already running (PID: {pid})"), }); } @@ -380,7 +380,7 @@ async fn discover_projects() -> Vec { .into_iter() .filter_map(|s| { let p = PathBuf::from(&s); - if p.is_dir() && crate::tokensave::TokenSave::is_initialized(&p) { + if p.is_dir() && crate::tracedecay::TraceDecay::is_initialized(&p) { Some(p) } else { None @@ -423,7 +423,7 @@ fn create_watcher(project_root: &Path, tx: mpsc::Sender) -> Option Result<()> { }) }) }) - .map_err(|e| TokenSaveError::Config { + .map_err(|e| TraceDecayError::Config { message: format!("daemon error: {e}"), }) } @@ -546,10 +546,10 @@ pub async fn run(foreground: bool) -> Result<()> { /// Stop the running daemon. pub fn stop() -> Result<()> { let daemon = build_daemon()?; - daemon.stop().map_err(|e| TokenSaveError::Config { + daemon.stop().map_err(|e| TraceDecayError::Config { message: format!("{e}"), })?; - eprintln!("tokensave daemon stopped"); + eprintln!("tracedecay daemon stopped"); Ok(()) } @@ -557,11 +557,11 @@ pub fn stop() -> Result<()> { pub fn status() -> i32 { match running_daemon_pid() { Some(pid) => { - eprintln!("tokensave daemon is running (PID: {pid})"); + eprintln!("tracedecay daemon is running (PID: {pid})"); 0 } None => { - eprintln!("tokensave daemon is not running"); + eprintln!("tracedecay daemon is not running"); 1 } } @@ -570,7 +570,7 @@ pub fn status() -> i32 { /// Install autostart service (launchd/systemd/Windows Service). pub fn enable_autostart() -> Result<()> { let daemon = build_daemon()?; - daemon.install_service().map_err(|e| TokenSaveError::Config { + daemon.install_service().map_err(|e| TraceDecayError::Config { message: format!("{e}"), })?; eprintln!("\x1b[32m✔\x1b[0m Autostart service installed"); @@ -580,7 +580,7 @@ pub fn enable_autostart() -> Result<()> { /// Remove autostart service. pub fn disable_autostart() -> Result<()> { let daemon = build_daemon()?; - daemon.uninstall_service().map_err(|e| TokenSaveError::Config { + daemon.uninstall_service().map_err(|e| TraceDecayError::Config { message: format!("{e}"), })?; eprintln!("\x1b[32m✔\x1b[0m Autostart service removed"); @@ -635,16 +635,16 @@ In the `match command { ... }` block, add: ```rust Commands::Daemon { foreground, stop, status, enable_autostart, disable_autostart } => { if stop { - tokensave::daemon::stop()?; + tracedecay::daemon::stop()?; } else if status { - let code = tokensave::daemon::status(); + let code = tracedecay::daemon::status(); std::process::exit(code); } else if enable_autostart { - tokensave::daemon::enable_autostart()?; + tracedecay::daemon::enable_autostart()?; } else if disable_autostart { - tokensave::daemon::disable_autostart()?; + tracedecay::daemon::disable_autostart()?; } else { - tokensave::daemon::run(foreground).await?; + tracedecay::daemon::run(foreground).await?; } } ``` @@ -658,7 +658,7 @@ fn check_daemon(dc: &mut DoctorCounters) { eprintln!("\n\x1b[1mDaemon\x1b[0m"); match crate::daemon::running_daemon_pid() { Some(pid) => dc.pass(&format!("Daemon is running (PID: {pid})")), - None => dc.warn("Daemon is not running — run `tokensave daemon` to start"), + None => dc.warn("Daemon is not running — run `tracedecay daemon` to start"), } if crate::daemon::is_autostart_enabled() { #[cfg(target_os = "macos")] @@ -668,7 +668,7 @@ fn check_daemon(dc: &mut DoctorCounters) { #[cfg(not(any(target_os = "macos", target_os = "linux")))] dc.pass("Autostart enabled"); } else { - dc.warn("Autostart not configured — run `tokensave daemon --enable-autostart`"); + dc.warn("Autostart not configured — run `tracedecay daemon --enable-autostart`"); } } ``` @@ -682,11 +682,11 @@ Run: `cargo build && cargo test` - [ ] **Step 5: Manual test** ```bash -./target/debug/tokensave daemon --status -./target/debug/tokensave daemon --foreground & -./target/debug/tokensave daemon --status -./target/debug/tokensave daemon --stop -./target/debug/tokensave doctor | grep -A2 Daemon +./target/debug/tracedecay daemon --status +./target/debug/tracedecay daemon --foreground & +./target/debug/tracedecay daemon --status +./target/debug/tracedecay daemon --stop +./target/debug/tracedecay doctor | grep -A2 Daemon ``` - [ ] **Step 6: Commit** @@ -706,10 +706,10 @@ feat: daemon CLI subcommand and doctor integration Add a new `## [Unreleased]` or version section with: ```markdown ### Added -- **Daemon mode** — `tokensave daemon` watches all tracked projects for file changes and runs incremental syncs automatically; debounce configurable via `daemon_debounce` in `~/.tokensave/config.toml` (default `"15s"`) -- **Daemon management** — `--stop`, `--status`, `--foreground` flags for process control; PID file at `~/.tokensave/daemon.pid` +- **Daemon mode** — `tracedecay daemon` watches all tracked projects for file changes and runs incremental syncs automatically; debounce configurable via `daemon_debounce` in `~/.tracedecay/config.toml` (default `"15s"`) +- **Daemon management** — `--stop`, `--status`, `--foreground` flags for process control; PID file at `~/.tracedecay/daemon.pid` - **Autostart service** — `--enable-autostart` / `--disable-autostart` generates and manages a launchd plist (macOS) or systemd user unit (Linux) -- **Doctor daemon checks** — `tokensave doctor` now reports daemon running status and autostart configuration +- **Doctor daemon checks** — `tracedecay doctor` now reports daemon running status and autostart configuration ``` - [ ] **Step 2: Full test suite** diff --git a/docs/superpowers/plans/2026-06-09-tokensave-lcm-session-rewrite.md b/docs/superpowers/plans/2026-06-09-tokensave-lcm-session-rewrite.md deleted file mode 100644 index 76a34514..00000000 --- a/docs/superpowers/plans/2026-06-09-tokensave-lcm-session-rewrite.md +++ /dev/null @@ -1,1748 +0,0 @@ -# TokenSave LCM Session Rewrite Implementation Plan - -> **Rebrand note:** The project has since been renamed **TraceDecay** (binary/crate `tracedecay`, MCP tools `tracedecay_*`). This dated planning artifact keeps the TokenSave-era names it was written with; read `tokensave` / `tokensave_*` as `tracedecay` / `tracedecay_*` when applying it to the current codebase. - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Rewrite TokenSave's existing session internals into a lossless, LCM-grade session store inside the existing `sessions.db`, while preserving compatible `tokensave_message_search` behavior and adding deterministic LCM load/expand/status/compression APIs. - -**Architecture:** Rust owns the durable LCM store, idempotent schema migrations, raw-message and summary-DAG state, bounded derived indexes/snippets, storage path containment, and public CLI/MCP JSON contracts. The generated Hermes Python plugin owns Hermes `ContextEngine` lifecycle registration and Hermes auxiliary LLM calls, calling Rust through the existing `tokensave tool ... --json --args` subprocess bridge. Codegraph `ContextBuilder` and `src/memory/*` fact storage stay separate from session compression and are only regression-tested for non-coupling. - -**Tech Stack:** Rust 1.95, `libsql`/SQLite FTS5, `tokio`, `serde`/`serde_json`, generated Hermes Python plugin files, `python3 -m py_compile`, `cargo test`, `cargo fmt --all -- --check`, and `cargo clippy --all-targets`. - ---- - -## Source Anchors - -- Approved design: `docs/superpowers/specs/2026-06-09-tokensave-lcm-session-rewrite-design.md`. -- Current DB/session write path: `src/global_db.rs`, `src/sessions/mod.rs`, `src/sessions/source.rs`, `src/sessions/cursor.rs`. -- Current search tool: `src/mcp/tools/handlers/session.rs`, `src/mcp/tools/definitions.rs`, `src/mcp/tools/handlers/mod.rs`. -- Current Hermes generator and subprocess bridge: `src/agents/hermes.rs`, `src/tool_command.rs`. -- Existing tests to evolve: `tests/session_global_db_test.rs`, `tests/mcp_handler_test.rs`, `tests/agent_test.rs`. -- Current authoritative cap to remove from new writes: `MAX_SESSION_MESSAGE_TEXT_BYTES` in `src/global_db.rs`; after this plan, any cap belongs only to derived snippets, FTS/index text, MCP response truncation, or bounded rendering. - -## File Structure Map - -Create these focused Rust modules: - -- `src/sessions/lcm/mod.rs` - public module boundary, exports types and store helpers. -- `src/sessions/lcm/types.rs` - stable Rust structs/enums for raw messages, payload refs, summary nodes, lifecycle state, query inputs, and JSON outputs. -- `src/sessions/lcm/schema.rs` - schema version constants and idempotent migration DDL for LCM tables inside the existing `sessions.db`. -- `src/sessions/lcm/store.rs` - `LcmStore<'db>` wrapper that binds `GlobalDb` plus an explicit storage root and exposes raw ingest, query, DAG, payload, and compression operations. -- `src/sessions/lcm/raw.rs` - lossless raw-message ingest, compatibility projection writes, legacy-row carry-forward, and authoritative content loading. -- `src/sessions/lcm/payload.rs` - externalized payload creation, hashing, permissions, basename-only refs, root containment, and paginated expansion. -- `src/sessions/lcm/dag.rs` - summary DAG persistence, source lineage, subtree expansion, and depth/window metadata. -- `src/sessions/lcm/query.rs` - search/load/describe/status query assembly, stable cursors, bounded snippets, and JSON result types. -- `src/sessions/lcm/compression.rs` - deterministic compression lifecycle primitives, fake summarizer injection, frontier/debt state transitions, and active-context assembly. -- `src/sessions/lcm/security.rs` - ingest-protection classification for large/binary-ish payloads, data URI/base64 detection, sensitive-redaction metadata, and integrity scan helpers. -- `src/sessions/lcm/hermes.rs` - Rust JSON request/response contracts used by the generated Hermes `ContextEngine` adapter. - -Modify these existing Rust files: - -- `src/sessions/mod.rs` - add `pub mod lcm;` and re-export the public LCM types required by handlers/tests. -- `src/global_db.rs` - call the LCM schema migration during `GlobalDb::open_at`, expose a crate-private connection accessor for LCM modules, and change `upsert_session_message` so new authoritative content is lossless while compatibility/index fields remain bounded. -- `src/sessions/source.rs` - route parsed provider messages through the LCM raw ingest path while preserving incremental parse offsets and provider-normalized session metadata. -- `src/sessions/cursor.rs` - keep `project_session_db_path(project_root).join("sessions.db")` behavior and pass the project-local `.tokensave` storage root into `LcmStore`. -- `src/mcp/tools/definitions.rs` - add additive LCM tool schemas for `tokensave_lcm_status`, `tokensave_lcm_load_session`, `tokensave_lcm_grep`, `tokensave_lcm_describe`, `tokensave_lcm_expand`, `tokensave_lcm_expand_query`, `tokensave_lcm_preflight`, and `tokensave_lcm_compress`. -- `src/mcp/tools/handlers/session.rs` - keep `handle_message_search` compatible and add LCM handlers that call deterministic Rust APIs. -- `src/mcp/tools/handlers/mod.rs` - register the new LCM handler names in `handle_tool_call`. -- `src/agents/hermes.rs` - generate the Hermes `ContextEngine` adapter, LCM bridge helpers, local/profile storage configuration, auxiliary summarizer calls, reasoning stripping, and Python tests embedded in Rust fixtures. -- `src/main.rs` - only if install flags need to pass explicit Hermes profile/locality metadata into `HermesIntegration`; otherwise leave unchanged. -- `src/tool_command.rs` - only if LCM bridge tests need stronger `--project` coverage for subprocess calls; keep the existing `--json --args` contract. - -Create these tests: - -- `tests/session_lcm_schema_test.rs` - schema migration, idempotency, legacy carry-forward, and single-DB assertions. -- `tests/session_lcm_raw_test.rs` - lossless raw ingest, capped derived snippets, compatibility projections, and authoritative load. -- `tests/session_lcm_payload_test.rs` - payload externalization, path containment, permissions, hashes, missing/unreferenced scans, and cross-session denial. -- `tests/session_lcm_dag_test.rs` - summary DAG persistence, source lineage, subtree expansion, and restart recovery. -- `tests/session_lcm_query_test.rs` - load/grep/describe/status APIs and deterministic pagination. -- `tests/session_lcm_compression_test.rs` - lifecycle/frontier/debt primitives with deterministic fake summarizers. -- `tests/session_lcm_ingest_protection_test.rs` - data URI/base64/large tool output protection and bounded index text. -- `tests/hermes_lcm_bridge_test.rs` - generated Python context engine, subprocess bridge calls, no-op/fake summarizer behavior, auxiliary LLM routing, reasoning stripping, and fallback behavior. - -Modify these tests: - -- `tests/session_global_db_test.rs` - replace capped-authoritative assertions with lossless storage assertions and keep search/filter compatibility. -- `tests/mcp_handler_test.rs` - assert new LCM tool definitions, handler dispatch, and unchanged `tokensave_message_search` provider/scope schema. -- `tests/agent_test.rs` - extend Hermes generated-plugin tests for `ContextEngine` registration, local/profile storage locality, Python compile checks, and non-overwrite memory provider behavior. - -Do not modify these production areas except for regression tests proving separation: - -- `src/context/builder.rs` - codegraph context retrieval remains independent from LCM. -- `src/memory/*` - fact memory remains independent from summary DAG state. - -## Cross-Cutting Invariants - -- The existing `sessions.db` is the only primary TokenSave-managed session database. Migrations evolve it in place; tests must fail if a second primary LCM DB path is introduced. -- New authoritative session content is lossless. `session_messages.text` may become a bounded compatibility projection, but the raw message store must preserve full content inline or through an externalized payload ref. -- Existing rows that were already capped remain best-effort legacy data and must be marked `legacy_truncated = true` when carried into the LCM raw store. -- FTS/index text, snippets, MCP response text, and display previews are derived and bounded. They must never be the only copy of new raw content. -- Storage roots are explicit: project-local installs use `crate::config::get_tokensave_dir(project_root)`; non-local Hermes profile installs use the selected Hermes profile directory plus `.tokensave`. -- Python bridge calls use `tokensave tool --json --args ` and optional `--project ` when the context engine knows the project root. Do not introduce PyO3 or native bindings in this implementation. -- LCM summaries are not memory facts. `summary_nodes` and lifecycle/frontier rows live in the session DB, not in `src/memory/*`. - ---- - -## Task 1: Session Schema Migration Foundation - -**Files:** -- Create: `src/sessions/lcm/mod.rs` -- Create: `src/sessions/lcm/types.rs` -- Create: `src/sessions/lcm/schema.rs` -- Modify: `src/sessions/mod.rs` -- Modify: `src/global_db.rs` -- Create: `tests/session_lcm_schema_test.rs` - -- [ ] **Step 1: Write failing schema migration tests** - -Add tests that open an old-style `sessions.db`, run `GlobalDb::open_at`, and assert the LCM tables exist in that same file. - -```rust -#[tokio::test] -async fn lcm_schema_migrates_legacy_sessions_db_in_place() { - let tmp = tempfile::TempDir::new().unwrap(); - let db_path = tmp.path().join(".tokensave").join("sessions.db"); - std::fs::create_dir_all(db_path.parent().unwrap()).unwrap(); - - let old_db = libsql::Builder::new_local(&db_path).build().await.unwrap(); - let conn = old_db.connect().unwrap(); - conn.execute_batch( - "CREATE TABLE sessions ( - provider TEXT NOT NULL, - session_id TEXT NOT NULL, - project_key TEXT NOT NULL, - project_path TEXT NOT NULL, - title TEXT, - started_at INTEGER, - ended_at INTEGER, - transcript_path TEXT, - metadata_json TEXT, - PRIMARY KEY(provider, session_id) - ); - CREATE TABLE session_messages ( - provider TEXT NOT NULL, - message_id TEXT NOT NULL, - session_id TEXT NOT NULL, - role TEXT NOT NULL, - timestamp INTEGER, - ordinal INTEGER NOT NULL, - text TEXT NOT NULL, - kind TEXT, - model TEXT, - tool_names TEXT, - source_path TEXT, - source_offset INTEGER, - metadata_json TEXT, - PRIMARY KEY(provider, message_id) - ); - INSERT INTO sessions(provider, session_id, project_key, project_path) - VALUES ('cursor', 'legacy-session', '/tmp/project', '/tmp/project'); - INSERT INTO session_messages(provider, message_id, session_id, role, ordinal, text) - VALUES ('cursor', 'legacy-message', 'legacy-session', 'assistant', 1, 'legacy text');", - ).await.unwrap(); - drop(conn); - drop(old_db); - - let db = tokensave::global_db::GlobalDb::open_at(&db_path).await.unwrap(); - assert_eq!(db.lcm_schema_version().await.unwrap(), tokensave::sessions::lcm::LCM_SCHEMA_VERSION); - - let legacy = db - .lcm_load_raw_message("cursor", "legacy-message") - .await - .expect("legacy message should be carried into raw store"); - assert_eq!(legacy.session_id, "legacy-session"); - assert_eq!(legacy.content, "legacy text"); - assert!(legacy.legacy_source); - assert!(!legacy.legacy_truncated); -} -``` - -- [ ] **Step 2: Run the red test** - -Run: `cargo test --test session_lcm_schema_test lcm_schema_migrates_legacy_sessions_db_in_place -- --nocapture` - -Expected: FAIL with missing `tokensave::sessions::lcm`, missing `GlobalDb::lcm_schema_version`, or missing `GlobalDb::lcm_load_raw_message`. - -- [ ] **Step 3: Add LCM module boundary and schema version types** - -Sketch: - -```rust -// src/sessions/lcm/mod.rs -pub mod schema; -pub mod types; - -pub use schema::LCM_SCHEMA_VERSION; -pub use types::{LcmRawMessage, LcmStorageKind}; -``` - -```rust -// src/sessions/lcm/types.rs -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct LcmRawMessage { - pub provider: String, - pub message_id: String, - pub session_id: String, - pub store_id: i64, - pub role: String, - pub ordinal: i64, - pub timestamp: Option, - pub content: String, - pub content_hash: String, - pub storage_kind: LcmStorageKind, - pub payload_ref: Option, - pub legacy_source: bool, - pub legacy_truncated: bool, - pub metadata_json: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum LcmStorageKind { - Inline, - External, -} -``` - -- [ ] **Step 4: Add idempotent schema migration inside `GlobalDb::open_at`** - -Sketch: - -```rust -// src/sessions/lcm/schema.rs -pub const LCM_SCHEMA_VERSION: i64 = 1; - -pub(crate) async fn ensure_lcm_schema(conn: &libsql::Connection) -> Option<()> { - conn.execute_batch( - "CREATE TABLE IF NOT EXISTS session_schema_migrations ( - name TEXT PRIMARY KEY, - version INTEGER NOT NULL, - applied_at INTEGER NOT NULL DEFAULT (unixepoch()) - ); - CREATE TABLE IF NOT EXISTS lcm_raw_messages ( - provider TEXT NOT NULL, - message_id TEXT NOT NULL, - session_id TEXT NOT NULL, - store_id INTEGER PRIMARY KEY AUTOINCREMENT, - role TEXT NOT NULL, - ordinal INTEGER NOT NULL, - timestamp INTEGER, - content TEXT, - content_hash TEXT NOT NULL, - storage_kind TEXT NOT NULL CHECK(storage_kind IN ('inline', 'external')), - payload_ref TEXT, - snippet_text TEXT NOT NULL, - index_text TEXT NOT NULL, - legacy_source INTEGER NOT NULL DEFAULT 0, - legacy_truncated INTEGER NOT NULL DEFAULT 0, - metadata_json TEXT, - UNIQUE(provider, message_id), - FOREIGN KEY(provider, session_id) REFERENCES sessions(provider, session_id) ON DELETE CASCADE - ); - CREATE INDEX IF NOT EXISTS idx_lcm_raw_session_order - ON lcm_raw_messages(provider, session_id, store_id); - CREATE VIRTUAL TABLE IF NOT EXISTS lcm_raw_messages_fts USING fts5( - index_text, role, metadata_json, - content='lcm_raw_messages', - content_rowid='store_id' - );", - ).await.ok()?; - - carry_forward_legacy_messages(conn).await?; - conn.execute( - "INSERT INTO session_schema_migrations(name, version) - VALUES ('lcm', ?1) - ON CONFLICT(name) DO UPDATE SET version = excluded.version, applied_at = unixepoch()", - libsql::params![LCM_SCHEMA_VERSION], - ).await.ok()?; - Some(()) -} -``` - -Call `crate::sessions::lcm::schema::ensure_lcm_schema(&conn).await?;` immediately after the existing `ensure_session_parent_columns(&conn).await?;` in `GlobalDb::open_at`. - -- [ ] **Step 5: Add introspection helpers used by tests** - -Sketch: - -```rust -impl GlobalDb { - pub async fn lcm_schema_version(&self) -> Option { - let mut rows = self.conn.query( - "SELECT version FROM session_schema_migrations WHERE name = 'lcm'", - (), - ).await.ok()?; - rows.next().await.ok()??.get::(0).ok() - } - - pub async fn lcm_load_raw_message( - &self, - provider: &str, - message_id: &str, - ) -> Option { - crate::sessions::lcm::schema::load_raw_message(&self.conn, provider, message_id).await - } -} -``` - -- [ ] **Step 6: Prove migration idempotency** - -Add `lcm_schema_migration_is_idempotent` in `tests/session_lcm_schema_test.rs` that calls `GlobalDb::open_at(&db_path)` twice and asserts: - -```rust -assert_eq!(count_rows(&db_path, "lcm_raw_messages").await, 1); -assert_eq!(schema_version(&db_path).await, tokensave::sessions::lcm::LCM_SCHEMA_VERSION); -``` - -Run: `cargo test --test session_lcm_schema_test -- --nocapture` - -Expected: PASS. - -- [ ] **Step 7: Commit checkpoint** - -```bash -git add src/sessions/mod.rs src/sessions/lcm/mod.rs src/sessions/lcm/types.rs src/sessions/lcm/schema.rs src/global_db.rs tests/session_lcm_schema_test.rs -git commit -m "Add LCM session schema migrations" -``` - -## Task 2: Lossless Raw-Message Ingest Model - -**Files:** -- Create: `src/sessions/lcm/raw.rs` -- Create: `src/sessions/lcm/store.rs` -- Modify: `src/sessions/lcm/mod.rs` -- Modify: `src/sessions/lcm/types.rs` -- Modify: `src/global_db.rs` -- Modify: `src/sessions/source.rs` -- Modify: `tests/session_global_db_test.rs` -- Create: `tests/session_lcm_raw_test.rs` - -- [ ] **Step 1: Replace the capped-authoritative regression test with a lossless red test** - -In `tests/session_global_db_test.rs`, replace `upsert_session_message_truncates_oversized_text_deterministically` with: - -```rust -#[tokio::test] -async fn upsert_session_message_preserves_oversized_text_losslessly() { - let tmp = TempDir::new().unwrap(); - let db = open_isolated_db(&tmp).await; - let session = sample_session("cursor", "session-1", "project-a"); - db.upsert_session(&session).await; - - let oversized = format!("{}{}", "x".repeat(300_000), "::lossless-tail"); - let message = sample_message("cursor", "message-1", "session-1", &oversized); - assert!(db.upsert_session_message(&message).await); - - let compatibility = db - .get_session_message("cursor", "message-1") - .await - .expect("compatibility message should exist"); - assert!(compatibility.text.len() <= tokensave::sessions::lcm::MAX_DERIVED_TEXT_CHARS); - assert!(compatibility.text.contains("[derived snippet truncated by tokensave]")); - - let raw = db - .lcm_load_raw_message("cursor", "message-1") - .await - .expect("raw message should exist"); - assert_eq!(raw.content, oversized); - assert!(raw.content.ends_with("::lossless-tail")); - assert!(!raw.legacy_source); - assert!(!raw.legacy_truncated); -} -``` - -- [ ] **Step 2: Run the red test** - -Run: `cargo test --test session_global_db_test upsert_session_message_preserves_oversized_text_losslessly -- --nocapture` - -Expected: FAIL because `GlobalDb::upsert_session_message` still calls `capped_session_message_text` before storing the only authoritative text copy. - -- [ ] **Step 3: Add explicit derived-text helpers** - -Sketch: - -```rust -// src/sessions/lcm/types.rs -pub const MAX_DERIVED_TEXT_CHARS: usize = 64 * 1024; -pub const DERIVED_TRUNCATION_MARKER: &str = "\n[derived snippet truncated by tokensave]"; -``` - -```rust -// src/sessions/lcm/raw.rs -pub fn derived_text_for_index(raw: &str) -> String { - if raw.chars().count() <= MAX_DERIVED_TEXT_CHARS { - return raw.to_string(); - } - let mut out = raw.chars().take(MAX_DERIVED_TEXT_CHARS).collect::(); - out.push_str(DERIVED_TRUNCATION_MARKER); - out -} -``` - -- [ ] **Step 4: Add raw ingest API and make `upsert_session_message` write both layers** - -Sketch: - -```rust -impl GlobalDb { - pub async fn upsert_session_message(&self, message: &SessionMessageRecord) -> bool { - let Some(raw) = crate::sessions::lcm::raw::prepare_raw_message(message) else { - return false; - }; - if !crate::sessions::lcm::raw::upsert_raw_message(&self.conn, &raw).await { - return false; - } - let derived = crate::sessions::lcm::raw::derived_text_for_index(&message.text); - self.upsert_session_message_projection(message, &derived).await - } -} -``` - -Keep the existing `session_messages` table and FTS triggers as compatibility projections. Move the previous SQL body into a private `upsert_session_message_projection`. - -- [ ] **Step 5: Route transcript ingestion through the same write path** - -`src/sessions/source.rs` already calls `db.upsert_session_message(message)`. Keep that call so all providers inherit lossless behavior through one path. Add a test in `tests/session_lcm_raw_test.rs` using a fake `TranscriptSource` that emits a 300 KiB assistant message and assert the raw message tail survives. - -Run: `cargo test --test session_lcm_raw_test transcript_ingest_preserves_lossless_raw_content -- --nocapture` - -Expected before implementation: FAIL with missing fake-source helpers or missing raw content. Expected after implementation: PASS. - -- [ ] **Step 6: Prove search still uses bounded compatibility text** - -Add `search_uses_bounded_projection_but_load_recovers_raw`: - -```rust -let results = db.search_session_messages("cursor", Some("project-a"), "unique-search-token", 10).await; -assert_eq!(results.len(), 1); -assert!(results[0].message.text.len() <= tokensave::sessions::lcm::MAX_DERIVED_TEXT_CHARS + 64); -assert_eq!( - db.lcm_load_raw_message("cursor", "message-1").await.unwrap().content, - oversized -); -``` - -Run: `cargo test --test session_lcm_raw_test -- --nocapture` - -Expected: PASS. - -- [ ] **Step 7: Commit checkpoint** - -```bash -git add src/global_db.rs src/sessions/source.rs src/sessions/lcm/mod.rs src/sessions/lcm/types.rs src/sessions/lcm/raw.rs src/sessions/lcm/store.rs tests/session_global_db_test.rs tests/session_lcm_raw_test.rs -git commit -m "Make session raw ingest lossless" -``` - -## Task 3: External Payload Containment and Derived Index Separation - -**Files:** -- Create: `src/sessions/lcm/payload.rs` -- Create: `src/sessions/lcm/security.rs` -- Modify: `src/sessions/lcm/mod.rs` -- Modify: `src/sessions/lcm/types.rs` -- Modify: `src/sessions/lcm/schema.rs` -- Modify: `src/sessions/lcm/raw.rs` -- Create: `tests/session_lcm_payload_test.rs` -- Create: `tests/session_lcm_ingest_protection_test.rs` - -- [ ] **Step 1: Write failing externalization tests** - -Add `externalizes_large_tool_payload_with_recoverable_ref`: - -```rust -#[tokio::test] -async fn externalizes_large_tool_payload_with_recoverable_ref() { - let tmp = tempfile::TempDir::new().unwrap(); - let db = open_lcm_db(tmp.path()).await; - insert_session(&db, "cursor", "session-1").await; - - let payload = format!("tool output\n{}", "A".repeat(900_000)); - let message = raw_message("cursor", "tool-1", "session-1", "tool", &payload) - .with_kind("tool_result"); - db.lcm_store(tmp.path().join(".tokensave")) - .ingest_raw_message(&message) - .await - .unwrap(); - - let raw = db.lcm_load_raw_message("cursor", "tool-1").await.unwrap(); - assert_eq!(raw.storage_kind, LcmStorageKind::External); - assert!(raw.payload_ref.as_deref().unwrap().ends_with(".payload")); - assert_eq!( - db.lcm_expand_payload("cursor", "session-1", raw.payload_ref.as_deref().unwrap(), 0, payload.len()) - .await - .unwrap() - .content, - payload - ); -} -``` - -Run: `cargo test --test session_lcm_payload_test externalizes_large_tool_payload_with_recoverable_ref -- --nocapture` - -Expected: FAIL because payload externalization APIs do not exist. - -- [ ] **Step 2: Define payload metadata and storage root API** - -Sketch: - -```rust -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct LcmPayloadRef { - pub payload_ref: String, - pub provider: String, - pub session_id: String, - pub message_id: String, - pub kind: String, - pub content_hash: String, - pub byte_count: u64, - pub char_count: u64, - pub created_at: i64, -} -``` - -```rust -pub fn payload_dir(storage_root: &Path) -> PathBuf { - storage_root.join("lcm-payloads") -} -``` - -For project-local Cursor/Hermes installs, `storage_root` is project `.tokensave`. For non-local Hermes profile installs, `storage_root` is the selected Hermes profile root joined with `.tokensave`. - -- [ ] **Step 3: Add schema for payload refs** - -Add to `ensure_lcm_schema`: - -```sql -CREATE TABLE IF NOT EXISTS lcm_external_payloads ( - payload_ref TEXT PRIMARY KEY, - provider TEXT NOT NULL, - session_id TEXT NOT NULL, - message_id TEXT NOT NULL, - kind TEXT NOT NULL, - content_hash TEXT NOT NULL, - byte_count INTEGER NOT NULL, - char_count INTEGER NOT NULL, - created_at INTEGER NOT NULL DEFAULT (unixepoch()), - metadata_json TEXT, - UNIQUE(provider, message_id, payload_ref), - FOREIGN KEY(provider, session_id) REFERENCES sessions(provider, session_id) ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS idx_lcm_external_payloads_owner - ON lcm_external_payloads(provider, session_id); -``` - -- [ ] **Step 4: Implement basename-only refs, containment, permissions, and hashes** - -Sketch: - -```rust -pub fn validate_payload_ref(payload_ref: &str) -> Result<&str> { - let path = std::path::Path::new(payload_ref); - let is_basename = path.components().count() == 1 - && !payload_ref.contains('/') - && !payload_ref.contains('\\') - && payload_ref != "." - && payload_ref != ".."; - if is_basename { Ok(payload_ref) } else { Err(LcmError::InvalidPayloadRef) } -} - -pub async fn write_external_payload( - root: &Path, - provider: &str, - session_id: &str, - message_id: &str, - content: &str, -) -> Result { - let hash = sha256_hex(content.as_bytes()); - let payload_ref = format!("{provider}-{session_id}-{message_id}-{hash}.payload"); - validate_payload_ref(&payload_ref)?; - let dir = payload_dir(root); - create_private_dir(&dir)?; - let path = dir.join(&payload_ref); - ensure_contained(&dir, &path)?; - write_private_file(&path, content.as_bytes())?; - Ok(LcmPayloadRef { payload_ref, provider: provider.to_string(), session_id: session_id.to_string(), message_id: message_id.to_string(), kind: "message".to_string(), content_hash: hash, byte_count: content.len() as u64, char_count: content.chars().count() as u64, created_at: unixepoch(), metadata_json: None }) -} -``` - -- [ ] **Step 5: Add protection tests for unsafe refs and cross-session expansion** - -Tests: - -```rust -#[test] -fn rejects_payload_ref_path_traversal() { - for bad in ["../secret", "/tmp/secret", "nested/file", r"nested\file", ".", ".."] { - assert!(tokensave::sessions::lcm::payload::validate_payload_ref(bad).is_err()); - } -} -``` - -```rust -#[tokio::test] -async fn denies_cross_session_payload_expansion() { - let ref_for_a = insert_external_payload(&db, "cursor", "session-a", "message-a", "secret").await; - let denied = db.lcm_expand_payload("cursor", "session-b", &ref_for_a, 0, 100).await; - assert!(matches!(denied, Err(LcmError::PayloadNotOwnedBySession))); -} -``` - -Run: `cargo test --test session_lcm_payload_test -- --nocapture` - -Expected: PASS after containment implementation. - -- [ ] **Step 6: Add data URI/base64/large-output classification tests** - -`tests/session_lcm_ingest_protection_test.rs`: - -```rust -#[test] -fn classifies_data_uri_and_long_base64_for_externalization() { - let data_uri = format!("data:image/png;base64,{}", "A".repeat(20_000)); - assert!(should_externalize("assistant", Some("tool_result"), &data_uri)); - - let base64_run = "Q".repeat(80_000); - assert!(should_externalize("assistant", Some("message"), &base64_run)); - - assert!(!should_externalize("assistant", Some("message"), "short useful text")); -} -``` - -Run: `cargo test --test session_lcm_ingest_protection_test -- --nocapture` - -Expected: PASS after `src/sessions/lcm/security.rs` classifies large/binary-ish content. - -- [ ] **Step 7: Commit checkpoint** - -```bash -git add src/sessions/lcm/mod.rs src/sessions/lcm/types.rs src/sessions/lcm/schema.rs src/sessions/lcm/raw.rs src/sessions/lcm/payload.rs src/sessions/lcm/security.rs tests/session_lcm_payload_test.rs tests/session_lcm_ingest_protection_test.rs -git commit -m "Add LCM payload containment" -``` - -## Task 4: Summary DAG Tables, Types, and Lineage Expansion - -**Files:** -- Create: `src/sessions/lcm/dag.rs` -- Modify: `src/sessions/lcm/mod.rs` -- Modify: `src/sessions/lcm/types.rs` -- Modify: `src/sessions/lcm/schema.rs` -- Create: `tests/session_lcm_dag_test.rs` - -- [ ] **Step 1: Write failing DAG persistence test** - -```rust -#[tokio::test] -async fn summary_node_preserves_source_lineage_and_expands_sources() { - let tmp = tempfile::TempDir::new().unwrap(); - let db = open_lcm_db(tmp.path()).await; - insert_raw_messages(&db, "cursor", "session-1", ["alpha", "beta", "gamma"]).await; - - let node = db.lcm_insert_summary_node(LcmSummaryNodeDraft { - provider: "cursor".into(), - conversation_id: "conversation-1".into(), - session_id: "session-1".into(), - depth: 0, - summary_text: "alpha through gamma".into(), - source_refs: vec![ - LcmSourceRef::RawMessage { store_id: 1 }, - LcmSourceRef::RawMessage { store_id: 2 }, - LcmSourceRef::RawMessage { store_id: 3 }, - ], - source_token_count: 30, - summary_token_count: 4, - source_time_start: Some(1_715_000_000), - source_time_end: Some(1_715_000_030), - expand_hint: Some("3 raw messages".into()), - metadata_json: None, - }).await.unwrap(); - - let expanded = db.lcm_expand_summary_node("cursor", "session-1", &node.node_id).await.unwrap(); - assert_eq!(expanded.sources.len(), 3); - assert_eq!(expanded.sources[0].content, "alpha"); - assert_eq!(expanded.summary.summary_text, "alpha through gamma"); -} -``` - -Run: `cargo test --test session_lcm_dag_test summary_node_preserves_source_lineage_and_expands_sources -- --nocapture` - -Expected: FAIL with missing `LcmSummaryNodeDraft`, `LcmSourceRef`, and DAG APIs. - -- [ ] **Step 2: Add DAG schema** - -Add: - -```sql -CREATE TABLE IF NOT EXISTS lcm_summary_nodes ( - node_id TEXT PRIMARY KEY, - provider TEXT NOT NULL, - conversation_id TEXT NOT NULL, - session_id TEXT NOT NULL, - depth INTEGER NOT NULL, - summary_text TEXT NOT NULL, - summary_hash TEXT NOT NULL, - summary_token_count INTEGER NOT NULL, - source_token_count INTEGER NOT NULL, - source_time_start INTEGER, - source_time_end INTEGER, - expand_hint TEXT, - metadata_json TEXT, - created_at INTEGER NOT NULL DEFAULT (unixepoch()) -); -CREATE TABLE IF NOT EXISTS lcm_summary_sources ( - node_id TEXT NOT NULL, - source_kind TEXT NOT NULL CHECK(source_kind IN ('raw_message', 'summary_node')), - source_id TEXT NOT NULL, - ordinal INTEGER NOT NULL, - PRIMARY KEY(node_id, ordinal), - FOREIGN KEY(node_id) REFERENCES lcm_summary_nodes(node_id) ON DELETE CASCADE -); -CREATE INDEX IF NOT EXISTS idx_lcm_summary_nodes_session - ON lcm_summary_nodes(provider, session_id, depth, created_at); -CREATE VIRTUAL TABLE IF NOT EXISTS lcm_summary_nodes_fts USING fts5( - summary_text, expand_hint, metadata_json, - content='lcm_summary_nodes', - content_rowid='rowid' -); -``` - -- [ ] **Step 3: Define stable DAG types** - -```rust -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum LcmSourceRef { - RawMessage { store_id: i64 }, - SummaryNode { node_id: String }, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct LcmSummaryNode { - pub node_id: String, - pub provider: String, - pub conversation_id: String, - pub session_id: String, - pub depth: i64, - pub summary_text: String, - pub source_refs: Vec, - pub summary_token_count: i64, - pub source_token_count: i64, - pub source_time_start: Option, - pub source_time_end: Option, - pub expand_hint: Option, - pub metadata_json: Option, -} -``` - -- [ ] **Step 4: Implement deterministic node IDs and source expansion** - -Use a deterministic ID to make tests stable: - -```rust -pub fn summary_node_id(provider: &str, session_id: &str, depth: i64, source_refs: &[LcmSourceRef], summary_text: &str) -> String { - let input = serde_json::json!({ - "provider": provider, - "session_id": session_id, - "depth": depth, - "source_refs": source_refs, - "summary_hash": sha256_hex(summary_text.as_bytes()), - }); - format!("sum_{}", sha256_hex(input.to_string().as_bytes())) -} -``` - -Expansion must check `(provider, session_id)` ownership before returning raw messages or child summary nodes. - -- [ ] **Step 5: Add restart recovery test** - -Create `summary_dag_survives_reopen` that inserts a node, drops `GlobalDb`, reopens the same `sessions.db`, and expands the node. - -Run: `cargo test --test session_lcm_dag_test -- --nocapture` - -Expected: PASS. - -- [ ] **Step 6: Commit checkpoint** - -```bash -git add src/sessions/lcm/mod.rs src/sessions/lcm/types.rs src/sessions/lcm/schema.rs src/sessions/lcm/dag.rs tests/session_lcm_dag_test.rs -git commit -m "Add LCM summary DAG storage" -``` - -## Task 5: Search, Load, Expand, Describe, and Status Rust APIs - -**Files:** -- Create: `src/sessions/lcm/query.rs` -- Modify: `src/sessions/lcm/mod.rs` -- Modify: `src/sessions/lcm/types.rs` -- Modify: `src/sessions/lcm/raw.rs` -- Modify: `src/sessions/lcm/dag.rs` -- Modify: `src/sessions/lcm/payload.rs` -- Create: `tests/session_lcm_query_test.rs` - -- [ ] **Step 1: Write failing query API tests** - -```rust -#[tokio::test] -async fn load_session_returns_ordered_raw_pages_with_stable_cursor() { - let tmp = tempfile::TempDir::new().unwrap(); - let db = open_lcm_db(tmp.path()).await; - insert_raw_messages(&db, "cursor", "session-1", ["one", "two", "three"]).await; - - let page = db.lcm_load_session(LcmLoadSessionRequest { - provider: "cursor".into(), - session_id: "session-1".into(), - after_store_id: None, - limit: 2, - role: None, - start_time: None, - end_time: None, - content_slice: None, - }).await.unwrap(); - - assert_eq!(page.messages.iter().map(|m| m.content.as_str()).collect::>(), ["one", "two"]); - assert_eq!(page.next_cursor.as_deref(), Some("2")); - - let second = db.lcm_load_session(LcmLoadSessionRequest { - after_store_id: Some(2), - ..page.request_for_next() - }).await.unwrap(); - assert_eq!(second.messages[0].content, "three"); - assert!(second.next_cursor.is_none()); -} -``` - -Run: `cargo test --test session_lcm_query_test load_session_returns_ordered_raw_pages_with_stable_cursor -- --nocapture` - -Expected: FAIL because query request/response types do not exist. - -- [ ] **Step 2: Add query request/response types** - -Sketch: - -```rust -pub struct LcmLoadSessionRequest { - pub provider: String, - pub session_id: String, - pub after_store_id: Option, - pub limit: usize, - pub role: Option, - pub start_time: Option, - pub end_time: Option, - pub content_slice: Option, -} - -pub struct LcmGrepRequest { - pub provider: String, - pub query: String, - pub scope: LcmScope, - pub session_id: Option, - pub include_summaries: bool, - pub limit: usize, -} - -pub enum LcmScope { - Current, - Session, - All, -} -``` - -- [ ] **Step 3: Implement load and expand with pagination** - -Rules: - -- `limit` clamps to `1..=100`. -- `content_slice` returns bounded `content` plus `content_range`, never silently discards raw content. -- External payload expansion uses `LcmPayloadRef` ownership checks from Task 3. -- Summary expansion uses DAG source ownership checks from Task 4. - -Run: `cargo test --test session_lcm_query_test load_session_returns_ordered_raw_pages_with_stable_cursor -- --nocapture` - -Expected: PASS. - -- [ ] **Step 4: Write and implement combined grep/status/describe tests** - -Tests: - -```rust -#[tokio::test] -async fn grep_searches_raw_snippets_and_summary_nodes() { - let hits = db.lcm_grep(LcmGrepRequest { - provider: "cursor".into(), - query: "billing migration".into(), - scope: LcmScope::Session, - session_id: Some("session-1".into()), - include_summaries: true, - limit: 10, - }).await.unwrap(); - assert!(hits.iter().any(|hit| hit.kind == "raw_message")); - assert!(hits.iter().any(|hit| hit.kind == "summary_node")); - assert!(hits.iter().all(|hit| hit.snippet.len() <= MAX_DERIVED_TEXT_CHARS + 64)); -} -``` - -```rust -#[tokio::test] -async fn status_reports_schema_frontier_payload_and_debt_counts() { - let status = db.lcm_status("cursor", Some("session-1")).await.unwrap(); - assert_eq!(status.schema_version, LCM_SCHEMA_VERSION); - assert_eq!(status.raw_message_count, 3); - assert_eq!(status.summary_node_count, 1); - assert_eq!(status.missing_payload_count, 0); - assert_eq!(status.maintenance_debt_count, 0); -} -``` - -Run: `cargo test --test session_lcm_query_test -- --nocapture` - -Expected: PASS. - -- [ ] **Step 5: Commit checkpoint** - -```bash -git add src/sessions/lcm/mod.rs src/sessions/lcm/types.rs src/sessions/lcm/raw.rs src/sessions/lcm/dag.rs src/sessions/lcm/payload.rs src/sessions/lcm/query.rs tests/session_lcm_query_test.rs -git commit -m "Add LCM query APIs" -``` - -## Task 6: MCP Tool Definitions, Handlers, and Message Search Compatibility - -**Files:** -- Modify: `src/mcp/tools/definitions.rs` -- Modify: `src/mcp/tools/handlers/session.rs` -- Modify: `src/mcp/tools/handlers/mod.rs` -- Modify: `tests/mcp_handler_test.rs` -- Modify: `tests/session_global_db_test.rs` - -- [ ] **Step 1: Write failing tool-definition tests** - -In `tests/mcp_handler_test.rs`: - -```rust -#[test] -fn lcm_tool_schemas_are_registered_with_stable_names() { - let tools = get_tool_definitions(); - let names = tools.iter().map(|tool| tool.name.as_str()).collect::>(); - - for expected in [ - "tokensave_lcm_status", - "tokensave_lcm_load_session", - "tokensave_lcm_grep", - "tokensave_lcm_describe", - "tokensave_lcm_expand", - "tokensave_lcm_expand_query", - "tokensave_lcm_preflight", - "tokensave_lcm_compress", - ] { - assert!(names.contains(expected), "missing {expected}"); - } -} -``` - -Run: `cargo test --test mcp_handler_test lcm_tool_schemas_are_registered_with_stable_names -- --nocapture` - -Expected: FAIL because definitions are not registered. - -- [ ] **Step 2: Add additive definitions with precise JSON schemas** - -Sketch for one tool: - -```rust -fn def_lcm_load_session() -> ToolDefinition { - def( - "tokensave_lcm_load_session", - "LCM Load Session", - "Load ordered lossless raw session messages with pagination and bounded response slicing.", - json!({ - "type": "object", - "properties": { - "provider": {"type": "string", "description": "Provider id, default cursor."}, - "session_id": {"type": "string", "description": "Provider-local session id."}, - "after_store_id": {"type": "number", "description": "Return rows after this raw store id."}, - "limit": {"type": "number", "description": "Maximum rows, clamped to 100."}, - "role": {"type": "string", "description": "Optional role filter."}, - "content_offset": {"type": "number", "description": "Character offset for content slice."}, - "content_limit": {"type": "number", "description": "Maximum characters returned per message."} - }, - "required": ["session_id"] - }), - ) -} -``` - -Use `def_rw` for `tokensave_lcm_preflight` and `tokensave_lcm_compress` because they ingest or mutate lifecycle state. - -- [ ] **Step 3: Add handler dispatch and JSON output contracts** - -Sketch: - -```rust -pub(super) async fn handle_lcm_load_session(cg: &TokenSave, args: Value) -> Result { - let provider = string_arg(&args, "provider").unwrap_or("cursor"); - let session_id = required_string_arg(&args, "session_id")?; - let db = open_project_session_db_or_unavailable(cg.project_root()).await?; - let page = db.lcm_load_session(LcmLoadSessionRequest::from_args(provider, session_id, &args)?).await?; - Ok(tool_json(&json!({ - "status": "ok", - "provider": provider, - "session_id": session_id, - "messages": page.messages, - "next_cursor": page.next_cursor, - }))) -} -``` - -Keep `truncate_response` at the outer MCP rendering layer, not in raw storage. - -- [ ] **Step 4: Preserve `tokensave_message_search` behavior** - -Add a compatibility test that reuses current assertions: - -```rust -#[tokio::test] -async fn message_search_preserves_provider_project_parent_scope_shape_after_lcm() { - let (cg, _dir) = setup_project().await; - seed_lcm_and_projection_rows(cg.project_root()).await; - - let result = handle_tool_call( - &cg, - "tokensave_message_search", - json!({ - "query": "orchard dispatch", - "provider": "cursor", - "project_key": cg.project_root().to_string_lossy(), - "scope": "subagents_only", - "parent_session_id": "parent", - "limit": 10 - }), - None, - None, - ).await.unwrap(); - - let payload: serde_json::Value = serde_json::from_str(extract_text(&result.value)).unwrap(); - assert_eq!(payload["status"], "ok"); - assert_eq!(payload["scope"], "subagents_only"); - assert_eq!(payload["results"].as_array().unwrap().len(), 1); - assert!(payload["results"][0]["message"].get("text").is_some()); -} -``` - -Run: `cargo test --test mcp_handler_test message_search_preserves_provider_project_parent_scope_shape_after_lcm -- --nocapture` - -Expected: PASS after handler compatibility is wired. - -- [ ] **Step 5: Add CLI bridge smoke test for `tokensave tool ... --json --args`** - -In `tests/mcp_handler_test.rs` or a new CLI integration test: - -```rust -let output = std::process::Command::new(env!("CARGO_BIN_EXE_tokensave")) - .current_dir(cg.project_root()) - .args([ - "tool", - "tokensave_lcm_status", - "--json", - "--args", - r#"{"provider":"cursor"}"#, - ]) - .output() - .unwrap(); -assert!(output.status.success()); -let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); -assert_eq!(json["content"][0]["type"], "text"); -``` - -Run: `cargo test --test mcp_handler_test lcm_tool_schemas_are_registered_with_stable_names message_search_preserves_provider_project_parent_scope_shape_after_lcm -- --nocapture` - -Expected: PASS. - -- [ ] **Step 6: Commit checkpoint** - -```bash -git add src/mcp/tools/definitions.rs src/mcp/tools/handlers/session.rs src/mcp/tools/handlers/mod.rs tests/mcp_handler_test.rs tests/session_global_db_test.rs -git commit -m "Expose LCM session tools" -``` - -## Task 7: Hermes Context Engine Registration and Storage Locality - -**Files:** -- Modify: `src/agents/hermes.rs` -- Modify: `tests/agent_test.rs` -- Create: `tests/hermes_lcm_bridge_test.rs` - -- [ ] **Step 1: Write failing generated-plugin registration test** - -In `tests/agent_test.rs`: - -```rust -#[test] -fn test_hermes_generated_python_registers_lcm_context_engine() { - let home = TempDir::new().unwrap(); - HermesIntegration.install(&make_install_ctx(home.path())).unwrap(); - let init_py = std::fs::read_to_string(home.path().join(".hermes/plugins/tokensave/__init__.py")).unwrap(); - - assert!(init_py.contains("class TokenSaveContextEngine")); - assert!(init_py.contains("ctx.register_context_engine")); - assert!(init_py.contains("tools.call_tokensave_tool(\"tokensave_lcm_preflight\"")); - assert!(init_py.contains("tools.call_tokensave_tool(\"tokensave_lcm_compress\"")); -} -``` - -Run: `cargo test --test agent_test test_hermes_generated_python_registers_lcm_context_engine -- --nocapture` - -Expected: FAIL because generated Python does not register a context engine yet. - -- [ ] **Step 2: Generate explicit storage locality metadata** - -Add helper output in `plugin_init()`: - -```python -def _storage_args(project_root=None, hermes_home=None): - args = {} - if project_root: - args["storage_scope"] = "project_local" - args["project_root"] = str(project_root) - elif hermes_home: - args["storage_scope"] = "hermes_profile" - args["hermes_home"] = str(hermes_home) - else: - args["storage_scope"] = "hermes_profile" - return args -``` - -Local Hermes install (`tokensave install --local --agent hermes` without `--profile`) continues to write under `project/.hermes/plugins/tokensave` and instructs users to launch with `HERMES_HOME=project/.hermes`. The LCM storage args still point Rust at the project `.tokensave/sessions.db` when `project_root` is known. - -Non-local/profile Hermes install uses the active Hermes home/profile as the storage root; Rust resolves the session DB at `/.tokensave/sessions.db`. - -- [ ] **Step 3: Add generated `TokenSaveContextEngine` skeleton** - -Sketch: - -```python -class TokenSaveContextEngine: - def __init__(self): - self.hermes_home = None - self.project_root = None - self.active_session_id = None - - def initialize(self, session_id=None, hermes_home=None, project_root=None, **kwargs): - self.active_session_id = session_id - self.hermes_home = hermes_home - self.project_root = project_root or kwargs.get("cwd") - - def should_compress_preflight(self, messages, **kwargs): - args = _storage_args(self.project_root, self.hermes_home) - args.update({"session_id": self.active_session_id, "messages": messages}) - return json.loads(tools.call_tokensave_tool("tokensave_lcm_preflight", args)) - - def compress(self, messages, current_tokens=None, focus_topic=None, **kwargs): - args = _storage_args(self.project_root, self.hermes_home) - args.update({ - "session_id": self.active_session_id, - "messages": messages, - "current_tokens": current_tokens, - "focus_topic": focus_topic, - }) - return json.loads(tools.call_tokensave_tool("tokensave_lcm_compress", args)) -``` - -In `register(ctx)`, call `ctx.register_context_engine(TokenSaveContextEngine())` only when the method exists, matching the current optional registration style for commands and memory providers. - -- [ ] **Step 4: Add Python compile and fake context tests** - -In `tests/hermes_lcm_bridge_test.rs`, install the plugin into a temp home and run Python that imports `__init__.py`, supplies a fake `ctx`, and asserts `register_context_engine` receives a `TokenSaveContextEngine`. - -Run: `cargo test --test hermes_lcm_bridge_test generated_context_engine_registers_when_supported -- --nocapture` - -Expected: PASS after generated Python changes. - -- [ ] **Step 5: Commit checkpoint** - -```bash -git add src/agents/hermes.rs tests/agent_test.rs tests/hermes_lcm_bridge_test.rs -git commit -m "Register Hermes LCM context engine" -``` - -## Task 8: Python Bridge Calls and Deterministic No-Op/Fake Summarizer Tests - -**Files:** -- Modify: `src/agents/hermes.rs` -- Modify: `src/sessions/lcm/hermes.rs` -- Modify: `src/sessions/lcm/types.rs` -- Modify: `tests/hermes_lcm_bridge_test.rs` - -- [ ] **Step 1: Write failing bridge-call argument test** - -In `tests/hermes_lcm_bridge_test.rs`, generate plugin files and run a Python script that monkeypatches `tools.subprocess.run`: - -```python -calls = [] -def fake_run(argv, check, capture_output, text, timeout, shell): - calls.append(argv) - payload = {"content": [{"type": "text", "text": "{\"status\":\"ok\",\"should_compress\":false,\"messages\":[]}"}]} - return Result(0, json.dumps(payload), "") - -tools.subprocess.run = fake_run -engine = plugin.TokenSaveContextEngine() -engine.initialize(session_id="session-1", project_root="/tmp/project") -result = engine.should_compress_preflight([{"role": "user", "content": "hello"}]) - -assert result["status"] == "ok" -assert calls[0][0] == tools.TOKENSAVE_BIN -assert calls[0][1:4] == ["tool", "tokensave_lcm_preflight", "--json"] -assert "--args" in calls[0] -assert '"storage_scope": "project_local"' in calls[0][-1] -``` - -Run: `cargo test --test hermes_lcm_bridge_test context_engine_preflight_uses_tokensave_tool_json_args -- --nocapture` - -Expected: FAIL until the generated context engine uses `tools.call_tokensave_tool` with the correct names and storage args. - -- [ ] **Step 2: Normalize nested MCP JSON text in generated Python** - -Current `tools.call_tokensave_tool` returns the full JSON-RPC result string. Add a helper: - -```python -def call_tokensave_json(name: str, args: dict, **kwargs): - raw = call_tokensave_tool(name, args, **kwargs) - outer = json.loads(raw) - text = outer.get("content", [{}])[0].get("text", "{}") - return json.loads(text) -``` - -Use this helper inside `TokenSaveContextEngine`. - -- [ ] **Step 3: Add deterministic fake summarizer contract** - -Rust request/response sketch: - -```rust -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct LcmCompressionRequest { - pub provider: String, - pub session_id: String, - pub messages: Vec, - pub current_tokens: Option, - pub focus_topic: Option, - pub summarizer: LcmSummarizerMode, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(tag = "mode", rename_all = "snake_case")] -pub enum LcmSummarizerMode { - Noop, - Fake { summary_text: String }, - HermesAuxiliary, -} -``` - -The generated Python uses `HermesAuxiliary` in production. Rust tests use `Noop` or `Fake`. - -- [ ] **Step 4: Add no-op preflight/compress tests** - -Tests: - -```rust -#[tokio::test] -async fn noop_summarizer_ingests_messages_without_summary_nodes() { - let response = db.lcm_compress(LcmCompressionRequest { - provider: "cursor".into(), - session_id: "session-1".into(), - messages: vec![json!({"role": "user", "content": "fresh"})], - current_tokens: Some(100), - focus_topic: None, - summarizer: LcmSummarizerMode::Noop, - }).await.unwrap(); - - assert_eq!(response.status, "ok"); - assert_eq!(response.summary_nodes_created, 0); - assert_eq!(db.lcm_load_session(raw_load("cursor", "session-1")).await.unwrap().messages.len(), 1); -} -``` - -Run: `cargo test --test hermes_lcm_bridge_test context_engine_preflight_uses_tokensave_tool_json_args -- --nocapture` - -Expected: PASS. - -- [ ] **Step 5: Commit checkpoint** - -```bash -git add src/agents/hermes.rs src/sessions/lcm/hermes.rs src/sessions/lcm/types.rs tests/hermes_lcm_bridge_test.rs -git commit -m "Add Hermes LCM bridge contracts" -``` - -## Task 9: Compression Lifecycle Primitives and Deterministic Summarizer Injection - -**Files:** -- Create: `src/sessions/lcm/compression.rs` -- Modify: `src/sessions/lcm/mod.rs` -- Modify: `src/sessions/lcm/types.rs` -- Modify: `src/sessions/lcm/schema.rs` -- Modify: `src/sessions/lcm/dag.rs` -- Modify: `src/sessions/lcm/query.rs` -- Create: `tests/session_lcm_compression_test.rs` - -- [ ] **Step 1: Write failing lifecycle schema tests** - -```rust -#[tokio::test] -async fn lifecycle_frontier_survives_reopen() { - let tmp = tempfile::TempDir::new().unwrap(); - let db_path = tmp.path().join(".tokensave/sessions.db"); - let db = GlobalDb::open_at(&db_path).await.unwrap(); - db.lcm_update_lifecycle(LcmLifecycleUpdate { - provider: "cursor".into(), - conversation_id: "conversation-1".into(), - current_session_id: "session-1".into(), - current_frontier_store_id: Some(42), - last_finalized_session_id: Some("session-0".into()), - last_finalized_frontier_store_id: Some(40), - maintenance_debt: vec![LcmMaintenanceDebt::RawBacklog { from_store_id: 41, to_store_id: 42 }], - }).await.unwrap(); - drop(db); - - let reopened = GlobalDb::open_at(&db_path).await.unwrap(); - let state = reopened.lcm_lifecycle_state("cursor", "conversation-1").await.unwrap(); - assert_eq!(state.current_frontier_store_id, Some(42)); - assert_eq!(state.last_finalized_session_id.as_deref(), Some("session-0")); - assert_eq!(state.maintenance_debt.len(), 1); -} -``` - -Run: `cargo test --test session_lcm_compression_test lifecycle_frontier_survives_reopen -- --nocapture` - -Expected: FAIL with missing lifecycle table/types. - -- [ ] **Step 2: Add lifecycle/frontier/debt schema** - -```sql -CREATE TABLE IF NOT EXISTS lcm_lifecycle_state ( - provider TEXT NOT NULL, - conversation_id TEXT NOT NULL, - current_session_id TEXT NOT NULL, - last_finalized_session_id TEXT, - current_frontier_store_id INTEGER, - last_finalized_frontier_store_id INTEGER, - rollover_at INTEGER, - reset_at INTEGER, - maintenance_at INTEGER, - updated_at INTEGER NOT NULL DEFAULT (unixepoch()), - PRIMARY KEY(provider, conversation_id) -); -CREATE TABLE IF NOT EXISTS lcm_maintenance_debt ( - provider TEXT NOT NULL, - conversation_id TEXT NOT NULL, - debt_id TEXT NOT NULL, - debt_kind TEXT NOT NULL, - from_store_id INTEGER, - to_store_id INTEGER, - metadata_json TEXT, - created_at INTEGER NOT NULL DEFAULT (unixepoch()), - PRIMARY KEY(provider, conversation_id, debt_id), - FOREIGN KEY(provider, conversation_id) - REFERENCES lcm_lifecycle_state(provider, conversation_id) ON DELETE CASCADE -); -``` - -- [ ] **Step 3: Add fake summarizer trait and deterministic compression API** - -Sketch: - -```rust -#[async_trait::async_trait] -pub trait LcmSummarizer: Send + Sync { - async fn summarize(&self, request: LcmSummarizeRequest) -> Result; -} - -pub struct FakeSummarizer { - pub summary_text: String, -} - -#[async_trait::async_trait] -impl LcmSummarizer for FakeSummarizer { - async fn summarize(&self, request: LcmSummarizeRequest) -> Result { - Ok(LcmSummarizeOutput { - summary_text: self.summary_text.clone(), - source_token_count: request.source_token_count, - summary_token_count: estimate_tokens(&self.summary_text), - }) - } -} -``` - -If the repo avoids `async-trait`, define the trait as synchronous and keep Hermes auxiliary calls in generated Python. The Rust fake summarizer is enough for deterministic compression tests. - -- [ ] **Step 4: Implement preflight-ingests behavior** - -Test: - -```rust -#[tokio::test] -async fn preflight_can_request_compression_when_ingest_protection_changes_replay() { - let response = db.lcm_preflight(LcmPreflightRequest { - provider: "cursor".into(), - session_id: "session-1".into(), - messages: vec![json!({"role": "assistant", "content": format!("data:image/png;base64,{}", "A".repeat(100_000))})], - current_tokens: Some(100), - }).await.unwrap(); - - assert!(response.should_compress); - assert_eq!(response.reason, "ingest_protection_changed_replay"); - assert!(response.replay_messages[0]["content"].as_str().unwrap().contains("[externalized payload")); -} -``` - -- [ ] **Step 5: Implement frontier advance, fresh tail preservation, and DAG node creation** - -Test: - -```rust -#[tokio::test] -async fn fake_summarizer_compacts_backlog_and_preserves_fresh_tail() { - insert_raw_messages(&db, "cursor", "session-1", ["old-1", "old-2", "fresh-1", "fresh-2"]).await; - let response = db.lcm_compress_with_summarizer( - compress_request("cursor", "session-1").fresh_tail_count(2), - &FakeSummarizer { summary_text: "old summary".into() }, - ).await.unwrap(); - - assert_eq!(response.summary_nodes_created, 1); - assert_eq!(response.replay_messages.last().unwrap()["content"], "fresh-2"); - assert_eq!(response.frontier.current_frontier_store_id, Some(2)); -} -``` - -Run: `cargo test --test session_lcm_compression_test -- --nocapture` - -Expected: PASS. - -- [ ] **Step 6: Commit checkpoint** - -```bash -git add src/sessions/lcm/mod.rs src/sessions/lcm/types.rs src/sessions/lcm/schema.rs src/sessions/lcm/dag.rs src/sessions/lcm/query.rs src/sessions/lcm/compression.rs tests/session_lcm_compression_test.rs -git commit -m "Add deterministic LCM compression lifecycle" -``` - -## Task 10: Hermes Auxiliary LLM Bridge, Reasoning Stripping, and Fallbacks - -**Files:** -- Modify: `src/agents/hermes.rs` -- Modify: `src/sessions/lcm/hermes.rs` -- Modify: `src/sessions/lcm/types.rs` -- Modify: `tests/hermes_lcm_bridge_test.rs` - -- [ ] **Step 1: Write failing reasoning-strip test for generated Python** - -Python fixture in `tests/hermes_lcm_bridge_test.rs`: - -```python -class Aux: - def __init__(self): - self.calls = [] - def call_llm(self, **kwargs): - self.calls.append(kwargs) - return "hidden chain\nUseful compact summary" - -agent = type("Agent", (), {"auxiliary_client": Aux()})() -engine = plugin.TokenSaveContextEngine() -engine.initialize(session_id="session-1", project_root="/tmp/project", agent=agent) -summary = engine._call_auxiliary_summary("Summarize", [{"role": "user", "content": "raw"}]) - -assert summary["status"] == "ok" -assert summary["text"] == "Useful compact summary" -assert agent.auxiliary_client.calls[0]["task"] == "compression" -``` - -Run: `cargo test --test hermes_lcm_bridge_test auxiliary_summary_strips_reasoning_tags -- --nocapture` - -Expected: FAIL until auxiliary bridge helper exists. - -- [ ] **Step 2: Add generated Python reasoning stripper** - -Sketch: - -```python -REASONING_TAGS = ["think", "thinking", "reasoning", "thought", "REASONING_SCRATCHPAD"] - -def _strip_reasoning(text: str) -> str: - output = text or "" - for tag in REASONING_TAGS: - output = re.sub(fr"<{tag}>.*?", "", output, flags=re.DOTALL | re.IGNORECASE) - return output.strip() -``` - -- [ ] **Step 3: Add auxiliary summary route chain and cooldown state** - -Keep state process-local in Python: - -```python -class TokenSaveContextEngine: - def __init__(self): - self._route_failures = {} - self._cooldown_until = {} - - def _call_auxiliary_summary(self, prompt, messages, **kwargs): - routes = kwargs.get("routes") or [{"model": kwargs.get("model"), "temperature": 0.1}] - for route in routes: - key = route.get("model") or "default" - if self._cooldown_until.get(key, 0) > time.time(): - continue - try: - text = self.agent.auxiliary_client.call_llm( - task="compression", - messages=[{"role": "system", "content": prompt}, *messages], - temperature=route.get("temperature", 0.1), - max_tokens=route.get("max_tokens", 2048), - timeout=route.get("timeout", 60), - model=route.get("model"), - ) - stripped = _strip_reasoning(str(text)) - if stripped: - return {"status": "ok", "text": stripped, "route": key} - except Exception as exc: - self._route_failures[key] = self._route_failures.get(key, 0) + 1 - self._cooldown_until[key] = time.time() + min(300, 2 ** self._route_failures[key]) - return {"status": "fallback", "text": _deterministic_truncation(messages)} -``` - -- [ ] **Step 4: Pass auxiliary summaries back to Rust** - -`compress()` flow in generated Python: - -1. Call `tokensave_lcm_compress` with `summarizer.mode = "request_auxiliary"`. -2. If Rust returns `status = "needs_summary"` with a prompt and source messages, call `_call_auxiliary_summary`. -3. Call `tokensave_lcm_compress` again with `summarizer.mode = "provided"` and `summary_text`. -4. Return the final replay messages and frontier/status JSON to Hermes. - -The Rust contract: - -```rust -#[serde(tag = "mode", rename_all = "snake_case")] -pub enum LcmSummarizerMode { - Noop, - Fake { summary_text: String }, - RequestAuxiliary, - Provided { summary_text: String, route: Option }, -} -``` - -- [ ] **Step 5: Add fallback behavior tests** - -Tests: - -```python -class FailingAux: - def call_llm(self, **kwargs): - raise RuntimeError("route unavailable") - -engine.initialize(session_id="session-1", project_root="/tmp/project", agent=type("Agent", (), {"auxiliary_client": FailingAux()})()) -summary = engine._call_auxiliary_summary("Summarize", [{"role": "user", "content": "A" * 10_000}]) -assert summary["status"] == "fallback" -assert len(summary["text"]) < 10_000 -assert summary["text"].endswith("[deterministic compression fallback]") -``` - -Run: `cargo test --test hermes_lcm_bridge_test -- --nocapture` - -Expected: PASS. - -- [ ] **Step 6: Commit checkpoint** - -```bash -git add src/agents/hermes.rs src/sessions/lcm/hermes.rs src/sessions/lcm/types.rs tests/hermes_lcm_bridge_test.rs -git commit -m "Integrate Hermes auxiliary LCM summaries" -``` - -## Task 11: Ingest Protection, Diagnostics, Doctor, and Regression Tests - -**Files:** -- Modify: `src/sessions/lcm/security.rs` -- Modify: `src/sessions/lcm/query.rs` -- Modify: `src/mcp/tools/definitions.rs` -- Modify: `src/mcp/tools/handlers/session.rs` -- Modify: `src/agents/mod.rs` -- Modify: `src/agents/hermes.rs` -- Modify: `tests/session_lcm_ingest_protection_test.rs` -- Modify: `tests/session_lcm_payload_test.rs` -- Modify: `tests/agent_test.rs` -- Modify: `tests/mcp_handler_test.rs` -- Modify: `tests/session_lcm_query_test.rs` - -- [ ] **Step 1: Write failing doctor/status integrity tests** - -```rust -#[tokio::test] -async fn lcm_status_reports_missing_and_unreferenced_payloads_without_previewing_content() { - let tmp = tempfile::TempDir::new().unwrap(); - let db = open_lcm_db(tmp.path()).await; - let secret = "SUPER_SECRET_PAYLOAD"; - let payload_ref = insert_external_payload(&db, "cursor", "session-1", "message-1", secret).await; - std::fs::remove_file(payload_path(tmp.path(), &payload_ref)).unwrap(); - std::fs::write(payload_dir(tmp.path()).join("orphan.payload"), "orphan secret").unwrap(); - - let status = db.lcm_status("cursor", Some("session-1")).await.unwrap(); - assert_eq!(status.missing_payload_count, 1); - assert_eq!(status.unreferenced_payload_count, 1); - let rendered = serde_json::to_string(&status).unwrap(); - assert!(!rendered.contains(secret)); - assert!(!rendered.contains("orphan secret")); -} -``` - -Run: `cargo test --test session_lcm_payload_test lcm_status_reports_missing_and_unreferenced_payloads_without_previewing_content -- --nocapture` - -Expected: FAIL until integrity scan fields exist. - -- [ ] **Step 2: Add diagnostics JSON to `tokensave_lcm_status`** - -Status output sketch: - -```json -{ - "status": "ok", - "schema_version": 1, - "storage_scope": "project_local", - "raw_message_count": 12, - "summary_node_count": 2, - "payload": { - "externalized_count": 1, - "missing_count": 0, - "unreferenced_count": 0, - "root_contained": true - }, - "lifecycle": { - "current_session_id": "session-1", - "current_frontier_store_id": 10, - "maintenance_debt_count": 0 - }, - "redaction": { - "enabled": false, - "lossy_records": 0 - } -} -``` - -Do not include payload content in diagnostics. - -- [ ] **Step 3: Add regression scan against authoritative caps** - -Test: - -```rust -#[test] -fn no_authoritative_session_write_uses_legacy_text_cap() { - let source = std::fs::read_to_string("src/global_db.rs").unwrap(); - assert!(!source.contains("MAX_SESSION_MESSAGE_TEXT_BYTES")); - assert!(!source.contains("SESSION_MESSAGE_TRUNCATION_MARKER")); - - let lcm_raw = std::fs::read_to_string("src/sessions/lcm/raw.rs").unwrap(); - assert!(lcm_raw.contains("MAX_DERIVED_TEXT_CHARS")); - assert!(lcm_raw.contains("derived_text_for_index")); -} -``` - -Run: `cargo test --test session_lcm_ingest_protection_test no_authoritative_session_write_uses_legacy_text_cap -- --nocapture` - -Expected: PASS after old authoritative cap symbols are removed or renamed to derived-only constants. - -- [ ] **Step 4: Add separation regressions for codegraph and fact memory** - -In `tests/session_lcm_query_test.rs`: - -```rust -#[test] -fn lcm_modules_do_not_depend_on_context_builder_or_memory_fact_store() { - for path in [ - "src/sessions/lcm/raw.rs", - "src/sessions/lcm/dag.rs", - "src/sessions/lcm/query.rs", - "src/sessions/lcm/compression.rs", - ] { - let source = std::fs::read_to_string(path).unwrap(); - assert!(!source.contains("ContextBuilder")); - assert!(!source.contains("MemoryCategory")); - assert!(!source.contains("memory_facts")); - } -} -``` - -Run: `cargo test --test session_lcm_query_test lcm_modules_do_not_depend_on_context_builder_or_memory_fact_store -- --nocapture` - -Expected: PASS. - -- [ ] **Step 5: Extend Hermes doctor tests** - -In `tests/agent_test.rs`, assert Hermes healthcheck recognizes generated LCM plugin files and local/profile storage hints: - -```rust -assert!(init_py.contains("TokenSaveContextEngine")); -assert!(init_py.contains("storage_scope")); -assert!(manifest.contains("tokensave_lcm_status")); -assert!(manifest.contains("tokensave_lcm_compress")); -``` - -Run: `cargo test --test agent_test hermes -- --nocapture` - -Expected: PASS. - -- [ ] **Step 6: Run focused regression suite** - -Run: - -```bash -cargo test --test session_lcm_schema_test -- --nocapture -cargo test --test session_lcm_raw_test -- --nocapture -cargo test --test session_lcm_payload_test -- --nocapture -cargo test --test session_lcm_dag_test -- --nocapture -cargo test --test session_lcm_query_test -- --nocapture -cargo test --test session_lcm_compression_test -- --nocapture -cargo test --test session_lcm_ingest_protection_test -- --nocapture -cargo test --test hermes_lcm_bridge_test -- --nocapture -cargo test --test session_global_db_test -- --nocapture -cargo test --test mcp_handler_test -- --nocapture -cargo test --test agent_test hermes -- --nocapture -``` - -Expected: all commands PASS. - -- [ ] **Step 7: Commit checkpoint** - -```bash -git add src/sessions/lcm/security.rs src/sessions/lcm/query.rs src/mcp/tools/definitions.rs src/mcp/tools/handlers/session.rs src/agents/mod.rs src/agents/hermes.rs tests/session_lcm_ingest_protection_test.rs tests/session_lcm_payload_test.rs tests/agent_test.rs tests/mcp_handler_test.rs tests/session_lcm_query_test.rs -git commit -m "Add LCM diagnostics and regressions" -``` - -## Final Verification - -Run these after all implementation tasks are complete: - -```bash -cargo fmt --all -- --check -cargo clippy --all-targets -cargo test -``` - -Expected: - -- `cargo fmt --all -- --check` exits 0 with no formatting drift. -- `cargo clippy --all-targets` exits 0 with no new warnings. -- `cargo test` exits 0 across the full Rust test suite. - -Manual verification for generated Hermes Python: - -```bash -cargo test --test agent_test hermes -- --nocapture -cargo test --test hermes_lcm_bridge_test -- --nocapture -``` - -Expected: - -- Generated `plugin.yaml`, `schemas.py`, `schemas.json`, `tools.py`, `__init__.py`, and `skills/tokensave/SKILL.md` compile and register. -- `TokenSaveContextEngine` registers when Hermes exposes `register_context_engine`. -- The generated bridge calls `tokensave tool ... --json --args` and can parse nested MCP text JSON. -- Auxiliary summaries strip reasoning tags and fall back to deterministic truncation when all routes fail. - -## Self-Review - -- Spec coverage: The tasks cover in-place `sessions.db` migration, lossless raw storage, bounded derived search/display text, externalized payload containment, summary DAG lineage, lifecycle/frontier/debt state, deterministic Rust query APIs, additive MCP tools, compatibility for `tokensave_message_search`, Hermes generated Python lifecycle registration, subprocess bridge use, auxiliary LLM reasoning stripping/fallbacks, storage locality, ingest protection, diagnostics, and separation from codegraph/memory systems. -- Red-flag scan: The plan contains concrete file paths, test names, commands, expected red/green outcomes, commit checkpoints, and code/API sketches for each code-changing task. -- Type consistency: `LcmRawMessage`, `LcmStorageKind`, `LcmPayloadRef`, `LcmSourceRef`, `LcmSummaryNode`, `LcmLoadSessionRequest`, `LcmGrepRequest`, `LcmSummarizerMode`, and `TokenSaveContextEngine` are introduced before later tasks use them. -- Caveat: This plan chooses `/.tokensave/sessions.db` for non-local Hermes profile storage so the implementation has one concrete path rule. A later PyO3/native binding milestone remains outside this plan and should require measured bridge overhead before it starts. diff --git a/docs/superpowers/specs/2026-03-27-daemon-mode-design.md b/docs/superpowers/specs/2026-03-27-daemon-mode-design.md index 7b8b3d7a..6b103f92 100644 --- a/docs/superpowers/specs/2026-03-27-daemon-mode-design.md +++ b/docs/superpowers/specs/2026-03-27-daemon-mode-design.md @@ -1,27 +1,27 @@ # Daemon Mode Design Spec -> **Rebrand note:** The project has since been renamed **TraceDecay** (binary/crate `tracedecay`, MCP tools `tracedecay_*`). This dated design artifact keeps the TokenSave-era names it was written with. +> **Rebrand note:** The project has since been renamed **TraceDecay** (binary/crate `tracedecay`, MCP tools `tracedecay_*`). This dated design artifact keeps the TraceDecay-era names it was written with. ## Goal -A background daemon that watches all tracked tokensave projects for file changes and automatically runs incremental syncs, keeping the code graph up-to-date without manual `tokensave sync` invocations. +A background daemon that watches all tracked tracedecay projects for file changes and automatically runs incremental syncs, keeping the code graph up-to-date without manual `tracedecay sync` invocations. ## CLI Surface ``` -tokensave daemon # start (forks to background, writes PID file) -tokensave daemon --foreground # stay in foreground (for debugging / service managers) -tokensave daemon --stop # kill running daemon via PID file -tokensave daemon --status # check if running -tokensave daemon --enable-autostart # install launchd/systemd service -tokensave daemon --disable-autostart # remove service +tracedecay daemon # start (forks to background, writes PID file) +tracedecay daemon --foreground # stay in foreground (for debugging / service managers) +tracedecay daemon --stop # kill running daemon via PID file +tracedecay daemon --status # check if running +tracedecay daemon --enable-autostart # install launchd/systemd service +tracedecay daemon --disable-autostart # remove service ``` All flags are mutually exclusive. `--foreground` is useful for debugging and when the daemon is managed by an external service manager. ## Config -Add `daemon_debounce` to `~/.tokensave/config.toml`: +Add `daemon_debounce` to `~/.tracedecay/config.toml`: ```toml daemon_debounce = "15s" @@ -36,7 +36,7 @@ A simple duration parser understands `s` (seconds) and `m` (minutes). Examples: | Component | File | Responsibility | |-----------|------|----------------| | DaemonRunner | `src/daemon.rs` | Core event loop: project discovery, file watching, debounce, sync dispatch | -| PID management | `src/daemon.rs` | Write/read/check `~/.tokensave/daemon.pid` | +| PID management | `src/daemon.rs` | Write/read/check `~/.tracedecay/daemon.pid` | | Duration parser | `src/daemon.rs` | Parse `"15s"` / `"1m"` strings into `Duration` | | Service installer | `src/daemon.rs` | Generate launchd plist (macOS) / systemd user unit (Linux) | | CLI integration | `src/main.rs` | `Commands::Daemon` enum variant with flags | @@ -50,24 +50,24 @@ A simple duration parser understands `s` (seconds) and `m` (minutes). Examples: 3. **File change event:** When `notify` fires an event, determine which project it belongs to (by matching the event path against watched project roots). Mark that project as "dirty" and start/reset its per-project debounce timer. -4. **Debounce fires (default 15s after last change):** Open `TokenSave::open()` for the dirty project, call `sync()`, log the result (files added/modified/removed, duration). Update global DB token count. +4. **Debounce fires (default 15s after last change):** Open `TraceDecay::open()` for the dirty project, call `sync()`, log the result (files added/modified/removed, duration). Update global DB token count. -5. **Filtering:** Ignore change events inside `.tokensave/`, `.git/`, `node_modules/`, `target/`, `.build/`, and other common build output directories. Also ignore events for files that don't match any supported language extension. +5. **Filtering:** Ignore change events inside `.tracedecay/`, `.git/`, `node_modules/`, `target/`, `.build/`, and other common build output directories. Also ignore events for files that don't match any supported language extension. ### Self-Daemonizing -On `tokensave daemon` (without `--foreground`): +On `tracedecay daemon` (without `--foreground`): 1. Fork the process using `fork()` (Unix) or equivalent -2. Detach from terminal (setsid, close stdin/stdout/stderr, redirect to log file `~/.tokensave/daemon.log`) -3. Write PID to `~/.tokensave/daemon.pid` +2. Detach from terminal (setsid, close stdin/stdout/stderr, redirect to log file `~/.tracedecay/daemon.log`) +3. Write PID to `~/.tracedecay/daemon.pid` 4. Enter the main event loop On `--foreground`: skip the fork, keep stderr/stdout attached, still write PID file. ### PID File Management -- **Path:** `~/.tokensave/daemon.pid` +- **Path:** `~/.tracedecay/daemon.pid` - **Write:** On daemon start, write the PID as a plain integer - **Read:** On `--stop` and `--status`, read the PID and check if the process is alive (`kill(pid, 0)` on Unix) - **Stale detection:** If PID file exists but the process is dead, treat as not running. On start, overwrite stale PID files. @@ -80,8 +80,8 @@ Read PID file, check process alive, send SIGTERM. Wait up to 5 seconds for proce ### `--status` Read PID file, check process alive. Print: -- "tokensave daemon is running (PID: 12345)" or -- "tokensave daemon is not running" +- "tracedecay daemon is running (PID: 12345)" or +- "tracedecay daemon is not running" Exit code 0 if running, 1 if not. @@ -89,17 +89,17 @@ Exit code 0 if running, 1 if not. **macOS (launchd):** -Write `~/Library/LaunchAgents/com.tokensave.daemon.plist`: +Write `~/Library/LaunchAgents/com.tracedecay.daemon.plist`: ```xml Label - com.tokensave.daemon + com.tracedecay.daemon ProgramArguments - /path/to/tokensave + /path/to/tracedecay daemon --foreground @@ -108,9 +108,9 @@ Write `~/Library/LaunchAgents/com.tokensave.daemon.plist`: KeepAlive StandardOutPath - ~/.tokensave/daemon.log + ~/.tracedecay/daemon.log StandardErrorPath - ~/.tokensave/daemon.log + ~/.tracedecay/daemon.log ``` @@ -119,13 +119,13 @@ Then run `launchctl load `. **Linux (systemd):** -Write `~/.config/systemd/user/tokensave-daemon.service`: +Write `~/.config/systemd/user/tracedecay-daemon.service`: ```ini [Unit] -Description=tokensave file watcher daemon +Description=tracedecay file watcher daemon [Service] -ExecStart=/path/to/tokensave daemon --foreground +ExecStart=/path/to/tracedecay daemon --foreground Restart=on-failure RestartSec=5 @@ -133,13 +133,13 @@ RestartSec=5 WantedBy=default.target ``` -Then run `systemctl --user enable --now tokensave-daemon.service`. +Then run `systemctl --user enable --now tracedecay-daemon.service`. ### `--disable-autostart` **macOS:** `launchctl unload `, then delete the plist file. -**Linux:** `systemctl --user disable --now tokensave-daemon.service`, then delete the unit file. +**Linux:** `systemctl --user disable --now tracedecay-daemon.service`, then delete the unit file. ## Error Handling @@ -151,7 +151,7 @@ Then run `systemctl --user enable --now tokensave-daemon.service`. ## Doctor Integration -Add a "Daemon" section to `tokensave doctor` output: +Add a "Daemon" section to `tracedecay doctor` output: ``` Daemon @@ -160,7 +160,7 @@ Daemon or ``` Daemon - ! Daemon is not running — run `tokensave daemon` to start + ! Daemon is not running — run `tracedecay daemon` to start ``` Also check if autostart is enabled: @@ -169,7 +169,7 @@ Also check if autostart is enabled: ``` or ``` - ! Autostart not configured — run `tokensave daemon --enable-autostart` + ! Autostart not configured — run `tracedecay daemon --enable-autostart` ``` ## Dependencies @@ -179,7 +179,7 @@ or ## Logging -The daemon writes to `~/.tokensave/daemon.log`. Log format: +The daemon writes to `~/.tracedecay/daemon.log`. Log format: ``` [2026-03-27 14:32:01] started, watching 3 projects diff --git a/docs/superpowers/specs/2026-06-09-tokensave-lcm-session-rewrite-design.md b/docs/superpowers/specs/2026-06-09-tokensave-lcm-session-rewrite-design.md deleted file mode 100644 index 093fbbbe..00000000 --- a/docs/superpowers/specs/2026-06-09-tokensave-lcm-session-rewrite-design.md +++ /dev/null @@ -1,241 +0,0 @@ -# TokenSave LCM Session Rewrite Design - -> **Rebrand note:** The project has since been renamed **TraceDecay** (binary/crate `tracedecay`, MCP tools `tracedecay_*`). This dated design artifact keeps the TokenSave-era names it was written with; read `tokensave` / `tokensave_*` as `tracedecay` / `tracedecay_*` when applying it to the current codebase. - -Date: 2026-06-09 -Branch: `feature/lcm-comparison` -Status: Approved design direction - -## Goals - -Port Hermes LCM into TokenSave by fully rewriting TokenSave's current simple session internals into an LCM implementation while preserving the useful public session-search surface. - -The design goals are: - -- Make the existing project-local `sessions.db` the authoritative LCM-capable session database through schema migrations, not by introducing a second parallel LCM database for TokenSave-managed sessions. -- Replace the simple provider-normalized session-message internals with lossless LCM-grade raw-message storage, summary-DAG, lifecycle/frontier, externalized-payload, and lineage state. -- Preserve the useful public behavior of provider-normalized transcript search, especially `tokensave_message_search`, while allowing its internals to be rebuilt on top of LCM-grade storage and indexed snippets. -- Remove authoritative session text caps for new writes. Caps may still exist for display snippets, FTS/index text, MCP response truncation, or safety-bounded rendering, but the authoritative stored raw session content must be lossless and expandable/recoverable. -- Use a hybrid ownership model: Rust owns durable/indexed state, migrations, deterministic query/DAG APIs, storage locality, and path-safety rules; the generated Hermes Python plugin owns Hermes `ContextEngine` lifecycle integration and Hermes auxiliary LLM calls. -- Start with the existing generated Python bridge that shells to `tokensave tool ... --json --args`, then consider PyO3 or native Python bindings later only if the bridge becomes a measured bottleneck. -- Keep TokenSave's codegraph context retrieval and fact memory systems separate from LCM session compression. - -## Non-Goals - -This design does not implement the port. It defines the target architecture and constraints for a later implementation. - -Non-goals are: - -- Do not add PyO3, maturin, or native Python bindings as the first milestone. -- Do not replace `src/context/builder.rs` with LCM. Codegraph context building remains graph/search/source retrieval for code intelligence. -- Do not repurpose `src/memory/*` fact storage into a summary DAG. The fact store remains structured, user/project memory; LCM stores transcript lineage and compression state. -- Do not require Hermes users to migrate to project-local storage for non-local installs. Storage locality follows current TokenSave and Hermes install rules. -- Do not keep the existing 256 KiB session-message cap as an authoritative storage limit for new session content. It can only survive as a derived-view limit for search/display/response safety. -- Do not preserve branch-only internal shapes that conflict with the approved design. The durable compatibility boundary is public session search and existing session DB data. - -## Architecture - -TokenSave should absorb Hermes LCM by turning the session layer into a Rust-owned LCM core with a Hermes-specific Python adapter. - -The high-level split: - -- Rust session/LCM core: - - Owns `sessions.db` schema, migrations, WAL/busy-timeout settings, storage path selection, path containment, and externalized-payload metadata. - - Replaces the simple provider-normalized message table as the authoritative raw-content layer with lossless LCM raw storage, while retaining compatibility projections/indexes for provider-normalized search. - - Exposes deterministic tool/API operations for search, load, describe, expand, expand-query support, compression status, lifecycle/frontier state, and DAG traversal. - - Preserves stable JSON outputs where existing MCP/Hermes tools depend on them. -- Generated Hermes Python plugin: - - Continues to be installed from `src/agents/hermes.rs`. - - Registers TokenSave tools, the `pre_llm_call` hook, and the `TokensaveMemoryProvider`. - - Hosts the Hermes `ContextEngine` adapter surface and calls TokenSave through `tokensave tool ... --json --args`. - - Calls Hermes `agent.auxiliary_client.call_llm` for LCM summarization and query expansion prompts that require an auxiliary model. - -This keeps durable correctness, migrations, and storage safety in Rust while avoiding a native extension boundary during the first port. - -## Storage/Migration Model - -The existing `sessions.db` becomes the LCM-capable session DB, and LCM replaces the simple session internals rather than sitting beside them as a second subsystem. - -Today, TokenSave stores project-local sessions at `crate::config::get_tokensave_dir(project_root).join("sessions.db")` through `src/sessions/cursor.rs`. `src/global_db.rs` creates provider-normalized tables: - -- `sessions(provider, session_id, project_key, project_path, title, started_at, ended_at, transcript_path, metadata_json, parent_session_id, is_subagent, agent_id, parent_tool_use_id)` -- `session_messages(provider, message_id, session_id, role, timestamp, ordinal, text, kind, model, tool_names, source_path, source_offset, metadata_json)` -- `session_messages_fts` over `text`, `role`, `kind`, `model`, and `tool_names` -- parse offsets and accounting tables used by existing transcript ingestion - -LCM migrations should rewrite/evolve this database instead of creating a separate TokenSave LCM DB. The migrated schema should support both existing transcript search behavior and LCM-grade raw storage: - -- Preserve `sessions` and provider-normalized message search as compatibility projections, not as the only authoritative session representation. -- Add or migrate to LCM raw-message identity with stable `store_id` ordering, conversation/session linkage, source/provider fields, role, lossless content reference, tool-call fields, timestamps, token estimates, pinned state, and metadata. -- Add summary DAG tables equivalent in capability to Hermes `summary_nodes`: `node_id`, session/conversation, depth, summary content/reference, token counts, source token counts, source ids, source type, source time window, expand hint, and FTS/search metadata. -- Add lifecycle/frontier state equivalent in capability to Hermes `lcm_lifecycle_state`: conversation id, current session, last finalized session, current frontier store id, last finalized frontier store id, debt markers, rollover/reset/maintenance timestamps, and update time. -- Add migration state/schema version tables in the existing DB layer so old `sessions.db` files can be upgraded idempotently. -- Add externalized-payload metadata that links placeholders in indexed text to payload files, content hashes, kind, role/tool metadata, session/conversation ownership, byte/char counts, and creation time. - -The current `MAX_SESSION_MESSAGE_TEXT_BYTES` cap of 256 KiB must be removed from authoritative storage for new writes. It may remain appropriate for FTS snippets, compatibility search rows, display previews, MCP response truncation, and safety-bounded rendering, but the LCM store must preserve full raw content either inline in a dedicated full-content column/table or by externalizing payloads and storing safe placeholders plus recoverable payload references. FTS should index bounded, safe text or snippets, not necessarily the full authoritative payload. - -Existing rows that were already capped cannot be made lossless retroactively. Migrations should carry those rows forward as best-effort legacy data, mark them as legacy/truncated when detectable, and ensure all new writes use lossless authoritative storage. - -Storage locality follows TokenSave install rules: - -- `--local` Hermes installs use project-local TokenSave storage under the project `.tokensave`, including the migrated `sessions.db` and local externalized payload directory. -- Non-local Hermes installs store LCM state under the Hermes profile/home location, matching Hermes LCM's current profile-local behavior. -- The path selector must be explicit so the same Rust APIs can open either the project-local DB or the Hermes-profile DB without guessing. - -## Rust Session/LCM Core - -The Rust core should become the source of truth for LCM state. It should not merely mirror the Python reference implementation table-for-table; it should provide the same semantics through TokenSave-native types and migrations. - -Core responsibilities: - -- Ingest provider-normalized transcript messages into the LCM raw-message model and derive provider-normalized search records from that model where useful. -- Guarantee lossless authoritative raw storage for new writes. Search snippets, FTS columns, MCP responses, and rendered previews may be capped, but `load`/`expand` APIs must be able to recover full content or the full externalized payload. -- Maintain append-first raw-message ordering and store-id lineage. Existing Hermes LCM allows one narrow rewrite for GC placeholders on externalized tool result rows; TokenSave should model any such mutation explicitly and keep source lineage intact. -- Keep the existing search path for `tokensave_message_search` over provider-normalized transcripts, while making LCM search able to combine raw messages and summary nodes with source/session filters. -- Implement summary-DAG persistence and deterministic traversal/expansion APIs in Rust. Python can request operations, but Rust should own source-id resolution, session ownership checks, FTS queries, pagination, and snippet construction. -- Track lifecycle/frontier state durably so session rollover, reset, maintenance debt, and current compaction frontier survive process restarts. -- Keep codegraph context retrieval independent. `ContextBuilder` continues to combine code search, graph traversal, and source extraction; LCM tools operate on conversation/session state. -- Keep fact memory independent. `MemoryCategory`, fact records, trust scores, entity links, and feedback are not summary nodes and should not be overloaded for transcript compression. - -The first Rust API surface can be tool-driven rather than a public library API. The important boundary is deterministic JSON in and out, because the generated Hermes plugin can call it through the existing bridge. - -## Hermes Python Context-Engine Adapter - -Hermes' current LCM reference owns a `ContextEngine`-style lifecycle: - -- `should_compress_preflight(messages)` is not a pure predicate. It ingests messages and can return `true` when the replay-safe view differs because ingest protection sanitized or externalized content. -- `compress(messages, current_tokens, focus_topic)` ingests, selects raw backlog outside the fresh tail, summarizes leaf chunks, optionally condenses summary nodes, advances the lifecycle frontier, assembles active prompt context, and returns sanitized replay messages. -- `on_session_start`, rollover/reset handling, auxiliary-session markers, and foreground session binding maintain lifecycle/frontier state. -- Tool calls can ingest live messages before search so current-turn content is discoverable. - -TokenSave's generated Hermes plugin should adapt this lifecycle to Rust-owned state rather than reimplementing storage in Python. The adapter should: - -- Register a Hermes context engine or equivalent hook surface for preflight, compression, session start, rollover, reset, and tool calls. -- Call TokenSave tools through the generated `tools.py` subprocess bridge with JSON args and JSON responses. -- Keep only process-local coordination in Python, such as active auxiliary session markers and bridge timeout/error handling, when that state does not need to be durable. -- Delegate all durable raw-message, DAG, frontier, payload, and search mutations to Rust. -- Preserve the existing TokenSave Hermes install/config behavior: generate plugin files, register `pre_llm_call`, register the memory provider, and refuse to overwrite an existing non-TokenSave Hermes memory provider. - -The adapter should also preserve Hermes' behavior that auxiliary LLM sessions are stateless for compression, so summarizer/query-expansion calls do not recursively ingest or compact themselves. - -## LLM Bridge - -Hermes LCM summarization depends on Hermes auxiliary model calls. The initial TokenSave port should keep those calls in generated Python: - -- Summary calls use `agent.auxiliary_client.call_llm` with task `compression`, messages containing the summary prompt, temperature, max tokens, optional timeout, and routed model settings. -- Reasoning tags such as ``, ``, ``, ``, and `` must be stripped before summary text is persisted. -- Model fallback and circuit breaker semantics should be preserved: try the configured primary/fallback route chain, stop hammering a failing route during cooldown, and fall back to deterministic truncation when LLM summarization cannot produce a smaller summary. -- Rust should store the resulting summary, source lineage, token accounting, and failure/status metadata. Python should not be the durable owner of summary nodes. - -For tools like `lcm_expand_query` that synthesize an answer from expanded context, Python may keep the auxiliary LLM call initially. Rust should provide deterministic context selection and expansion payloads so the LLM prompt is built from safe, session-authorized material. - -## Tool/API Surface - -`tokensave_message_search` remains stable or compatibly enhanced. - -Existing behavior to preserve: - -- Required `query`. -- Default provider of `cursor`. -- Optional `project_key`, `parent_session_id`, `include_subagents`, `scope`, and `limit`. -- Results containing provider-normalized session and message records with scores. -- The handler opens the project-local `sessions.db` and searches `session_messages_fts`. - -Enhancements should be additive at the public API boundary. Internally, search may be served from LCM-derived compatibility tables or indexed snippets rather than from the old capped text column. Result metadata may indicate legacy truncation, externalized payload references, or LCM availability, but existing callers should still be able to use the current fields. - -New LCM-oriented tools may be added rather than overloading `tokensave_message_search`: - -- `tokensave_lcm_status`: current session, lifecycle/frontier, compression status, debt, payload stats, and schema health. -- `tokensave_lcm_load_session`: ordered raw-message pages by explicit session id, with role/time filters, content slicing, and stable cursors. -- `tokensave_lcm_grep`: combined raw-message and summary-node search with current/session/all scope controls. -- `tokensave_lcm_describe`: session DAG overview or summary-node subtree metadata. -- `tokensave_lcm_expand`: expand a raw message, summary node, externalized payload, or source subtree with authorization checks and content pagination. -- `tokensave_lcm_expand_query`: deterministic context selection in Rust plus optional Hermes auxiliary synthesis in Python. -- `tokensave_lcm_compress` or internal equivalents: preflight/ingest/compress operations used by the Hermes adapter. - -Names can be finalized during implementation, but the behavioral split should remain: `tokensave_message_search` is compatibility transcript search; LCM tools expose compression-aware session state. - -## Ingest Protection/Security - -Hermes LCM contains important storage-boundary protections that should carry into TokenSave's Rust-owned store: - -- Externalize obvious large or binary-ish payloads, including data URI/base64 media, long base64 runs, oversized raw payloads, and large tool outputs. -- Store compact placeholders in indexed text while preserving recoverable payload content in full-content storage or externalized files with metadata and hashes. -- Keep externalized payload files private where possible, equivalent to `0700` directories and `0600` payload files. -- Reject payload refs that are not basenames. Do not allow `/`, `\`, parent traversal, symlink escape, or arbitrary absolute paths. -- Enforce storage root containment. Local installs stay under project `.tokensave`; non-local profile installs stay under the selected Hermes home/profile root. -- Keep session/conversation ownership checks on payload expansion so a ref from another session cannot be expanded casually. -- Preserve optional sensitive-pattern redaction as metadata-only/lossy when enabled, and make that lossiness visible in status/doctor output. -- Include integrity scans for missing/unreferenced payloads and SQLite/FTS health without previewing sensitive payload contents. - -The security model should treat search indexes, snippets, and rendered previews as derived, bounded, and safe-to-display. The authoritative raw content path must be lossless, separately controlled, paginated, and authorized. - -## Lifecycle/Compression Behavior - -The compression semantics should match Hermes LCM's observable behavior while moving durable state to Rust. - -Key behavior to preserve: - -- Preflight can ingest. It must be allowed to return compression-needed when ingest protection changes the replay view even if token thresholds alone would not require compression. -- Compression bypasses ignored sessions, stateless sessions, and auxiliary thread contexts. -- Compression respects a cooldown after boundary skips and can force overflow recovery when context pressure is critical. -- The fresh tail stays raw in active context. Raw backlog outside the fresh tail is summarized into leaf DAG nodes, with dynamic leaf chunk behavior where configured. -- Summary nodes preserve source ids, source token counts, time windows, depth, and expand hints so future `describe`/`expand` calls can recover lineage. -- Condensation can summarize lower-depth nodes into higher-depth nodes when needed, preserving a DAG rather than a flat rolling summary. -- Active prompt assembly combines the leading system anchor when present, relevant summary context, preserved objective context when needed, and the fresh tail. It must sanitize tool-call/tool-result pairing before returning replay messages. -- Rollover resets the active frontier for the new session while preserving the last finalized session/frontier for the conversation. -- Maintenance debt records when raw backlog could not yet be compacted, so deferred maintenance can run later instead of losing state. - -TokenSave should keep deterministic state transitions in Rust and leave nondeterministic summarization text generation to Hermes auxiliary LLM calls through Python. - -## Testing Strategy - -Testing should cover the design boundaries rather than only happy-path tool output. - -Required test areas: - -- Migration tests from existing `sessions.db` files with only `sessions`, `session_messages`, `session_messages_fts`, parse offsets, and parent/subagent columns into the LCM-capable schema. -- Migration tests proving already-capped legacy rows are carried forward as best-effort legacy data, marked as truncated/legacy where detectable, and never treated as newly lossless content. -- Compatibility tests proving `tokensave_message_search` retains current provider/project/subagent filtering and result shape. -- Raw-storage tests proving new content above 256 KiB is preserved authoritatively and recoverable through load/expand APIs while search indexes, snippets, MCP responses, and display renderers use safe bounded views. -- Regression tests proving no new authoritative session write path uses `MAX_SESSION_MESSAGE_TEXT_BYTES` or any replacement cap before durable raw storage/externalization. -- Externalization tests for large tool output, data URI/base64 payloads, basename-only refs, private permissions, root containment, missing payloads, and cross-session expansion denial. -- Lifecycle tests for session start, rollover, reset, frontier advance, last-finalized frontier preservation, maintenance debt, and restart recovery. -- Compression tests with fake Hermes auxiliary LLM calls covering leaf compaction, condensation, fallback chain, circuit breaker, deterministic truncation fallback, reasoning stripping, and active-context assembly. -- Generated Hermes plugin tests proving install layout, config editing, memory provider registration, `pre_llm_call`, JSON bridge calls, and non-overwrite behavior for existing memory providers. -- Tool tests for load/grep/describe/expand/expand-query/status using deterministic fixtures. -- Regression tests that `src/context/builder.rs` and `src/memory/*` remain independent from LCM session compression. - -## Rollout/Backward Compatibility - -Rollout should be incremental but converge on one session store design. - -Compatibility commitments: - -- Existing `sessions.db` files are migrated in place with idempotent schema migrations. -- Existing capped rows are preserved as best-effort legacy data. New session writes after migration must be lossless in the LCM raw store. -- Existing provider-normalized transcript ingestion continues to support `sessions` and `session_messages` behavior, but those simple internals become compatibility projections over the LCM-grade session store where practical. -- `tokensave_message_search` remains stable or only compatibly enhanced. -- Existing local installs continue to store under project `.tokensave`; non-local Hermes installs continue to store under the Hermes profile/home. -- Existing Hermes plugin generation remains the installation mechanism, but its generated Python grows the LCM adapter and tool bridge calls. - -Rollout shape: - -- Introduce schema migrations and read-only LCM inspection APIs first, including proof that new authoritative writes are lossless and old capped rows are flagged as legacy where detectable. -- Add Hermes adapter preflight/ingest with compression disabled or status-only until storage and search compatibility are verified. -- Enable compression in Hermes after raw storage, externalization, lifecycle, and active-context assembly tests pass. -- Measure bridge overhead before considering PyO3/native bindings. A later native milestone should be justified by latency, packaging, or reliability evidence. - -## Open Questions - -- Should non-local Hermes-profile LCM storage reuse a file named `sessions.db` for consistency, or keep a Hermes-specific filename while using the same migrated schema? -- What is the exact Rust table layout for full raw content: inline full-content table, externalized-by-default payload table, or a hybrid threshold policy? -- Which LCM operations should be public MCP tools versus internal Hermes-plugin-only tools? -- Should TokenSave expose LCM summary nodes to non-Hermes providers once their transcripts are indexed, or initially limit compression lifecycle to Hermes sessions? -- How much of `lcm_expand_query` synthesis should remain Python-only after context selection, and what JSON contract should Rust provide for reproducible prompt construction? - -## Self-Review - -This spec intentionally describes one plan: fully rewrite the existing TokenSave session internals into a lossless LCM-capable store inside the existing `sessions.db`, with Rust owning durable state and generated Python owning Hermes lifecycle/auxiliary LLM integration. It rejects the earlier separate-DB starting point, rejects capped authoritative storage for new writes, and rejects PyO3 as the first milestone. - -No placeholders or TBD markers remain. The open questions are bounded design choices for implementation, not unresolved contradictions in the approved architecture. diff --git a/scripts/tokensave-dev-mcp.sh b/scripts/tokensave-dev-mcp.sh deleted file mode 100755 index b0f49054..00000000 --- a/scripts/tokensave-dev-mcp.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Development MCP launcher for Cursor. -# -# Stdio MCP servers cannot hot-reload their tool definitions after Cursor has -# connected. This wrapper keeps development fast by running the current -# worktree source with cargo, so restarting/reconnecting the MCP server in -# Cursor picks up code changes without installing a new global binary. - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)" - -MANIFEST="$REPO_ROOT/Cargo.toml" -DEFAULT_PROJECT_ROOT="$REPO_ROOT" -PROJECT_ROOT="${TOKENSAVE_DEV_PROJECT_ROOT:-$DEFAULT_PROJECT_ROOT}" - -exec cargo run --quiet --manifest-path "$MANIFEST" -- serve --path "$PROJECT_ROOT" "$@" diff --git a/src/agents/codex.rs b/src/agents/codex.rs index 5adc6529..3fb1373e 100644 --- a/src/agents/codex.rs +++ b/src/agents/codex.rs @@ -414,7 +414,7 @@ fn uninstall_tracedecay_mcp_if_present(config_path: &Path) { } if let Err(err) = uninstall_mcp_server(config_path) { eprintln!( - " Could not remove legacy tracedecay MCP config from {}: {err}", + " Could not remove project-local Codex MCP config from {}: {err}", config_path.display() ); } diff --git a/src/agents/cursor.rs b/src/agents/cursor.rs index d3052abb..e9dc798a 100644 --- a/src/agents/cursor.rs +++ b/src/agents/cursor.rs @@ -1325,7 +1325,7 @@ mod tests { let mcp = load_json_file(&cursor_dir.join("mcp.json")); assert!( mcp["mcpServers"].get("tracedecay").is_none(), - "legacy tracedecay MCP entry must be removed" + "project-local MCP entry must be removed" ); assert!( mcp["mcpServers"].get("other").is_some(), diff --git a/src/global_db.rs b/src/global_db.rs index d6cc43a3..9e229018 100644 --- a/src/global_db.rs +++ b/src/global_db.rs @@ -529,6 +529,34 @@ fn like_pattern(query: &str) -> String { pattern } +fn repo_identity_aliases(git_common_dir: Option<&Path>) -> Vec { + let mut aliases = Vec::new(); + if let Some(path) = git_common_dir { + aliases.push(format!( + "git-common-dir:{}", + GlobalDb::canonical_project_key(path) + )); + } + aliases +} + +fn normalize_git_remote_url(remote: &str) -> Option { + let remote = remote.trim(); + if remote.is_empty() { + return None; + } + let mut normalized = remote.trim_end_matches('/').to_string(); + if let Some(rest) = normalized.strip_prefix("git@") { + if let Some((host, path)) = rest.split_once(':') { + normalized = format!("https://{host}/{path}"); + } + } + if let Some(stripped) = normalized.strip_suffix(".git") { + normalized = stripped.to_string(); + } + Some(normalized.to_ascii_lowercase()) +} + async fn session_column_exists(conn: &Connection, column: &str) -> bool { let Ok(mut rows) = conn.query("PRAGMA table_info(sessions)", ()).await else { return false; @@ -1008,7 +1036,7 @@ impl GlobalDb { let now = crate::tracedecay::current_timestamp(); let canonical_root = Self::canonical_project_key(project_root); let display_root = project_root.to_string_lossy().to_string(); - let git_common_dir = git_common_dir.map(|path| path.to_string_lossy().to_string()); + let git_common_dir_text = git_common_dir.map(|path| path.to_string_lossy().to_string()); self.conn .execute( "INSERT INTO code_projects @@ -1026,7 +1054,7 @@ impl GlobalDb { project_id, canonical_root, display_root, - opt_text(git_common_dir.as_deref()), + opt_text(git_common_dir_text.as_deref()), opt_text(git_remote_url), opt_text(default_branch), now, @@ -1035,6 +1063,9 @@ impl GlobalDb { .await .ok()?; self.upsert_project_alias(project_root, project_id).await?; + for alias in repo_identity_aliases(git_common_dir) { + self.upsert_project_alias_key(&alias, project_id).await?; + } self.get_code_project(project_id).await } @@ -1043,8 +1074,16 @@ impl GlobalDb { alias_path: &Path, project_id: &str, ) -> Option { - let now = crate::tracedecay::current_timestamp(); let alias = Self::canonical_project_key(alias_path); + self.upsert_project_alias_key(&alias, project_id).await + } + + async fn upsert_project_alias_key( + &self, + alias: &str, + project_id: &str, + ) -> Option { + let now = crate::tracedecay::current_timestamp(); self.conn .execute( "INSERT INTO project_aliases (alias_path, project_id, last_seen_at) @@ -1061,7 +1100,7 @@ impl GlobalDb { .query( "SELECT alias_path, project_id, last_seen_at FROM project_aliases WHERE alias_path = ?1", - params![Self::canonical_project_key(alias_path)], + params![alias], ) .await .ok()?; @@ -1186,6 +1225,67 @@ impl GlobalDb { alias_path: &Path, ) -> Option { let alias = Self::canonical_project_key(alias_path); + self.resolve_project_store_by_alias_key(&alias).await + } + + pub async fn resolve_project_store_by_identity( + &self, + project_root: &Path, + git_common_dir: Option<&Path>, + ) -> Option { + let mut aliases = Vec::with_capacity(2); + aliases.push(Self::canonical_project_key(project_root)); + aliases.extend(repo_identity_aliases(git_common_dir)); + for alias in aliases { + if let Some(resolution) = self.resolve_project_store_by_alias_key(&alias).await { + return Some(resolution); + } + } + None + } + + pub async fn resolve_unique_project_store_by_git_remote( + &self, + git_remote_url: &str, + ) -> Option { + let remote = normalize_git_remote_url(git_remote_url)?; + let mut rows = self + .conn + .query( + "SELECT project_id, canonical_root, display_root, git_common_dir, + git_remote_url, default_branch, created_at, last_seen_at + FROM code_projects + WHERE git_remote_url IS NOT NULL AND git_remote_url != '' + ORDER BY project_id", + (), + ) + .await + .ok()?; + let mut match_project = None; + while let Some(row) = rows.next().await.ok()? { + let project = row_to_code_project(&row, 0)?; + let Some(stored_remote) = project + .git_remote_url + .as_deref() + .and_then(normalize_git_remote_url) + else { + continue; + }; + if stored_remote == remote { + if match_project.is_some() { + return None; + } + match_project = Some(project); + } + } + self.resolve_project_store_for_project(&match_project?) + .await + } + + async fn resolve_project_store_by_alias_key( + &self, + alias: &str, + ) -> Option { let mut rows = self .conn .query( @@ -1217,6 +1317,34 @@ impl GlobalDb { }) } + async fn resolve_project_store_for_project( + &self, + project: &CodeProjectRecord, + ) -> Option { + let mut rows = self + .conn + .query( + "SELECT store_id, project_id, store_kind, storage_mode, store_relpath, + manifest_relpath, created_at, last_verified_at, last_write_at + FROM store_instances + WHERE project_id = ?1 + ORDER BY COALESCE(last_verified_at, created_at) DESC, store_id + LIMIT 1", + params![project.project_id.as_str()], + ) + .await + .ok()?; + let store = row_to_store_instance(&rows.next().await.ok()??, 0)?; + let graph_scopes = self.list_graph_scopes_for_store(&store.store_id).await; + let artifacts = self.list_store_artifacts(&store.store_id).await; + Some(ProjectStoreResolution { + project: project.clone(), + store, + graph_scopes, + artifacts, + }) + } + pub async fn list_code_projects(&self, limit: usize) -> Vec { let limit = i64::try_from(limit).unwrap_or(i64::MAX); let Ok(mut rows) = self diff --git a/src/tracedecay.rs b/src/tracedecay.rs index ed509ca6..669605f3 100644 --- a/src/tracedecay.rs +++ b/src/tracedecay.rs @@ -324,7 +324,7 @@ impl TraceDecay { /// Writes a default configuration to the resolved project store and /// initializes a fresh `SQLite` database. pub async fn init(project_root: &Path) -> Result { - let store_layout = storage::resolve_layout_for_current_profile(project_root)?; + let store_layout = Self::resolve_store_layout_for_project(project_root).await?; let config = TraceDecayConfig { root_dir: project_root.to_string_lossy().to_string(), ..TraceDecayConfig::default() @@ -364,6 +364,45 @@ impl TraceDecay { &self.db } + async fn resolve_store_layout_for_project(project_root: &Path) -> Result { + if storage::read_enrollment_marker(project_root)?.is_some() { + return storage::resolve_layout_for_current_profile(project_root); + } + + let profile_root = storage::default_profile_root()?; + let git_common_dir = git_common_dir(project_root); + let git_remote_url = git_remote_url(project_root); + if let Some(global_db) = GlobalDb::open().await { + let resolution = match global_db + .resolve_project_store_by_identity(project_root, git_common_dir.as_deref()) + .await + { + Some(resolution) => Some(resolution), + None => match git_remote_url.as_deref() { + Some(remote) => { + global_db + .resolve_unique_project_store_by_git_remote(remote) + .await + } + None => None, + }, + }; + + if let Some(resolution) = resolution { + return storage::profile_sharded_layout( + project_root, + &profile_root, + &storage::EnrollmentMarker { + project_id: resolution.project.project_id, + storage_mode: storage::StorageMode::ProfileSharded, + }, + ); + } + } + + storage::default_profile_sharded_layout(project_root, &profile_root) + } + /// Opens an existing `TraceDecay` project at the given root. /// /// If branch metadata exists, resolves the current git branch, auto-adds @@ -373,7 +412,7 @@ impl TraceDecay { /// If the previous operation was interrupted (dirty sentinel exists), /// the database is integrity-checked and rebuilt if corrupted. pub async fn open(project_root: &Path) -> Result { - let store_layout = storage::resolve_layout_for_current_profile(project_root)?; + let store_layout = Self::resolve_store_layout_for_project(project_root).await?; let config = load_config_from_path(project_root, &store_layout.config_path)?; let active_branch = branch::current_branch(project_root); Self::auto_track_active_branch(project_root, active_branch.as_deref()).await?; @@ -496,7 +535,7 @@ impl TraceDecay { /// status/verification commands that must be able to inspect read-only /// stores without mutating them. pub async fn open_read_only(project_root: &Path) -> Result { - let store_layout = storage::resolve_layout_for_current_profile(project_root)?; + let store_layout = Self::resolve_store_layout_for_project(project_root).await?; let config = load_config_from_path(project_root, &store_layout.config_path)?; let active_branch = branch::current_branch(project_root); @@ -605,7 +644,7 @@ impl TraceDecay { /// /// Returns an error if the branch is not tracked or the DB doesn't exist. pub async fn open_branch(project_root: &Path, branch_name: &str) -> Result { - let store_layout = storage::resolve_layout_for_current_profile(project_root)?; + let store_layout = Self::resolve_store_layout_for_project(project_root).await?; let config = load_config_from_path(project_root, &store_layout.config_path)?; let meta = branch_meta::load_branch_meta(&store_layout.data_root).ok_or_else(|| { @@ -664,6 +703,10 @@ impl TraceDecay { else { return; }; + + static REGISTRY_WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + let _registry_write = REGISTRY_WRITE_LOCK.lock().await; + let Some(global_db) = GlobalDb::open().await else { return; }; @@ -839,11 +882,12 @@ fn profile_store_id(project_id: &str) -> String { fn git_common_dir(project_root: &Path) -> Option { git_output(project_root, &["rev-parse", "--git-common-dir"]).map(|path| { let common_dir = PathBuf::from(path); - if common_dir.is_absolute() { + let resolved = if common_dir.is_absolute() { common_dir } else { project_root.join(common_dir) - } + }; + resolved.canonicalize().unwrap_or(resolved) }) } diff --git a/tests/agent_test.rs b/tests/agent_test.rs index e8947d90..c2994e4a 100644 --- a/tests/agent_test.rs +++ b/tests/agent_test.rs @@ -2451,20 +2451,20 @@ fn test_local_install_cursor_removes_legacy_project_mcp_hooks_and_rule() { assert!( !cursor_dir.join("mcp.json").exists(), - "local install should remove legacy tracedecay-only project MCP config" + "local install should remove project-local MCP config" ); assert!( !cursor_dir.join("hooks.json").exists(), - "local install should remove legacy tracedecay-only project hooks" + "local install should remove project-local hooks" ); assert!( !cursor_dir.join("rules/tracedecay.mdc").exists(), - "local install should remove legacy tracedecay project rule" + "local install should remove project-local rule" ); } /// Global `tracedecay install --agent cursor` runs with the project as cwd and -/// must sweep legacy project-local tracedecay artifacts there (old installs +/// must sweep project-local tracedecay artifacts there (old installs /// predate the plugin), while preserving user-authored entries alongside them. #[test] fn test_global_install_cursor_sweeps_legacy_project_artifacts_at_cwd() { @@ -2507,7 +2507,7 @@ fn test_global_install_cursor_sweeps_legacy_project_artifacts_at_cwd() { .unwrap(); assert!( mcp["mcpServers"].get("tracedecay").is_none(), - "legacy project tracedecay MCP entry should be swept" + "project-local MCP entry should be swept" ); assert!( mcp["mcpServers"].get("other").is_some(), @@ -2515,12 +2515,12 @@ fn test_global_install_cursor_sweeps_legacy_project_artifacts_at_cwd() { ); assert!( !cursor_dir.join("rules/tracedecay.mdc").exists(), - "legacy project tracedecay rule should be swept" + "project-local rule should be swept" ); } -/// The legacy sweep must never modify files *through* a symlinked `.cursor` -/// that escapes the project. A symlinked `.cursor` with no legacy tracedecay +/// The project-local sweep must never modify files *through* a symlinked `.cursor` +/// that escapes the project. A symlinked `.cursor` with no TraceDecay /// artifacts is left alone (the plugin owns all surfaces, so there is nothing /// to write project-locally), but once legacy artifacts are detected behind /// the symlink the install refuses rather than reaching outside the project. diff --git a/tests/config_test.rs b/tests/config_test.rs index b6b4aad2..3b3fdd52 100644 --- a/tests/config_test.rs +++ b/tests/config_test.rs @@ -142,8 +142,9 @@ fn test_add_to_gitignore_adds_newline_if_missing() { #[test] fn test_resolve_path_with_value() { - let result = resolve_path(Some("/tmp/myproject".to_string())); - assert_eq!(result, std::path::PathBuf::from("/tmp/myproject")); + let path = std::env::temp_dir().join("myproject"); + let result = resolve_path(Some(path.to_string_lossy().into_owned())); + assert_eq!(result, path); } #[test] diff --git a/tests/global_registry_test.rs b/tests/global_registry_test.rs index 206ca966..3abfcd35 100644 --- a/tests/global_registry_test.rs +++ b/tests/global_registry_test.rs @@ -6,6 +6,21 @@ use tracedecay::global_db::{GlobalDb, GraphScopeUpsert, StoreArtifactUpsert, Sto static GLOBAL_REGISTRY_TEST_LOCK: Mutex<()> = Mutex::const_new(()); +async fn upsert_test_store(db: &GlobalDb, project_id: &str, store_id: &str) { + db.upsert_store_instance(StoreInstanceUpsert { + store_id: store_id.to_string(), + project_id: project_id.to_string(), + store_kind: "code_project".to_string(), + storage_mode: "profile_sharded".to_string(), + store_relpath: format!("projects/{project_id}"), + manifest_relpath: Some(format!("projects/{project_id}/store_manifest.json")), + last_verified_at: Some(100), + last_write_at: Some(101), + }) + .await + .unwrap(); +} + async fn table_exists(db_path: &Path, table: &str) -> bool { let db = libsql::Builder::new_local(db_path).build().await.unwrap(); let conn = db.connect().unwrap(); @@ -194,7 +209,20 @@ async fn open_at_creates_registry_tables_and_round_trips_registry_records() { .await .unwrap(); assert_eq!(context.project.project_id, "proj_registry"); - assert_eq!(context.aliases.len(), 1); + let alias_paths: Vec<_> = context + .aliases + .iter() + .map(|alias| alias.alias_path.as_str()) + .collect(); + let canonical_project_root = project_root + .canonicalize() + .unwrap() + .to_string_lossy() + .to_string(); + assert!(alias_paths.contains(&canonical_project_root.as_str())); + assert!(alias_paths + .iter() + .any(|alias| alias.starts_with("git-common-dir:"))); assert_eq!(context.stores.len(), 1); assert_eq!(context.stores[0].store.store_id, "store_registry"); assert_eq!(context.stores[0].graph_scopes.len(), 1); @@ -206,6 +234,87 @@ async fn open_at_creates_registry_tables_and_round_trips_registry_records() { ); } +#[tokio::test] +async fn registry_resolves_store_by_repo_identity_aliases() { + let _guard = GLOBAL_REGISTRY_TEST_LOCK.lock().await; + let dir = TempDir::new().unwrap(); + let db = GlobalDb::open_at(&dir.path().join("global.db")) + .await + .unwrap(); + let original = dir.path().join("original"); + let renamed = dir.path().join("renamed"); + let common_dir = dir.path().join("git/common"); + std::fs::create_dir_all(&original).unwrap(); + std::fs::create_dir_all(&renamed).unwrap(); + std::fs::create_dir_all(&common_dir).unwrap(); + + db.upsert_code_project( + "proj_repo_identity", + &original, + Some(&common_dir), + Some("git@github.com:ScriptedAlchemy/tracedecay.git"), + Some("main"), + ) + .await + .unwrap(); + upsert_test_store(&db, "proj_repo_identity", "store_repo_identity").await; + + let by_common_dir = db + .resolve_project_store_by_identity(&renamed, Some(&common_dir)) + .await + .unwrap(); + assert_eq!(by_common_dir.project.project_id, "proj_repo_identity"); + + let by_remote = db + .resolve_unique_project_store_by_git_remote( + "https://github.com/ScriptedAlchemy/tracedecay.git", + ) + .await + .unwrap(); + assert_eq!(by_remote.store.store_id, "store_repo_identity"); +} + +#[tokio::test] +async fn registry_remote_resolution_is_conservative_when_ambiguous() { + let _guard = GLOBAL_REGISTRY_TEST_LOCK.lock().await; + let dir = TempDir::new().unwrap(); + let db = GlobalDb::open_at(&dir.path().join("global.db")) + .await + .unwrap(); + let one = dir.path().join("one"); + let two = dir.path().join("two"); + std::fs::create_dir_all(&one).unwrap(); + std::fs::create_dir_all(&two).unwrap(); + + db.upsert_code_project( + "proj_one", + &one, + None, + Some("git@github.com:ScriptedAlchemy/tracedecay.git"), + Some("main"), + ) + .await + .unwrap(); + upsert_test_store(&db, "proj_one", "store_one").await; + db.upsert_code_project( + "proj_two", + &two, + None, + Some("https://github.com/ScriptedAlchemy/tracedecay"), + Some("main"), + ) + .await + .unwrap(); + upsert_test_store(&db, "proj_two", "store_two").await; + + assert!(db + .resolve_unique_project_store_by_git_remote( + "https://github.com/ScriptedAlchemy/tracedecay.git" + ) + .await + .is_none()); +} + #[tokio::test] async fn legacy_projects_tokens_saved_schema_and_queries_still_work() { let _guard = GLOBAL_REGISTRY_TEST_LOCK.lock().await; diff --git a/tests/mcp_cli_serve_test.rs b/tests/mcp_cli_serve_test.rs index ebb29a73..619ca559 100644 --- a/tests/mcp_cli_serve_test.rs +++ b/tests/mcp_cli_serve_test.rs @@ -55,6 +55,7 @@ fn tracedecay_command_with_home(home: &Path) -> Command { .env("HOME", &home) .env("USERPROFILE", &home) .env("XDG_CONFIG_HOME", home.join(".config")) + .env("TRACEDECAY_DATA_DIR", home.join(".tracedecay")) .env("TRACEDECAY_GLOBAL_DB", home.join(".tracedecay/global.db")); command } diff --git a/tests/profile_storage_migration_test.rs b/tests/profile_storage_migration_test.rs index ffa307f1..38e33339 100644 --- a/tests/profile_storage_migration_test.rs +++ b/tests/profile_storage_migration_test.rs @@ -82,6 +82,20 @@ fn portable_relpath(path: &str) -> String { path.replace('\\', "/") } +fn run_git(project: &Path, args: &[&str]) { + let output = std::process::Command::new("git") + .args(args) + .current_dir(project) + .output() + .unwrap_or_else(|err| panic!("failed to run git {args:?}: {err}")); + assert!( + output.status.success(), + "git {args:?} failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + async fn table_exists(db_path: &std::path::Path, table: &str) -> bool { let db = libsql::Builder::new_local(db_path).build().await.unwrap(); let conn = db.connect().unwrap(); @@ -482,6 +496,55 @@ async fn trace_decay_init_uses_profile_shard_when_enrolled() { ); } +#[tokio::test] +async fn trace_decay_open_matches_renamed_git_checkout_by_registered_remote() { + let _guard = HOME_ENV_LOCK.lock().await; + let dir = TempDir::new().unwrap(); + let root = canonical_temp_path(dir.path()); + let home = root.join("home"); + let project = root.join("repo-before-rename"); + let renamed = root.join("repo-after-rename"); + fs::create_dir_all(&project).unwrap(); + run_git(&project, &["init"]); + run_git( + &project, + &[ + "remote", + "add", + "origin", + "git@github.com:ScriptedAlchemy/tracedecay.git", + ], + ); + let _home_guard = HomeEnvGuard::set(&home); + + let initialized = TraceDecay::init(&project).await.unwrap(); + let original_project_id = initialized + .store_layout() + .identity + .project_id + .clone() + .unwrap(); + let original_data_root = initialized.store_layout().data_root.clone(); + drop(initialized); + fs::rename(&project, &renamed).unwrap(); + + let reopened = TraceDecay::open(&renamed).await.unwrap(); + + assert_eq!( + reopened.store_layout().identity.project_id.as_deref(), + Some(original_project_id.as_str()) + ); + assert_eq!(reopened.store_layout().data_root, original_data_root); + assert!( + !home + .join(".tracedecay/projects") + .join(tracedecay::storage::default_profile_project_id(&renamed)) + .join("tracedecay.db") + .exists(), + "renamed checkout must not create a second path-hash profile shard" + ); +} + #[tokio::test] async fn trace_decay_open_branch_uses_profile_shard_branch_db() { let _guard = HOME_ENV_LOCK.lock().await; diff --git a/tests/tool_first_touch_test.rs b/tests/tool_first_touch_test.rs index 6e606328..f56fb85e 100644 --- a/tests/tool_first_touch_test.rs +++ b/tests/tool_first_touch_test.rs @@ -27,6 +27,7 @@ fn run_tool(cwd: &Path, home: &Path, args: &[&str]) -> std::process::Output { .current_dir(cwd) .env("HOME", home) .env("USERPROFILE", home) + .env("TRACEDECAY_DATA_DIR", home.join(".tracedecay")) .env("TRACEDECAY_GLOBAL_DB", home.join(".tracedecay/global.db")) .arg("tool") .args(args) diff --git a/tests/tracedecay_test.rs b/tests/tracedecay_test.rs index 299f1682..b6b4a3f5 100644 --- a/tests/tracedecay_test.rs +++ b/tests/tracedecay_test.rs @@ -13,7 +13,7 @@ use tracedecay::types::{EdgeKind, NodeKind}; /// Creates a temporary Rust project with cross-file calls, then initializes /// and indexes a `TraceDecay`. -async fn setup() -> (TraceDecay, TempDir) { +async fn setup() -> (TempDir, TraceDecay) { let dir = TempDir::new().unwrap(); let project = dir.path(); fs::create_dir_all(project.join("src")).unwrap(); @@ -39,7 +39,7 @@ pub fn helper() { foo(); } let cg = TraceDecay::init(project).await.unwrap(); cg.index_all().await.unwrap(); - (cg, dir) + (dir, cg) } fn run_git(project: &std::path::Path, args: &[&str]) { @@ -118,7 +118,7 @@ fn test_is_test_file_case_insensitive() { #[tokio::test] async fn test_get_all_files() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let files = cg.get_all_files().await.unwrap(); assert!( files.len() >= 2, @@ -132,7 +132,7 @@ async fn test_get_all_files() { #[tokio::test] async fn test_get_all_nodes() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let nodes = cg.get_all_nodes().await.unwrap(); assert!( !nodes.is_empty(), @@ -145,7 +145,7 @@ async fn test_get_all_nodes() { #[tokio::test] async fn test_get_all_edges() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let edges = cg.get_all_edges().await.unwrap(); // foo() calls bar(), so there should be at least one edge assert!(!edges.is_empty(), "should have at least one edge"); @@ -157,7 +157,7 @@ async fn test_get_all_edges() { #[tokio::test] async fn test_get_file_dependents() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; // utils.rs calls foo from lib.rs, so lib.rs has utils.rs as a dependent // (or utils depends on lib). Let's check if lib.rs has dependents. let dependents = cg.get_file_dependents("src/lib.rs").await.unwrap(); @@ -176,7 +176,7 @@ async fn test_get_file_dependents() { #[tokio::test] async fn test_find_dead_code_functions() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let dead = cg .find_dead_code(&[NodeKind::Function], false) .await @@ -202,7 +202,7 @@ async fn test_find_dead_code_functions() { #[tokio::test] async fn test_find_dead_code_custom_kinds() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; // Look for dead structs — our test project has none, should return empty let dead = cg.find_dead_code(&[NodeKind::Struct], false).await.unwrap(); assert!( @@ -217,7 +217,7 @@ async fn test_find_dead_code_custom_kinds() { #[tokio::test] async fn test_get_file_coupling_fan_in() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let coupling = cg.get_file_coupling(true, None, 10).await.unwrap(); // Even if coupling is empty (due to how the extractor resolves cross-file refs), // the method should succeed. @@ -229,7 +229,7 @@ async fn test_get_file_coupling_fan_in() { #[tokio::test] async fn test_get_file_coupling_fan_out() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let coupling = cg.get_file_coupling(false, None, 10).await.unwrap(); for (path, count) in &coupling { assert!(!path.is_empty()); @@ -243,7 +243,7 @@ async fn test_get_file_coupling_fan_out() { #[tokio::test] async fn test_check_file_staleness_not_stale() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; // Right after indexing, files should not be stale let stale = cg.check_file_staleness(&["src/lib.rs".to_string()]).await; // Immediately after indexing, the file should not be stale @@ -256,7 +256,7 @@ async fn test_check_file_staleness_not_stale() { #[tokio::test] async fn test_check_file_staleness_after_modification() { - let (cg, dir) = setup().await; + let (dir, cg) = setup().await; // Wait a moment, then modify the file so mtime > indexed_at std::thread::sleep(std::time::Duration::from_secs(2)); @@ -441,7 +441,7 @@ async fn sync_if_stale_silent_does_not_create_duplicate_row_for_backslash_path() #[tokio::test] async fn test_tokens_saved_round_trip() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; // Initially should be 0 let initial = cg.get_tokens_saved().await.unwrap(); @@ -464,7 +464,7 @@ async fn test_tokens_saved_round_trip() { #[tokio::test] async fn test_get_complexity_ranked() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let ranked = cg.get_complexity_ranked(None, None, 10).await.unwrap(); // Should return functions/methods from our indexed project assert!( @@ -484,7 +484,7 @@ async fn test_get_complexity_ranked() { #[tokio::test] async fn test_get_undocumented_public_symbols_no_filter() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let undoc = cg.get_undocumented_public_symbols(None, 50).await.unwrap(); // foo is pub and has no docstring let names: Vec<&str> = undoc.iter().map(|n| n.name.as_str()).collect(); @@ -497,7 +497,7 @@ async fn test_get_undocumented_public_symbols_no_filter() { #[tokio::test] async fn test_get_undocumented_public_symbols_with_prefix() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let undoc = cg .get_undocumented_public_symbols(Some("src/utils"), 50) .await @@ -518,7 +518,7 @@ async fn test_get_undocumented_public_symbols_with_prefix() { #[tokio::test] async fn test_get_node_distribution() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let dist = cg.get_node_distribution(None).await.unwrap(); assert!(!dist.is_empty(), "should have node distribution data"); // Each entry is (file_path, kind, count) @@ -556,7 +556,7 @@ async fn test_is_initialized() { #[tokio::test] async fn test_get_god_classes_empty() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let god = cg.get_god_classes(None, 10).await.unwrap(); // Pure Rust project with no classes should return empty assert!( @@ -571,7 +571,7 @@ async fn test_get_god_classes_empty() { #[tokio::test] async fn test_get_inheritance_depth_empty() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let depths = cg.get_inheritance_depth(None, 10).await.unwrap(); assert!( depths.is_empty(), @@ -585,7 +585,7 @@ async fn test_get_inheritance_depth_empty() { #[tokio::test] async fn test_search() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let results = cg.search("foo", 10).await.unwrap(); assert!(!results.is_empty(), "should find 'foo' via search"); assert_eq!(results[0].node.name, "foo"); @@ -597,7 +597,7 @@ async fn test_search() { #[tokio::test] async fn test_get_stats() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; let stats = cg.get_stats().await.unwrap(); assert!(stats.node_count > 0, "should have nodes"); assert!(stats.file_count > 0, "should have files"); @@ -727,7 +727,7 @@ async fn sync_if_stale_silent_waits_for_peer_then_returns_ok() { /// unconditionally. #[tokio::test] async fn last_sync_timestamp_uses_metadata_not_indexed_at() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; // Backdate every file's `indexed_at` to simulate a long-quiet repo // (typical state before a no-change sync). We use `1` rather than 0 @@ -762,7 +762,7 @@ async fn last_sync_timestamp_uses_metadata_not_indexed_at() { /// only an `init`) honest. #[tokio::test] async fn last_sync_timestamp_falls_back_to_indexed_at_without_metadata() { - let (cg, _dir) = setup().await; + let (_dir, cg) = setup().await; cg.db() .conn() .execute(