Skip to content

perf(opencode): run session listing off the event loop in a worker#391

Merged
danshapiro merged 2 commits into
mainfrom
perf/opencode-marker-cache
Jun 4, 2026
Merged

perf(opencode): run session listing off the event loop in a worker#391
danshapiro merged 2 commits into
mainfrom
perf/opencode-marker-cache

Conversation

@danshapiro
Copy link
Copy Markdown
Owner

Problem

OpencodeProvider.listSessionsDirect() ran a ~180 ms / ~432 MB synchronous node:sqlite scan on Freshell's shared event loop every ~7 s while OpenCode is active. The scan is the hasThreeViewsMarker leading-wildcard LIKE over part.data + message.data. Because the indexer refreshes on activity in any provider (Claude/Codex too), this blocking scan fired ~12×/min and froze every terminal and pane under multi-agent load.

The cache/DB-change-gate approaches were investigated first and falsified by live measurement (OpenCode's -wal advances every refresh, so a gate would never hit). The converged fix is to run the query off the event-loop thread.

Change

Move the blocking query into a worker_thread:

  • opencode-listing-query.ts — pure query module; lazy node:sqlite import, per-table marker detection.
  • opencode-listing.worker.ts — sentinel-guarded worker entry. The main thread loads it by exact sibling URL (.ts in dev/test, .js in prod) to dodge NodeNext's broken .js.ts remap inside workers.
  • opencode-listing-runner.ts — spawn-per-call, shape-validated, timeout-bounded worker runner; plus an in-process runner for tests.
  • opencode.ts — injectable query runner; returns [] for an absent/empty DB but throws on read failure so the indexer never prunes the sidebar on a transient worker hiccup.
  • session-indexer.ts — preserves cached direct-provider sessions on listing failure (full-scan + incremental paths).

Tests

  • Unit: query / runner / worker modules.
  • Integration: spawns the real worker, proves the loop stays responsive while the slow query runs, and exercises the compiled .js worker path.
  • TS-worker test guard uses process.features.typescript as a capability probe (not Node-version math), so it's correct across the Electron 22.12 runtime, Node 22.18+, and 23.x.

Verification

Full coordinated suite green (client + server + electron). Validated empirically on a live 533 MB OpenCode DB: the worker returns 343 rows while the main loop keeps ticking, with no fd leak over 100 spawns.

Plan: docs/superpowers/plans/2026-06-04-opencode-listing-offthread-worker.md (the superseded 2026-06-03 gate plan is retained as the measurement record).

🤖 Generated with Claude Code

codex and others added 2 commits June 4, 2026 09:57
Plan for moving OpencodeProvider.listSessionsDirect()'s synchronous
node:sqlite marker scan off the shared event loop into a worker_thread.
Retains the superseded 2026-06-03 DB-change-gate plan as the record of
how live measurement falsified the per-session-cache and gate approaches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OpencodeProvider.listSessionsDirect() ran a ~180ms / ~432MB synchronous
node:sqlite scan (the hasThreeViewsMarker LIKE over part.data+message.data)
on the shared event loop every ~7s while OpenCode is active, freezing every
terminal and pane under multi-agent load. Move the blocking query into a
worker_thread so it never blocks the loop.

- Extract the synchronous DB work into a pure query module
  (opencode-listing-query.ts) with a lazy node:sqlite import and per-table
  marker detection.
- Add a sentinel-guarded worker entry (opencode-listing.worker.ts). The main
  thread loads it by exact sibling URL (.ts in dev/test, .js in prod) to
  dodge NodeNext's broken .js->.ts module remap inside workers.
- Add a spawn-per-call, shape-validated, timeout-bounded worker runner
  (opencode-listing-runner.ts) plus an in-process runner for tests.
- Provider returns [] for an absent/empty DB but THROWS on read failure, so
  the indexer never prunes the sidebar on a transient worker hiccup.
- Indexer preserves cached direct-provider sessions on listing failure
  (full scan + incremental paths).

Covered by unit tests (query/runner/worker) and integration tests that spawn
the real worker, prove the loop stays responsive while it runs, and exercise
the compiled .js worker path. The TS-worker test guard uses
process.features.typescript as a capability probe instead of version math.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@danshapiro danshapiro merged commit a24f22b into main Jun 4, 2026
1 check passed
@danshapiro danshapiro deleted the perf/opencode-marker-cache branch June 4, 2026 17:01
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f43a9f200

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

s.time_updated AS lastActivityAt,
p.worktree AS projectPath,
${markerExpr} AS hasThreeViewsMarker
FROM session s
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return an empty result when the schema is not created yet

If opencode.db already exists but OpenCode has not created the session table yet (for example a freshly-created/zero-byte DB, or a reset DB before schema initialization), this query throws no such table: session. Because listSessionsDirect() now rethrows worker/query failures and the indexer preserves cached direct sessions on that path, this scenario is treated as a transient read failure instead of an empty database, leaving stale OpenCode sessions in the sidebar until a later successful scan. Guard for a missing session table and return { rows: [], schemaMissingParentId: false } before preparing this SELECT.

Useful? React with 👍 / 👎.

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.

2 participants