Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 62 additions & 7 deletions src/provider/language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ export class CursorLanguageModel implements LanguageModelV3 {
}
}

const acquired = await acquireAgent({
// Shared acquire params. The retry path reuses this verbatim (minus
// resumeAgentId) so a fresh agent can never drift from the first attempt's
// config (sandbox, settingSources, MCP, etc.).
const baseAcquire = {
apiKey: this.requireApiKey(),
modelSelection,
mode,
Expand All @@ -198,9 +201,13 @@ export class CursorLanguageModel implements LanguageModelV3 {
...(mcpServers ? { mcpServers } : {}),
...(this.config.agents ? { agents: this.config.agents } : {}),
...(poolKey ? { name: `opencode/${sessionID!.slice(-8)}` } : {}),
...(resumeAgentId ? { resumeAgentId } : {}),
...(poolKey ? { poolKey } : {}),
...(record ? { record } : {}),
};

const acquired = await acquireAgent({
...baseAcquire,
...(resumeAgentId ? { resumeAgentId } : {}),
});

// A resumed agent already remembers the prior conversation, so send only the
Expand All @@ -210,13 +217,61 @@ export class CursorLanguageModel implements LanguageModelV3 {
promptToCursorMessage(options.prompt))
: promptToCursorMessage(options.prompt);

let yielded = false;
let releasedOriginal = false;
try {
yield* streamAgentTurn(acquired.agent, message, {
mode,
abortSignal: options.abortSignal,
});
try {
for await (const event of streamAgentTurn(acquired.agent, message, {
mode,
abortSignal: options.abortSignal,
})) {
yielded = true;
yield event;
}
} catch (err) {
// Resume-aware retry: a resumed agent can pass resume() yet fail the
// actual send when Cursor's server has already expired the agent (its
// server-side retention is shorter than our local 7-day reuse window,
// and not documented). If nothing has been emitted downstream yet and
// the user hasn't aborted, transparently re-create a fresh agent and
// replay the full transcript — self-healing, no context loss. The
// fresh agent re-pools under the same session (overwriting the dead
// agentId) via acquireAgent's existing pooling path.
if (
acquired.resumed &&
!yielded &&
!options.abortSignal?.aborted
) {
acquired.release();
releasedOriginal = true;
// A fresh create (no resumeAgentId) re-pools under the same
// session, overwriting the dead agentId. If re-acquiring itself
// fails (e.g. transient create error), surface that but keep the
// original resume failure as the cause for diagnosability.
let retry: Awaited<ReturnType<typeof acquireAgent>>;
try {
retry = await acquireAgent({ ...baseAcquire });
} catch (retryErr) {
if (retryErr instanceof Error && retryErr.cause === undefined) {
retryErr.cause = err;
}
throw retryErr;
}
try {
const replay = promptToCursorMessage(options.prompt);
yield* streamAgentTurn(retry.agent, replay, {
mode,
abortSignal: options.abortSignal,
});
} finally {
retry.release();
}
} else {
throw err;
}
}
} finally {
acquired.release();
if (!releasedOriginal) acquired.release();
}
}

Expand Down
Loading