Skip to content

Parallel pi sessions in one workspace hit database is locked — expose a per-session stateRoot for the SDK agent store #150

Description

@austinm911

Summary

When multiple pi sessions run composer concurrently in the same repo (e.g. orchestrated worker panes), turns occasionally fail with stopReason: "error" / errorMessage: "database is locked" from the cursor-sdk api layer.

Environment

  • pi 0.80.3, pi-cursor-sdk current, @cursor/sdk 1.0.23
  • macOS, Node >= 22, model cursor/composer-2.5
  • 3-9 concurrent pi sessions in the same workspace

Root cause (traced in @cursor/sdk 1.0.23)

The SDK's default local agent store is per-workspace: getDefaultSdkStateRoot(workspaceRef) -> ~/.cursor/projects/<workspace>/sdk-agent-store/<hash>/index.db, shared by every SDK process in that repo. It's opened WAL with a hard-coded PRAGMA busy_timeout = 5000, and SqliteAgentRunStore wraps writes in BEGIN IMMEDIATE — including checkpoint blob persistence, which grows with agent state. Under concurrent writers, a transaction holding the write lock >5s causes another session's write to fail SQLITE_BUSY, surfaced as an errored turn.

An aggravating factor: nothing prunes the store, so it accumulates dead agents (mine reached 460MB / 83 stale agents), lengthening exactly the blob-write transactions that hold the lock.

Example failure

{"message": {"api": "cursor-sdk", "model": "composer-2.5",
 "stopReason": "error", "errorMessage": "database is locked"}}

~11s into a fresh session, first turn, while sibling sessions were active in the same workspace.

Proposed fix

SqliteLocalAgentStoreOptions.stateRoot already exists in the SDK. pi-cursor-sdk could pass a per-pi-session state root (e.g. derived from the pi session id, under the default root), so concurrent sessions never share an index.db. Since pi's own session JSONL is the durable record and each pi session maps to its own SDK agent lifecycle, cross-session sharing of the store appears to buy nothing for pi's use case.

Alternatives/complements:

  • retry SQLITE_BUSY turn-level instead of erroring the turn
  • prune terminal agents from the store on dispose (the SDK's blob-store cleanup only removes fully-empty agent dirs)
  • document the contention + manual prune (sdk-agent-store/ regenerates)

Workarounds we use meanwhile

Cap parallel composer sessions per repo at ~3; on the error, re-prompt the session (transient); prune the store when idle.

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