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.
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 thecursor-sdkapi layer.Environment
@cursor/sdk1.0.23cursor/composer-2.5Root cause (traced in
@cursor/sdk1.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-codedPRAGMA busy_timeout = 5000, andSqliteAgentRunStorewraps writes inBEGIN 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.stateRootalready 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 anindex.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:
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.