Skip to content

Index Cursor, opencode and Gemini CLI sessions - #57

Open
devcodes9 wants to merge 2 commits into
feat/agsearch-skillfrom
feat/multi-harness-adapters
Open

Index Cursor, opencode and Gemini CLI sessions#57
devcodes9 wants to merge 2 commits into
feat/agsearch-skillfrom
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 this diff is the
adapters alone. It retargets to main automatically when #55 merges.

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.

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.
@devcodes9
devcodes9 changed the base branch from main to feat/agsearch-skill September 5, 2026 09:26
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