@evomap/evolver-runtime-adapters 2.0.0-beta.2 → 2.0.0-beta.22

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.
@@ -6,5 +6,11 @@ export declare const geminiAdapter: SessionLogAdapter;
6
6
  export declare const antigravityAdapter: SessionLogAdapter;
7
7
  export declare const genericChatAdapter: SessionLogAdapter;
8
8
  export declare const kimiAdapter: SessionLogAdapter;
9
+ export declare const kiroRemovedAdapter: SessionLogAdapter;
10
+ export declare const opencodeRemovedAdapter: SessionLogAdapter;
11
+ /** Explicitly removed transcript adapters — path fail-closed only. Never walk this list for content probes. */
12
+ export declare const REMOVED_ADAPTERS: readonly SessionLogAdapter[];
9
13
  export declare const ADAPTERS: readonly SessionLogAdapter[];
10
- export declare function adapterForPath(path: string): SessionLogAdapter | undefined;
14
+ export declare function adapterForPath(path: string): SessionLogAdapter | undefined;
15
+ /** True when the agent id has an explicit removed transcript sentinel (see REMOVED_ADAPTERS). */
16
+ export declare function isRemovedAdapter(agent: string): boolean;
package/dist/adapters.js CHANGED
@@ -291,26 +291,71 @@ function anthropicSessionSystemPrompt(chunk) {
291
291
  }
292
292
  return undefined;
293
293
  }
294
- function anthropicStyleSession(chunk) {
294
+ const NATIVE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
295
+ const CLAUDE_TRANSCRIPT_PATH = /(?:^|[/\\])\.claude[/\\]projects[/\\](?:[^/\\]+[/\\])*([A-Za-z0-9][A-Za-z0-9._-]{0,127})\.jsonl$/;
296
+ const CURSOR_TRANSCRIPT_PATHS = [
297
+ /(?:^|[/\\])\.cursor[/\\]projects[/\\](?:[^/\\]+[/\\])*agent-transcripts[/\\]([A-Za-z0-9][A-Za-z0-9._-]{0,127})\.jsonl$/,
298
+ /(?:^|[/\\])\.cursor[/\\]projects[/\\](?:[^/\\]+[/\\])*agent-transcripts[/\\]([A-Za-z0-9][A-Za-z0-9._-]{0,127})[/\\]\1\.jsonl$/,
299
+ ];
300
+ function transcriptSessionId(chunk, keys) {
301
+ const values = new Set();
302
+ for (const row of parseJsonlLines(chunk)) {
303
+ for (const key of keys) {
304
+ if (!hasOwn(row, key))
305
+ continue;
306
+ const value = row[key];
307
+ if (typeof value !== 'string' || !NATIVE_SESSION_ID.test(value))
308
+ return { invalid: true };
309
+ values.add(value);
310
+ if (values.size > 1)
311
+ return { invalid: true };
312
+ }
313
+ }
314
+ return { value: values.values().next().value, invalid: false };
315
+ }
316
+ function nativeResumeIdentity(harness, path, chunk, pathPattern, recordKeys) {
317
+ const pathId = pathPattern.exec(path)?.[1];
318
+ if (!pathId || !NATIVE_SESSION_ID.test(pathId))
319
+ return undefined;
320
+ const recordId = transcriptSessionId(chunk, recordKeys);
321
+ if (recordId.invalid || (recordId.value !== undefined && recordId.value !== pathId))
322
+ return undefined;
323
+ return { harness, sessionId: pathId };
324
+ }
325
+ function cursorResumeIdentity(path, chunk) {
326
+ for (const pattern of CURSOR_TRANSCRIPT_PATHS) {
327
+ const identity = nativeResumeIdentity('cursor', path, chunk, pattern, ['sessionId', 'session_id', 'conversationId', 'conversation_id']);
328
+ if (identity)
329
+ return identity;
330
+ }
331
+ return undefined;
332
+ }
333
+ function anthropicStyleSession(chunk, sessionIdKeys = []) {
295
334
  const systemPrompt = anthropicSessionSystemPrompt(chunk);
335
+ const sessionId = transcriptSessionId(chunk, sessionIdKeys);
296
336
  return {
297
337
  turns: anthropicStyleTranscript(chunk),
298
338
  ...(systemPrompt ? { systemPrompt } : {}),
339
+ ...(!sessionId.invalid && sessionId.value ? { sessionId: sessionId.value } : {}),
299
340
  };
300
341
  }
301
342
  export const claudeCodeAdapter = {
302
343
  agent: 'claude-code',
303
344
  detect: (p) => /\.claude[/\\]projects[/\\].*\.jsonl$/.test(p) || /claude.*\.jsonl$/i.test(p),
345
+ resumeIdentityFromSource: (path, chunk) => nativeResumeIdentity('claude-code', path, chunk, CLAUDE_TRANSCRIPT_PATH, ['sessionId', 'session_id']),
304
346
  parse: anthropicStyleTranscript,
305
- parseSession: anthropicStyleSession,
347
+ parseSession: (chunk) => anthropicStyleSession(chunk, ['sessionId', 'session_id']),
306
348
  };
307
- // Verified against real cursor agent-transcripts (~/.cursor/projects/<proj>/agent-transcripts/<uuid>.jsonl):
349
+ // Verified against real cursor agent-transcripts
350
+ // (~/.cursor/projects/<proj>/agent-transcripts/<session-id>.jsonl, with newer nested layouts also accepted):
308
351
  // same Anthropic content-block shape as claude-code — observed blocks are text + tool_use (no tool_result). Other
309
352
  // .jsonl that live under .cursor (eval datasets: task_id/canonical_solution, no role) carry no turn and parse to [].
310
353
  export const cursorAdapter = {
311
354
  agent: 'cursor',
312
355
  detect: (p) => /\.cursor[/\\].*\.jsonl$/.test(p) || /cursor.*\.jsonl$/i.test(p),
356
+ resumeIdentityFromSource: cursorResumeIdentity,
313
357
  parse: anthropicStyleTranscript,
358
+ parseSession: (chunk) => anthropicStyleSession(chunk, ['sessionId', 'session_id', 'conversationId', 'conversation_id']),
314
359
  };
315
360
  // Verified against codex-cli 0.137.0 rollout logs (~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl). Each line is
316
361
  // {timestamp, type, payload}. Codex writes the SAME conversation twice: a high-level `event_msg` stream
@@ -1217,7 +1262,42 @@ export const kimiAdapter = {
1217
1262
  parseSession: (chunk) => kimiSessions(chunk)[0] ?? { turns: [] },
1218
1263
  parseSessions: kimiSessions,
1219
1264
  };
1220
- // Only verified adapters are registered. opencode/kiro live in git history — re-add with a real-log fixture.
1265
+ // ── Removed runtime adapters ─────────────────────────────────────────────────
1266
+ // Kiro and OpenCode transcript adapters were removed in V2: no sanitized real-log golden fixture exists for
1267
+ // either runtime, so shipping a guessed schema would silently yield 0 turns on real logs while appearing
1268
+ // supported. The MCP config installer (setup-hooks --runtime=kiro|opencode) remains functional for injection.
1269
+ //
1270
+ // Sentinels live in REMOVED_ADAPTERS (NOT in ADAPTERS) so content probes that walk ADAPTERS and call
1271
+ // parse() cannot throw "kiro removed" on unrelated JSON (trajectoryExport.adapterForContent).
1272
+ // Path-based selection uses adapterForPath, which checks REMOVED_ADAPTERS first; matching paths then fail
1273
+ // closed on parse with an actionable error.
1274
+ const REMOVED_ADAPTER_MESSAGE = (agent) => `${agent} transcript adapter was removed from evolver v2 — no sanitized real-log golden fixture exists. `
1275
+ + `Re-add with a verified golden test (see adapters.test.ts). `
1276
+ + `MCP injection (setup-hooks --runtime=${agent}) is still supported; only transcript ingest is blocked.`;
1277
+ function removedAdapter(agent, detect) {
1278
+ return {
1279
+ agent,
1280
+ detect,
1281
+ parse: () => { throw new Error(REMOVED_ADAPTER_MESSAGE(agent)); },
1282
+ parseSession: () => { throw new Error(REMOVED_ADAPTER_MESSAGE(agent)); },
1283
+ parseSessions: () => { throw new Error(REMOVED_ADAPTER_MESSAGE(agent)); },
1284
+ };
1285
+ }
1286
+ export const kiroRemovedAdapter = removedAdapter('kiro', (p) => /(^|[/\\])\.kiro([/\\]|$)/i.test(p) || /(^|[/\\])kiro[^/\\]*\.jsonl?$/i.test(p));
1287
+ export const opencodeRemovedAdapter = removedAdapter('opencode', (p) => /(^|[/\\])\.opencode([/\\]|$)/i.test(p) || /(^|[/\\])opencode[^/\\]*\.jsonl?$/i.test(p));
1288
+ /** Explicitly removed transcript adapters — path fail-closed only. Never walk this list for content probes. */
1289
+ export const REMOVED_ADAPTERS = [kiroRemovedAdapter, opencodeRemovedAdapter];
1290
+ // Only verified adapters are registered for content/path discovery. Removed runtimes are listed in
1291
+ // REMOVED_ADAPTERS and selected only via adapterForPath path match (then parse throws).
1221
1292
  // genericChatAdapter is LAST so any tool-specific path (claude/cursor/codex/gemini/antigravity/kimi) resolves first.
1222
1293
  export const ADAPTERS = [claudeCodeAdapter, codexAdapter, cursorAdapter, geminiAdapter, antigravityAdapter, kimiAdapter, genericChatAdapter];
1223
- export function adapterForPath(path) { return ADAPTERS.find((a) => a.detect(path)); }
1294
+ export function adapterForPath(path) {
1295
+ const removed = REMOVED_ADAPTERS.find((a) => a.detect(path));
1296
+ if (removed)
1297
+ return removed;
1298
+ return ADAPTERS.find((a) => a.detect(path));
1299
+ }
1300
+ /** True when the agent id has an explicit removed transcript sentinel (see REMOVED_ADAPTERS). */
1301
+ export function isRemovedAdapter(agent) {
1302
+ return REMOVED_ADAPTERS.some((a) => a.agent === agent);
1303
+ }
@@ -1,8 +1,13 @@
1
1
  import type { NormalizedSession } from './types.js';
2
+ export type CursorStateVscdbErrorStage = 'open' | 'schema' | 'query';
3
+ export declare class CursorStateVscdbError extends Error {
4
+ readonly stage: CursorStateVscdbErrorStage;
5
+ constructor(stage: CursorStateVscdbErrorStage, message: string, cause?: unknown);
6
+ }
2
7
  /**
3
8
  * Read Cursor chat sessions out of a `state.vscdb` sqlite database (read-only). Returns one NormalizedSession per
4
- * composer that has at least one real (non-meta) turn. Never throws on a malformed/locked db returns []. The db
5
- * is opened read-only, so it is safe to run against a live Cursor profile.
9
+ * composer that has at least one real (non-meta) turn. A valid database with no sessions returns []; database open,
10
+ * query, and incompatible-schema failures throw CursorStateVscdbError. The database is always opened read-only.
6
11
  */
7
12
  export declare function parseCursorStateVscdb(dbPath: string): NormalizedSession[];
8
13
  /** True for a path that looks like a Cursor globalStorage state.vscdb. */
@@ -1,6 +1,14 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { correlateToolNames, isMetaText } from './types.js';
3
3
  const nodeRequire = createRequire(import.meta.url);
4
+ export class CursorStateVscdbError extends Error {
5
+ stage;
6
+ constructor(stage, message, cause) {
7
+ super(`Cursor state database ${stage} error: ${message}`, { cause });
8
+ this.name = 'CursorStateVscdbError';
9
+ this.stage = stage;
10
+ }
11
+ }
4
12
  function isBunRuntime() {
5
13
  return typeof process.versions === 'object' && typeof process.versions.bun === 'string';
6
14
  }
@@ -37,11 +45,9 @@ function openReadOnlySqliteDatabase(path) {
37
45
  // bubbleId:<composer>:<bubble> rows; extracting user/assistant text, assistant `thinking` as a reasoning turn,
38
46
  // and `toolFormerData` as a tool_use + tool_result pair; per-bubble token counts; session model/createdAt.
39
47
  //
40
- // NOT yet covered / known gaps (the local machine's DB had only EMPTY composers, so these are schema-documented
41
- // but not golden-verified against real populated bubbles): Cursor's code-edit "diff" capability blocks
42
- // (codeBlockData / originalFileStates) are NOT reconstructed into before/after diffs; sub-composer / best-of-N
43
- // branch bubbles are read flat (no tree structure); attachment/context payloads are ignored. Extend with a real
44
- // populated-DB fixture before trusting those.
48
+ // Composer roots can also carry an inline `conversation` array (including sub-composer roots). Attachment bodies
49
+ // and explicit code-block content are preserved as turns. We intentionally do not synthesize before/after diffs
50
+ // from codeBlockData/originalFileStates because those fields vary by Cursor version.
45
51
  function isRecord(value) {
46
52
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
47
53
  }
@@ -90,9 +96,61 @@ function bubbleToolTurns(bubble, toolUseId) {
90
96
  }
91
97
  return turns;
92
98
  }
93
- function bubbleToTurns(bubble, bubbleId) {
94
- if (!isRecord(bubble))
99
+ function explicitBodyText(value) {
100
+ if (typeof value === 'string')
101
+ return value;
102
+ if (!isRecord(value))
103
+ return '';
104
+ return asString(value['text']) || asString(value['content']);
105
+ }
106
+ function bubbleAttachmentTexts(bubble) {
107
+ const directValue = bubble['attachments'];
108
+ if (directValue !== undefined && !Array.isArray(directValue)) {
109
+ throw new CursorStateVscdbError('schema', 'conversation attachments must be an array');
110
+ }
111
+ const direct = Array.isArray(directValue) ? directValue : [];
112
+ const toolValue = isRecord(bubble['toolFormerData']) ? bubble['toolFormerData']['attachments'] : undefined;
113
+ if (toolValue !== undefined && !Array.isArray(toolValue)) {
114
+ throw new CursorStateVscdbError('schema', 'tool attachments must be an array');
115
+ }
116
+ const tool = Array.isArray(toolValue) ? toolValue : [];
117
+ return [...direct, ...tool].map((attachment) => {
118
+ if (!isRecord(attachment)) {
119
+ throw new CursorStateVscdbError('schema', 'attachment must be an object');
120
+ }
121
+ return explicitBodyText(attachment['body']);
122
+ }).filter(Boolean);
123
+ }
124
+ function bubbleCodeBlockTexts(bubble) {
125
+ const codeBlocks = bubble['codeBlocks'];
126
+ if (codeBlocks === undefined)
95
127
  return [];
128
+ if (!Array.isArray(codeBlocks)) {
129
+ throw new CursorStateVscdbError('schema', 'conversation codeBlocks must be an array');
130
+ }
131
+ return codeBlocks.map(explicitContentText).filter(Boolean);
132
+ }
133
+ function explicitContentText(value) {
134
+ if (typeof value === 'string')
135
+ return value;
136
+ if (!isRecord(value))
137
+ return '';
138
+ return asString(value['content']) || asString(value['text']) || asString(value['code']) || explicitBodyText(value['body']);
139
+ }
140
+ function contentTurns(role, texts, existingText = '') {
141
+ const seen = new Set(existingText ? [existingText] : []);
142
+ return texts.flatMap((text) => {
143
+ const trimmed = text.trim();
144
+ if (!trimmed || seen.has(trimmed))
145
+ return [];
146
+ seen.add(trimmed);
147
+ return [{ role, text, isMeta: isMetaText(text) }];
148
+ });
149
+ }
150
+ function bubbleToTurns(bubble, bubbleId) {
151
+ if (!isRecord(bubble)) {
152
+ throw new CursorStateVscdbError('schema', `conversation bubble ${bubbleId} is missing or invalid`);
153
+ }
96
154
  const type = finiteNumber(bubble['type']);
97
155
  const inputTokens = isRecord(bubble['tokenCount']) ? finiteNumber(bubble['tokenCount']['inputTokens']) : undefined;
98
156
  const outputTokens = isRecord(bubble['tokenCount']) ? finiteNumber(bubble['tokenCount']['outputTokens']) : undefined;
@@ -101,9 +159,14 @@ function bubbleToTurns(bubble, bubbleId) {
101
159
  if (type === 1) {
102
160
  // user bubble
103
161
  turns.push({ role: 'user', text, isMeta: isMetaText(text) });
162
+ turns.push(...contentTurns('user', bubbleAttachmentTexts(bubble), text));
163
+ turns.push(...contentTurns('user', bubbleCodeBlockTexts(bubble), text));
104
164
  return turns;
105
165
  }
106
- // assistant (type === 2) or unknown -> treat as assistant content
166
+ if (type !== 2) {
167
+ throw new CursorStateVscdbError('schema', `conversation bubble ${bubbleId} has an unsupported type`);
168
+ }
169
+ // assistant bubble
107
170
  const thinking = bubbleThinkingText(bubble);
108
171
  if (thinking) {
109
172
  turns.push({ role: 'assistant', text: thinking, reasoning: true, isMeta: false });
@@ -118,31 +181,139 @@ function bubbleToTurns(bubble, bubbleId) {
118
181
  });
119
182
  }
120
183
  turns.push(...bubbleToolTurns(bubble, bubbleId));
184
+ turns.push(...contentTurns('assistant', bubbleAttachmentTexts(bubble), text));
185
+ turns.push(...contentTurns('assistant', bubbleCodeBlockTexts(bubble), text));
121
186
  return turns;
122
187
  }
123
188
  function conversationHeaders(composer) {
124
189
  const headers = composer['fullConversationHeadersOnly'];
125
- if (!Array.isArray(headers))
190
+ if (headers === undefined)
126
191
  return [];
127
- return headers
128
- .map((header) => (isRecord(header) ? { bubbleId: asString(header['bubbleId']), type: finiteNumber(header['type']) } : { bubbleId: '' }))
129
- .filter((header) => header.bubbleId);
192
+ if (!Array.isArray(headers)) {
193
+ throw new CursorStateVscdbError('schema', 'fullConversationHeadersOnly must be an array');
194
+ }
195
+ return headers.map((header) => {
196
+ if (!isRecord(header))
197
+ throw new CursorStateVscdbError('schema', 'conversation header must be an object');
198
+ const bubbleId = asString(header['bubbleId']);
199
+ if (!bubbleId)
200
+ throw new CursorStateVscdbError('schema', 'conversation header bubbleId is missing');
201
+ return { bubbleId, type: finiteNumber(header['type']) };
202
+ });
130
203
  }
131
- function composerToSession(composer, composerId, readBubble) {
204
+ function inlineConversation(composer) {
205
+ const conversation = composer['conversation'];
206
+ if (conversation === undefined)
207
+ return [];
208
+ if (!Array.isArray(conversation)) {
209
+ throw new CursorStateVscdbError('schema', 'conversation must be an array');
210
+ }
211
+ return conversation.map((bubble, index) => ({
212
+ bubbleId: isRecord(bubble) ? asString(bubble['bubbleId']) || `inline-${index + 1}` : `inline-${index + 1}`,
213
+ value: bubble,
214
+ }));
215
+ }
216
+ function codeBlockContentByBubble(composer, readCodeBlockDiff) {
217
+ const byBubble = new Map();
218
+ const trailing = [];
219
+ const codeBlockData = composer['codeBlockData'];
220
+ if (codeBlockData === undefined)
221
+ return { byBubble, trailing };
222
+ if (!isRecord(codeBlockData)) {
223
+ throw new CursorStateVscdbError('schema', 'codeBlockData must be an object');
224
+ }
225
+ const seen = new Set();
226
+ const add = (text, bubbleId) => {
227
+ const trimmed = text.trim();
228
+ const key = `${bubbleId}\0${trimmed}`;
229
+ if (!trimmed || seen.has(key))
230
+ return;
231
+ seen.add(key);
232
+ if (bubbleId) {
233
+ const texts = byBubble.get(bubbleId);
234
+ if (texts)
235
+ texts.push(text);
236
+ else
237
+ byBubble.set(bubbleId, [text]);
238
+ }
239
+ else
240
+ trailing.push(text);
241
+ };
242
+ const stack = Object.values(codeBlockData).reverse()
243
+ .map((value) => ({ value, bubbleId: '' }));
244
+ let visited = 0;
245
+ while (stack.length > 0) {
246
+ if (++visited > 100_000)
247
+ throw new CursorStateVscdbError('schema', 'codeBlockData is too deeply nested');
248
+ const current = stack.pop();
249
+ if (Array.isArray(current.value)) {
250
+ for (let index = current.value.length - 1; index >= 0; index--) {
251
+ stack.push({ value: current.value[index], bubbleId: current.bubbleId });
252
+ }
253
+ continue;
254
+ }
255
+ if (!isRecord(current.value))
256
+ continue;
257
+ const bubbleId = asString(current.value['bubbleId']) || current.bubbleId;
258
+ const directText = asString(current.value['content']) || asString(current.value['text'])
259
+ || asString(current.value['code']) || asString(current.value['body']);
260
+ if (directText)
261
+ add(directText, bubbleId);
262
+ const diffId = asString(current.value['diffId']);
263
+ if (diffId) {
264
+ const diff = readCodeBlockDiff(diffId);
265
+ if (!isRecord(diff) || !Array.isArray(diff['newModelDiffWrtV0'])) {
266
+ throw new CursorStateVscdbError('schema', 'code block diff must contain newModelDiffWrtV0');
267
+ }
268
+ for (const line of diff['newModelDiffWrtV0']) {
269
+ if (!isRecord(line)) {
270
+ throw new CursorStateVscdbError('schema', 'code block diff line must be an object');
271
+ }
272
+ const modified = line['modified'];
273
+ if (typeof modified === 'string')
274
+ add(modified, bubbleId);
275
+ else if (Array.isArray(modified) && modified.every((value) => typeof value === 'string')) {
276
+ add(modified.join('\n'), bubbleId);
277
+ }
278
+ else
279
+ throw new CursorStateVscdbError('schema', 'code block diff line must contain modified text');
280
+ }
281
+ }
282
+ const children = Object.values(current.value).filter((child) => Array.isArray(child) || isRecord(child));
283
+ for (let index = children.length - 1; index >= 0; index--) {
284
+ stack.push({ value: children[index], bubbleId });
285
+ }
286
+ }
287
+ return { byBubble, trailing };
288
+ }
289
+ function composerToSession(composer, composerId, readBubble, readCodeBlockDiff) {
132
290
  const headers = conversationHeaders(composer);
133
- const conversationMap = isRecord(composer['conversationMap']) ? composer['conversationMap'] : undefined;
134
- const orderedBubbleIds = headers.length > 0
135
- ? headers.map((header) => header.bubbleId)
136
- : (conversationMap ? Object.keys(conversationMap) : []);
291
+ const inline = inlineConversation(composer);
292
+ const inlineById = new Map(inline.map((bubble) => [bubble.bubbleId, bubble.value]));
293
+ const conversationMapValue = composer['conversationMap'];
294
+ if (conversationMapValue !== undefined && !isRecord(conversationMapValue)) {
295
+ throw new CursorStateVscdbError('schema', 'conversationMap must be an object');
296
+ }
297
+ const conversationMap = isRecord(conversationMapValue) ? conversationMapValue : undefined;
298
+ const codeBlocks = codeBlockContentByBubble(composer, (diffId) => readCodeBlockDiff(composerId, diffId));
299
+ const orderedBubbles = headers.length > 0
300
+ ? headers.map((header) => ({ bubbleId: header.bubbleId, value: (conversationMap && conversationMap[header.bubbleId] !== undefined)
301
+ ? conversationMap[header.bubbleId]
302
+ : readBubble(composerId, header.bubbleId) ?? inlineById.get(header.bubbleId) }))
303
+ : inline.length > 0
304
+ ? inline
305
+ : (conversationMap ? Object.entries(conversationMap).map(([bubbleId, value]) => ({ bubbleId, value })) : []);
137
306
  const turns = [];
138
- for (const bubbleId of orderedBubbleIds) {
139
- const bubble = (conversationMap && conversationMap[bubbleId] !== undefined)
140
- ? conversationMap[bubbleId]
141
- : readBubble(composerId, bubbleId);
142
- turns.push(...bubbleToTurns(bubble, bubbleId));
307
+ for (const bubble of orderedBubbles) {
308
+ turns.push(...bubbleToTurns(bubble.value, bubble.bubbleId));
309
+ turns.push(...contentTurns('assistant', codeBlocks.byBubble.get(bubble.bubbleId) ?? []));
143
310
  }
311
+ turns.push(...contentTurns('assistant', codeBlocks.trailing));
144
312
  const model = isRecord(composer['modelConfig']) ? asString(composer['modelConfig']['modelName']) : '';
145
313
  const createdAt = finiteNumber(composer['createdAt']);
314
+ if (createdAt !== undefined && Math.abs(createdAt) > 8_640_000_000_000_000) {
315
+ throw new CursorStateVscdbError('schema', 'composer createdAt is outside the supported date range');
316
+ }
146
317
  return {
147
318
  turns: correlateToolNames(turns),
148
319
  sessionId: composerId,
@@ -155,51 +326,93 @@ function composerToSession(composer, composerId, readBubble) {
155
326
  }
156
327
  /**
157
328
  * Read Cursor chat sessions out of a `state.vscdb` sqlite database (read-only). Returns one NormalizedSession per
158
- * composer that has at least one real (non-meta) turn. Never throws on a malformed/locked db returns []. The db
159
- * is opened read-only, so it is safe to run against a live Cursor profile.
329
+ * composer that has at least one real (non-meta) turn. A valid database with no sessions returns []; database open,
330
+ * query, and incompatible-schema failures throw CursorStateVscdbError. The database is always opened read-only.
160
331
  */
161
332
  export function parseCursorStateVscdb(dbPath) {
162
333
  let db;
163
334
  try {
164
- db = openReadOnlySqliteDatabase(dbPath);
165
- const composerRows = db
166
- .prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'")
167
- .all();
168
- const bubbleStmt = db.prepare('SELECT value FROM cursorDiskKV WHERE key = ?');
169
- const readBubble = (composerId, bubbleId) => {
335
+ try {
336
+ db = openReadOnlySqliteDatabase(dbPath);
337
+ }
338
+ catch (error) {
339
+ throw new CursorStateVscdbError('open', 'unable to open the file read-only', error);
340
+ }
341
+ let columns;
342
+ try {
343
+ columns = db.prepare('PRAGMA table_info(cursorDiskKV)').all();
344
+ }
345
+ catch (error) {
346
+ throw new CursorStateVscdbError('query', 'unable to inspect cursorDiskKV', error);
347
+ }
348
+ const columnNames = new Set(columns.flatMap((column) => isRecord(column) ? [asString(column['name'])] : []));
349
+ if (!columnNames.has('key') || !columnNames.has('value')) {
350
+ throw new CursorStateVscdbError('schema', 'cursorDiskKV with key and value columns is required');
351
+ }
352
+ let composerRows;
353
+ let valueStmt;
354
+ try {
355
+ composerRows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'").all();
356
+ valueStmt = db.prepare('SELECT value FROM cursorDiskKV WHERE key = ?');
357
+ }
358
+ catch (error) {
359
+ throw new CursorStateVscdbError('query', 'unable to query Cursor conversations', error);
360
+ }
361
+ const parseJsonValue = (value, label) => {
362
+ const text = typeof value === 'string'
363
+ ? value
364
+ : value instanceof Uint8Array
365
+ ? new TextDecoder().decode(value)
366
+ : undefined;
367
+ if (text === undefined) {
368
+ throw new CursorStateVscdbError('schema', `${label} value must be JSON text or a UTF-8 blob`);
369
+ }
170
370
  try {
171
- const row = bubbleStmt.get(`bubbleId:${composerId}:${bubbleId}`);
172
- if (!row || typeof row.value !== 'string')
173
- return undefined;
174
- return JSON.parse(row.value);
371
+ return JSON.parse(text);
175
372
  }
176
- catch {
177
- return undefined;
373
+ catch (error) {
374
+ throw new CursorStateVscdbError('schema', `${label} contains invalid JSON`, error);
375
+ }
376
+ };
377
+ const readValue = (key, label) => {
378
+ let row;
379
+ try {
380
+ row = valueStmt.get(key);
381
+ }
382
+ catch (error) {
383
+ throw new CursorStateVscdbError('query', `unable to query a Cursor ${label}`, error);
178
384
  }
385
+ if (!row)
386
+ return undefined;
387
+ return parseJsonValue(row.value, label);
179
388
  };
389
+ const readBubble = (composerId, bubbleId) => readValue(`bubbleId:${composerId}:${bubbleId}`, 'conversation bubble');
390
+ const readCodeBlockDiff = (composerId, diffId) => readValue(`codeBlockDiff:${composerId}:${diffId}`, 'code block diff');
180
391
  const sessions = [];
392
+ let firstComposerError;
181
393
  for (const row of composerRows) {
182
- if (typeof row.value !== 'string')
183
- continue;
184
- let composer;
185
394
  try {
186
- composer = JSON.parse(row.value);
395
+ const composer = parseJsonValue(row.value, 'composer');
396
+ if (!isRecord(composer))
397
+ throw new CursorStateVscdbError('schema', 'composer must be a JSON object');
398
+ const composerId = asString(row.key).replace(/^composerData:/, '') || asString(composer['composerId']);
399
+ if (!composerId)
400
+ throw new CursorStateVscdbError('schema', 'composer id is missing');
401
+ const session = composerToSession(composer, composerId, readBubble, readCodeBlockDiff);
402
+ if (session.turns.some((turn) => turn.isMeta !== true))
403
+ sessions.push(session);
187
404
  }
188
- catch {
189
- continue;
405
+ catch (error) {
406
+ if (!(error instanceof CursorStateVscdbError) || error.stage !== 'schema')
407
+ throw error;
408
+ firstComposerError ??= error;
190
409
  }
191
- if (!isRecord(composer))
192
- continue;
193
- const composerId = asString(row.key).replace(/^composerData:/, '') || asString(composer['composerId']);
194
- const session = composerToSession(composer, composerId, readBubble);
195
- if (session.turns.some((turn) => turn.isMeta !== true))
196
- sessions.push(session);
197
410
  }
411
+ // Returning only the healthy composers would silently produce an incomplete archive.
412
+ if (firstComposerError)
413
+ throw firstComposerError;
198
414
  return sessions;
199
415
  }
200
- catch {
201
- return [];
202
- }
203
416
  finally {
204
417
  try {
205
418
  db?.close();
@@ -0,0 +1,19 @@
1
+ import type { NormalizedSession } from './types.js';
2
+ export type RuntimeSessionEvidenceCoverage = 'complete' | 'partial' | 'empty';
3
+ export type RuntimeSessionEvidenceGapCode = 'empty_session' | 'missing_tool_result' | 'unmatched_tool_result';
4
+ export interface RuntimeSessionEvidenceCounts {
5
+ nonMetaTurns: number;
6
+ toolCalls: number;
7
+ toolResults: number;
8
+ matchedToolResults: number;
9
+ missingToolResults: number;
10
+ unmatchedToolResults: number;
11
+ failedToolResults: number;
12
+ }
13
+ export interface RuntimeSessionEvidenceSummary {
14
+ coverage: RuntimeSessionEvidenceCoverage;
15
+ counts: RuntimeSessionEvidenceCounts;
16
+ gapCodes: RuntimeSessionEvidenceGapCode[];
17
+ }
18
+ /** Summarizes transcript structure only; complete coverage does not verify the task outcome. */
19
+ export declare function summarizeSessionEvidence(session: Pick<NormalizedSession, 'turns'>): RuntimeSessionEvidenceSummary;
@@ -0,0 +1,73 @@
1
+ function toolUseId(turn) {
2
+ const id = turn.toolUseId;
3
+ return id?.trim() ? id : undefined;
4
+ }
5
+ function isToolCall(turn) {
6
+ return turn.role === 'assistant'
7
+ && turn.toolResult === undefined
8
+ && (turn.toolUseId !== undefined
9
+ || turn.toolInput !== undefined
10
+ || turn.toolName !== undefined);
11
+ }
12
+ function isToolResult(turn) {
13
+ return turn.role === 'tool';
14
+ }
15
+ /** Summarizes transcript structure only; complete coverage does not verify the task outcome. */
16
+ export function summarizeSessionEvidence(session) {
17
+ const turns = session.turns.filter((turn) => turn.isMeta !== true);
18
+ const pendingCallsById = new Map();
19
+ let toolCalls = 0;
20
+ let toolResults = 0;
21
+ let matchedToolResults = 0;
22
+ let unmatchedToolResults = 0;
23
+ let failedToolResults = 0;
24
+ for (const turn of turns) {
25
+ if (isToolCall(turn)) {
26
+ toolCalls += 1;
27
+ const id = toolUseId(turn);
28
+ if (id)
29
+ pendingCallsById.set(id, (pendingCallsById.get(id) ?? 0) + 1);
30
+ continue;
31
+ }
32
+ if (isToolResult(turn)) {
33
+ toolResults += 1;
34
+ if (turn.errorMessage !== undefined)
35
+ failedToolResults += 1;
36
+ const id = toolUseId(turn);
37
+ const pending = id ? pendingCallsById.get(id) ?? 0 : 0;
38
+ if (id && pending > 0) {
39
+ matchedToolResults += 1;
40
+ if (pending === 1)
41
+ pendingCallsById.delete(id);
42
+ else
43
+ pendingCallsById.set(id, pending - 1);
44
+ }
45
+ else {
46
+ unmatchedToolResults += 1;
47
+ }
48
+ }
49
+ }
50
+ const missingToolResults = toolCalls - matchedToolResults;
51
+ const counts = {
52
+ nonMetaTurns: turns.length,
53
+ toolCalls,
54
+ toolResults,
55
+ matchedToolResults,
56
+ missingToolResults,
57
+ unmatchedToolResults,
58
+ failedToolResults,
59
+ };
60
+ const gapCodes = [];
61
+ if (turns.length === 0)
62
+ gapCodes.push('empty_session');
63
+ else {
64
+ if (missingToolResults > 0)
65
+ gapCodes.push('missing_tool_result');
66
+ if (unmatchedToolResults > 0)
67
+ gapCodes.push('unmatched_tool_result');
68
+ }
69
+ const coverage = turns.length === 0
70
+ ? 'empty'
71
+ : gapCodes.length > 0 ? 'partial' : 'complete';
72
+ return { coverage, counts, gapCodes };
73
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export declare const PACKAGE = "@evomap/evolver-runtime-adapters";
2
2
  export * from './types.js';
3
+ export * from './evidenceSummary.js';
3
4
  export * from './adapters.js';
4
5
  export * from './cursorState.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export const PACKAGE = '@evomap/evolver-runtime-adapters';
2
2
  export * from './types.js';
3
+ export * from './evidenceSummary.js';
3
4
  export * from './adapters.js';
4
5
  export * from './cursorState.js';
package/dist/types.d.ts CHANGED
@@ -67,11 +67,17 @@ export interface NormalizedSession {
67
67
  sourceRecord?: unknown;
68
68
  rawRows?: unknown[];
69
69
  }
70
+ export interface NativeResumeIdentity {
71
+ harness: 'claude-code' | 'cursor';
72
+ sessionId: string;
73
+ }
70
74
  export interface SessionLogAdapter {
71
75
  readonly agent: string;
72
76
  detect(path: string): boolean;
73
77
  /** Derive a stable runtime session id when the transcript itself does not carry one. */
74
78
  sessionIdFromPath?(path: string): string | undefined;
79
+ /** Resolve a harness-bound native session identity from a verified transcript source. */
80
+ resumeIdentityFromSource?(path: string, rawChunk: string): NativeResumeIdentity | undefined;
75
81
  parse(rawChunk: string): NormalizedTurn[];
76
82
  parseSession?(rawChunk: string): NormalizedSession;
77
83
  parseSessions?(rawChunk: string): NormalizedSession[];
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@evomap/evolver-runtime-adapters",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.22",
4
4
  "private": false,
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": "^22.13.0 || >=23.4.0"
8
+ },
6
9
  "description": "五 runtime 会话日志适配器 (CC/codex/cursor/kiro/opencode)",
7
10
  "main": "./dist/index.js",
8
11
  "types": "./dist/index.d.ts",
@@ -12,6 +15,10 @@
12
15
  "default": "./dist/index.js"
13
16
  }
14
17
  },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/EvoMap/evolver.git"
21
+ },
15
22
  "publishConfig": {
16
23
  "access": "public",
17
24
  "tag": "v2-beta"