Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -346,13 +346,13 @@ export function makeCursorAgentSdkReplayRunner(
new CursorReplayFrameMismatchError({
scenario: transcript.scenario,
cursor,
expected: { type: "interaction.update | run.completed" },
expected: { type: "interaction.update | run.retry | run.completed" },
actual: entry.frame,
}),
);
}
const frame = entry.frame as CursorProtocolPayload;
if (frame.type === "interaction.update") {
if (frame.type === "interaction.update" || frame.type === "run.retry") {
if (frame.runId !== runId) {
return yield* recordFailure(
new CursorReplayFrameMismatchError({
Expand All @@ -364,6 +364,7 @@ export function makeCursorAgentSdkReplayRunner(
);
}
advance();
if (frame.type === "run.retry") continue;
yield* (sendInput.onDelta?.(frame.update) ?? Effect.void).pipe(
Effect.mapError((cause) =>
recordFailure(
Expand Down Expand Up @@ -396,7 +397,7 @@ export function makeCursorAgentSdkReplayRunner(
new CursorReplayFrameMismatchError({
scenario: transcript.scenario,
cursor,
expected: { type: "interaction.update | run.completed" },
expected: { type: "interaction.update | run.retry | run.completed" },
actual: frame,
}),
);
Expand Down Expand Up @@ -753,6 +754,14 @@ export const recordCursorAgentSdkReplayTranscript = Effect.fn(
};
case "run.completed":
return { ...frame, result: sanitizeReplayValue(frame.result, replacements) as RunResult };
case "run.retry":
return {
...frame,
diagnostic: sanitizeReplayValue(
frame.diagnostic,
replacements,
) as typeof frame.diagnostic,
};
default:
return frame;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
makeCursorAgentSdkReplayRunner,
} from "../Adapters/CursorAdapterV2.testkit.ts";
import * as CursorAdapterV2Testkit from "../Adapters/CursorAdapterV2.testkit.ts";
import type * as CursorAgentSdk from "@t3tools/provider-cursor/server/CursorAgentSdk";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as EffectWorker from "../EffectWorker.ts";
import * as SqlitePersistence from "../../persistence/Sqlite.ts";
Expand Down Expand Up @@ -151,16 +152,83 @@ const readClaudeCancelledSubagentResumeTranscript = Effect.fn(
});
return yield* ClaudeOrchestratorReplayHarness.decodeTranscript({ ...recorded, entries });
});
const readCursorTranscript = Effect.fn("readCursorRecoveryTranscript")(function* () {
const readCursorTranscript = Effect.fn("readCursorRecoveryTranscript")(function* (
checkpointFailure: boolean,
) {
const transcript = yield* readRawTranscript(
new URL("./fixtures/provider_thread_resume/cursor_transcript.ndjson", import.meta.url),
);
return yield* CursorOrchestratorReplayHarness.decodeTranscript(
const recorded = yield* CursorOrchestratorReplayHarness.decodeTranscript(
materializeReplayTranscriptRuntimeInstructions(transcript, {
driver: ProviderDriverKind.make("cursor"),
model: CURSOR_MODEL_SELECTION.model,
}),
);
if (!checkpointFailure) return recorded;
let secondSession = false;
const oldAgent = String(recorded.metadata?.nativeAgentId);
const newAgent = "agent-after-checkpoint-failure";
return yield* CursorOrchestratorReplayHarness.decodeTranscript({
...recorded,
entries: recorded.entries.flatMap<CursorAgentSdkReplayTranscript["entries"][number]>(
(entry) => {
if (entry.type !== "emit_inbound" && entry.type !== "expect_outbound") return entry;
let frame = entry.frame as CursorAgentSdk.CursorAgentSdkProtocolLogEvent["payload"];
if (entry.label === "run.completed:1" && frame.type === "run.completed") {
return [
{
type: "emit_inbound",
label: "run.retry:1",
frame: {
type: "run.retry",
runId: frame.result.id,
diagnostic: {
requestId: frame.result.requestId,
originalRequestId: frame.result.requestId,
attempt: 0,
decision: "blocked_after_turn_ended",
turnEnded: true,
checkpoint: "none",
cause: { name: "Error", message: "Checkpoint persistence failed" },
},
},
},
{
...entry,
frame: {
...frame,
result: {
...frame.result,
status: "error",
error: {
message:
"Cursor could not confirm that this answer was saved. Automatic replay was stopped; the next message will recover in a fresh Cursor session.",
},
},
},
},
];
}
if (frame.type === "agent.open" && frame.operation === "resume") {
secondSession = true;
const { agentId: _agentId, ...rest } = frame;
frame = { ...rest, operation: "create" };
}
if (entry.label === "run.start:2" && frame.type === "run.start") {
frame = {
...frame,
message: `Context handoff (full_thread_summary):\n<dynamic-summary>\n\nUser message:\n${frame.message}`,
};
}
return {
...entry,
frame: secondSession
? JSON.parse(JSON.stringify(frame).replaceAll(oldAgent, newAgent))
: frame,
};
},
),
});
});

function splitAfterFirstIdle(materialized: MaterializedOrchestratorFixtureInput) {
Expand All @@ -182,6 +250,7 @@ function splitAfterFirstIdle(materialized: MaterializedOrchestratorFixtureInput)
const runCursorRecovery = Effect.fn("runCursorRecovery")(function* (input: {
readonly transcript: CursorAgentSdkReplayTranscript;
readonly runner: ReturnType<typeof makeCursorAgentSdkReplayRunner>;
readonly checkpointFailure: boolean;
}) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
Expand Down Expand Up @@ -211,16 +280,32 @@ const runCursorRecovery = Effect.fn("runCursorRecovery")(function* (input: {
Layer.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
),
};
const sentMessages: string[] = [];
const runner: typeof input.runner = {
...input.runner,
open: (openInput) =>
input.runner.open(openInput).pipe(
Effect.map((session) => ({
...session,
send: (sendInput) => {
sentMessages.push(
typeof sendInput.message === "string" ? sendInput.message : sendInput.message.text,
);
return session.send(sendInput);
},
})),
),
};
const harness = {
...CursorOrchestratorReplayHarness,
makeProviderAdapterRegistryLayer: () =>
CursorAdapterV2Testkit.layer(input.transcript, {
runner: input.runner,
runner,
assertCompleteOnFinalize: false,
}),
};

yield* Effect.scoped(
const before = yield* Effect.scoped(
runOrchestratorV2ProviderReplayScenario(
{
name: "provider_thread_resume/cursor:first-runtime",
Expand All @@ -234,6 +319,11 @@ const runCursorRecovery = Effect.fn("runCursorRecovery")(function* (input: {
options,
),
);
if (input.checkpointFailure) {
const first = projectionFor(before, input.transcript.scenario);
assert.equal(first.providerThreads[0]?.status, "error");
assertAssistantTextIncludes(first, FIRST_FINAL);
}

const result = yield* Effect.scoped(
runOrchestratorV2ProviderReplayScenario(
Expand All @@ -255,7 +345,7 @@ const runCursorRecovery = Effect.fn("runCursorRecovery")(function* (input: {
result,
transcript: input.transcript,
runCount: 2,
runStatuses: ["completed", "completed"],
runStatuses: [input.checkpointFailure ? "failed" : "completed", "completed"],
});
const projection = projectionFor(result, input.transcript.scenario);
assertSemanticProjectionIntegrity(projection);
Expand All @@ -269,6 +359,20 @@ const runCursorRecovery = Effect.fn("runCursorRecovery")(function* (input: {
assertAssistantTextIncludes(projection, FIRST_FINAL);
assertAssistantTextIncludes(projection, SECOND_FINAL);
assert.lengthOf(projection.providerThreads, 1);
if (input.checkpointFailure) {
assert.include(sentMessages[1], "Context handoff (full_thread_summary):");
assert.include(sentMessages[1], FIRST_FINAL);
assert.match(
sentMessages[1]!,
/\[Historical assistant;[^\]]+\]\nprovider thread resume fixture first turn complete/,
);
assert.include(sentMessages[1], PROVIDER_THREAD_RESUME_SECOND_PROMPT);
assert.equal(
projection.providerThreads[0]?.nativeThreadRef?.nativeId,
"agent-after-checkpoint-failure",
);
assert.equal(projection.contextHandoffs.at(-1)?.delivery?.status, "inline");
}
});

describe("orchestrator replay recovery", () => {
Expand Down Expand Up @@ -370,14 +474,14 @@ describe("orchestrator replay recovery", () => {
),
);

it.effect(
"resumes a provider-native Cursor thread after recreating the orchestrator runtime",
() =>
it.effect.each([false, true])(
"recovers a Cursor thread after recreating the orchestrator runtime (unconfirmed checkpoint: %s)",
(checkpointFailure) =>
Effect.scoped(
Effect.gen(function* () {
const transcript = yield* readCursorTranscript();
const transcript = yield* readCursorTranscript(checkpointFailure);
const runner = makeCursorAgentSdkReplayRunner(transcript);
yield* runCursorRecovery({ transcript, runner });
yield* runCursorRecovery({ transcript, runner, checkpointFailure });
yield* runner.assertComplete;
}).pipe(
provideDeterministicTestRuntime,
Expand Down
2 changes: 1 addition & 1 deletion packages/provider-cursor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"test": "vp test run --config ../../vite.config.ts --dir ."
},
"dependencies": {
"@cursor/sdk": "1.2.0",
"@cursor/sdk": "1.3.1",
"@napi-rs/keyring": "^1.3.0",
"@t3tools/contracts": "workspace:*",
"@t3tools/provider-core": "workspace:*",
Expand Down
55 changes: 55 additions & 0 deletions packages/provider-cursor/src/server/CursorAgentSdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as Layer from "effect/Layer";
import * as Exit from "effect/Exit";
import * as Cause from "effect/Cause";
import { vi } from "vite-plus/test";
import * as NodeDiagnosticsChannel from "node:diagnostics_channel";

import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
import * as CursorAgentSdk from "./CursorAgentSdk.ts";
Expand Down Expand Up @@ -136,6 +137,60 @@ const layerTest = CursorAgentSdk.layer.pipe(
);

describe("CursorAgentSdkRunner", () => {
it.effect("logs an early retry failure under its run without recording request state", () =>
Effect.gen(function* () {
const events: CursorAgentSdk.CursorAgentSdkProtocolLogEvent[] = [];
const retryCause = Object.assign(new Error("Checkpoint persistence failed"), {
cause: Object.assign(new Error("disk full"), { code: "ENOSPC" }),
request: { authorization: "must-not-be-logged", prompt: "private prompt" },
});
cursorSdkMock.send.mockImplementationOnce(async () => {
NodeDiagnosticsChannel.channel("s2code.cursor.retry").publish({
requestId: "request-cursor-agent-sdk-test",
originalRequestId: "request-cursor-agent-sdk-test",
attempt: 0,
decision: "blocked_after_turn_ended",
turnEnded: true,
checkpoint: "none",
cause: retryCause,
});
return {
id: "run-cursor-agent-sdk-test",
agentId: "agent-cursor-agent-sdk-test",
wait: cursorSdkMock.runWait,
cancel: cursorSdkMock.runCancel,
};
});
const runner = yield* CursorAgentSdk.makeCursorAgentSdkRunner(
() => (event) =>
Effect.sync(() => {
events.push(event);
}),
);
const session = yield* runner.open({
operation: "create",
options: {},
threadId: ThreadId.make("cursor-diagnostics"),
providerSessionId: ProviderSessionId.make("cursor-diagnostics"),
});
const run = yield* session.send({ message: "hi" });
yield* run.wait;
const retry = events.find((event) => event.payload.type === "run.retry");
assert.equal(retry?.payload.type, "run.retry");
if (retry?.payload.type !== "run.retry") return;
assert.equal(retry.payload.runId, "run-cursor-agent-sdk-test");
assert.deepEqual(retry.payload.diagnostic.cause, {
name: "Error",
message: "Checkpoint persistence failed",
cause: { name: "Error", message: "disk full", code: "ENOSPC" },
});
assert.deepEqual(
events.map((event) => event.payload.type),
["agent.open", "agent.opened", "run.start", "run.started", "run.retry", "run.completed"],
);
}).pipe(Effect.provide(CursorSdk.layer)),
);

it.effect("constructs without loading and reports first-use load failures as runner errors", () =>
Effect.gen(function* () {
const failure = new Error("missing SDK chunk");
Expand Down
Loading
Loading