Skip to content

feat: native Cursor conversations, /model and /effort for Cursor - #13

Merged
emanuilo merged 13 commits into
mainfrom
feat/cursor-sessions
Oct 18, 2026
Merged

emanuilo merged 13 commits into
mainfrom
feat/cursor-sessions

Conversation

@emanuilo

@emanuilo emanuilo commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #12; merge that first. This PR's base then switches to main.

Summary

The Cursor backend gets native, persistent conversations like Claude's in #12: one bridge process, one open agent per chat, resumed after a restart.

 run_turn(chat, context, message)
-  launch Node bridge                          # every turn
-  create agent; send system prompt + last 10 messages + message
-  close agent and bridge                      # transcript in memory only, lost on restart
+  bridge = shared bridge ?? launch(cwd = memory_dir)          # lazy; agents stored under ~/.cursor, keyed by cwd
+  agent  = open agent for chat
+        ?? agents.resume(session_id, model)                    # force on first send: take over a run that died with the old process
+        ?? create agent                                        # system prompt goes in its first message
+  agent.send(context + message, model, effort param)
+  failure → drop the agent (and the bridge if unreachable); resume next turn
backends/
├── model_selection.py   # ModelSelectionMixin: model/effort, model-list lookups, strict id check,
│                        #   saving to .env — shared by Claude and Cursor (was duplicated)
└── cursor.py
      run_turn
        _ensure_agent → "open" | "resumed" | "created"   # system prompt only when created,
        _send                                            #   force only when resumed
+ /new              closes the chat's agent
+ /model, /effort   models from client.models.list, same button menus as Claude;
+                   /effort maps to a model's reasoning/effort parameter when it has one
+                   saved as CURSOR_MODEL / CURSOR_EFFORT, applied per send (no reconnect)
  consolidation     reuses the shared bridge

Evidence

  • Before: pytest on feat: persistent Claude sessions, /new, /model, /effort #12 → 317 passed
    After: pytest → 341 passed, ruff clean. Cursor tests cover:
    bridge launched once, with cwd = memory dir; closed on shutdown
    agent reused across turns; system prompt only in a new agent's first message
    restart → agents.resume(session_id) with model and force; resume failure → fresh agent
    failed turn drops the agent / bridge
    /model and /effort lists, selection, persistence; an id is refused when
      the list can't be loaded (as for Claude)
    bridge robustness: stderr lines > 64 KiB, timeouts don't drop the bridge,
      an exited bridge is relaunched, shutdown with a session added meanwhile
    
  • Bridge launch verified live (working dir, clean shutdown, ~0.7 s).
  • Not verified live: a full Cursor turn. The only available key is on Cursor's free plan, which the SDK rejects ("Cloud Agent is not available for free users"). Resume and model handling are checked against the SDK source and with mocks.
  • Merged with fix: memory system bugs + end-to-end validation suite #14 in a scratch worktree: all 400 unit tests pass.

Merge Danger

Door: two-way

Revert returns Cursor to per-turn bridges and the in-memory transcript. Saved Cursor session ids are then ignored.

Blast Radius: cursor-backend

🤖 Generated with Claude Code

- Reuse one Claude CLI client per chat instead of spawning one per turn
- Resume conversations after restart (sessions.json), fresh session on failure
- Replace the rolling history window with native sessions; /new resets
- Static system prompt; time and memory context move into the user message
- Load no ~/.claude settings/plugins/MCP servers, no built-in tools, dontAsk
- Telegram /model and /effort, persisted to ~/.memclaw/.env
- Background consolidation after the reply
- Default to claude-sonnet-5-5 at medium effort; bump claude-agent-sdk
- /new forgets the session before awaiting the backend reset, so a
  message sent right after it can't resume the old session
- /model refuses to switch when the model list can't be loaded, instead
  of saving an unchecked (possibly mistyped) id that breaks every turn
- the fallback cost estimate includes cache-write tokens
- long-id model buttons carry a hash of the id instead of a list
  position, so they can't pick a different model if the list changed
- drain the bridge's stderr in chunks: readline() raised on a line over
  64 KiB, ending the drain, after which a full pipe would block the bridge
- a timeout no longer counts as a dead bridge, so one slow run doesn't
  drop the shared bridge and abort other chats' runs
- relaunch a bridge that exited between calls instead of failing the next
  message (or /model) first
- shutdown iterates over a copy of the sessions, so a message arriving
  mid-shutdown can't skip closing the bridge and MCP server
- one builder for the locked-down CLI options, shared by chat sessions and
  one-shots, so the lockdown can't drift between them
- turn cost computed in _turn_cost, out of run_turn's connection handling
- agent.handle's logging, pending-note acknowledgement and session saving
  pulled into named helpers
- clear the repo's ruff findings (unused imports, ambiguous names)
- model/effort state, model-list lookups, the strict model check and
  saving to ~/.memclaw/.env move into ModelSelectionMixin, used by both
  the Claude and Cursor backends. Cursor's /model now also refuses an id
  it can't check when the model list doesn't load.
- Cursor run_turn: _ensure_agent says whether the agent was already open,
  resumed or created (replacing two flags), and _send does the send
@emanuilo
emanuilo changed the base branch from feat/claude-persistent-sessions to main October 18, 2026 17:08
The Cursor backend is written and checked against the 0.1.x SDK; CI
installed 1.0.38 (>=0.1.0 allowed it), which no longer exports
UnknownAgentError. Pin to <0.2 so a major SDK upgrade is a deliberate
change.
0.1.4 calls it UnknownAgentError, 0.1.9 AgentNotFoundError (both
CursorAgentError subclasses, which is what the backend catches). The
suite passes on both versions.
@emanuilo
emanuilo merged commit 7df437b into main Oct 18, 2026
4 checks passed
@emanuilo
emanuilo deleted the feat/cursor-sessions branch October 18, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant