Skip to content

feat: add OpenCode session history support - #27

Merged
wesm merged 14 commits into
mainfrom
opencode-support
Feb 24, 2026
Merged

feat: add OpenCode session history support#27
wesm merged 14 commits into
mainfrom
opencode-support

Conversation

@wesm

@wesm wesm commented Feb 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Add OpenCode (opencode.ai) as a fourth supported agent alongside Claude Code, Codex, and Gemini CLI
  • OpenCode stores sessions in a SQLite database (~/.local/share/opencode/opencode.db) with schema: projectsessionmessagepart, where role and part type live inside JSON data columns
  • Parse sessions, messages, tool calls, and reasoning parts from the OpenCode DB into the same ParsedSession/ParsedMessage structures used by other agents
  • Add persistent skip cache (skipped_files table) so non-interactive sessions survive process restarts without re-parsing ~7000 codex files
  • Add per-session change detection for OpenCode using time_updated comparison instead of fragile WAL fingerprinting
  • Add incremental message sync (append-only) to avoid expensive FTS5 delete+reinsert when re-syncing large active sessions
  • Add sync telemetry (timing logs per phase) for diagnosing performance
  • Add OPENCODE_DIR env var and config, purple agent color in UI
  • Use gemini-3-pro-preview model for insight generation

Test plan

  • CGO_ENABLED=1 go test -tags fts5 ./... — all tests pass
  • golangci-lint run ./... — no issues
  • Manual: make build && ./agentsview with OpenCode installed — sessions appear with correct project names, messages, tool calls
  • Verify second restart skips opencode sessions (0 updated) and file sync uses incremental append

🤖 Generated with Claude Code

wesm and others added 8 commits February 24, 2026 09:45
Add OpenCode (opencode.ai) as a fourth supported agent. OpenCode stores
session data in a SQLite database at ~/.local/share/opencode/opencode.db
rather than individual files, so this uses a separate DB-backed sync
path.

- Parser: opens the opencode DB read-only, queries project/session/
  message/part tables, builds ParsedSession + ParsedMessage from parts
  (text, tool, reasoning types)
- Sync engine: syncOpenCode() checks DB file mtime as change signal,
  per-session skip uses time_updated stored as file_mtime, virtual file
  paths use dbPath#sessionID for uniqueness
- Config: OPENCODE_DIR env var, defaults to ~/.local/share/opencode
- Frontend: purple agent dot color for opencode sessions
- Tests: comprehensive parser tests with in-memory test DB

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- WAL-aware change detection: use composite fingerprint from both
  opencode.db and opencode.db-wal (mtime + size) so writes that
  live in the WAL file are not missed between checkpoints
- Contiguous ordinals: use separate counter incremented only on
  append, not loop index, so skipped roles/empty messages don't
  create gaps
- Log per-session errors: buildOpenCodeSession failures now logged
  with session ID instead of silently dropped
- Tests: ordinal continuity test with mixed roles and empty content,
  sqliteFingerprint unit tests for WAL presence/modification/absence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pass --model gemini-3-pro-preview to the gemini CLI when generating
insights, and record the model name in the result.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract geminiInsightModel constant to avoid drift between the CLI
flag and Result.Model. Add TestGenerateGemini_ModelFlag with a fake
gemini binary that captures argv and verifies --model flag and
Result.Model are both set correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes for startup performance and OpenCode sync:

1. OpenCode parser queried non-existent `path` column instead
   of `worktree` in the project table, causing sync failures.

2. Non-interactive codex files (~7000) were re-parsed on every
   restart because the skip cache was in-memory only. Add a
   `skipped_files` table that persists skip decisions across
   restarts. The engine pre-populates its in-memory cache from
   the DB on startup, eliminating redundant parsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The real OpenCode database stores role and part type inside JSON
data columns, not as separate SQL columns. Rewrite the parser to
extract these from JSON blobs and update all test fixtures.

- Message: extract role from json_extract(data, '$.role')
- Part: extract type from json_extract(data, '$.type')
- Part query uses session_id directly (no JOIN through message)
- Fix tool callID JSON tag (was "id", real data uses "callID")

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace fragile sqliteFingerprint-based skip (which re-parsed all
opencode sessions whenever the WAL file changed) with per-session
time_updated comparison. Only sessions whose time_updated actually
changed are re-parsed.

Add timing logs to SyncAll phases (file workers, opencode sync,
opencode writes, persist skip cache) so bottlenecks are visible.

- Add ListOpenCodeSessionMeta for lightweight change detection
- Remove sqliteFingerprint (and its test file)
- syncOpenCode now queries session metadata first, compares each
  against stored file_mtime, and only parses changed sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes for sync performance:

1. GetFileInfoByPath (and GetSessionFileInfo) returned ok=false
   when file_size was NULL, which is the case for all opencode
   sessions (virtual paths with size 0). This caused all opencode
   sessions to be fully re-parsed and FTS5-reindexed on every
   restart. Remove the NULL check — row existence is sufficient.

2. writeBatch now uses incremental append instead of full
   replace. Session files are append-only, so when re-syncing a
   session that already has messages in the DB, only new messages
   (ordinal > max stored ordinal) are inserted. This avoids the
   expensive FTS5 delete+reinsert cycle for existing messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Feb 24, 2026

Copy link
Copy Markdown

roborev: Combined Review (fbfefa9)

Summary Verdict: The code changes introduce no security vulnerabilities, but contain one High and two Medium severity issues related to data synchronization correctness and performance.

High

Incremental message sync can leave stale message content

  • File: internal/sync/engine.go:
    737
  • Description: writeMessages() only appends messages with ordinal > maxOrd. If a session updates existing messages in place (same ordinals
    , changed content/tool parts), delta == 0 and nothing is written. This is especially likely with OpenCode’s part-based updates where a message can gain content without creating a new ordinal.
  • Suggested Fix: Fall back to ReplaceSessionMessages when maxOrd >= len(msgs )-1, or compare a checksum/last-message fingerprint and replace when existing ordinals changed.

Medium

OpenCode session deletions are not reconciled

  • File: [internal/sync/engine.go:305](/home/roborev/.roborev/clones/wesm/
    agentsview/internal/sync/engine.go:305)
  • Description: syncOpenCode() handles “new/changed” sessions only. If a session is removed from opencode.db, it is never removed from the agentsview DB, so stale sessions can persist indefinitely.
  • Suggested
    Fix:
    Diff current OpenCode IDs vs stored agent='opencode' sessions and delete missing ones (or mark disappeared) during sync.

O(N) database connection overhead and redundant table scans

  • Files: internal/sync/engine.go (in syncOpenCode) and
    internal/parser/opencode.go (in ParseOpenCodeSession)
  • Description: syncOpenCode iterates over changed session IDs and calls ParseOpenCodeSession for each. ParseOpenCodeSession opens/closes the OpenCode SQLite database and fetches the entire
    project table on every call. During bulk updates (like initial sync), this causes O(N) database connection overhead and O(N) redundant project table scans, significantly degrading performance.
  • Suggested Fix: Open the OpenCode database once in syncOpenCode, load the project mapping into
    memory once, and pass the active database connection to an internal parsing function for the changed sessions.

Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

Add OpenCode to all agent lists, supported agents table,
and environment variable documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Feb 24, 2026

Copy link
Copy Markdown

roborev: Combined Review (8692475)

Verdict: The PR introduces a high-severity regression in message syncing and lacks live-update support for OpenCode sessions, but is free of security
vulnerabilities.

High

Incremental message writes leave stale or incorrect data

  • Files: internal/sync/engine.go#L738, internal/sync/engine.go#L749, internal/sync/engine.go#L1005

Problem: The incremental append optimization in writeMessages only inserts messages with an ordinal > maxOrd. If an existing message is modified (e.g., streaming tokens appended, message edited, tool results backfilled) or if the session is truncated/cleared, these changes are ignored because their ordinals are <= maxOrd. This causes the database to permanently diverge from the actual session state.

  • Suggested Fix: Fall back to a full replace (e.g., restore ReplaceSessionMessages) when data is not strictly append-only, or explicitly update affected existing rows and tool_calls when backfilled values change.

Medium

OpenCode sessions do not live-update via session watch flow

  • Files: internal/sync/engine.go#L797, internal/server/events.go#L68, cmd/agentsview/main.go#L215
  • Problem: FindSourceFile returns "" for opencode:* sessions, and the watch logic only syncs when it has a resolvable file path. Additionally, the startup watcher setup does not monitor the OPENCODE_DIR.
  • Impact: OpenCode sessions will
    not emit timely session_updated events, meaning UI updates will rely solely on periodic or manual syncs.
  • Suggested Fix: Add an OpenCode-specific branch in the watch polling logic (e.g., direct SyncSingleSession by ID), and/or watch the opencode. db file for changes.

Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

Two fixes from code review:

1. persistSkipCache now returns the snapshot size so callers
   log it without reading len(e.skipCache) unsynchronized.

2. SyncSingleSession and syncSingleOpenCode use a new
   writeSessionFull method that does ReplaceSessionMessages
   instead of incremental append. Explicit re-syncs need to
   rebuild all messages, not just append new ones.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Feb 24, 2026

Copy link
Copy Markdown

roborev: Combined Review (7048e84)

Verdict: The proposed changes introduce OpenCode support but contain high-severity synchronization flaws and medium-severity race conditions that must be addressed.

🔴 High

Bulk sync can silently keep stale message content when existing messages change in place

  • Files: internal/sync/engine.go (lines 742, 751, 752-788, 777)
  • Problem:
    The incremental append optimization in writeMessages only inserts messages where Ordinal > maxOrd. If a session changes without adding higher ordinals (e.g., live streaming content updates, late-paired tool results, or truncations where total message count decreases), messages are not rewritten. For OpenCode, this is risky because session
    rows can update while message count/ordinals stay the same, allowing stale message content to persist indefinitely.
  • Suggested Fix: Update the logic to capture modifications to existing ordinals (e.g., using INSERT OR REPLACE), fallback to full replace whenever source mtime changes but no append delta is detected, or use
    full replacement (ReplaceSessionMessages) for OpenCode sessions to ensure accurate state replication.

🟡 Medium

OpenCode sessions do not participate in SSE watch updates

  • Files: internal/sync/engine.go:862, internal/server/events.go:33,
    internal/server/events.go:79
  • Problem: FindSourceFile returns "" for opencode: sessions, and watch logic only syncs when a source path can be resolved. As a result, /watch for OpenCode effectively sends heartbeats only, omitting
    session_updated events.
  • Suggested Fix: Add OpenCode-specific watch polling (e.g., compare time_updated for that session and call SyncSingleSession on change).

Skip-cache persistence can race and reintroduce stale skipped entries

  • Files:
    internal/sync/engine.go:544, internal/db/skipped.go:34, cmd/agentsview/main.go:121, cmd/agentsview/main.go:219, internal/server/events.go:16 7
  • Problem: persistSkipCache performs a full-table replace from a snapshot. Concurrent sync runs (periodic, watcher-triggered, API-triggered) can write older snapshots after a clear, which restores stale skip rows and keeps files incorrectly skipped until their mtime changes.
  • Suggested
    Fix:
    Serialize sync entry points (single-flight/mutex) or switch skipped-file persistence to incremental upsert/delete operations.

Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

wesm and others added 2 commits February 24, 2026 13:34
Extract shared conversion helpers (toDBSession, toDBMessages) from
writeBatch and writeSessionFull to eliminate duplication and reduce
drift risk between the two write paths.

Add TestSyncSingleSessionReplacesContent to verify that explicit
re-syncs replace existing message content when ordinals are
unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OpenCode messages can change in place (streaming updates, tool
result pairing), so the incremental append optimization used for
file-based agents is not safe. Use writeSessionFull for OpenCode
sessions in the bulk SyncAll path, matching the behavior already
used by SyncSingleSession.

The incremental append remains for Claude/Codex/Gemini where
session files are strictly append-only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Feb 24, 2026

Copy link
Copy Markdown

roborev: Combined Review (7a79cb4)

Summary Verdict: The codebase is free of security vulnerabilities under
the current threat model, but there are two medium-severity logic bugs in the synchronization engine that need to be addressed.

Medium

  • File: [internal/sync/engine.go](/home/roborev/.roborev/clones/wesm/agentsview/internal/sync/engine
    .go#L720)
    Issue: writeMessages() silently does nothing when a session is modified in-place (same/lower max ordinal) or truncated. The current logic only inserts messages with ordinal > maxOrd; if none exist, it returns early. As a result, the
    database can keep stale content or tail messages after non-append edits, even when the source file changed and was reparsed.
    Suggested Fix: If parsed data is not a strict append (for example, last parsed ordinal <= maxOrd, or any mismatch), fall back to ReplaceSessionMessages(sessionID, ms gs).

  • File: internal/sync/engine.go
    Issue: syncOpenCode() handles changed sessions but never
    removes sessions that have been deleted from OpenCode's source DB. Deleted upstream sessions will remain in agentsview indefinitely, leading to stale rows in analytics, search, and the UI.
    Suggested Fix: After loading OpenCode session metadata, diff against existing agent='opencode' sessions in agentsview and delete
    rows that are no longer present.


Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

Windows timer resolution (~15ms) can miss a 1ns deadline,
causing the timeout handler to not fire before the handler
completes. Use Microsecond instead — still too short for any
real handler, but within Windows timer granularity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Feb 24, 2026

Copy link
Copy Markdown

roborev: Combined Review (f68bfa2)

Overall Verdict: The changes introduce a few medium-severity synchronization risks related to file updates and deletions, but no security vulnerabilities were found.

Medium

Regression risk in bulk sync message writes

  • File:
    internal/sync/engine.go:720
    (writeMessages, called from writeBatch)
  • Description: SyncAll now does append-only inserts
    based on MaxOrdinal. If a session file is rewritten in place (same ordinals, changed content) or truncated, delta == 0 and existing rows are never replaced, leaving stale DB content.
  • Suggested Fix: Append only when a true append is proven; otherwise fallback to ReplaceSession Messages (for example, when file hash changed but no higher ordinals are found, or when parsed message count is <= stored max ordinal+1 without matching append semantics).

OpenCode deletions/empty parses are not reconciled

  • File: internal/sync/engine.go:312
    (syncOpenCode)
  • Description: The sync only upserts changed sessions from current metadata. Sessions removed from OpenCode (or changed to an unparseable/empty
    state where sess == nil) are not deleted from agentsview, so stale sessions can persist indefinitely.
  • Suggested Fix: Reconcile against the current OpenCode session ID set and delete missing ones; also delete existing session rows when a changed session now parses to nil.

Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

The test used a sub-millisecond timeout expecting real handlers
to always exceed it. On Windows (timer resolution ~15ms), fast
empty-DB handlers could return before the timer fired, causing
flaky 200-instead-of-503 failures.

Use a 10ms timeout with a 100ms handler delay injected via
withHandlerDelay test option, so the handler deterministically
exceeds the deadline regardless of platform timer granularity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Feb 24, 2026

Copy link
Copy Markdown

roborev: Combined Review (46019f4)

Verdict: Security checks passed, but the
sync engine changes introduce high-severity regressions in message persistence and session reconciliation.

High

  • Location: internal/sync/engine.go:714, 804 (writeMessages function)
  • Issue: The new incremental append logic drops any parsed messages where ordinal <= maxOrd. This breaks tool result pairing because pairAndFilter merges new tool results into the previous assistant message (which has an ordinal <= maxOrd), meaning these updates are never written to the database. Furthermore, this approach prevents deleted messages from being removed if a session file is truncated or rewritten in-place
    .
  • Suggested Fix: Fall back to using full replacement (ReplaceSessionMessages) unless strict append-safety is guaranteed, or explicitly UPSERT modified messages and handle file truncation. At a minimum, if delta == 0 after a changed-file parse, perform a full replace.

Medium

  • Location: internal/sync/engine.go:314
  • Issue: syncOpenCode only upserts changed sessions found in current metadata and never reconciles removed sessions. If an OpenCode session is deleted or filtered to nil, stale rows will remain in the database indefinitely
    .
  • Suggested Fix: Build the current OpenCode session ID set and prune DB rows for agent='opencode' that no longer exist on disk. Additionally, delete session rows when the parser returns nil for a previously known OpenCode session.

Synthesized from 4 reviews (agents: codex, gemini | types: default, security)

@wesm
wesm merged commit bdd2dd3 into main Feb 24, 2026
6 checks passed
@wesm wesm mentioned this pull request Feb 24, 2026
cursor Bot referenced this pull request in diazMelgarejo/periscope Jun 1, 2026
## Summary

- Add OpenCode (opencode.ai) as a fourth supported agent alongside
Claude Code, Codex, and Gemini CLI
- OpenCode stores sessions in a SQLite database
(`~/.local/share/opencode/opencode.db`) with schema: `project` →
`session` → `message` → `part`, where role and part type live inside
JSON `data` columns
- Parse sessions, messages, tool calls, and reasoning parts from the
OpenCode DB into the same `ParsedSession`/`ParsedMessage` structures
used by other agents
- Add persistent skip cache (`skipped_files` table) so non-interactive
sessions survive process restarts without re-parsing ~7000 codex files
- Add per-session change detection for OpenCode using `time_updated`
comparison instead of fragile WAL fingerprinting
- Add incremental message sync (append-only) to avoid expensive FTS5
delete+reinsert when re-syncing large active sessions
- Add sync telemetry (timing logs per phase) for diagnosing performance
- Add `OPENCODE_DIR` env var and config, purple agent color in UI
- Use `gemini-3-pro-preview` model for insight generation

## Test plan

- [ ] `CGO_ENABLED=1 go test -tags fts5 ./...` — all tests pass
- [ ] `golangci-lint run ./...` — no issues
- [ ] Manual: `make build && ./agentsview` with OpenCode installed —
sessions appear with correct project names, messages, tool calls
- [ ] Verify second restart skips opencode sessions (0 updated) and file
sync uses incremental append

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@wesm
wesm deleted the opencode-support branch June 25, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant