@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
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Session-keyed Cursor SDK store — the on-disk SQLite store a session's
3
+ * agent records, runs and checkpoints live in, opened once per session and
4
+ * handed to every `Agent.create` / `Agent.resume` of that session
5
+ * (stigmer/stigmer#1053).
6
+ *
7
+ * Why this exists: `@cursor/sdk` 1.0.31 removed the `platform` option the
8
+ * runner used to key the SDK's store (`platform.workspaceRef` /
9
+ * `platform.stateRoot`) and replaced it with a caller-owned
10
+ * `local.store: LocalAgentStore`. The SDK's own instruction is "open once per
11
+ * workspace / state root and reuse across `Agent.create` / `resume`". So the
12
+ * store has a lifetime the runner owns, and this module is where it lives.
13
+ *
14
+ * Where the store lives on disk is unchanged from the `platform` era:
15
+ * `{workspaceRootDir}/.stigmer/cursor-sdk-state/{sessionId}` — the durable
16
+ * workspace volume rather than `$HOME`, so native `Agent.resume()` survives
17
+ * pod restart, reschedule and snapshot restore; keyed by sessionId so sessions
18
+ * sharing one volume (a workflow sandbox's child agent executions) never
19
+ * collide. The `workspaceRef` is the same synthetic `stigmer-session:<id>`,
20
+ * deliberately not a filesystem path, so lookups are stable regardless of
21
+ * `process.cwd()` (in a cloud sandbox that is the runner's app directory, not
22
+ * the workspace).
23
+ *
24
+ * Lifetime — the session's, on the two hooks the adapter already has:
25
+ * - `sessionStore()` opens on first use and memoizes the OPEN PROMISE per
26
+ * session, so two activities racing on one session (the agent cache's
27
+ * documented race: the loser resolves its own agent) await the same open
28
+ * instead of racing two opens on one file. A failed open is not memoized;
29
+ * the next call retries.
30
+ * - `releaseSessionStore(sessionId)` is called from the adapter's
31
+ * `releaseSession` — the runtime saying the session is done on this host.
32
+ * - `releaseAllSessionStores()` is called from the adapter's `shutdown`.
33
+ * - A re-acquire under a DIFFERENT state root (the session moved volumes)
34
+ * releases the store it held and opens the new one — the same rule the agent
35
+ * cache applies to a fingerprint mismatch: a handle for the old binding can
36
+ * never serve the new one, and holding it would only pin a file.
37
+ * Nothing else disposes a store. In particular the store does NOT ride the
38
+ * agent handle's `close()` — the handle has five close sites and a second
39
+ * resource on each would be a parallel lifecycle — and it does not enter the
40
+ * agent cache, whose exclusive-checkout model is about one handle per turn.
41
+ *
42
+ * At 1.0.13 the SDK memoized its platform (with open SQLite handles) for the
43
+ * whole process; a per-session store released when the session is released is
44
+ * a tighter lifetime, not a looser one.
45
+ *
46
+ * `SessionStores` is the mechanism with the opener injected, so its rules are
47
+ * unit-tested without a SQLite file; the module-level instance binds the SDK's
48
+ * `SqliteLocalAgentStore.open` (`@cursor/sdk/sqlite`, the subpath entry that
49
+ * keeps the sqlite driver off the main package's import path). In a hermetic
50
+ * run that subpath is doubled beside `@cursor/sdk` itself (`scripted-sdk.ts`).
51
+ *
52
+ * THIS MODULE'S STATIC GRAPH IS SDK-FREE, like `agent-session-cache.ts`: the
53
+ * adapter imports both statically, and the adapter's own static graph must
54
+ * stay SDK-free so the composition roots can import it before `boot` installs
55
+ * the interceptors (`adapter.ts` header). The SDK's sqlite entry is loaded
56
+ * with a dynamic import at the first open — the runner's
57
+ * dynamic-import-for-order convention — and only its TYPE is imported here.
58
+ */
59
+
60
+ import { mkdirSync } from "node:fs";
61
+ import { join } from "node:path";
62
+ import type { SqliteLocalAgentStore } from "@cursor/sdk/sqlite";
63
+
64
+ const CURSOR_SDK_STATE_DIR = ".stigmer/cursor-sdk-state";
65
+
66
+ /** Where a session's store lives and the ref the SDK scopes it under. */
67
+ export interface SessionStoreLocation {
68
+ /** Synthetic, stable across activity invocations: `stigmer-session:<sessionId>`. */
69
+ readonly workspaceRef: string;
70
+ /** `{workspaceRootDir}/.stigmer/cursor-sdk-state/{sessionId}`, created eagerly. */
71
+ readonly stateRoot: string;
72
+ }
73
+
74
+ /** The slice of the SDK store whose lifetime this module owns. */
75
+ export interface ReleasableStore {
76
+ readonly stateRoot: string;
77
+ dispose(): Promise<void>;
78
+ }
79
+
80
+ /**
81
+ * Compute a session's store location. Both inputs are required and must be
82
+ * non-empty: the state root is keyed by sessionId, so an empty sessionId would
83
+ * collapse every session sharing the volume onto one store and corrupt their
84
+ * conversation state; an empty root would put the store somewhere that does
85
+ * not survive a restart. The directory is created eagerly to prevent ENOENT on
86
+ * the SDK's first write.
87
+ */
88
+ export function resolveSessionStoreLocation(sessionId: string, workspaceRootDir: string): SessionStoreLocation {
89
+ if (!sessionId) {
90
+ throw new Error(
91
+ "resolveSessionStoreLocation: sessionId is required but was empty. The Cursor SDK " +
92
+ "state store is keyed by sessionId; an empty value would collide across sessions " +
93
+ "sharing a workspace volume (e.g. a workflow sandbox's child agent executions).",
94
+ );
95
+ }
96
+ if (!workspaceRootDir) {
97
+ throw new Error(
98
+ "resolveSessionStoreLocation: workspaceRootDir is required but was empty. The Cursor " +
99
+ "SDK state store must live on the durable workspace volume to survive restarts.",
100
+ );
101
+ }
102
+ const stateRoot = join(workspaceRootDir, CURSOR_SDK_STATE_DIR, sessionId);
103
+ mkdirSync(stateRoot, { recursive: true });
104
+ return { workspaceRef: `stigmer-session:${sessionId}`, stateRoot };
105
+ }
106
+
107
+ /** One store per session, opened through `open`, held until released. */
108
+ export class SessionStores<S extends ReleasableStore> {
109
+ private readonly held = new Map<string, { readonly stateRoot: string; readonly store: Promise<S> }>();
110
+
111
+ constructor(private readonly open: (location: SessionStoreLocation) => Promise<S>) {}
112
+
113
+ /** The session's store, opened on first use; the same instance on every later call. */
114
+ acquire(sessionId: string, workspaceRootDir: string): Promise<S> {
115
+ const location = resolveSessionStoreLocation(sessionId, workspaceRootDir);
116
+ const existing = this.held.get(sessionId);
117
+ if (existing && existing.stateRoot === location.stateRoot) return existing.store;
118
+ if (existing) {
119
+ console.log(
120
+ `session-store: session=${sessionId} re-acquired under a different state root ` +
121
+ `(held=${existing.stateRoot}, requested=${location.stateRoot}) — releasing the held store`,
122
+ );
123
+ void this.release(sessionId);
124
+ }
125
+ const store = this.open(location);
126
+ this.held.set(sessionId, { stateRoot: location.stateRoot, store });
127
+ // A failed open must not be memoized: the next acquire retries. Only the
128
+ // entry THIS open created is removed (a later re-acquire may have replaced it).
129
+ store.catch(() => {
130
+ if (this.held.get(sessionId)?.store === store) this.held.delete(sessionId);
131
+ });
132
+ return store;
133
+ }
134
+
135
+ /** Disposes and forgets the session's store, if any. A later acquire reopens. */
136
+ async release(sessionId: string): Promise<void> {
137
+ const entry = this.held.get(sessionId);
138
+ if (!entry) return;
139
+ this.held.delete(sessionId);
140
+ await disposeQuietly(sessionId, entry.store);
141
+ }
142
+
143
+ /** Worker shutdown: dispose every held store. */
144
+ async releaseAll(): Promise<void> {
145
+ const entries = [...this.held.entries()];
146
+ this.held.clear();
147
+ await Promise.all(entries.map(([sessionId, entry]) => disposeQuietly(sessionId, entry.store)));
148
+ }
149
+
150
+ /** How many stores are held — for assertions. */
151
+ get size(): number {
152
+ return this.held.size;
153
+ }
154
+ }
155
+
156
+ async function disposeQuietly<S extends ReleasableStore>(sessionId: string, store: Promise<S>): Promise<void> {
157
+ try {
158
+ await (await store).dispose();
159
+ } catch (err) {
160
+ // Best effort: a store whose open failed has nothing to dispose, and a
161
+ // dispose that fails on a dead handle changes nothing the session needs.
162
+ console.warn(
163
+ `session-store: dispose failed for session=${sessionId}:`,
164
+ err instanceof Error ? err.message : err,
165
+ );
166
+ }
167
+ }
168
+
169
+ const sessionStores = new SessionStores<SqliteLocalAgentStore>(async (location) => {
170
+ const { SqliteLocalAgentStore } = await import("@cursor/sdk/sqlite");
171
+ return SqliteLocalAgentStore.open(location);
172
+ });
173
+
174
+ /** The session's SDK store, opened on first use — pass as `local.store` on create AND resume. */
175
+ export function sessionStore(sessionId: string, workspaceRootDir: string): Promise<SqliteLocalAgentStore> {
176
+ return sessionStores.acquire(sessionId, workspaceRootDir);
177
+ }
178
+
179
+ /** The session is done on this host (the adapter's `releaseSession`). */
180
+ export function releaseSessionStore(sessionId: string): Promise<void> {
181
+ return sessionStores.release(sessionId);
182
+ }
183
+
184
+ /** Worker shutdown (the adapter's `shutdown`). */
185
+ export function releaseAllSessionStores(): Promise<void> {
186
+ return sessionStores.releaseAll();
187
+ }
@@ -118,7 +118,8 @@ export type VisionOutcome =
118
118
  /**
119
119
  * What a harness can actually display inline. The split exists because the
120
120
  * Cursor local transport re-sniffs magic bytes and recognizes ONLY PNG and
121
- * JPEG (verified against @cursor/sdk 1.0.13 dist the declared mimeType is
121
+ * JPEG (verified against the @cursor/sdk dist at 1.0.13 and again at 1.0.31:
122
+ * the attachment sniff map is exactly JPEG and PNG; the declared mimeType is
122
123
  * discarded), while the LangChain providers accept all four types.
123
124
  */
124
125
  export interface VisionProfile {