opencode-lcm 0.11.0

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 (65) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/LICENSE +21 -0
  3. package/README.md +207 -0
  4. package/dist/archive-transform.d.ts +45 -0
  5. package/dist/archive-transform.js +81 -0
  6. package/dist/constants.d.ts +12 -0
  7. package/dist/constants.js +16 -0
  8. package/dist/doctor.d.ts +22 -0
  9. package/dist/doctor.js +44 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.js +306 -0
  12. package/dist/logging.d.ts +14 -0
  13. package/dist/logging.js +28 -0
  14. package/dist/options.d.ts +3 -0
  15. package/dist/options.js +217 -0
  16. package/dist/preview-providers.d.ts +20 -0
  17. package/dist/preview-providers.js +246 -0
  18. package/dist/privacy.d.ts +16 -0
  19. package/dist/privacy.js +92 -0
  20. package/dist/search-ranking.d.ts +12 -0
  21. package/dist/search-ranking.js +98 -0
  22. package/dist/sql-utils.d.ts +31 -0
  23. package/dist/sql-utils.js +80 -0
  24. package/dist/store-artifacts.d.ts +50 -0
  25. package/dist/store-artifacts.js +374 -0
  26. package/dist/store-retention.d.ts +39 -0
  27. package/dist/store-retention.js +90 -0
  28. package/dist/store-search.d.ts +37 -0
  29. package/dist/store-search.js +298 -0
  30. package/dist/store-snapshot.d.ts +133 -0
  31. package/dist/store-snapshot.js +325 -0
  32. package/dist/store-types.d.ts +14 -0
  33. package/dist/store-types.js +5 -0
  34. package/dist/store.d.ts +316 -0
  35. package/dist/store.js +3673 -0
  36. package/dist/types.d.ts +117 -0
  37. package/dist/types.js +1 -0
  38. package/dist/utils.d.ts +35 -0
  39. package/dist/utils.js +414 -0
  40. package/dist/workspace-path.d.ts +1 -0
  41. package/dist/workspace-path.js +15 -0
  42. package/dist/worktree-key.d.ts +1 -0
  43. package/dist/worktree-key.js +6 -0
  44. package/package.json +61 -0
  45. package/src/archive-transform.ts +147 -0
  46. package/src/bun-sqlite.d.ts +18 -0
  47. package/src/constants.ts +20 -0
  48. package/src/doctor.ts +83 -0
  49. package/src/index.ts +330 -0
  50. package/src/logging.ts +41 -0
  51. package/src/options.ts +297 -0
  52. package/src/preview-providers.ts +298 -0
  53. package/src/privacy.ts +122 -0
  54. package/src/search-ranking.ts +145 -0
  55. package/src/sql-utils.ts +107 -0
  56. package/src/store-artifacts.ts +666 -0
  57. package/src/store-retention.ts +152 -0
  58. package/src/store-search.ts +440 -0
  59. package/src/store-snapshot.ts +582 -0
  60. package/src/store-types.ts +16 -0
  61. package/src/store.ts +4926 -0
  62. package/src/types.ts +132 -0
  63. package/src/utils.ts +444 -0
  64. package/src/workspace-path.ts +20 -0
  65. package/src/worktree-key.ts +5 -0
@@ -0,0 +1,582 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { withTransaction } from './sql-utils.js';
5
+ import type { SqlDatabaseLike } from './store-types.js';
6
+ import { resolveWorkspacePath } from './workspace-path.js';
7
+ import { normalizeWorktreeKey } from './worktree-key.js';
8
+
9
+ export type SnapshotScope = 'session' | 'root' | 'worktree' | 'all';
10
+
11
+ export type SnapshotWorktreeMode = 'auto' | 'preserve' | 'current';
12
+
13
+ export type SessionRow = {
14
+ session_id: string;
15
+ title: string | null;
16
+ session_directory: string | null;
17
+ worktree_key: string | null;
18
+ parent_session_id: string | null;
19
+ root_session_id: string | null;
20
+ lineage_depth: number | null;
21
+ pinned: number | null;
22
+ pin_reason: string | null;
23
+ updated_at: number;
24
+ compacted_at: number | null;
25
+ deleted: number;
26
+ event_count: number;
27
+ };
28
+
29
+ export type MessageRow = {
30
+ message_id: string;
31
+ session_id: string;
32
+ created_at: number;
33
+ info_json: string;
34
+ };
35
+
36
+ export type PartRow = {
37
+ part_id: string;
38
+ session_id: string;
39
+ message_id: string;
40
+ sort_key: number;
41
+ part_json: string;
42
+ };
43
+
44
+ export type SummaryNodeRow = {
45
+ node_id: string;
46
+ session_id: string;
47
+ level: number;
48
+ node_kind: string;
49
+ start_index: number;
50
+ end_index: number;
51
+ message_ids_json: string;
52
+ summary_text: string;
53
+ created_at: number;
54
+ };
55
+
56
+ export type SummaryEdgeRow = {
57
+ session_id: string;
58
+ parent_id: string;
59
+ child_id: string;
60
+ child_position: number;
61
+ };
62
+
63
+ export type SummaryStateRow = {
64
+ session_id: string;
65
+ archived_count: number;
66
+ latest_message_created: number;
67
+ archived_signature: string | null;
68
+ root_node_ids_json: string;
69
+ updated_at: number;
70
+ };
71
+
72
+ export type ArtifactRow = {
73
+ artifact_id: string;
74
+ session_id: string;
75
+ message_id: string;
76
+ part_id: string;
77
+ artifact_kind: string;
78
+ field_name: string;
79
+ preview_text: string;
80
+ content_text: string;
81
+ content_hash: string | null;
82
+ metadata_json: string;
83
+ char_count: number;
84
+ created_at: number;
85
+ };
86
+
87
+ export type ArtifactBlobRow = {
88
+ content_hash: string;
89
+ content_text: string;
90
+ char_count: number;
91
+ created_at: number;
92
+ };
93
+
94
+ export type SnapshotPayload = {
95
+ version: 1;
96
+ exportedAt: number;
97
+ scope: string;
98
+ sessions: SessionRow[];
99
+ messages: MessageRow[];
100
+ parts: PartRow[];
101
+ resumes: Array<{ session_id: string; note: string; updated_at: number }>;
102
+ artifacts: ArtifactRow[];
103
+ artifact_blobs: ArtifactBlobRow[];
104
+ summary_nodes: SummaryNodeRow[];
105
+ summary_edges: SummaryEdgeRow[];
106
+ summary_state: SummaryStateRow[];
107
+ };
108
+
109
+ export type ExportSnapshotInput = {
110
+ filePath: string;
111
+ sessionID?: string;
112
+ scope?: string;
113
+ };
114
+
115
+ export type ImportSnapshotInput = {
116
+ filePath: string;
117
+ mode?: 'replace' | 'merge';
118
+ worktreeMode?: SnapshotWorktreeMode;
119
+ };
120
+
121
+ export type SnapshotExportBindings = {
122
+ workspaceDirectory: string;
123
+ normalizeScope(scope?: string): SnapshotScope | undefined;
124
+ resolveScopeSessionIDs(scope?: string, sessionID?: string): string[] | undefined;
125
+ readScopedSessionRowsSync(sessionIDs?: string[]): SessionRow[];
126
+ readScopedMessageRowsSync(sessionIDs?: string[]): MessageRow[];
127
+ readScopedPartRowsSync(sessionIDs?: string[]): PartRow[];
128
+ readScopedResumeRowsSync(
129
+ sessionIDs?: string[],
130
+ ): Array<{ session_id: string; note: string; updated_at: number }>;
131
+ readScopedArtifactRowsSync(sessionIDs?: string[]): ArtifactRow[];
132
+ readScopedArtifactBlobRowsSync(sessionIDs?: string[]): ArtifactBlobRow[];
133
+ readScopedSummaryRowsSync(sessionIDs?: string[]): SummaryNodeRow[];
134
+ readScopedSummaryEdgeRowsSync(sessionIDs?: string[]): SummaryEdgeRow[];
135
+ readScopedSummaryStateRowsSync(sessionIDs?: string[]): SummaryStateRow[];
136
+ };
137
+
138
+ export type SnapshotImportBindings = {
139
+ workspaceDirectory: string;
140
+ getDb(): SqlDatabaseLike;
141
+ clearSessionDataSync(sessionID: string): void;
142
+ backfillArtifactBlobsSync(): void;
143
+ refreshAllLineageSync(): void;
144
+ syncAllDerivedSessionStateSync(force: boolean): void;
145
+ refreshSearchIndexesSync(sessionIDs?: string[]): void;
146
+ };
147
+
148
+ export async function exportStoreSnapshot(
149
+ bindings: SnapshotExportBindings,
150
+ input: ExportSnapshotInput,
151
+ ): Promise<string> {
152
+ const scope = bindings.normalizeScope(input.scope) ?? 'session';
153
+ const sessionIDs = bindings.resolveScopeSessionIDs(scope, input.sessionID);
154
+ const sessions = bindings.readScopedSessionRowsSync(sessionIDs);
155
+ const snapshot: SnapshotPayload = {
156
+ version: 1,
157
+ exportedAt: Date.now(),
158
+ scope,
159
+ sessions,
160
+ messages: bindings.readScopedMessageRowsSync(sessionIDs),
161
+ parts: bindings.readScopedPartRowsSync(sessionIDs),
162
+ resumes: bindings.readScopedResumeRowsSync(sessionIDs),
163
+ artifacts: bindings.readScopedArtifactRowsSync(sessionIDs),
164
+ artifact_blobs: bindings.readScopedArtifactBlobRowsSync(sessionIDs),
165
+ summary_nodes: bindings.readScopedSummaryRowsSync(sessionIDs),
166
+ summary_edges: bindings.readScopedSummaryEdgeRowsSync(sessionIDs),
167
+ summary_state: bindings.readScopedSummaryStateRowsSync(sessionIDs),
168
+ };
169
+
170
+ const targetPath = path.isAbsolute(input.filePath)
171
+ ? path.normalize(input.filePath)
172
+ : resolveWorkspacePath(bindings.workspaceDirectory, input.filePath);
173
+ await writeFile(targetPath, JSON.stringify(snapshot, null, 2), 'utf8');
174
+ return [
175
+ `file=${targetPath}`,
176
+ `scope=${scope}`,
177
+ `sessions=${snapshot.sessions.length}`,
178
+ `worktrees=${listSnapshotWorktreeKeys(sessions).length}`,
179
+ `messages=${snapshot.messages.length}`,
180
+ `parts=${snapshot.parts.length}`,
181
+ `artifacts=${snapshot.artifacts.length}`,
182
+ `artifact_blobs=${snapshot.artifact_blobs.length}`,
183
+ `summary_nodes=${snapshot.summary_nodes.length}`,
184
+ ].join('\n');
185
+ }
186
+
187
+ export async function importStoreSnapshot(
188
+ bindings: SnapshotImportBindings,
189
+ input: ImportSnapshotInput,
190
+ ): Promise<string> {
191
+ const sourcePath = path.isAbsolute(input.filePath)
192
+ ? path.normalize(input.filePath)
193
+ : resolveWorkspacePath(bindings.workspaceDirectory, input.filePath);
194
+ const snapshot = parseSnapshotPayload(await readFile(sourcePath, 'utf8'));
195
+ const db = bindings.getDb();
196
+ const sessionIDs = [...new Set(snapshot.sessions.map((row) => row.session_id))];
197
+ const worktreeMode = resolveSnapshotWorktreeMode(input.worktreeMode);
198
+ const collisionSessionIDs = input.mode === 'merge' ? readExistingSessionIDs(db, sessionIDs) : [];
199
+ if (collisionSessionIDs.length > 0) {
200
+ throw new Error(
201
+ `Snapshot merge would overwrite existing sessions: ${collisionSessionIDs.slice(0, 5).join(', ')}. Re-run with mode=replace or import a snapshot without those session IDs.`,
202
+ );
203
+ }
204
+
205
+ const sourceWorktreeKeys = listSnapshotWorktreeKeys(snapshot.sessions);
206
+ const targetWorktreeKey = normalizeWorktreeKey(bindings.workspaceDirectory);
207
+ const shouldRehome = shouldRehomeImportedSessions(
208
+ sourceWorktreeKeys,
209
+ targetWorktreeKey,
210
+ worktreeMode,
211
+ );
212
+ const importedSessions = shouldRehome
213
+ ? snapshot.sessions.map((row) =>
214
+ rehomeImportedSessionRow(row, bindings.workspaceDirectory, targetWorktreeKey),
215
+ )
216
+ : snapshot.sessions;
217
+
218
+ withTransaction(db, 'importSnapshot', () => {
219
+ if (input.mode !== 'merge') {
220
+ for (const sessionID of sessionIDs) bindings.clearSessionDataSync(sessionID);
221
+ }
222
+
223
+ const insertSession = db.prepare(
224
+ `INSERT INTO sessions (session_id, title, session_directory, worktree_key, parent_session_id, root_session_id, lineage_depth, pinned, pin_reason, updated_at, compacted_at, deleted, event_count)
225
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
226
+ ON CONFLICT(session_id) DO UPDATE SET
227
+ title = excluded.title,
228
+ session_directory = excluded.session_directory,
229
+ worktree_key = excluded.worktree_key,
230
+ parent_session_id = excluded.parent_session_id,
231
+ root_session_id = excluded.root_session_id,
232
+ lineage_depth = excluded.lineage_depth,
233
+ pinned = excluded.pinned,
234
+ pin_reason = excluded.pin_reason,
235
+ updated_at = excluded.updated_at,
236
+ compacted_at = excluded.compacted_at,
237
+ deleted = excluded.deleted,
238
+ event_count = excluded.event_count`,
239
+ );
240
+ const insertMessage = db.prepare(
241
+ `INSERT OR REPLACE INTO messages (message_id, session_id, created_at, info_json) VALUES (?, ?, ?, ?)`,
242
+ );
243
+ const insertPart = db.prepare(
244
+ `INSERT OR REPLACE INTO parts (part_id, session_id, message_id, sort_key, part_json) VALUES (?, ?, ?, ?, ?)`,
245
+ );
246
+ const insertResume = db.prepare(
247
+ `INSERT OR REPLACE INTO resumes (session_id, note, updated_at) VALUES (?, ?, ?)`,
248
+ );
249
+ const insertBlob = db.prepare(
250
+ `INSERT OR REPLACE INTO artifact_blobs (content_hash, content_text, char_count, created_at) VALUES (?, ?, ?, ?)`,
251
+ );
252
+ const insertArtifact = db.prepare(
253
+ `INSERT OR REPLACE INTO artifacts (artifact_id, session_id, message_id, part_id, artifact_kind, field_name, preview_text, content_text, content_hash, metadata_json, char_count, created_at)
254
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
255
+ );
256
+ const insertNode = db.prepare(
257
+ `INSERT OR REPLACE INTO summary_nodes (node_id, session_id, level, node_kind, start_index, end_index, message_ids_json, summary_text, created_at)
258
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
259
+ );
260
+ const insertEdge = db.prepare(
261
+ `INSERT OR REPLACE INTO summary_edges (session_id, parent_id, child_id, child_position) VALUES (?, ?, ?, ?)`,
262
+ );
263
+ const insertState = db.prepare(
264
+ `INSERT OR REPLACE INTO summary_state (session_id, archived_count, latest_message_created, archived_signature, root_node_ids_json, updated_at)
265
+ VALUES (?, ?, ?, ?, ?, ?)`,
266
+ );
267
+
268
+ for (const row of importedSessions) {
269
+ insertSession.run(
270
+ row.session_id,
271
+ row.title,
272
+ row.session_directory,
273
+ row.worktree_key,
274
+ row.parent_session_id,
275
+ row.root_session_id,
276
+ row.lineage_depth,
277
+ row.pinned ?? 0,
278
+ row.pin_reason,
279
+ row.updated_at,
280
+ row.compacted_at,
281
+ row.deleted,
282
+ row.event_count,
283
+ );
284
+ }
285
+ for (const row of snapshot.messages)
286
+ insertMessage.run(row.message_id, row.session_id, row.created_at, row.info_json);
287
+ for (const row of snapshot.parts)
288
+ insertPart.run(row.part_id, row.session_id, row.message_id, row.sort_key, row.part_json);
289
+ for (const row of snapshot.resumes) insertResume.run(row.session_id, row.note, row.updated_at);
290
+ for (const row of snapshot.artifact_blobs)
291
+ insertBlob.run(row.content_hash, row.content_text, row.char_count, row.created_at);
292
+ for (const row of snapshot.artifacts) {
293
+ insertArtifact.run(
294
+ row.artifact_id,
295
+ row.session_id,
296
+ row.message_id,
297
+ row.part_id,
298
+ row.artifact_kind,
299
+ row.field_name,
300
+ row.preview_text,
301
+ row.content_text,
302
+ row.content_hash,
303
+ row.metadata_json,
304
+ row.char_count,
305
+ row.created_at,
306
+ );
307
+ }
308
+ for (const row of snapshot.summary_nodes) {
309
+ insertNode.run(
310
+ row.node_id,
311
+ row.session_id,
312
+ row.level,
313
+ row.node_kind,
314
+ row.start_index,
315
+ row.end_index,
316
+ row.message_ids_json,
317
+ row.summary_text,
318
+ row.created_at,
319
+ );
320
+ }
321
+ for (const row of snapshot.summary_edges)
322
+ insertEdge.run(row.session_id, row.parent_id, row.child_id, row.child_position);
323
+ for (const row of snapshot.summary_state) {
324
+ insertState.run(
325
+ row.session_id,
326
+ row.archived_count,
327
+ row.latest_message_created,
328
+ row.archived_signature ?? '',
329
+ row.root_node_ids_json,
330
+ row.updated_at,
331
+ );
332
+ }
333
+ });
334
+
335
+ bindings.backfillArtifactBlobsSync();
336
+ bindings.refreshAllLineageSync();
337
+ bindings.syncAllDerivedSessionStateSync(true);
338
+ bindings.refreshSearchIndexesSync(sessionIDs);
339
+ return [
340
+ `file=${sourcePath}`,
341
+ `mode=${input.mode ?? 'replace'}`,
342
+ `worktree_mode=${worktreeMode}`,
343
+ `effective_worktree_mode=${shouldRehome ? 'current' : 'preserve'}`,
344
+ `sessions=${snapshot.sessions.length}`,
345
+ `source_worktrees=${sourceWorktreeKeys.length}`,
346
+ `rehomed_sessions=${shouldRehome ? importedSessions.length : 0}`,
347
+ `messages=${snapshot.messages.length}`,
348
+ `parts=${snapshot.parts.length}`,
349
+ `artifacts=${snapshot.artifacts.length}`,
350
+ `artifact_blobs=${snapshot.artifact_blobs.length}`,
351
+ `summary_nodes=${snapshot.summary_nodes.length}`,
352
+ ].join('\n');
353
+ }
354
+
355
+ function parseSnapshotPayload(content: string): SnapshotPayload {
356
+ const value = JSON.parse(content) as unknown;
357
+ const record = expectRecord(value, 'Snapshot file');
358
+ const version = record.version;
359
+ if (version !== 1) {
360
+ throw new Error(`Unsupported snapshot version: ${String(version)}`);
361
+ }
362
+
363
+ return {
364
+ version: 1,
365
+ exportedAt: expectNumber(record.exportedAt, 'exportedAt'),
366
+ scope: expectString(record.scope, 'scope'),
367
+ sessions: expectArray(record.sessions, 'sessions', parseSessionRow),
368
+ messages: expectArray(record.messages, 'messages', parseMessageRow),
369
+ parts: expectArray(record.parts, 'parts', parsePartRow),
370
+ resumes: expectArray(record.resumes, 'resumes', parseResumeRow),
371
+ artifacts: expectArray(record.artifacts, 'artifacts', parseArtifactRow),
372
+ artifact_blobs: expectArray(record.artifact_blobs, 'artifact_blobs', parseArtifactBlobRow),
373
+ summary_nodes: expectArray(record.summary_nodes, 'summary_nodes', parseSummaryNodeRow),
374
+ summary_edges: expectArray(record.summary_edges, 'summary_edges', parseSummaryEdgeRow),
375
+ summary_state: expectArray(record.summary_state, 'summary_state', parseSummaryStateRow),
376
+ };
377
+ }
378
+
379
+ function parseSessionRow(value: unknown): SessionRow {
380
+ const row = expectRecord(value, 'sessions[]');
381
+ return {
382
+ session_id: expectString(row.session_id, 'sessions[].session_id'),
383
+ title: expectNullableString(row.title, 'sessions[].title'),
384
+ session_directory: expectNullableString(row.session_directory, 'sessions[].session_directory'),
385
+ worktree_key: expectNullableString(row.worktree_key, 'sessions[].worktree_key'),
386
+ parent_session_id: expectNullableString(row.parent_session_id, 'sessions[].parent_session_id'),
387
+ root_session_id: expectNullableString(row.root_session_id, 'sessions[].root_session_id'),
388
+ lineage_depth: expectNullableNumber(row.lineage_depth, 'sessions[].lineage_depth'),
389
+ pinned: expectNullableNumber(row.pinned, 'sessions[].pinned'),
390
+ pin_reason: expectNullableString(row.pin_reason, 'sessions[].pin_reason'),
391
+ updated_at: expectNumber(row.updated_at, 'sessions[].updated_at'),
392
+ compacted_at: expectNullableNumber(row.compacted_at, 'sessions[].compacted_at'),
393
+ deleted: expectNumber(row.deleted, 'sessions[].deleted'),
394
+ event_count: expectNumber(row.event_count, 'sessions[].event_count'),
395
+ };
396
+ }
397
+
398
+ function parseMessageRow(value: unknown): MessageRow {
399
+ const row = expectRecord(value, 'messages[]');
400
+ return {
401
+ message_id: expectString(row.message_id, 'messages[].message_id'),
402
+ session_id: expectString(row.session_id, 'messages[].session_id'),
403
+ created_at: expectNumber(row.created_at, 'messages[].created_at'),
404
+ info_json: expectString(row.info_json, 'messages[].info_json'),
405
+ };
406
+ }
407
+
408
+ function parsePartRow(value: unknown): PartRow {
409
+ const row = expectRecord(value, 'parts[]');
410
+ return {
411
+ part_id: expectString(row.part_id, 'parts[].part_id'),
412
+ session_id: expectString(row.session_id, 'parts[].session_id'),
413
+ message_id: expectString(row.message_id, 'parts[].message_id'),
414
+ sort_key: expectNumber(row.sort_key, 'parts[].sort_key'),
415
+ part_json: expectString(row.part_json, 'parts[].part_json'),
416
+ };
417
+ }
418
+
419
+ function parseResumeRow(value: unknown): { session_id: string; note: string; updated_at: number } {
420
+ const row = expectRecord(value, 'resumes[]');
421
+ return {
422
+ session_id: expectString(row.session_id, 'resumes[].session_id'),
423
+ note: expectString(row.note, 'resumes[].note'),
424
+ updated_at: expectNumber(row.updated_at, 'resumes[].updated_at'),
425
+ };
426
+ }
427
+
428
+ function parseArtifactRow(value: unknown): ArtifactRow {
429
+ const row = expectRecord(value, 'artifacts[]');
430
+ return {
431
+ artifact_id: expectString(row.artifact_id, 'artifacts[].artifact_id'),
432
+ session_id: expectString(row.session_id, 'artifacts[].session_id'),
433
+ message_id: expectString(row.message_id, 'artifacts[].message_id'),
434
+ part_id: expectString(row.part_id, 'artifacts[].part_id'),
435
+ artifact_kind: expectString(row.artifact_kind, 'artifacts[].artifact_kind'),
436
+ field_name: expectString(row.field_name, 'artifacts[].field_name'),
437
+ preview_text: expectString(row.preview_text, 'artifacts[].preview_text'),
438
+ content_text: expectString(row.content_text, 'artifacts[].content_text'),
439
+ content_hash: expectNullableString(row.content_hash, 'artifacts[].content_hash'),
440
+ metadata_json: expectString(row.metadata_json, 'artifacts[].metadata_json'),
441
+ char_count: expectNumber(row.char_count, 'artifacts[].char_count'),
442
+ created_at: expectNumber(row.created_at, 'artifacts[].created_at'),
443
+ };
444
+ }
445
+
446
+ function parseArtifactBlobRow(value: unknown): ArtifactBlobRow {
447
+ const row = expectRecord(value, 'artifact_blobs[]');
448
+ return {
449
+ content_hash: expectString(row.content_hash, 'artifact_blobs[].content_hash'),
450
+ content_text: expectString(row.content_text, 'artifact_blobs[].content_text'),
451
+ char_count: expectNumber(row.char_count, 'artifact_blobs[].char_count'),
452
+ created_at: expectNumber(row.created_at, 'artifact_blobs[].created_at'),
453
+ };
454
+ }
455
+
456
+ function parseSummaryNodeRow(value: unknown): SummaryNodeRow {
457
+ const row = expectRecord(value, 'summary_nodes[]');
458
+ return {
459
+ node_id: expectString(row.node_id, 'summary_nodes[].node_id'),
460
+ session_id: expectString(row.session_id, 'summary_nodes[].session_id'),
461
+ level: expectNumber(row.level, 'summary_nodes[].level'),
462
+ node_kind: expectString(row.node_kind, 'summary_nodes[].node_kind'),
463
+ start_index: expectNumber(row.start_index, 'summary_nodes[].start_index'),
464
+ end_index: expectNumber(row.end_index, 'summary_nodes[].end_index'),
465
+ message_ids_json: expectString(row.message_ids_json, 'summary_nodes[].message_ids_json'),
466
+ summary_text: expectString(row.summary_text, 'summary_nodes[].summary_text'),
467
+ created_at: expectNumber(row.created_at, 'summary_nodes[].created_at'),
468
+ };
469
+ }
470
+
471
+ function parseSummaryEdgeRow(value: unknown): SummaryEdgeRow {
472
+ const row = expectRecord(value, 'summary_edges[]');
473
+ return {
474
+ session_id: expectString(row.session_id, 'summary_edges[].session_id'),
475
+ parent_id: expectString(row.parent_id, 'summary_edges[].parent_id'),
476
+ child_id: expectString(row.child_id, 'summary_edges[].child_id'),
477
+ child_position: expectNumber(row.child_position, 'summary_edges[].child_position'),
478
+ };
479
+ }
480
+
481
+ function parseSummaryStateRow(value: unknown): SummaryStateRow {
482
+ const row = expectRecord(value, 'summary_state[]');
483
+ return {
484
+ session_id: expectString(row.session_id, 'summary_state[].session_id'),
485
+ archived_count: expectNumber(row.archived_count, 'summary_state[].archived_count'),
486
+ latest_message_created: expectNumber(
487
+ row.latest_message_created,
488
+ 'summary_state[].latest_message_created',
489
+ ),
490
+ archived_signature: expectNullableString(
491
+ row.archived_signature,
492
+ 'summary_state[].archived_signature',
493
+ ),
494
+ root_node_ids_json: expectString(row.root_node_ids_json, 'summary_state[].root_node_ids_json'),
495
+ updated_at: expectNumber(row.updated_at, 'summary_state[].updated_at'),
496
+ };
497
+ }
498
+
499
+ function expectArray<T>(value: unknown, field: string, parseItem: (item: unknown) => T): T[] {
500
+ if (!Array.isArray(value)) throw new Error(`Snapshot field "${field}" must be an array.`);
501
+ return value.map((item) => parseItem(item));
502
+ }
503
+
504
+ function expectRecord(value: unknown, field: string): Record<string, unknown> {
505
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
506
+ throw new Error(`${field} must be an object.`);
507
+ }
508
+ return value as Record<string, unknown>;
509
+ }
510
+
511
+ function expectString(value: unknown, field: string): string {
512
+ if (typeof value !== 'string') throw new Error(`Snapshot field "${field}" must be a string.`);
513
+ return value;
514
+ }
515
+
516
+ function expectNumber(value: unknown, field: string): number {
517
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
518
+ throw new Error(`Snapshot field "${field}" must be a finite number.`);
519
+ }
520
+ return value;
521
+ }
522
+
523
+ function expectNullableString(value: unknown, field: string): string | null {
524
+ if (value === null) return null;
525
+ return expectString(value, field);
526
+ }
527
+
528
+ function expectNullableNumber(value: unknown, field: string): number | null {
529
+ if (value === null) return null;
530
+ return expectNumber(value, field);
531
+ }
532
+
533
+ function listSnapshotWorktreeKeys(
534
+ rows: Array<{ session_directory: string | null; worktree_key: string | null }>,
535
+ ): string[] {
536
+ return [
537
+ ...new Set(
538
+ rows
539
+ .map((row) => row.worktree_key ?? normalizeWorktreeKey(row.session_directory ?? undefined))
540
+ .filter(Boolean),
541
+ ),
542
+ ] as string[];
543
+ }
544
+
545
+ function resolveSnapshotWorktreeMode(mode?: string): SnapshotWorktreeMode {
546
+ return mode === 'preserve' || mode === 'current' ? mode : 'auto';
547
+ }
548
+
549
+ function shouldRehomeImportedSessions(
550
+ sourceWorktreeKeys: string[],
551
+ targetWorktreeKey: string | undefined,
552
+ worktreeMode: SnapshotWorktreeMode,
553
+ ): boolean {
554
+ if (worktreeMode === 'preserve') return false;
555
+ if (worktreeMode === 'current') return Boolean(targetWorktreeKey);
556
+ if (!targetWorktreeKey) return false;
557
+ if (sourceWorktreeKeys.length === 0) return true;
558
+ return sourceWorktreeKeys.length === 1 && sourceWorktreeKeys[0] !== targetWorktreeKey;
559
+ }
560
+
561
+ function rehomeImportedSessionRow(
562
+ session: SessionRow,
563
+ directory: string,
564
+ worktreeKey?: string,
565
+ ): SessionRow {
566
+ return {
567
+ ...session,
568
+ session_directory: directory,
569
+ worktree_key: worktreeKey ?? null,
570
+ };
571
+ }
572
+
573
+ function readExistingSessionIDs(db: SqlDatabaseLike, sessionIDs: string[]): string[] {
574
+ if (sessionIDs.length === 0) return [];
575
+
576
+ const rows = db
577
+ .prepare(
578
+ `SELECT session_id FROM sessions WHERE session_id IN (${sessionIDs.map(() => '?').join(', ')}) ORDER BY session_id ASC`,
579
+ )
580
+ .all(...sessionIDs) as Array<{ session_id: string }>;
581
+ return rows.map((row) => row.session_id);
582
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared type definitions for the SQLite store layer.
3
+ * These types are used across store.ts, sql-utils.ts, and related modules.
4
+ */
5
+
6
+ export type SqlStatementLike = {
7
+ run(...args: unknown[]): unknown;
8
+ get(...args: unknown[]): unknown;
9
+ all(...args: unknown[]): unknown;
10
+ };
11
+
12
+ export type SqlDatabaseLike = {
13
+ exec(sql: string): unknown;
14
+ close(): void;
15
+ prepare(sql: string): SqlStatementLike;
16
+ };