Skip to content

opencode session list hangs for ~2 minutes due to unnecessary full InstanceBootstrap #37435

Description

@suxiaogang223

Summary

Running opencode session list blocks the terminal for approximately 2 minutes before showing results, even though the underlying SQL query completes in ~0.007 seconds. The root cause is that the command triggers a full InstanceBootstrap (config loading, plugin init, LSP startup, VCS init, file watchers, snapshot system, etc.) — all of which are unnecessary for a simple read-only database query.

Environment

  • opencode version: 1.18.x (current dev branch)
  • Database: 94 sessions, SQLite WAL mode, 131 MB after VACUUM

Steps to reproduce

time opencode session list
# Blocks for ~2 minutes before printing the session table

Root cause

1. SessionListCommand does not set instance: false

packages/opencode/src/cli/cmd/session.ts:70-86:

export const SessionListCommand = effectCmd({
  command: "list",
  describe: "list sessions",
  // ← missing `instance: false`
  builder: (yargs) => ...
  handler: Effect.fn("Cli.session.list")(function* (args) {
    const sessions = yield* Session.Service.use((svc) => svc.list(...))
    ...
  }),
})

The instance option in effectCmd defaults to true (packages/opencode/src/cli/effect-cmd.ts:46):

/**
 * Whether the command needs a project InstanceContext. Defaults to true.
 *
 * `true` (default): ... Runs InstanceBootstrap (config + plugin
 * init + LSP/File/etc forks) eagerly.
 *
 * `false`: skip the instance entirely. ... Use `false` for commands
 * that don't read project state (e.g. `models`, `serve`, `web`, `account`, `db`, `upgrade`).
 */
instance?: boolean | ((args: Args) => boolean)

When instance: true (the default), effectCmd loads the full InstanceStore, which triggers InstanceBootstrap.run:

packages/opencode/src/project/bootstrap.ts:32-46:

const run = Effect.gen(function* () {
  yield* config.get()       // load configuration
  yield* plugin.init()      // initialize plugins (can mutate config)
  yield* Effect.forEach(
    [lsp, shareNext, format, vcs, snapshot, project],
    (s) => s.init()         // start LSP, file watchers, VCS, snapshot, etc.
    ...
  )
})

This boots the entire opencode runtime — including starting language servers, file watchers, git monitors, snapshot system, share service, and formatting service — just to run a single SELECT query on the session table that takes 0.007 seconds.

2. Session.list() requires InstanceState.context

packages/opencode/src/session/session.ts:548-555:

const list = Effect.fn("Session.list")(function* (input?: ListInput) {
  const ctx = yield* InstanceState.context    // ← requires full instance
  return yield* listByProject(db, {
    projectID: ctx.project.id,                 // ← used for WHERE filter
    ...
  })
})

Because list() yields InstanceState.context to get ctx.project.id, simply setting instance: false would cause a defect (the InstanceRef would be undefined).

3. Other commands already skip InstanceBootstrap

Multiple commands correctly set instance: false and work fine:

Command File instance
db db.ts:11 false
account (all subcommands) account.ts:180,195,210,220,230 false
providers providers.ts:253,500 false
serve serve.ts:12 false
web web.ts:37 false
session list session.ts:70 not set → defaults to true

4. Evidence that SQL is not the bottleneck

-- Query plan shows indexed scan + temp sort (expected for ORDER BY without covering index)
EXPLAIN QUERY PLAN
SELECT * FROM session WHERE project_id=? AND parent_id IS NULL
ORDER BY time_updated DESC LIMIT 100;
-- |--SEARCH session USING INDEX session_project_idx (project_id=?)
-- `--USE TEMP B-TREE FOR ORDER BY

-- Actual execution time: ~0.007 seconds

The database has 94 sessions, 3,806 messages, and 16,449 parts. The session table scan is trivially fast even without a covering index on time_updated.

Proposed fix

Two possible approaches:

Option A (simpler): Bypass InstanceState in the CLI handler

Modify SessionListCommand to set instance: false and query sessions directly via Database.Service + directory filter (instead of project_id), following the same pattern used by the db command:

export const SessionListCommand = effectCmd({
  command: "list",
  instance: false,  // ← ADD THIS
  ...
  handler: Effect.fn("Cli.session.list")(function* (args) {
    const { db } = yield* Database.Service
    const rows = yield* db
      .select()
      .from(SessionTable)
      .where(and(
        eq(SessionTable.directory, process.cwd()),
        isNull(SessionTable.parent_id)
      ))
      .orderBy(desc(SessionTable.time_updated))
      .limit(args.maxCount ?? 100)
      .all()
    // ... format and output
  }),
})

Option B: Add a lightweight listByDirectory method to the session service

Create a new method that doesn't require InstanceState.context, using directory as the filter key instead of project_id.

Additional note

The same issue likely affects session delete (same file, same missing instance: false), as well as the stats command which also queries sessions via the full InstanceBootstrap.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions