Zakira.Retrace 0.1.0

dotnet tool install --global Zakira.Retrace --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Zakira.Retrace --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Zakira.Retrace&version=0.1.0
                    
nuke :add-package Zakira.Retrace --version 0.1.0
                    

Zakira.Retrace

Search, read, and resume AI coding sessions across every harness you use.

Retrace reads the session stores that OpenCode, GitHub Copilot CLI, and Copilot in VS Code already write to disk, indexes them into one local hybrid search index, and lets you find a past conversation and reopen it — from a terminal or from an agent over MCP.

Everything is local. No account, no API key, no network at query time, no background process.

dotnet tool install -g Zakira.Retrace
retrace index build
retrace search "how did I set up the retry policy"

Or without installing anything:

dnx Zakira.Retrace search "how did I set up the retry policy"

Why

Your past sessions are the best documentation you have of your own decisions, and they are effectively write-only. Each harness stores them in its own private format, none of them search across each other, and two of the three have no usable search at all. "I solved this exact problem three weeks ago" is a thought you have constantly and can act on almost never.

Retrace makes that corpus queryable, and — since it also runs as an MCP server — makes it queryable by the agents themselves.

Sources

Source Store Search Resume
OpenCode ~/.local/share/opencode/opencode.db, with automatic fallback to the legacy JSON tree Retrace index opencode --session <id>, --fork supported
GitHub Copilot CLI ~/.copilot/session-store.db, plus the optional JSONL event log for tool calls Retrace index, or the harness's own FTS5 index in --live mode copilot --resume=<id>
Copilot in VS Code workspaceStorage/<hash>/chatSessions/*.json and *.jsonl, across stable and Insiders Retrace index Opens the workspace only — see below

All reads are strictly read-only. Every harness keeps its database open while running, so Retrace opens them with Mode=ReadOnly plus PRAGMA query_only, disables connection pooling, and falls back to a consistent snapshot copy (database plus -wal and -shm) when the live file will not open.

On VS Code resume: VS Code has no command-line switch that reopens a specific chat session. retrace resume on a VS Code session prints code <folder>, tells you plainly that the conversation is not restored, and points you at retrace show to read it. That is the honest limit of what is possible; the alternative would be a command that looks like it worked and did not.

Commands

retrace sources                 List sources and whether each is readable
retrace doctor                  Check config, sources, index, and models
retrace info                    Resolved paths, model, and search settings

retrace list                    List sessions, newest first
retrace search <query>          Search session content
retrace show <session>          Print a transcript
retrace export <session>        Export a full transcript as Markdown or JSON
retrace files <session>         List the files a session touched
retrace resume <session>        Print (or --exec) the command that reopens it
retrace tag <session>           Add, remove, or list tags

retrace index build|refresh|status|clear
retrace deps install onnx       Download the local embedding model
retrace config path|init|list|get|set
retrace mcp serve               Run as an MCP server

Filters

list and search share the same filters, so learning them once is enough:

retrace search "connection pooling" --here            # sessions in the current directory
retrace search "flaky test" --since 7d                # 7d, 12h, 30m, or 2026-01-31
retrace list --repo Orchestra --agent build
retrace list --source opencode --model claude-opus-5
retrace list --workspace P:\Github\MyProject --min-messages 10

Output

Every command takes --output json or --output ndjson, so Retrace composes with other tools:

retrace list --output json | jq -r '.[] | select(.cost > 1) | .uri'
retrace search "migration" --output ndjson | head -5

Identifying a session

Anywhere a session is expected you can pass a full URI, a bare native id, or an unambiguous prefix. An ambiguous prefix is an error listing the candidates, never a guess:

retrace show retrace://opencode/ses_3aa5f96adffelfPdJUmLd91g7p
retrace show ses_3aa5f96adffelfPdJUmLd91g7p
retrace show ses_3aa5f96a

Search fuses two signals with reciprocal rank fusion, then applies recency and workspace boosts:

  1. Keyword — SQLite FTS5 with BM25 ranking.
  2. Semantic — local ONNX sentence embeddings, retrieved in two tiers: one centroid vector per session builds a shortlist, and only then are per-passage vectors scored. Scoring every passage vector on every query would mean reading the whole vector table each time.

RRF is used rather than a weighted sum because BM25 and cosine live on incomparable scales — BM25 is unbounded and corpus-dependent, cosine is bounded to [-1, 1] — so combining them directly means an arbitrary constant decides the outcome. Fusing ranks avoids that.

retrace search "vector storage"                 # hybrid (default)
retrace search "vector storage" --lexical-only  # keyword only
retrace search "vector storage" --semantic-only # vectors only
retrace search "vector storage" --deep          # skip the shortlist, higher recall
retrace search "vector storage" --scores        # show per-signal contributions
retrace search "\"exact phrase\""               # quoted phrases are exact

Whatever you type is safe: every token is quoted before it reaches FTS5, so a stray ", -, *, or NEAR( is searched for rather than interpreted as query syntax.

Semantic search is optional. Without a model installed, hybrid silently degrades to keyword-only rather than failing — a missing optional dependency should not break a working feature.

What gets indexed

Content Keyword Vectors
User messages, assistant prose, reasoning yes yes
Tool names, titles, arguments, output; file paths and edits yes no

Tool output is the overwhelming majority of stored bytes in every harness, and it embeds badly: a vector of a build log sits in a dense, noisy region of the space and mostly returns false positives. It stays keyword-searchable — "which session ran that command" is a real question — but never gets a vector. That single decision is what keeps the vector store proportional to how much you actually talked rather than to how much stdout got captured.

Indexing

retrace index build              # first build
retrace index refresh            # incremental
retrace index build --force      # re-read everything
retrace index build --prune      # drop sessions deleted from their source
retrace index build --no-embed   # keyword-only, much faster
retrace index status

Scoping a build by time

Embedding a large source in one pass can take hours. --since and --until narrow a build to a window, so you can add vectors where you actually search without paying for your entire history:

retrace index build --force --since 90d --source copilot-cli
retrace index build --force --since 2026-01-01 --until 2026-06-30

Two things worth knowing:

  • --since on its own is usually a no-op. A session already in the index is skipped — first by the source watermark, then by its unchanged content hash. To add vectors to sessions that are already indexed you need --force as well, which is why the examples above use it.
  • A scoped build never advances the watermark. If it did, a later plain retrace index build would conclude everything older had already been handled and would never backfill the sessions the window excluded. Leaving the watermark alone costs one cheap re-enumeration later, which the content-hash check absorbs without reading a transcript or running inference.

Refresh is incremental in two independent layers: a per-source watermark narrows the candidates to sessions whose store says they changed, and a per-session content hash then skips the ones whose indexable content did not actually move. The second layer matters because several harnesses touch a session row for reasons that do not alter the conversation.

Retrace never runs in the background. When a query finds the index stale, the top-up happens inline, inside that command, and only after a cheap watermark comparison confirms a source moved. Set index.autoRefresh to false to require explicit refreshes, or pass --live to bypass the index entirely and read the sources directly.

Because that work sits in front of an interactive query, it is deliberately constrained so a query can never become slow merely because indexing is outstanding:

  • It does not embed. index.autoRefreshEmbed is false by default. Inference dominates the cost of indexing, and running a model over every session created today is not something a search should wait for. New sessions become keyword-searchable immediately and pick up vectors from retrace index refresh --backfill (see below).

  • It is time-boxed. index.autoRefreshMaxSeconds (default 5) caps the delay. Whatever finished is committed, and the query answers from what the index already holds. When the budget runs out, the shortfall is printed rather than silently paid for:

    Index is behind for opencode — run `retrace index refresh` to catch up.
    

Both limits exist because the alternative is worse than a stale answer: an unbounded inline build turns a routine retrace search into a multi-minute hang the first time a large store drifts. If you want a fully current index in a script, run retrace index refresh explicitly, or set index.autoRefreshMaxSeconds to 0 to remove the ceiling.

retrace doctor reports the gap directly, which is easy to miss otherwise since the live and indexed counts are separate numbers:

ok    index            11,916 session(s), 675,502 chunk(s), 125,109 vector(s), 1418.8 MB
warn  index freshness  not yet indexed: copilot-vscode +60 — run `retrace index refresh`
ok    vector coverage  copilot-cli 226/10,702  ·  copilot-vscode 329/329  ·  opencode 889/889

Backfilling vectors

A keyword-only refresh leaves sessions in the index with no vectors, and an ordinary refresh will not revisit them. The incremental skip is driven by the content hash, which answers has this conversation changed? — not is this session fully indexed? Nothing changed, so nothing is re-read, and those sessions stay absent from semantic search indefinitely.

--backfill asks the second question too:

retrace index refresh --backfill --source opencode      # just what is missing
retrace index refresh --backfill --since 90d            # bound it further

Only sessions genuinely missing vectors are re-read, so the cost tracks the size of the gap rather than the size of the source — the distinction between this and --force, which re-reads everything:

opencode              4 indexed     886 unchanged      0 removed     1,607 chunk(s)       661 vector(s)
4 session(s) in 10.9s using bge-small-en-v1.5

It is opt-in rather than automatic precisely because that gap can be enormous. A source left keyword-only on purpose represents hours of inference, and quietly folding it into every refresh would recreate the multi-minute stall the time-boxed top-up exists to prevent. The vector coverage row above is there so the state is visible when you want it, and silent when you do not; its denominator counts sessions that can be embedded, so a conversation too short to be worth a vector never shows up as a shortfall you cannot close.

For reference, on a store of ~12,500 sessions across all three harnesses, a full keyword-only build takes about two and a half minutes and produces a ~1 GB index; subsequent refreshes with nothing changed are instant.

Embeddings

retrace deps install onnx                                  # default model
retrace deps install onnx --model multilingual-e5-small
retrace deps status
Model Size Notes
bge-small-en-v1.5 (default) ~33 MB English, 384-dim, CLS pooling
snowflake-arctic-embed-s ~33 MB English, 384-dim
multilingual-e5-small ~118 MB Multilingual, SentencePiece tokenizer

Models are downloaded on first use, stored per model id so several can coexist, and verified against the declared content length before being moved into place — a truncated download otherwise surfaces much later as an opaque ONNX error.

Retrace refuses to query an index built with a different model than the one configured. Vectors from different models are not comparable even when their dimensions match, so the alternative is confident nonsense. retrace index build --force rebuilds after a model change.

Inference is capped at half the logical processors. ONNX Runtime otherwise claims every core, which on a many-core machine turns an index build into something that makes the whole system unresponsive rather than merely busy — a poor trade for work nobody is watching. Set embeddings.maxThreads to a specific number to override, or leave it at 0 for the default.

Configuration

retrace.json lives in your dotfiles, resolved in this order:

  1. --config <path>
  2. RETRACE_CONFIG_PATH
  3. $XDG_CONFIG_HOME/Zakira.Retrace/retrace.json
  4. The platform default (%APPDATA% on Windows, ~/Library/Application Support on macOS, ~/.config on Linux)

It is generated on first run with every option written out at its default value, including the ones whose default is null, so the file itself documents what can be tuned.

Path values are stored verbatim and expanded at read time, so $XDG_CONFIG_HOME/... and ~/... keep working across machines that share a dotfiles repository.

retrace config path
retrace config list
retrace config set search.rrfK 80
retrace config set sources.copilot-vscode.editors stable,insiders
retrace config set embeddings.enabled false

The index and downloaded models are not stored with the config. They are large, machine-specific, and rebuildable, so they go to %LOCALAPPDATA%\Zakira.Retrace (or $XDG_DATA_HOME), which keeps a gigabyte of derived data out of synchronised dotfiles.

MCP server

retrace mcp serve
{
  "servers": {
    "retrace": { "type": "stdio", "command": "retrace", "args": ["mcp", "serve"] }
  }
}

Or without installing: "command": "dnx", "args": ["Zakira.Retrace", "mcp", "serve"].

Tool Purpose
sessions-search Ranked search with excerpts
sessions-list Metadata-only browse
session-get Read a conversation, bounded and paginated
session-files Files a session touched
session-resume-command The command to reopen it — never executed
session-tags Read or change tags
sources-list Available harnesses
index-status, index-refresh Index freshness

Resources: retrace://sources, retrace://sessions/recent, retrace://sessions/{sourceId}/{sessionId}.

Two rules shape the tool surface:

  • Results are token-bounded. session-get returns prose only by default, caps output at 20,000 characters, and reports a nextTurnIndex cursor. An agent asking about a session should not have its context window filled with captured stdout.
  • Nothing starts a process. session-resume-command returns a string and stops. An agent asked a question about a session must not be able to launch an interactive harness as a side effect.

In stdio mode all logging goes to stderr, without exception, because stdout carries the JSON-RPC framing.

Exit codes

Code Meaning
0 Success
1 Usage or configuration error; also search/list finding nothing
2 A source was unavailable
3 The index has not been built
4 Session not found, or an ambiguous prefix
5 The index and the runtime disagree about the embedding model
70 Unexpected error (prints a stack trace; please report it)
130 Cancelled

Adding a source

Sources are separate projects behind one contract, so a new harness is additive:

  1. Implement ISessionSource in a new Zakira.Retrace.Sources.<Name> project.
  2. Optionally implement IIncrementalSource for cheap refreshes, and INativeSearchSource if the harness already maintains its own full-text index.
  3. Add an Add<Name>Source() extension and call it from the tool's composition root.

Nothing in Core changes. The same shape applies to non-harness corpora — an Obsidian vault, an exported chat archive — as long as it can be projected onto sessions and turns.

Layout

src/
  Zakira.Retrace.Abstractions/          Contracts and models. No package dependencies.
  Zakira.Retrace.Core/                  Config, storage, index, embeddings, catalog.
  Zakira.Retrace.Sources.OpenCode/
  Zakira.Retrace.Sources.CopilotCli/
  Zakira.Retrace.Sources.CopilotVsCode/
  Zakira.Retrace/                       CLI and MCP server. The packable tool.
tests/
  Zakira.Retrace.Core.UnitTests/        Config, paths, chunking, index, fusion, vectors.
  Zakira.Retrace.Sources.UnitTests/     Synthetic stores built to each harness's real schema,
                                        plus live-store tests that skip when a harness is absent.
  Zakira.Retrace.E2ETests/              Spawns the built binary; drives MCP over real stdio.

Every library ships inside the single Zakira.Retrace tool package, so dnx Zakira.Retrace pulls one artifact and gets every source.

Building

dotnet build Zakira.Retrace.slnx
dotnet test Zakira.Retrace.slnx
./pack.ps1

Requires the .NET 10 SDK.

License

The Unlicense. See LICENSE.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.1.0 84 8/26/2026

0.1.0 — first release. Sources: OpenCode, Copilot CLI, Copilot VS Code. Hybrid FTS5 + ONNX embedding search, incremental indexing, CLI and MCP server.