Skip to content

Dispose-time closeAll() races with a live instance idle handler -> "Idle processing error: TypeError: The database connection is not open" #443

Closed

Description

@junior-xj97-chan

Summary

A session.idle handler in one plugin instance dies with TypeError: The database connection is not open because another instance's dispose closes the process-wide connection pool. Reproduced twice on Windows — 2026-10-24 (2.33.x) and 2026-10-27 (2.34.0):

[ERROR] Idle processing error: {"error":"TypeError: The database connection is not open"}

It self-heals on the next idle (no data loss), but the profile-learning round is silently dropped and it shows up as a red error on the stats page.

Details

Log excerpt (2.34.0, 2026-10-27)

09:52:10      savePrompt -> session ses_...KrAsL
09:53:12      opencodeProvider takes precedence over memoryModel for auto-capture
09:53:15.981  Auto-capture skipped: {... "type":"skip"}
09:53:16.063  user-profile-learning: aborted: {"error":"TypeError: The database connection is not open"}
09:53:16.122  Idle processing error: {"error":"TypeError: The database connection is not open"}
09:54:12      user-profile-learning: check: {"count":14,"threshold":20}   <-- next idle, recovered

The 2026-10-24 00:02:55 occurrence had the same shape (skip -> aborted -> Idle processing error), followed 0.8 s later by a fresh plugin instance init. In both cases a session had just ended in the same opencode-cli process (several sessions are hosted in one process; OpenCode host fetch unavailable / opencode providers connected bursts mark instance init).

Root cause

  1. Idle timer: after performAutoCapture() it re-checks cleanedUp (dist/index.js:1044) and calls performUserProfileLearning() (dist/index.js:1046).
  2. Any instance's dispose -> cleanupPlugin() -> memoryClient.close() (dist/index.js:490) -> closeTursoAndInvalidateCaches() (dist/services/client.js:85) -> tursoConnectionManager.closeAll() (dist/services/turso/connection-manager.js:179). closeAll() closes every connection in the process, not just the disposing instance's.
  3. Managers cache the raw handle: UserPromptManager stores this.db from getConnection() (dist/services/user-prompt/user-prompt-manager.js:24) and reuses it in ready() (lines 36-45). If closeAll() lands between ready() returning the handle and the query, prepare() throws at @tursodatabase/database-common/dist/compat.js:118 / promise.js:216 (if (!this.open) throw TypeError("The database connection is not open")). The failing frame is countUnanalyzedForUserLearning() -> db.get (user-prompt-manager.js:157).
  4. The cleanedUp flag cannot prevent this: the dispose belongs to a different session's instance, so the running idle callback never observes it.
  5. Recovery is incidental: closeTursoAndInvalidateCaches() also resets the managers (dist/services/turso/lifecycle.js:8-16), so the next call re-opens lazily — which is why the following idle works.

Why the #412 fix does not cover this path

#412 fixed SkillsManager.getDb() by revalidating the cached handle against tursoConnectionManager (if (current === this.db) -> replace). UserPromptManager.ready(), UserProfileManager and AiSessionManager still trust this.db blindly, so the same stale-handle window is open for the idle/learning path.

Suggested fixes (either one closes the hole)

Minimal repro (in-process, faithful to the race)

const db = await userPromptManager.ready();       // cached handle, as in the idle path
await closeTursoAndInvalidateCaches();            // what another instance's dispose does
await db.get('SELECT 1');                         // TypeError: The database connection is not open

Real-world repro: open several sessions in one Desktop process, then close one while another one's idle handler is running capture/learning. Timing window is narrow (~ms), which matches the observed frequency (twice in 4 days here; earlier occurrences were roughly monthly).

Impact

  • Low severity: no corruption, no data loss. PRAGMA integrity_check ok on all DBs/shards, profile_learning_lock empty, the next idle recovers.
  • Cost: that round's profile learning is skipped, and users see Idle processing error counted on the stats page.

Environment

  • opencode-mem version: 2.34.0 (also observed on 2.33.x on 2026-10-24)
  • OpenCode version: Desktop 2.1.4 (host opencode-cli 1.4.4)
  • OS: Windows 11 (x64); many sessions sharing one opencode-cli process

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions