[SDK] Local agents do not retain conversation context between agent.send() calls`

Where does the bug appear (feature/product)?

Background Agent (GitHub, Slack, Web, Linear)

Describe the Bug

In @cursor/[email protected], local agents (local: { cwd }) do not retain
conversation context across multiple agent.send() calls on the same
SDKAgent instance. Every send behaves as a fresh, contextless prompt.
This contradicts the public docs which state:

“The agent retains conversation context across runs.”
(docs)

“Follow-up. Full context is retained.”
(docs streaming example)

Cloud agents (cloud: { repos }) work correctly — the same code path
with cloud config retains context. Issue is isolated to local mode.

Environment

  • @cursor/[email protected]
  • Node.js v22.22.2 (also reproduced on v18.20.8)
  • macOS 25.3.0 (darwin arm64)
  • Models tested: composer-2, gpt-5.4-nano
  • Personal API key (crsr_…)

Steps to Reproduce

import { Agent } from "@cursor/sdk";

const apiKey = process.env.CURSOR_API_KEY;
const dir = process.cwd();

async function streamText(run) {
  let txt = "";
  for await (const ev of run.stream()) {
    if (ev.type === "assistant") {
      for (const b of ev.message.content) if (b.type === "text") txt += b.text;
    }
  }
  await run.wait();
  return txt;
}

// LOCAL — fails
{
  const agent = await Agent.create({
    apiKey,
    model: { id: "composer-2" },
    local: { cwd: dir },
  });

  const r1 = await agent.send("My favourite colour is teal. Reply 'noted'.");
  console.log("L1:", await streamText(r1));        //  "noted"

  const r2 = await agent.send("What is my favourite colour?");
  console.log("L2:", await streamText(r2));        //  "Unknown" / "I don't know"

  await agent[Symbol.asyncDispose]();
}

// CLOUD — works
{
  const agent = await Agent.create({
    apiKey,
    model: { id: "composer-2" },
    cloud: { repos: [{ url: "https://github.com/<user>/<repo>", startingRef: "main" }] },
  });

  const r1 = await agent.send("My favourite colour is teal. Reply 'noted'.");
  console.log("C1:", await streamText(r1));        //  "noted"

  const r2 = await agent.send("What is my favourite colour?");
  console.log("C2:", await streamText(r2));        //  "teal"

  await agent[Symbol.asyncDispose]();
}

Observed behaviour

Run Mode Turn 1 reply Turn 2 reply (asked colour)
Local — composer-2 local "noted" "Searching the conversation history… No codeword from you in anything I can see for this chat" (or “Unknown”)
Local — gpt-5.4-nano local "noted" "Unknown"
Cloud — composer-2 cloud "noted" "teal" :white_check_mark:

Reproduces:

  • Across multiple models (composer-2, gpt-5.4-nano)
  • Across Agent.create()+ multi-send() and Agent.resume() patterns
  • In the same Node process and across separate processes
  • Following the exact pattern in cursor/cookbook/sdk/coding-agent-cli
    (passing name, model per-send, buildPrompt wrapper)

Diagnostic data

When inspecting the per-send pipeline:

  • onDelta callback never fires user-message-appended for prior turns
    during turn 2 — the SDK is not feeding any prior conversation into the
    follow-up run.
  • Agent.messages.list(agentId, { runtime: "local", cwd }) returns []
    immediately after a successful run that should have produced user/
    assistant messages.
  • The persisted index.db at
    ~/.cursor/projects/<process-cwd-slug>/sdk-agent-store/<hash>/index.db
    does show both runs in the runs table and full event payloads in
    run_events. So data is being persisted; it just isn’t loaded as
    context for follow-up sends.
  • The sdk-agent-store/ directory is keyed by the calling process’s cwd,
    not by the agent’s local.cwd — worth flagging in case that’s the
    source of the lookup mismatch.

Expected Behavior

Per docs and the cursor/cookbook/sdk/coding-agent-cli example (which
exposes a /reset slash command described as “Start a fresh agent and
clear context”, implying multi-turn context is the default), local
agents should retain conversation context across send() calls on the
same handle, and Agent.resume() should restore that context.

Operating System

MacOS

Version Information

Version: 3.2.16 (Universal)
VSCode Version: 1.105.1
Commit: 3e548838cf824b70851dd3ef27d0c6aae371b3f0
Date: 2026-04-28T21:07:47.682Z (1 day ago)
Layout: glass
Build Type: Stable
Release Track: Default
Electron: 39.8.1
Chromium: 142.0.7444.265
Node.js: 22.22.1
V8: 14.2.231.22-electron.0
OS: Darwin arm64 25.3.0

Does this stop you from using Cursor

No - Cursor works, but with this issue

Hey, thanks for the report. It’s rare to see such a thorough user-side investigation, and it really helps. The minimal repro, testing different models, inspecting index.db, the note about onDelta, and Agent.messages.list() returning [] all confirm the write path to the local store works, but the read path isn’t pulling history into the follow-up send().

This doesn’t match what the SDK docs promise for local mode. I’ve filed this internally as a separate bug. There are a few related tickets on the same local code path, but they don’t cover this specific context loss. I can’t share an ETA for a fix yet. When I have an update, I’ll post it in this thread.

I’ll also pass along your note that sdk-agent-store/ is keyed by process.cwd() rather than local.cwd(). We’ll check if that’s the source of a lookup mismatch.

As a temporary workaround if you need multi-turn locally right now, you can either stick with cloud: { repos } (like you noticed, it works there) or manually include prior turns in the prompt on each send(). Not ideal, but it should unblock you until we ship a fix.

Where does the bug appear (feature/product)?

Somewhere else…

Describe the Bug

Cursor SDK Issue: agent.send() does not retain conversation context across runs

Summary

When using @cursor/sdk with local agents, calling agent.send() multiple times on the same agent does not retain conversation context between runs. Each send() creates an isolated run with no awareness of previous messages, despite the agent being persistent (same agentId).

Environment

  • @cursor/sdk versions tested: v1.0.12, v1.0.11, v1.0.7 — all exhibit the same behavior
  • Node.js: v22.x
  • OS: Linux (Ubuntu, remote SSH host)
  • Agent type: local agent

Expected Behavior

Based on the SDK documentation, Agent.create() creates a durable agent, and subsequent agent.send() calls should maintain conversation context. The agent should be aware of previous messages and responses from earlier send() calls within the same agent.

Actual Behavior

Each agent.send() call creates a new, independent run. The agent has no memory of previous runs:

  • run.conversation() consistently returns turns=1 for each run
  • User message content shows as "(none)" in the conversation turns
  • The agent cannot recall any information from previous send() calls (e.g., a name told in the first message is unknown in the second)

Minimal Reproduction Script

#!/usr/bin/env node

/**
 * Minimal reproduction: @cursor/sdk agent.send() does not retain context across runs.
 *
 * Expected: Turn 2 should recall the name "Alice" from Turn 1.
 * Actual:   Turn 2 has no awareness of Turn 1.
 *
 * Usage:
 *   CURSOR_API_KEY=crsr_xxx node repro-context-loss.mjs
 */

import { Agent } from '@cursor/sdk'

const API_KEY = process.env.CURSOR_API_KEY
if (!API_KEY) {
  console.error('CURSOR_API_KEY is required')
  process.exit(1)
}

async function collectText(run) {
  let text = ''
  for await (const event of run.stream()) {
    if (event.type === 'assistant') {
      for (const block of event.message?.content ?? []) {
        if (block.type === 'text') text += block.text
      }
    }
  }
  await run.wait()
  return text
}

async function main() {
  console.log('Creating agent...')
  const agent = await Agent.create({
    apiKey: API_KEY,
    model: { id: 'claude-sonnet-4' },
    local: { cwd: process.cwd(), settingSources: [] },
  })
  console.log(`Agent created: ${agent.agentId}`)

  // ── Turn 1: establish context ──
  console.log('\n── Turn 1 ──')
  const run1 = await agent.send('My name is Alice. Please remember this. Reply with: "Got it, Alice."')
  const text1 = await collectText(run1)
  console.log(`Response: ${text1}`)

  // Inspect run1 conversation
  const conv1 = await run1.conversation()
  console.log(`run1.conversation(): turns=${conv1.length}`)

  // Check runs on the agent
  const runs1 = await Agent.listRuns(agent.agentId)
  console.log(`Agent runs after Turn 1: ${runs1.length}`)

  // ── Turn 2: test recall ──
  console.log('\n── Turn 2 ──')
  const run2 = await agent.send('What is my name? Just reply with the name, nothing else.')
  const text2 = await collectText(run2)
  console.log(`Response: ${text2}`)

  // Inspect run2 conversation
  const conv2 = await run2.conversation()
  console.log(`run2.conversation(): turns=${conv2.length}`)

  const runs2 = await Agent.listRuns(agent.agentId)
  console.log(`Agent runs after Turn 2: ${runs2.length}`)

  // ── Result ──
  const remembered = text2.toLowerCase().includes('alice')
  console.log(`\nResult: ${remembered ? '✓ PASS' : '✗ FAIL'} — Agent ${remembered ? 'remembered' : 'did NOT remember'} the name from Turn 1`)

  if (!remembered) {
    console.log('\nDiagnostic details:')
    console.log(`  run1 conversation turns: ${conv1.length}`)
    console.log(`  run2 conversation turns: ${conv2.length}`)
    console.log(`  Total runs on agent: ${runs2.length}`)
    console.log('  Each send() creates an isolated run with no context from prior runs.')
  }

  agent.close()
  process.exit(remembered ? 0 : 1)
}

main().catch(err => {
  console.error('Script crashed:', err)
  process.exit(1)
})

Reproduction Steps

  1. Install the SDK:

    npm install @cursor/sdk@latest
    
  2. Run the script with your API key:

    CURSOR_API_KEY=crsr_your_key_here node repro-context-loss.mjs
    
  3. Observe: Turn 2 response does NOT contain “Alice” — the agent has no memory of Turn 1.

Diagnostic Output (from our testing)

── Turn 1 ──
Response: Got it, Alice. I've noted your name. How can I help you?
run1.conversation(): turns=1           # ← only 1 turn, not accumulated
Agent runs after Turn 1: 1

── Turn 2 ──
Response: I don't have access to any previous conversation...
run2.conversation(): turns=1           # ← still only 1 turn, no context from run1
Agent runs after Turn 2: 2             # ← each send() creates a new run

Result: ✗ FAIL — Agent did NOT remember the name from Turn 1

Key observations:

  • run.conversation() always returns exactly 1 turn per run
  • User message content in the conversation shows as "(none)"
  • Agent.listRuns() confirms each send() creates a separate, independent run
  • The behavior is identical across SDK versions v1.0.12, v1.0.11, and v1.0.7

Workaround

We implemented manual conversation history injection — prepending formatted history to each agent.send() prompt:

// Before each send(), build a prompt with conversation history
function buildPromptWithHistory(history, newMessage) {
  if (history.length === 0) return newMessage

  const formatted = history
    .map(t => `${t.role === 'user' ? 'User' : 'Assistant'}: ${t.content}`)
    .join('\n\n')

  return `<conversation_history>\n${formatted}\n</conversation_history>\n\nBased on the conversation above, respond to this new message:\n\n${newMessage}`
}

// After each send(), record the turn
history.push({ role: 'user', content: userMessage })
history.push({ role: 'assistant', content: assistantResponse })

// Next send() includes full history
const prompt = buildPromptWithHistory(history, nextMessage)
const run = await agent.send(prompt)

This works but has significant drawbacks:

  1. No native context compression — we must manually manage token limits and summarization
  2. Prompt bloat — conversation history is re-sent with every message, consuming extra tokens
  3. Lost tool call context — the text-only history misses tool call details that the SDK would normally track

Feature Request

If multi-turn context retention is not currently supported for local agents via agent.send(), it would be very helpful to have one of:

  1. Native context retentionagent.send() automatically includes prior runs’ context
  2. A messages parameter — e.g., agent.send(message, { messages: [...] }) to explicitly pass conversation history in a structured format
  3. Clear documentation — explicitly state that agent.send() runs are isolated for local agents, and recommend the workaround pattern

Additional Context

We’re building an Electron desktop application that uses @cursor/sdk as the backend for a remote SSH workspace coding assistant. The application manages conversation sessions where users interact with an AI agent across multiple turns. The lack of native context retention was a critical UX issue — the agent appeared to “forget” everything after each message.

Steps to Reproduce

see desc.

Operating System

Linux

Version Information

cursor sdk:The behavior is identical across SDK versions v1.0.12, v1.0.11, and v1.0.7

Does this stop you from using Cursor

Sometimes - I can sometimes use Cursor

Cursor SDK Issue: agent.send() does not retain conversation context across runs

Summary

When using @cursor/sdk with local agents, calling agent.send() multiple times on the same agent does not retain conversation context between runs. Each send() creates an isolated run with no awareness of previous messages, despite the agent being persistent (same agentId).

Environment

  • @cursor/sdk versions tested: v1.0.12, v1.0.11, v1.0.7 — all exhibit the same behavior
  • Node.js: v22.x
  • OS: Linux (Ubuntu, remote SSH host)
  • Agent type: local agent

Expected Behavior

Based on the SDK documentation, Agent.create() creates a durable agent, and subsequent agent.send() calls should maintain conversation context. The agent should be aware of previous messages and responses from earlier send() calls within the same agent.

Actual Behavior

Each agent.send() call creates a new, independent run. The agent has no memory of previous runs:

  • run.conversation() consistently returns turns=1 for each run
  • User message content shows as "(none)" in the conversation turns
  • The agent cannot recall any information from previous send() calls (e.g., a name told in the first message is unknown in the second)

Minimal Reproduction Script

#!/usr/bin/env node

/**
 * Minimal reproduction: @cursor/sdk agent.send() does not retain context across runs.
 *
 * Expected: Turn 2 should recall the name "Alice" from Turn 1.
 * Actual:   Turn 2 has no awareness of Turn 1.
 *
 * Usage:
 *   CURSOR_API_KEY=crsr_xxx node repro-context-loss.mjs
 */

import { Agent } from '@cursor/sdk'

const API_KEY = process.env.CURSOR_API_KEY
if (!API_KEY) {
  console.error('CURSOR_API_KEY is required')
  process.exit(1)
}

async function collectText(run) {
  let text = ''
  for await (const event of run.stream()) {
    if (event.type === 'assistant') {
      for (const block of event.message?.content ?? []) {
        if (block.type === 'text') text += block.text
      }
    }
  }
  await run.wait()
  return text
}

async function main() {
  console.log('Creating agent...')
  const agent = await Agent.create({
    apiKey: API_KEY,
    model: { id: 'claude-sonnet-4' },
    local: { cwd: process.cwd(), settingSources: [] },
  })
  console.log(`Agent created: ${agent.agentId}`)

  // ── Turn 1: establish context ──
  console.log('\n── Turn 1 ──')
  const run1 = await agent.send('My name is Alice. Please remember this. Reply with: "Got it, Alice."')
  const text1 = await collectText(run1)
  console.log(`Response: ${text1}`)

  // Inspect run1 conversation
  const conv1 = await run1.conversation()
  console.log(`run1.conversation(): turns=${conv1.length}`)

  // Check runs on the agent
  const runs1 = await Agent.listRuns(agent.agentId)
  console.log(`Agent runs after Turn 1: ${runs1.length}`)

  // ── Turn 2: test recall ──
  console.log('\n── Turn 2 ──')
  const run2 = await agent.send('What is my name? Just reply with the name, nothing else.')
  const text2 = await collectText(run2)
  console.log(`Response: ${text2}`)

  // Inspect run2 conversation
  const conv2 = await run2.conversation()
  console.log(`run2.conversation(): turns=${conv2.length}`)

  const runs2 = await Agent.listRuns(agent.agentId)
  console.log(`Agent runs after Turn 2: ${runs2.length}`)

  // ── Result ──
  const remembered = text2.toLowerCase().includes('alice')
  console.log(`\nResult: ${remembered ? '✓ PASS' : '✗ FAIL'} — Agent ${remembered ? 'remembered' : 'did NOT remember'} the name from Turn 1`)

  if (!remembered) {
    console.log('\nDiagnostic details:')
    console.log(`  run1 conversation turns: ${conv1.length}`)
    console.log(`  run2 conversation turns: ${conv2.length}`)
    console.log(`  Total runs on agent: ${runs2.length}`)
    console.log('  Each send() creates an isolated run with no context from prior runs.')
  }

  agent.close()
  process.exit(remembered ? 0 : 1)
}

main().catch(err => {
  console.error('Script crashed:', err)
  process.exit(1)
})

Reproduction Steps

  1. Install the SDK:

    npm install @cursor/sdk@latest
    
  2. Run the script with your API key:

    CURSOR_API_KEY=crsr_your_key_here node repro-context-loss.mjs
    
  3. Observe: Turn 2 response does NOT contain “Alice” — the agent has no memory of Turn 1.

Diagnostic Output (from our testing)

── Turn 1 ──
Response: Got it, Alice. I've noted your name. How can I help you?
run1.conversation(): turns=1           # ← only 1 turn, not accumulated
Agent runs after Turn 1: 1

── Turn 2 ──
Response: I don't have access to any previous conversation...
run2.conversation(): turns=1           # ← still only 1 turn, no context from run1
Agent runs after Turn 2: 2             # ← each send() creates a new run

Result: ✗ FAIL — Agent did NOT remember the name from Turn 1

Key observations:

  • run.conversation() always returns exactly 1 turn per run
  • User message content in the conversation shows as "(none)"
  • Agent.listRuns() confirms each send() creates a separate, independent run
  • The behavior is identical across SDK versions v1.0.12, v1.0.11, and v1.0.7

Workaround

We implemented manual conversation history injection — prepending formatted history to each agent.send() prompt:

// Before each send(), build a prompt with conversation history
function buildPromptWithHistory(history, newMessage) {
  if (history.length === 0) return newMessage

  const formatted = history
    .map(t => `${t.role === 'user' ? 'User' : 'Assistant'}: ${t.content}`)
    .join('\n\n')

  return `<conversation_history>\n${formatted}\n</conversation_history>\n\nBased on the conversation above, respond to this new message:\n\n${newMessage}`
}

// After each send(), record the turn
history.push({ role: 'user', content: userMessage })
history.push({ role: 'assistant', content: assistantResponse })

// Next send() includes full history
const prompt = buildPromptWithHistory(history, nextMessage)
const run = await agent.send(prompt)

This works but has significant drawbacks:

  1. No native context compression — we must manually manage token limits and summarization
  2. Prompt bloat — conversation history is re-sent with every message, consuming extra tokens
  3. Lost tool call context — the text-only history misses tool call details that the SDK would normally track

Feature Request

If multi-turn context retention is not currently supported for local agents via agent.send(), it would be very helpful to have one of:

  1. Native context retentionagent.send() automatically includes prior runs’ context
  2. A messages parameter — e.g., agent.send(message, { messages: [...] }) to explicitly pass conversation history in a structured format
  3. Clear documentation — explicitly state that agent.send() runs are isolated for local agents, and recommend the workaround pattern

Additional Context

We’re building an Electron desktop application that uses @cursor/sdk as the backend for a remote SSH workspace coding assistant. The application manages conversation sessions where users interact with an AI agent across multiple turns. The lack of native context retention was a critical UX issue — the agent appeared to “forget” everything after each message.

We hope this can be fixed soon, as local agents are increasingly needed for enterprise scenarios.

@codjust, thanks for the second independent repro on Linux with claude-sonnet-4 across three SDK versions. This is a helpful expansion of coverage. We’re also tracking Tom_Peck’s report in this same thread. I can’t share an ETA for a fix yet, but we’ll post an update here when we have one.

Your workaround with manual history injection is basically what we recommend as a temporary solution until we fix native retention. The drawbacks you listed like no context compression, prompt bloat, and lost tool call context are valid. That’s exactly why we need native support, not just a workaround in the docs.

On the feature request, option 1 native context retention is what we’re aiming for. That’s the point of the fix. We’ll also update the docs so the behavior matches what’s promised.