@stigmer/runner 3.15.2 → 3.15.3-dev.20260914121148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +12 -9
  3. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -1
  4. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +5 -2
  5. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +5 -2
  6. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  7. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +11 -27
  8. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +13 -10
  9. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -1
  10. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +70 -26
  11. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +82 -32
  12. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -1
  13. package/dist/activities/execute-cursor/adapter.d.ts +4 -3
  14. package/dist/activities/execute-cursor/adapter.js +9 -5
  15. package/dist/activities/execute-cursor/adapter.js.map +1 -1
  16. package/dist/activities/execute-cursor/sdk-warmup.d.ts +15 -6
  17. package/dist/activities/execute-cursor/sdk-warmup.js +15 -6
  18. package/dist/activities/execute-cursor/sdk-warmup.js.map +1 -1
  19. package/dist/activities/execute-cursor/session-lifecycle.d.ts +38 -59
  20. package/dist/activities/execute-cursor/session-lifecycle.js +59 -96
  21. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  22. package/dist/activities/execute-cursor/session-store.d.ts +100 -0
  23. package/dist/activities/execute-cursor/session-store.js +157 -0
  24. package/dist/activities/execute-cursor/session-store.js.map +1 -0
  25. package/dist/shared/attachment-vision.d.ts +2 -1
  26. package/dist/shared/attachment-vision.js.map +1 -1
  27. package/package.json +5 -5
  28. package/src/activities/execute-cursor/__test-utils__/__tests__/scripted-double.test.ts +31 -7
  29. package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +10 -8
  30. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +5 -2
  31. package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +20 -28
  32. package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +94 -38
  33. package/src/activities/execute-cursor/__tests__/cursor-baseurl-routing.test.ts +10 -8
  34. package/src/activities/execute-cursor/__tests__/cursor-fetch-interceptor-bypass.test.ts +5 -4
  35. package/src/activities/execute-cursor/__tests__/cursor-generate-image-live.test.ts +7 -2
  36. package/src/activities/execute-cursor/__tests__/cursor-hook-protocol-live.test.ts +163 -0
  37. package/src/activities/execute-cursor/__tests__/cursor-sdk-auth-smoke.test.ts +13 -12
  38. package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +4 -1
  39. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +4 -1
  40. package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +3 -0
  41. package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +3 -0
  42. package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +4 -1
  43. package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +4 -1
  44. package/src/activities/execute-cursor/__tests__/hermetic/run-wait-arms.test.ts +4 -1
  45. package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +4 -1
  46. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +3 -0
  47. package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +4 -1
  48. package/src/activities/execute-cursor/__tests__/hermetic/unattributed-hook-block.test.ts +4 -1
  49. package/src/activities/execute-cursor/__tests__/hermetic/workspace-lock-timeout.test.ts +3 -0
  50. package/src/activities/execute-cursor/__tests__/session-lifecycle.test.ts +86 -58
  51. package/src/activities/execute-cursor/__tests__/session-store.test.ts +220 -0
  52. package/src/activities/execute-cursor/adapter.ts +9 -5
  53. package/src/activities/execute-cursor/sdk-warmup.ts +15 -6
  54. package/src/activities/execute-cursor/session-lifecycle.ts +63 -109
  55. package/src/activities/execute-cursor/session-store.ts +187 -0
  56. package/src/shared/attachment-vision.ts +2 -1
@@ -9,17 +9,20 @@
9
9
  * - `nextRunId` names the run the next send will be given, so a script can be
10
10
  * built with matching event ids before the send happens.
11
11
  * - `agentForWorkspaceRef` hands `Agent.create` the agent declared for the
12
- * session the create names (via `platform.workspaceRef`), consulted before
13
- * the ordered list and registered for `Agent.resume` by id; a create with no
14
- * session, or a session the resolver does not know, falls through to the
15
- * list as before.
12
+ * session the create names (via the `workspaceRef` of the scripted store it
13
+ * passes as `local.store`), consulted before the ordered list and
14
+ * registered for `Agent.resume` by id; a create with no store, or a session
15
+ * the resolver does not know, falls through to the list as before.
16
+ * - `ScriptedLocalAgentStore.open` (the `@cursor/sdk/sqlite` double) records
17
+ * every open and dispose on the bound SDK, so a scenario can assert the
18
+ * adapter's session-store lifetime.
16
19
  */
17
20
 
18
21
  import { describe, it, expect } from "vitest";
19
22
  import type { AgentOptions, SDKMessage } from "@cursor/sdk";
20
23
 
21
24
  import { ScriptedCursorAgent, defaultRunId, sdkEvents, step } from "../scripted-agent.js";
22
- import { ScriptedCursorSdk } from "../scripted-sdk.js";
25
+ import { ScriptedCursorSdk, ScriptedLocalAgentStore, bindScriptedSdk } from "../scripted-sdk.js";
23
26
 
24
27
  /** An agent whose declared turns each play the given events and finish. */
25
28
  function agent(id: string, turns: SDKMessage[][] = []): ScriptedCursorAgent {
@@ -76,8 +79,14 @@ describe("ScriptedCursorAgent.nextRunId", () => {
76
79
  });
77
80
 
78
81
  describe("ScriptedCursorSdk.agentForWorkspaceRef", () => {
82
+ // The scripted store carries only the members the runner touches, not the
83
+ // SDK's whole `LocalAgentStore`, hence the cast — the same one the mocked
84
+ // module makes when it stands in for `@cursor/sdk/sqlite`.
79
85
  const createFor = (workspaceRef: string | undefined): AgentOptions =>
80
- ({ apiKey: "k", ...(workspaceRef !== undefined ? { platform: { workspaceRef, stateRoot: "/tmp/x" } } : {}) }) as unknown as AgentOptions;
86
+ ({
87
+ apiKey: "k",
88
+ local: workspaceRef !== undefined ? { store: new ScriptedLocalAgentStore(workspaceRef, "/tmp/x") } : undefined,
89
+ }) as unknown as AgentOptions;
81
90
 
82
91
  it("hands out the session's agent before the ordered list and registers it for resume", () => {
83
92
  const forSession = agent("session-agent");
@@ -94,9 +103,24 @@ describe("ScriptedCursorSdk.agentForWorkspaceRef", () => {
94
103
  expect(sdk.resolutions.map((r) => `${r.kind}:${r.agentId}`)).toEqual(["create:session-agent", "resume:session-agent", "create:listed-agent"]);
95
104
  });
96
105
 
97
- it("a create without a platform ref falls through to the ordered list", () => {
106
+ it("a create without a store falls through to the ordered list", () => {
98
107
  const listed = agent("listed-only");
99
108
  const sdk = new ScriptedCursorSdk({ agents: [listed], catalog: [], agentForWorkspaceRef: () => agent("never") });
100
109
  expect(sdk.create(createFor(undefined))).toBe(listed);
101
110
  });
102
111
  });
112
+
113
+ describe("ScriptedLocalAgentStore (the @cursor/sdk/sqlite double)", () => {
114
+ it("open hands out a store carrying the ref and root asked for, and records opens and disposes on the bound SDK", async () => {
115
+ const sdk = new ScriptedCursorSdk({ agents: [], catalog: [] });
116
+ bindScriptedSdk(sdk);
117
+
118
+ const store = await ScriptedLocalAgentStore.open({ workspaceRef: "stigmer-session:ses-9", stateRoot: "/tmp/ses-9" });
119
+ expect(store.workspaceRef).toBe("stigmer-session:ses-9");
120
+ expect(store.stateRoot).toBe("/tmp/ses-9");
121
+ expect(sdk.stores).toEqual([store]);
122
+
123
+ await store.dispose();
124
+ expect(store.disposeCalls).toBe(1);
125
+ });
126
+ });
@@ -95,7 +95,7 @@ import type {
95
95
  } from "../../../__test-utils__/harness-contract/types.js";
96
96
  import type { HermeticEnvironment } from "../../../__test-utils__/hermetic-activity.js";
97
97
  import { createCursorAdapter } from "../adapter.js";
98
- import { resolvePlatformOptions } from "../session-lifecycle.js";
98
+ import { resolveSessionStoreLocation } from "../session-store.js";
99
99
  import { hookInputFor, streamArgsFor, STREAM_NAME, type GatedActionKind } from "./cursor-hook-harness.js";
100
100
  import { FIXTURE, SDK_CATALOG, hermeticCursorConfig, runWorkspaceHook } from "./hermetic-cursor.js";
101
101
  import { ScriptedCursorAgent, sdkEvents, step, type ScriptStep, type ScriptedRun } from "./scripted-agent.js";
@@ -216,9 +216,9 @@ class CursorSubject implements CursorContractSubject {
216
216
  private mintAgent(sessionId: string): ScriptedCursorAgent {
217
217
  const agent = new ScriptedCursorAgent({ agentId: `agent-kit-${++this.mintCounter}`, turns: [] });
218
218
  this.agents.set(sessionId, agent);
219
- // The adapter asks the SDK to create an agent for the session under this
220
- // ref; the double answers with this agent.
221
- const { workspaceRef } = resolvePlatformOptions(sessionId, this.config.workspaceRootDir);
219
+ // The adapter asks the SDK to create an agent for the session over a store
220
+ // opened under this ref; the double answers with this agent.
221
+ const { workspaceRef } = resolveSessionStoreLocation(sessionId, this.config.workspaceRootDir);
222
222
  this.agentsByWorkspaceRef.set(workspaceRef, agent);
223
223
  return agent;
224
224
  }
@@ -248,11 +248,13 @@ class CursorSubject implements CursorContractSubject {
248
248
  break;
249
249
  case "usage":
250
250
  script.push(
251
+ // The kit's delta leaves a count optional; the SDK's `turn-ended`
252
+ // states all four, and an absent count is zero, as the pricer reads it.
251
253
  step.turnEnded({
252
- inputTokens: s.delta.inputTokens,
253
- outputTokens: s.delta.outputTokens,
254
- cacheReadTokens: s.delta.cacheReadTokens,
255
- cacheWriteTokens: s.delta.cacheWriteTokens,
254
+ inputTokens: s.delta.inputTokens ?? 0,
255
+ outputTokens: s.delta.outputTokens ?? 0,
256
+ cacheReadTokens: s.delta.cacheReadTokens ?? 0,
257
+ cacheWriteTokens: s.delta.cacheWriteTokens ?? 0,
256
258
  }),
257
259
  );
258
260
  break;
@@ -11,12 +11,15 @@
11
11
  * registry the harness reads at setup, the Cursor `Config` slice, and the
12
12
  * per-scenario reset of the harness's module-level caches.
13
13
  *
14
- * What the two `vi.mock` calls a scenario file must carry look like — they are
15
- * hoisted by vitest and so cannot live here:
14
+ * What the three `vi.mock` calls a scenario file must carry look like — they
15
+ * are hoisted by vitest and so cannot live here (the SDK's sqlite entry is a
16
+ * separate module id, so it is doubled separately; `scripted-sdk.ts`):
16
17
  *
17
18
  * ```ts
18
19
  * vi.mock("@cursor/sdk", async () =>
19
20
  * (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule());
21
+ * vi.mock("@cursor/sdk/sqlite", async () =>
22
+ * (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSqliteModule());
20
23
  * vi.mock("../../../../client/stigmer-client.js", async () =>
21
24
  * (await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule());
22
25
  * ```
@@ -24,16 +24,12 @@
24
24
  * The same shape as the native harness's `__test-utils__/scripted-model.ts`
25
25
  * (`ScriptStep`, driven in order), applied to the SDK's event surface.
26
26
  *
27
- * Typing: `SDKMessage`, `Run`, `RunResult` and `SDKAgent` are the SDK's own
28
- * types (`@cursor/sdk` 1.0.13, concrete in `messages.d.ts` / `run.d.ts` /
29
- * `agent.d.ts`), so a script that drifts from the real event shape fails
30
- * `tsc`. The delta channel is the exception: `InteractionUpdate` re-exports
31
- * from `@anysphere/cursor-sdk-shared`, a workspace package the published SDK
32
- * does not ship, so under `skipLibCheck` it resolves to `any` — in production
33
- * code too (`turn-stream.ts`, `delta-enricher.ts` read it untyped). The one
34
- * delta this double emits, the `turn-ended` usage, is therefore typed HERE
35
- * ({@link TurnEndedUsageDelta}) against what `turn-stream.ts` reads from it,
36
- * and that gap is recorded in the entry's findings rather than papered over.
27
+ * Typing: `SDKMessage`, `Run`, `RunResult`, `SDKAgent` and the delta channel's
28
+ * `TurnEndedUpdate` are the SDK's own types (`@cursor/sdk` 1.0.31, concrete in
29
+ * `messages.d.ts` / `run.d.ts` / `agent.d.ts` / the vendored `delta-types`),
30
+ * so a script that drifts from the real event shape fails `tsc`. (At 1.0.13
31
+ * the delta types resolved to `any` and the one delta this double emits was
32
+ * typed locally; stigmer/stigmer#1053 closed that gap.)
37
33
  *
38
34
  * Cancellation: `run.cancel()` is how the harness stops a turn (first denial,
39
35
  * cost cap, stall). The double honours it the way the SDK does — the stream
@@ -50,6 +46,8 @@
50
46
  */
51
47
 
52
48
  import type {
49
+ AgentUsage,
50
+ GetUsageOptions,
53
51
  ModelSelection,
54
52
  Run,
55
53
  RunOperation,
@@ -59,6 +57,7 @@ import type {
59
57
  SDKMessage,
60
58
  SDKUserMessage,
61
59
  SendOptions,
60
+ TurnEndedUpdate,
62
61
  } from "@cursor/sdk";
63
62
  import type { ConversationTurn } from "@cursor/sdk";
64
63
 
@@ -66,21 +65,6 @@ import type { ConversationTurn } from "@cursor/sdk";
66
65
  // Script steps
67
66
  // ---------------------------------------------------------------------------
68
67
 
69
- /**
70
- * The `turn-ended` delta as `turn-stream.ts` reads it (`update.type ===
71
- * "turn-ended" && update.usage` -> `usageAccumulator.addTurn(update.usage)`).
72
- * See the module header for why this is typed locally.
73
- */
74
- export interface TurnEndedUsageDelta {
75
- readonly type: "turn-ended";
76
- readonly usage: {
77
- readonly inputTokens?: number;
78
- readonly outputTokens?: number;
79
- readonly cacheWriteTokens?: number;
80
- readonly cacheReadTokens?: number;
81
- };
82
- }
83
-
84
68
  /** What an `effect` step can reach: the run it is part of. */
85
69
  export interface EffectContext {
86
70
  readonly run: ScriptedRun;
@@ -89,7 +73,7 @@ export interface EffectContext {
89
73
 
90
74
  export type ScriptStep =
91
75
  | { readonly kind: "event"; readonly event: SDKMessage }
92
- | { readonly kind: "delta"; readonly update: TurnEndedUsageDelta }
76
+ | { readonly kind: "delta"; readonly update: TurnEndedUpdate }
93
77
  | { readonly kind: "effect"; readonly label: string; readonly run: (ctx: EffectContext) => void | Promise<void> }
94
78
  | { readonly kind: "result"; readonly result: Omit<RunResult, "id"> };
95
79
 
@@ -101,7 +85,8 @@ export const step = {
101
85
  event(event: SDKMessage): ScriptStep {
102
86
  return { kind: "event", event };
103
87
  },
104
- turnEnded(usage: TurnEndedUsageDelta["usage"]): ScriptStep {
88
+ /** The `turn-ended` delta `turn-stream.ts` prices; the SDK's shape, all four counts stated. */
89
+ turnEnded(usage: NonNullable<TurnEndedUpdate["usage"]>): ScriptStep {
105
90
  return { kind: "delta", update: { type: "turn-ended", usage } };
106
91
  },
107
92
  effect(label: string, run: (ctx: EffectContext) => void | Promise<void>): ScriptStep {
@@ -224,7 +209,7 @@ export class ScriptedRun implements Run {
224
209
  yield s.event;
225
210
  break;
226
211
  case "delta":
227
- await this.onDelta?.({ update: s.update as never });
212
+ await this.onDelta?.({ update: s.update });
228
213
  break;
229
214
  case "effect":
230
215
  await s.run({ run: this, agent: this.agent });
@@ -365,4 +350,11 @@ export class ScriptedCursorAgent implements SDKAgent {
365
350
  async downloadArtifact(_path: string): Promise<Buffer> {
366
351
  throw new Error("ScriptedCursorAgent: artifacts are not part of the scripted surface");
367
352
  }
353
+
354
+ async getUsage(_options?: GetUsageOptions): Promise<AgentUsage> {
355
+ // The harness prices the `turn-ended` delta itself (`usage-pricing.ts`) and
356
+ // never asks the SDK for billed usage; a call here is a test bug, like a
357
+ // `send()` with no script.
358
+ throw new Error("ScriptedCursorAgent: getUsage() is not part of the scripted surface");
359
+ }
368
360
  }
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * The `@cursor/sdk` module a hermetic `ExecuteCursor` run imports — every
3
- * runtime surface the activity path touches, backed by scripted agents.
3
+ * runtime surface the activity path touches, backed by scripted agents — and
4
+ * the `@cursor/sdk/sqlite` module beside it, backed by a store that holds
5
+ * nothing.
4
6
  *
5
- * The activity reaches the SDK at three runtime sites and nowhere else:
7
+ * The activity reaches the SDK at four runtime sites and nowhere else:
6
8
  *
7
- * - `session-lifecycle.ts`: `Agent.create(options)`, `Agent.resume(id, options)`,
8
- * `Agent.archive(id, ...)` — the agent handle.
9
+ * - `session-lifecycle.ts`: `Agent.create(options)`, `Agent.resume(id, options)`
10
+ * — the agent handle.
11
+ * - `session-store.ts`: `SqliteLocalAgentStore.open({ workspaceRef, stateRoot })`
12
+ * from `@cursor/sdk/sqlite` — the session's store the handle is created and
13
+ * resumed over (`local.store`).
9
14
  * - `service-tier.ts`: `Cursor.models.list({ apiKey })` — the catalog the
10
15
  * variant params (`fast`, `thinking`) are pinned from.
11
16
  * - `index.ts` outer catch: `import("@cursor/sdk")` for `CursorSdkError`, the
@@ -13,35 +18,42 @@
13
18
  *
14
19
  * Neither the SDK nor the client is injectable into the activity (the real
15
20
  * seam arrives with the harness contract; parent Q1/Q2), so — exactly as the
16
- * native activity tests already do for their boundary modules — the module is
17
- * substituted with `vi.mock`. Three existing tests each mock ONE of these
21
+ * native activity tests already do for their boundary modules — the modules
22
+ * are substituted with `vi.mock`. Three existing tests each mock ONE of these
18
23
  * surfaces (`session-lifecycle.test.ts`, `service-tier.test.ts`,
19
- * `sdk-warmup.test.ts`); a whole-activity run needs all three at once, which is
20
- * what this module is.
24
+ * `sdk-warmup.test.ts`); a whole-activity run needs all of them at once, which
25
+ * is what this module is.
21
26
  *
22
- * `vi.mock` is hoisted and must appear in the test file; the factory can
23
- * `await import()` this module and return {@link scriptedCursorSdkModule}. The
24
- * module's statics read the {@link ScriptedCursorSdk} bound for the current
27
+ * `vi.mock` is hoisted and must appear in the test file, once per module id
28
+ * (a subpath is its own module id, so `@cursor/sdk/sqlite` needs its own
29
+ * `vi.mock`); the factories `await import()` this module and return
30
+ * {@link scriptedCursorSdkModule} and {@link scriptedCursorSqliteModule}. The
31
+ * modules' statics read the {@link ScriptedCursorSdk} bound for the current
25
32
  * scenario ({@link bindScriptedSdk}), so one test file can run several
26
- * scenarios with different agents against the same mocked module.
33
+ * scenarios with different agents against the same mocked modules.
27
34
  *
28
35
  * Resolution rules a scenario declares, mirroring what the real SDK does:
29
36
  * - `Agent.create` hands out the agent declared for the session it is asked
30
- * for (`agentForSession`, when the scenario resolves agents per session —
31
- * the harness contract kit runs several sessions concurrently on one
32
- * adapter, and the order their creates arrive in is real I/O timing), else
33
- * the NEXT unclaimed agent in `agents` (turn 1 of a fresh session; the fresh
34
- * agent of a poisoned-handle recovery) — unless the scenario declared a
35
- * `createFailure`, which the FIRST create throws instead (the SDK refusing
37
+ * for (`agentForWorkspaceRef`, when the scenario resolves agents per
38
+ * session — the harness contract kit runs several sessions concurrently on
39
+ * one adapter, and the order their creates arrive in is real I/O timing),
40
+ * else the NEXT unclaimed agent in `agents` (turn 1 of a fresh session; the
41
+ * fresh agent of a poisoned-handle recovery) — unless the scenario declared
42
+ * a `createFailure`, which the FIRST create throws instead (the SDK refusing
36
43
  * to mint an agent: a 401 on the key, a validation error).
37
44
  * - `Agent.resume(id)` hands back the agent with that id if the scenario
38
45
  * declared it (in `agents` or `resumableAgents`, or handed out for a
39
46
  * session), else throws — `resolveAgent` then falls back to `create`, which
40
47
  * is the production shape of a lost handle.
41
48
  * - `Cursor.models.list` answers the scenario's catalog.
49
+ * - `SqliteLocalAgentStore.open` hands out a {@link ScriptedLocalAgentStore}
50
+ * carrying the ref and root it was asked for, and records the open; its
51
+ * `dispose()` is recorded too, so a scenario can assert the adapter's
52
+ * session-store lifetime (`releaseSession`, `shutdown`).
42
53
  */
43
54
 
44
55
  import type { AgentOptions, ModelListItem, SDKAgent } from "@cursor/sdk";
56
+ import type { SqliteLocalAgentStore, SqliteLocalAgentStoreOptions } from "@cursor/sdk/sqlite";
45
57
  import type { ScriptedCursorAgent } from "./scripted-agent.js";
46
58
 
47
59
  export interface ScriptedSdkOptions {
@@ -50,12 +62,12 @@ export interface ScriptedSdkOptions {
50
62
  /**
51
63
  * The agent `Agent.create` hands out for the session the create names,
52
64
  * consulted before the ordered list. The SDK's create options carry the
53
- * session as `platform.workspaceRef` (`session-lifecycle.ts`
54
- * `resolvePlatformOptions`: `stigmer-session:<sessionId>`), so the resolver
55
- * is keyed by that ref — a scenario computes the ref the same way the
56
- * adapter does. `undefined` falls through to the list. Consulted live on
57
- * every create, so a scenario may mint the session's agent only when it
58
- * learns of the session.
65
+ * session as the store's `workspaceRef` (`local.store`, opened by
66
+ * `session-store.ts`: `stigmer-session:<sessionId>`), so the resolver is
67
+ * keyed by that ref — a scenario computes the ref the same way the adapter
68
+ * does (`resolveSessionStoreLocation`). `undefined` falls through to the
69
+ * list. Consulted live on every create, so a scenario may mint the session's
70
+ * agent only when it learns of the session.
59
71
  */
60
72
  readonly agentForWorkspaceRef?: (workspaceRef: string) => ScriptedCursorAgent | undefined;
61
73
  /**
@@ -82,21 +94,46 @@ export interface RecordedResolution {
82
94
  }
83
95
 
84
96
  /**
85
- * The `platform.workspaceRef` a create names, read defensively: the platform
86
- * option's type re-exports from the unshipped `@anysphere/cursor-sdk-local-runtime`
87
- * and resolves to `any` under `skipLibCheck` (the same gap `scripted-agent.ts`
88
- * records for the delta channel), so the shape is checked here, not assumed.
97
+ * The session a create names: the `workspaceRef` of the store it passes as
98
+ * `local.store`. In a hermetic run that store is the one this module's sqlite
99
+ * double opened; anything else (a test that mocks `@cursor/sdk` with its own
100
+ * stub and passes no store) names no session and falls through to the list.
89
101
  */
90
102
  function workspaceRefOf(options: AgentOptions): string | undefined {
91
- const platform: unknown = (options as { platform?: unknown }).platform;
92
- if (typeof platform !== "object" || platform === null) return undefined;
93
- const ref = (platform as { workspaceRef?: unknown }).workspaceRef;
94
- return typeof ref === "string" ? ref : undefined;
103
+ const store = options.local?.store;
104
+ return store instanceof ScriptedLocalAgentStore ? store.workspaceRef : undefined;
105
+ }
106
+
107
+ /**
108
+ * The `@cursor/sdk/sqlite` store the adapter opens per session, holding
109
+ * nothing: the scripted agents carry their own state, so the store only has
110
+ * to be the thing `Agent.create` / `Agent.resume` are handed and the thing
111
+ * `releaseSession` / `shutdown` dispose. `open` and `dispose` are recorded on
112
+ * the bound {@link ScriptedCursorSdk} for lifetime assertions.
113
+ */
114
+ export class ScriptedLocalAgentStore {
115
+ disposeCalls = 0;
116
+
117
+ constructor(
118
+ readonly workspaceRef: string,
119
+ readonly stateRoot: string,
120
+ ) {}
121
+
122
+ static async open(options: SqliteLocalAgentStoreOptions): Promise<ScriptedLocalAgentStore> {
123
+ const store = new ScriptedLocalAgentStore(options.workspaceRef, options.stateRoot ?? "");
124
+ current().stores.push(store);
125
+ return store;
126
+ }
127
+
128
+ async dispose(): Promise<void> {
129
+ this.disposeCalls++;
130
+ }
95
131
  }
96
132
 
97
133
  export class ScriptedCursorSdk {
98
134
  readonly resolutions: RecordedResolution[] = [];
99
- readonly archived: string[] = [];
135
+ /** Every store `SqliteLocalAgentStore.open` handed out, in order. */
136
+ readonly stores: ScriptedLocalAgentStore[] = [];
100
137
  private nextCreate = 0;
101
138
  private createFailurePending: boolean;
102
139
  private readonly byId = new Map<string, ScriptedCursorAgent>();
@@ -145,10 +182,6 @@ export class ScriptedCursorSdk {
145
182
  return agent;
146
183
  }
147
184
 
148
- archive(agentId: string): void {
149
- this.archived.push(agentId);
150
- }
151
-
152
185
  listModels(): readonly ModelListItem[] {
153
186
  return this.options.catalog;
154
187
  }
@@ -242,7 +275,6 @@ export function scriptedCursorSdkModule(): Record<string, unknown> {
242
275
  create: async (options: AgentOptions): Promise<SDKAgent> => current().create(options),
243
276
  resume: async (agentId: string, options?: Partial<AgentOptions>): Promise<SDKAgent> =>
244
277
  current().resume(agentId, options),
245
- archive: async (agentId: string): Promise<void> => current().archive(agentId),
246
278
  },
247
279
  Cursor: {
248
280
  models: {
@@ -252,3 +284,27 @@ export function scriptedCursorSdkModule(): Record<string, unknown> {
252
284
  CursorSdkError: ScriptedCursorSdkError,
253
285
  };
254
286
  }
287
+
288
+ /**
289
+ * The factory a test passes to `vi.mock("@cursor/sdk/sqlite", ...)`, beside
290
+ * the one above:
291
+ *
292
+ * ```ts
293
+ * vi.mock("@cursor/sdk/sqlite", async () =>
294
+ * (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSqliteModule(),
295
+ * );
296
+ * ```
297
+ *
298
+ * The one export the runner reads from that entry (`session-store.ts`). The
299
+ * class is exported under the SDK's name so `session-store.ts`'s dynamic
300
+ * import destructures it unchanged; its type is the SDK's, which is why
301
+ * {@link ScriptedLocalAgentStore} carries `workspaceRef`, `stateRoot` and
302
+ * `dispose()` — the members the runner touches — and nothing else.
303
+ */
304
+ export function scriptedCursorSqliteModule(): Record<string, unknown> {
305
+ return {
306
+ SqliteLocalAgentStore: ScriptedLocalAgentStore satisfies {
307
+ open(options: SqliteLocalAgentStoreOptions): Promise<Pick<SqliteLocalAgentStore, "workspaceRef" | "stateRoot" | "dispose">>;
308
+ },
309
+ };
310
+ }
@@ -41,14 +41,15 @@ describeWithCursorKey("SDK routing with CURSOR_BACKEND_URL unset", () => {
41
41
  mkdirSync(stateRoot, { recursive: true });
42
42
 
43
43
  const { Agent } = await import("@cursor/sdk");
44
+ const { SqliteLocalAgentStore } = await import("@cursor/sdk/sqlite");
44
45
 
45
46
  const agent = await Agent.create({
46
47
  apiKey: CURSOR_API_KEY,
47
48
  model: { id: "claude-sonnet-4" },
48
- local: { cwd: stateRoot },
49
- platform: {
50
- workspaceRef: `routing-test-${Date.now()}`,
51
- stateRoot,
49
+ local: {
50
+ cwd: stateRoot,
51
+ // 1.0.31: the store is caller-owned (`session-store.ts` in production).
52
+ store: await SqliteLocalAgentStore.open({ workspaceRef: `routing-test-${Date.now()}`, stateRoot }),
52
53
  },
53
54
  });
54
55
 
@@ -62,14 +63,15 @@ describeWithCursorKey("SDK routing with CURSOR_BACKEND_URL unset", () => {
62
63
  mkdirSync(stateRoot, { recursive: true });
63
64
 
64
65
  const { Agent } = await import("@cursor/sdk");
66
+ const { SqliteLocalAgentStore } = await import("@cursor/sdk/sqlite");
65
67
 
66
68
  const agent = await Agent.create({
67
69
  apiKey: CURSOR_API_KEY,
68
70
  model: { id: "claude-sonnet-4" },
69
- local: { cwd: stateRoot },
70
- platform: {
71
- workspaceRef: `routing-send-test-${Date.now()}`,
72
- stateRoot,
71
+ local: {
72
+ cwd: stateRoot,
73
+ // 1.0.31: the store is caller-owned (`session-store.ts` in production).
74
+ store: await SqliteLocalAgentStore.open({ workspaceRef: `routing-send-test-${Date.now()}`, stateRoot }),
73
75
  },
74
76
  });
75
77
 
@@ -41,16 +41,17 @@ describeWithCursorKey("Fetch Interceptor vs Connect-Node Transport", () => {
41
41
 
42
42
  it("Agent.create() does NOT go through globalThis.fetch (proving interceptor is useless)", async () => {
43
43
  const { Agent } = await import("@cursor/sdk");
44
+ const { SqliteLocalAgentStore } = await import("@cursor/sdk/sqlite");
44
45
 
45
46
  const stateRoot = join(tmpdir(), `cursor-intercept-test-${Date.now()}`);
46
47
  mkdirSync(stateRoot, { recursive: true });
47
48
 
48
49
  const agent = await Agent.create({
49
50
  apiKey: CURSOR_API_KEY,
50
- local: { cwd: stateRoot },
51
- platform: {
52
- workspaceRef: `intercept-test-${Date.now()}`,
53
- stateRoot,
51
+ local: {
52
+ cwd: stateRoot,
53
+ // 1.0.31: the store is caller-owned (`session-store.ts` in production).
54
+ store: await SqliteLocalAgentStore.open({ workspaceRef: `intercept-test-${Date.now()}`, stateRoot }),
54
55
  },
55
56
  });
56
57
 
@@ -35,6 +35,7 @@
35
35
  */
36
36
 
37
37
  import { describe, it, expect } from "vitest";
38
+ import { SqliteLocalAgentStore } from "@cursor/sdk/sqlite";
38
39
  import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, readdirSync } from "node:fs";
39
40
  import { tmpdir } from "node:os";
40
41
  import { join } from "node:path";
@@ -87,8 +88,12 @@ describeWithCursorKey("generateImage live ground truth (issue #965)", () => {
87
88
  const agent = await Agent.create({
88
89
  apiKey: CURSOR_API_KEY,
89
90
  model: { id: "composer-2.5" },
90
- local: { cwd: workspaceRoot, settingSources: ["project"] },
91
- platform: { workspaceRef: `genimage-spike-${Date.now()}`, stateRoot },
91
+ local: {
92
+ cwd: workspaceRoot,
93
+ settingSources: ["project"],
94
+ // 1.0.31: the store is caller-owned (`session-store.ts` in production).
95
+ store: await SqliteLocalAgentStore.open({ workspaceRef: `genimage-spike-${Date.now()}`, stateRoot }),
96
+ },
92
97
  });
93
98
 
94
99
  // The runner's exact consumption shape (index.ts / turn-stream.ts):