Repository navigation
fix(provider-cursor): Cursor agents release their memory when a session closes - #99
Merged
Merged
Conversation
…on closes Without a store, the Cursor SDK opens one per workspace and caches it for the life of the process. Every agent created or resumed through it keeps all of its checkpoint blobs in memory, and agent.close() does not release them. On a long-running server this grew the native heap by several GB. The runner now opens a SqliteLocalAgentStore for each session, uses it for the agent and its run lookups, and disposes it when the session closes or fails to open. The store resolves the same default state root, so existing agents resume. Claude-Session: https://claude.ai/code/session_01StkPGjC6GMoMkcwbiPzuUv
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
alexcstark
added a commit
that referenced
this pull request
Nov 4, 2026
…on closes (#99) Without a store, the Cursor SDK opens one per workspace and caches it for the life of the process. Every agent created or resumed through it keeps all of its checkpoint blobs in memory, and agent.close() does not release them. On a long-running server this grew the native heap by several GB. The runner now opens a SqliteLocalAgentStore for each session, uses it for the agent and its run lookups, and disposes it when the session closes or fails to open. The store resolves the same default state root, so existing agents resume. Claude-Session: https://claude.ai/code/session_01StkPGjC6GMoMkcwbiPzuUv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The lax6e server's memory grew from 0.6 GB to 6.8 GB over six days of uptime, almost all of it native heap (
[heap]5.4 GB; the V8 heap was 1.35 GB). Process spawns copy the server's page tables, so that growth made everygitspawn about 10× slower and drove the event-loop stalls that left uploads and threads stuck.Cause
The Cursor chat path (
CursorAgentSdk.ts) passes nolocal.store. In that case the SDK opens a defaultSqliteLocalAgentStoreper workspace and caches it for the life of the process (a module-levelMapin@cursor/sdk1.0.35, unchanged in 1.0.38). Every agent created or resumed through it keeps its checkpoint blobs in memory asUint8Arraycopies. Resuming an agent loads all blobs it ever wrote. Only deleting the agent evicts them, andagent.close()doesn't. Text generation already avoided this by owning and disposing its store (pingdotgg#19310). The chat path didn't.Evidence (lax6e, upstream 0.1.0, measured 2026-11-04)
store.dbhandles open. Exactly 91 agent stores had been written since that server started, holding 1.83 GB of blob data.arrayBuffers315 MB, still retained after close and GC.arrayBuffers1 MB.Fix
The runner opens a
SqliteLocalAgentStoreper session when the caller passes none, and uses it for the agent and its run lookups (listRuns/cancelRun, which would otherwise reopen the default store). It disposes the store when the session closes, or when opening the agent fails.SqliteLocalAgentStore.open({ workspaceRef: cwd })resolves the same default state root the SDK uses, so existing agents still resume; the reproduction resumed seeded agents through it.Proof
CursorAgentSdk.test.ts: the close-order cases now also expect the store to be passed to the SDK and disposed afteragent.close, including when close fails. The resume-retry cases expect the store in run lookups. Withorigin/main's runner, 4 of 7 fail; with this change, 7/7 pass.provider-cursortests: 131/131.tsc-rspasses forprovider-cursorandapps/server.vp fmt --checkandvp lintpass on the changed files.b603e5bdfb) predates this change.🤖 Generated with Claude Code
https://claude.ai/code/session_01StkPGjC6GMoMkcwbiPzuUv