Skip to content

ACP: session/new persists an empty session row that never gets cleaned up #38064

Description

@ton618to

Summary

opencode acp creates and persists a durable session in SQLite the moment an ACP client (Zed, Neovim, etc.) issues session/new, before any prompt is ever sent. If the user then closes the editor panel, switches agents, or the client disconnects without prompting, the empty session row is orphaned forever — there is no cleanup on close and no session/delete in ACP 0.21.

Zed's agent panel triggers this routinely: opening the panel or picking the opencode agent from the dropdown provisions a new session eagerly. Every such interaction that the user abandons before typing leaves behind a New session - <ISO> row that keeps reappearing in the session picker.

Verified against upstream/dev at 849c2598abc7d2b40261e74b5826bc74ffc78308 (post-v1.18.4).

Reproduction

  1. opencode auth login and configure a working model.
  2. In Zed (or any ACP client), open the agent panel and pick opencode.
  3. Do not send any prompt. Close the panel, or switch to another agent, or quit the editor.
  4. Reopen the agent panel and list sessions (Zed's session history, or opencode TUI's session list).
  5. An empty session titled New session - 2026-07-21T... (zero messages, zero cost) appears and persists across restarts.

Repeat a few times → the picker fills with orphaned stubs.

Mechanism

newSession persists eagerly. packages/opencode/src/acp/service.ts:161-207 calls input.sdk.session.create({...}) unconditionally. That resolves to V2Session.create which:

  • Inserts a ProjectTable row,
  • Builds a SessionInfo with title: "New session - ${new Date(now).toISOString()}" (session.ts:228),
  • Publishes SessionV1.Event.Created, which the SessionProjector synchronously inserts into SessionTable.

By the time session/new returns, the row is durable — zero messages, zero cost, timestamp-placeholder title.

closeSession doesn't delete — it only aborts. service.ts:339-347:

const closeSession = ...(function* (params) {
  const removed = yield* session.remove(params.sessionId)   // in-memory Map only
  registeredMcp.delete(params.sessionId)
  sessionSnapshots.delete(params.sessionId)
  if (!removed) return {}
  yield* abortBackingSession(removed)                        // POST /session/abort
  return {}
})

session.remove (in packages/opencode/src/acp/session.ts) only clears the in-process Map<string, Info>. abortBackingSession calls session.abort, not session.delete. Nothing removes the row.

No session/delete in ACP. The SessionCapabilities schema in @agentclientprotocol/sdk@0.21.0 only exposes list, resume, fork, close. SessionCloseCapabilities docs are explicit: "By supplying {} it means that the agent supports closing of sessions." Close ≠ delete. Even if a client had a "delete" UI action, it would still resolve to session/close.

listSessions surfaces them forever. service.ts:244-288 merges sdk.session.list() (durable) with in-memory ACP sessions and returns both. Orphaned stubs from prior runs keep showing up.

Contrast with the TUI

opencode's own TUI creates the session lazily inside SessionV2.prompt(...) on first prompt, so no empty rows accumulate. Only the ACP entry point has this eager-persist behavior.

Suggested fixes (cheapest → most correct)

  1. Cleanup on close. In closeSession, if the backing session has zero messages, call sdk.session.delete(...) after abortBackingSession. Handles graceful disconnect (Zed closing the panel).
  2. Cleanup on connect. At the start of each ACP connection, sweep sessions under the project directory that have zero messages and were created by ACP. Cheap heuristic (SessionMessage count == 0 + a "created via ACP" marker). Handles crash/restart.
  3. Defer persistence (the real fix). Have newSession allocate a session ID without projecting Session.Created. Only persist on the first session/prompt. Aligns ACP with the TUI's lazy-persist behavior and eliminates the class of bug rather than sweeping it up. Requires plumbing an "unprojected" state through SessionV2.prompt.

(1) + (2) together are a solid stopgap needing no protocol changes. (3) is the correct long-term fix.

Environment

  • Verified reading: sst/opencode@849c2598ab (upstream/dev, post-v1.18.4)
  • ACP SDK: @agentclientprotocol/sdk@0.21.0
  • Client: Zed (agent panel); reproducible with any ACP client that eagerly calls session/new.

Related

  • Port ACP support to V2 core and APIs #35457 (Port ACP support to V2 core and APIs) — umbrella for the V2 port; doesn't mention this specific empty-session behavior. Regardless of the V2 migration, both newSession and closeSession on the current V2-compat code path exhibit this bug.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions