Skip to content

Index Cursor, opencode and Gemini CLI sessions - #57

Merged
devcodes9 merged 10 commits into
mainfrom
feat/multi-harness-adapters
Sep 5, 2026
Merged

Index Cursor, opencode and Gemini CLI sessions#57
devcodes9 merged 10 commits into
mainfrom
feat/multi-harness-adapters

Conversation

@devcodes9

@devcodes9 devcodes9 commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Stacked on #55 (the agsearch skill). Base is feat/agsearch-skill, so #55's six commits
are not duplicated here and this diff is the two adapter commits. GitHub retargets it to
main when #55 merges, so merge #55 first.

It does still touch two files #55 introduces: plugin.json (1 line) and SKILL.md
(2 lines), adding the new harnesses to their descriptions.

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.

Agent Read from Resumed with
Claude Code ~/.claude/projects/**/*.jsonl claude --resume <id>
Codex ~/.codex/sessions/**/*.jsonl codex resume <id>
Cursor ~/.cursor/chats/**/store.db cursor-agent --resume <id>
opencode ~/.local/share/opencode/opencode.db opencode run --session <id>
Gemini CLI ~/.gemini/tmp/**/chats/*.json gemini --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, labelled cc, and
resumed with claude --resume. Two of those tables had already drifted (codex against
cx). They now read one record per harness, so a new agent is a parser plus one entry.

Two assumptions also had to go:

  • One transcript file, one session. opencode keeps every session in a single database.
    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.
  • Every transcript is JSONL. Gemini writes one JSON object per session, so it was
    invisible regardless of what the source table said. The filename test now comes from the
    source record.

Notes per harness

  • Cursor stores chats as SQLite. Message blobs are 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 and per-message times were never recorded, so each row
    carries the session's updatedAtMs rather than inventing timestamps. Injected
    <user_info> context is stripped from user turns.
  • opencode keeps text in part rows with the role on the parent message. Only text
    parts are indexed; reasoning joins them under --thinking.
  • Gemini --resume takes a project-scoped index number, which is not a stable handle
    for a session found by search, so resume goes through --session-file. Gemini records a
    projectHash and no path, so cwd comes 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 IneligibleTierError on a free-tier
account, 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.

@devcodes9
devcodes9 changed the base branch from main to feat/agsearch-skill September 5, 2026 09:26
@devcodes9
devcodes9 deleted the branch main September 5, 2026 16:01
@devcodes9 devcodes9 closed this Sep 5, 2026
@devcodes9 devcodes9 reopened this Sep 5, 2026
@devcodes9
devcodes9 changed the base branch from feat/agsearch-skill to main September 5, 2026 16:02
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
devcodes9 force-pushed the feat/multi-harness-adapters branch from be799c9 to 7f56b5f Compare September 5, 2026 16:03
@devcodes9
devcodes9 merged commit d2a723b into main Sep 5, 2026
6 checks passed
@devcodes9
devcodes9 deleted the feat/multi-harness-adapters branch September 5, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Adapters: Gemini CLI, then opencode

1 participant