Index Cursor, opencode and Gemini CLI sessions - #57
Merged
Conversation
Adding a harness was supposed to be one line in the source list. It was not: the file extension, the parser preview uses, the row label, the preview label and the resume command each decided separately what a source was, so an unknown one was discovered as jsonl, parsed as Claude, labelled cc and resumed with claude --resume. Two of those tables had already drifted against each other. They now read one record per harness. A new agent is a parser plus one entry. Cursor keeps each chat as a SQLite store under ~/.cursor/chats. Message blobs are plain json beside binary merkle nodes and images, so the scan filters on the leading byte in SQL and opens the store read-only. Blob order is insertion order; per-message times were never recorded, so every row carries the session's updatedAtMs rather than inventing them. Gemini keeps one json object per session, which is why the global .jsonl filter had to go. Its --resume takes a project-scoped index number, not a stable id, so resume goes through --session-file instead. Claude Code and Codex behaviour is unchanged. Cache format bumps to 7. On a 852-session corpus, the 101 new Cursor sessions moved held-out ranking +0.004.
opencode keeps every session in one SQLite database instead of a file per session. The indexer could not represent that: it read the first row's id and registered it as the id for the whole file, so every session but one was invisible, and previewing that id would have shown all of them concatenated. It now registers each session a fragment contains, and reading one filters to it. Both are no-ops for a file that holds a single session, which is every harness indexed before this. Text lives in `part` rows, one per span, with the role on the parent `message`. Only `text` parts are indexed; `reasoning` joins them under --thinking, and tool calls and step markers are not conversation. Resume is `opencode run --session`. Verified against real sessions: three generated locally, each found by content from the middle of the conversation and read back in isolation. Held-out ranking over 268 queries is unchanged, 0.504 to 0.507.
`opencode run --session <id>` is the non-interactive form and exits with "You must provide a message or a command", so pressing enter on an opencode result did nothing. The bare command opens the TUI on that session, which is what resume means here. Every harness has a one-shot form and an interactive form and they are not the same command, so the exact argv for all five is now pinned by a test rather than left to whichever one the adapter author read first.
`cc` and `cx` were guessable when there were two harnesses. With five, `cu`, `oc` and `gm` are not, and the column they sit in is the one that tells you which tool a result came from. Each harness now has one name, used for the source column, the assistant turn label and the preview, so those three cannot drift apart the way the column and the turn label already had. The column width comes from the longest name, so adding a harness cannot ragged every row below it.
Cursor writes each session twice: a SQLite store under ~/.cursor/chats holding
raw API payloads, and a clean JSONL transcript under ~/.cursor/projects. I found
the store first and used it. The transcript is better on every axis that matters.
The store holds the payloads as sent, including injected hook and environment
context, and the tag-stripping pattern only matched bare tags, so roughly fifty
sessions were titled `<hooks_context description="Additional context provided by
session hooks">`. That text also became their searchable body. The transcript
carries only the conversation, and wraps what the user typed in <user_query>, so
the prompt is marked rather than guessed at.
Coverage is a strict superset: 147 sessions against 101, every store session
included. Dates come from the file mtime instead of a meta.json that 34 chats do
not have, and the directory comes from the project slug, so:
sessions 101 -> 147
blank date 39 -> 0
blank cwd 41 -> 3 (a deleted worktree and the empty window)
hooks_context titles ~50 -> 0
Indexing is also faster, 4.0s against 4.8s, with no database to scan.
`match` now takes a path rather than a filename, because `<id>/<id>.jsonl` and
`<id>/subagents/<other>.jsonl` are the same filename shape and only one is a
session.
Cursor and Gemini are labelled `cursor cli` and `gemini cli`: both have a
non-CLI product whose sessions live elsewhere and are not indexed, and the
column should not imply otherwise.
A skill invocation writes the whole SKILL.md into the transcript as a user turn, flagged isMeta. So do hook notices and image-paste markers. Nobody typed any of it, and one skill injection runs to 15k characters. Reading a session was where this showed. `agsearch read <id> "retention compounds"` on a 492-message session printed three skill dumps and then ran out of budget, so it elided every turn that matched and told the user it had kept them. With the injections gone the matching turn is the second thing on screen. The measured cost is a small one: held-out @1 0.483 to 0.480 and MRR 0.550 to 0.535, on the same corpus indexed both ways. The metric rewards a session for matching on any text it contains, including text nobody wrote, so it reads the removal of noise as a loss. The eval does not score previews at all, and the preview is what the skill reads. Worth being exact about the size: the raw injected text is 7.5M characters across 164 transcripts, but MSG_INDEX_CHARS already caps each message at 4k, so this removes 742k characters, or 1.7% of the indexed body.
Claude Code files tool results under the user's role, and nine in ten user-role entries in a transcript are one. The preview took that at face value, so reading a session showed `▌ you` above "The file has been updated successfully. (file state is current in your context)". On one 26-turn read, seven of the thirteen turns attributed to the user were tool output. For a human that is noise. For an agent, which is what the skill points at this output, it is wrong information about who said what, and nothing downstream can recover it. They keep their own role now and the gutter says `tool`. Still indexed, because the error string a search has to find lives in tool output rather than in anything anyone typed: "we hit this error before" is answered by an ECONNRESET in a bash result. Held-out ranking is unchanged at 0.491 against 0.494, the difference being one session that arrived between the two runs.
`cursor cli` and `gemini cli` read well in a terminal and broke everything downstream of them. The piped row is whitespace-separated columns, so a name with a space in it shifts every field after it: splitting a row gave source=cursor, match=cli, project=2/2. Hyphenated, and a test now holds every label to one token. Checked whether this wanted a machine-readable format instead, on twenty results with their matched snippets: the current text is ~1335 tokens, minified JSON is ~1760 (+32%, a repeated key per field per row), and a TOON-style table is ~1307 (-2%). The output is already a table with no repeated keys, and the snippet dominates it in every format, so a --json flag would cost an agent a third more tokens to read the same thing. Not adding one.
Ran the twenty trigger cases against the installed plugin. Eighteen passed, and the two that failed were the same missing distinction in opposite directions: "pick up where we left off on the database migration" did not fire, and "we discussed this a few messages ago" did. The description said "work started in another session", which is too weak to catch the first and gives no reason to refuse the second. It now asks for work from an earlier session and says plainly that it is not for earlier in the conversation you are already in. The first case fires after the change. The second still does, and on reflection that case was mislabelled rather than wrong behaviour: in a long or compacted session the earlier messages are no longer in context, and agsearch indexes the current session too, so searching for them is reasonable. Description is 485 characters, inside the 500 the guidance asks for.
The harness list is written out in four places. Adding Cursor, opencode and Gemini updated three of them and missed marketplace.json, which holds two descriptions of its own and still offered to search Claude Code and Codex. That is the first text anyone reads, before the README and before the plugin is installed. A test now holds every shipped description to naming all five, so the next harness cannot be added to some of them.
devcodes9
force-pushed
the
feat/multi-harness-adapters
branch
from
September 5, 2026 16:03
be799c9 to
7f56b5f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #40.
agsearch indexed Claude Code and Codex. It now indexes Cursor, opencode and Gemini CLI too,
in the same list, with each session resumed by the tool that created it.
~/.claude/projects/**/*.jsonlclaude --resume <id>~/.codex/sessions/**/*.jsonlcodex resume <id>~/.cursor/chats/**/store.dbcursor-agent --resume <id>~/.local/share/opencode/opencode.dbopencode run --session <id>~/.gemini/tmp/**/chats/*.jsongemini --session-file <path>Why it needed more than a new list entry
Adding a harness was meant to be one line. Five places decided independently what a source
was, so an unknown one was discovered as
.jsonl, parsed as Claude, labelledcc, andresumed with
claude --resume. Two of those tables had already drifted (codexagainstcx). They now read one record per harness, so a new agent is a parser plus one entry.Two assumptions also had to go:
The indexer took the first row's id as the id for the whole file, making every other
session unreachable. It now registers each session a file contains, and reading one
filters to it. No change for the four harnesses that write a file per session.
invisible regardless of what the source table said. The filename test now comes from the
source record.
Notes per harness
images, so the scan filters on the leading byte in SQL and opens the store read-only.
Blob order is insertion order and per-message times were never recorded, so each row
carries the session's
updatedAtMsrather than inventing timestamps. Injected<user_info>context is stripped from user turns.partrows with the role on the parentmessage. Onlytextparts are indexed;
reasoningjoins them under--thinking.--resumetakes a project-scoped index number, which is not a stable handlefor a session found by search, so resume goes through
--session-file. Gemini records aprojectHashand no path, socwdcomes from the sibling.project_root.Verification
Cursor and opencode were tested against real sessions on disk: found by content from the
middle of a conversation, previewed in isolation, and resumed with the right command.
Gemini is covered by fixtures only. The CLI returns
IneligibleTierErroron a free-tieraccount, so no session could be generated to test against, and the parser is written to the
confirmed on-disk schema.
Held-out ranking over 268 queries is unchanged by the larger corpus: 0.504 to 0.507 (@1).
169 tests pass. Cache format bumps to 7 and reindexes once on first run.