@remnic/import-lossless-claw 0.1.0 → 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +5 -2
- package/dist/index.d.ts +13 -1
- package/dist/index.js +126 -4
- package/dist/index.js.map +1 -1
- package/package.json +21 -13
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Joshua Warren
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -69,8 +69,11 @@ Re-running the importer inserts zero new rows. Messages dedupe on
|
|
|
69
69
|
|
|
70
70
|
- Multi-parent summary DAG → single-parent (lowest `ordinal` wins,
|
|
71
71
|
lexicographic tie-break). Count reported in result.
|
|
72
|
-
- `
|
|
73
|
-
|
|
72
|
+
- `large_files` and compaction telemetry — no Remnic LCM analog, skipped
|
|
73
|
+
silently.
|
|
74
|
+
|
|
75
|
+
`message_parts` is imported when present, including indexed `tool_name`
|
|
76
|
+
and `file_path` columns for structured recall.
|
|
74
77
|
|
|
75
78
|
See the migration doc for the full mapping table.
|
|
76
79
|
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ interface ImportLosslessClawResult {
|
|
|
22
22
|
sessionsTouched: string[];
|
|
23
23
|
messagesInserted: number;
|
|
24
24
|
messagesSkipped: number;
|
|
25
|
+
messagePartsInserted: number;
|
|
26
|
+
messagePartsSkipped: number;
|
|
25
27
|
summariesInserted: number;
|
|
26
28
|
summariesSkipped: number;
|
|
27
29
|
summariesMultiParentCollapsed: number;
|
|
@@ -76,6 +78,15 @@ interface LosslessClawMessage {
|
|
|
76
78
|
identity_hash: string | null;
|
|
77
79
|
created_at: string;
|
|
78
80
|
}
|
|
81
|
+
interface LosslessClawMessagePart {
|
|
82
|
+
message_id: string;
|
|
83
|
+
ordinal: number;
|
|
84
|
+
kind: string;
|
|
85
|
+
payload: string;
|
|
86
|
+
tool_name: string | null;
|
|
87
|
+
file_path: string | null;
|
|
88
|
+
created_at: string | null;
|
|
89
|
+
}
|
|
79
90
|
interface LosslessClawSummary {
|
|
80
91
|
summary_id: string;
|
|
81
92
|
kind: string;
|
|
@@ -103,6 +114,7 @@ interface LosslessClawSummaryMessage {
|
|
|
103
114
|
declare function assertLosslessClawSchema(db: Database.Database): void;
|
|
104
115
|
declare function listConversations(db: Database.Database): LosslessClawConversation[];
|
|
105
116
|
declare function listMessagesForConversation(db: Database.Database, conversationId: string): LosslessClawMessage[];
|
|
117
|
+
declare function listMessageParts(db: Database.Database): LosslessClawMessagePart[];
|
|
106
118
|
declare function listSummaries(db: Database.Database): LosslessClawSummary[];
|
|
107
119
|
declare function listSummaryParents(db: Database.Database): LosslessClawSummaryParent[];
|
|
108
120
|
declare function listSummaryMessages(db: Database.Database): LosslessClawSummaryMessage[];
|
|
@@ -198,4 +210,4 @@ declare function resolveSummarySession(messageIds: string[], sessionByMessageId:
|
|
|
198
210
|
*/
|
|
199
211
|
declare function indexSummaryDerivations(summaryMessages: LosslessClawSummaryMessage[], parents: LosslessClawSummaryParent[]): Map<string, SummaryDerivation>;
|
|
200
212
|
|
|
201
|
-
export { type ImportLosslessClawOptions, type ImportLosslessClawResult, LOSSLESS_CLAW_SOURCE_LABEL, type LosslessClawConversation, type LosslessClawMessage, type LosslessClawSummary, type LosslessClawSummaryMessage, type LosslessClawSummaryParent, type MappedMessage, type MappedSummaryNode, assertLosslessClawSchema, buildMessageMetadata, importLosslessClaw, indexSummaryDerivations, isMultiParent, listConversations, listMessagesForConversation, listSummaries, listSummaryMessages, listSummaryParents, mapMessage, mapSummary, openExistingLcmDatabaseReadOnly, openInMemoryDestinationDatabase, openSourceDatabase, pickCanonicalParent, resolveSessionId, resolveSummarySession };
|
|
213
|
+
export { type ImportLosslessClawOptions, type ImportLosslessClawResult, LOSSLESS_CLAW_SOURCE_LABEL, type LosslessClawConversation, type LosslessClawMessage, type LosslessClawMessagePart, type LosslessClawSummary, type LosslessClawSummaryMessage, type LosslessClawSummaryParent, type MappedMessage, type MappedSummaryNode, assertLosslessClawSchema, buildMessageMetadata, importLosslessClaw, indexSummaryDerivations, isMultiParent, listConversations, listMessageParts, listMessagesForConversation, listSummaries, listSummaryMessages, listSummaryParents, mapMessage, mapSummary, openExistingLcmDatabaseReadOnly, openInMemoryDestinationDatabase, openSourceDatabase, pickCanonicalParent, resolveSessionId, resolveSummarySession };
|
package/dist/index.js
CHANGED
|
@@ -60,6 +60,18 @@ function listMessagesForConversation(db, conversationId) {
|
|
|
60
60
|
"SELECT message_id, conversation_id, seq, role, content, token_count, identity_hash, created_at FROM messages WHERE conversation_id = ? ORDER BY seq"
|
|
61
61
|
).all(conversationId);
|
|
62
62
|
}
|
|
63
|
+
function listMessageParts(db) {
|
|
64
|
+
const hasTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='message_parts'").get();
|
|
65
|
+
if (!hasTable) return [];
|
|
66
|
+
const columns = new Set(
|
|
67
|
+
db.prepare("PRAGMA table_info(message_parts)").all().map((row) => row.name)
|
|
68
|
+
);
|
|
69
|
+
if (!columns.has("message_id")) return [];
|
|
70
|
+
const select = (name, fallback) => columns.has(name) ? name : `${fallback} AS ${name}`;
|
|
71
|
+
return db.prepare(
|
|
72
|
+
`SELECT message_id, ${select("ordinal", "0")}, ${select("kind", "'tool_call'")}, ${select("payload", "'{}'")}, ${select("tool_name", "NULL")}, ${select("file_path", "NULL")}, ${select("created_at", "NULL")} FROM message_parts ORDER BY message_id, ordinal`
|
|
73
|
+
).all();
|
|
74
|
+
}
|
|
63
75
|
function listSummaries(db) {
|
|
64
76
|
return db.prepare(
|
|
65
77
|
"SELECT summary_id, kind, depth, content, token_count, earliest_at, latest_at FROM summaries ORDER BY depth, summary_id"
|
|
@@ -174,6 +186,18 @@ function indexSummaryDerivations(summaryMessages, parents) {
|
|
|
174
186
|
// src/importer.ts
|
|
175
187
|
var NOOP_LOG = (_line) => {
|
|
176
188
|
};
|
|
189
|
+
var LCM_MESSAGE_PART_KINDS = /* @__PURE__ */ new Set([
|
|
190
|
+
"text",
|
|
191
|
+
"tool_call",
|
|
192
|
+
"tool_result",
|
|
193
|
+
"patch",
|
|
194
|
+
"file_read",
|
|
195
|
+
"file_write",
|
|
196
|
+
"step_start",
|
|
197
|
+
"step_finish",
|
|
198
|
+
"snapshot",
|
|
199
|
+
"retry"
|
|
200
|
+
]);
|
|
177
201
|
function importLosslessClaw(options) {
|
|
178
202
|
const { sourceDb, destDb } = options;
|
|
179
203
|
const dryRun = options.dryRun ?? false;
|
|
@@ -185,6 +209,8 @@ function importLosslessClaw(options) {
|
|
|
185
209
|
sessionsTouched: [],
|
|
186
210
|
messagesInserted: 0,
|
|
187
211
|
messagesSkipped: 0,
|
|
212
|
+
messagePartsInserted: 0,
|
|
213
|
+
messagePartsSkipped: 0,
|
|
188
214
|
summariesInserted: 0,
|
|
189
215
|
summariesSkipped: 0,
|
|
190
216
|
summariesMultiParentCollapsed: 0,
|
|
@@ -241,7 +267,7 @@ function importLosslessClaw(options) {
|
|
|
241
267
|
"INSERT INTO lcm_messages_fts (rowid, content) VALUES (?, ?)"
|
|
242
268
|
);
|
|
243
269
|
const existingScanStmt = destDb.prepare(
|
|
244
|
-
"SELECT turn_index, json_extract(metadata, '$.conversation_id') AS conv, json_extract(metadata, '$.source_seq') AS source_seq FROM lcm_messages WHERE session_id = ?"
|
|
270
|
+
"SELECT id, turn_index, json_extract(metadata, '$.conversation_id') AS conv, json_extract(metadata, '$.source_seq') AS source_seq FROM lcm_messages WHERE session_id = ?"
|
|
245
271
|
);
|
|
246
272
|
const existingBySession = /* @__PURE__ */ new Map();
|
|
247
273
|
const maxTurnBySession = /* @__PURE__ */ new Map();
|
|
@@ -253,7 +279,10 @@ function importLosslessClaw(options) {
|
|
|
253
279
|
for (const row of rows) {
|
|
254
280
|
if (row.turn_index > max) max = row.turn_index;
|
|
255
281
|
if (row.conv != null && row.source_seq != null) {
|
|
256
|
-
map.set(`${row.conv}|${row.source_seq}`,
|
|
282
|
+
map.set(`${row.conv}|${row.source_seq}`, {
|
|
283
|
+
turnIndex: row.turn_index,
|
|
284
|
+
rowId: row.id
|
|
285
|
+
});
|
|
257
286
|
}
|
|
258
287
|
}
|
|
259
288
|
existingBySession.set(session, map);
|
|
@@ -261,6 +290,7 @@ function importLosslessClaw(options) {
|
|
|
261
290
|
}
|
|
262
291
|
const sessionsTouched = /* @__PURE__ */ new Set();
|
|
263
292
|
const turnIndexByMessageId = /* @__PURE__ */ new Map();
|
|
293
|
+
const destRowIdByMessageId = /* @__PURE__ */ new Map();
|
|
264
294
|
function assignTurnIndices(forWrite) {
|
|
265
295
|
for (const session of sessionOrder) {
|
|
266
296
|
if (sessionFilter && !sessionFilter.has(session)) continue;
|
|
@@ -271,13 +301,14 @@ function importLosslessClaw(options) {
|
|
|
271
301
|
const key = `${conv.conversation_id}|${msg.seq}`;
|
|
272
302
|
const existingTurn = existing.get(key);
|
|
273
303
|
if (existingTurn !== void 0) {
|
|
274
|
-
turnIndexByMessageId.set(msg.message_id, existingTurn);
|
|
304
|
+
turnIndexByMessageId.set(msg.message_id, existingTurn.turnIndex);
|
|
305
|
+
destRowIdByMessageId.set(msg.message_id, existingTurn.rowId);
|
|
275
306
|
result.messagesSkipped += 1;
|
|
276
307
|
continue;
|
|
277
308
|
}
|
|
278
309
|
const ti = nextTurn++;
|
|
279
310
|
turnIndexByMessageId.set(msg.message_id, ti);
|
|
280
|
-
existing.set(key, ti);
|
|
311
|
+
existing.set(key, { turnIndex: ti, rowId: -1 });
|
|
281
312
|
if (forWrite) {
|
|
282
313
|
const mapped = mapMessage(conv, msg, ti);
|
|
283
314
|
const info = insertMessageStmt.run(
|
|
@@ -293,6 +324,9 @@ function importLosslessClaw(options) {
|
|
|
293
324
|
Number(info.lastInsertRowid),
|
|
294
325
|
mapped.content
|
|
295
326
|
);
|
|
327
|
+
const rowId = Number(info.lastInsertRowid);
|
|
328
|
+
destRowIdByMessageId.set(msg.message_id, rowId);
|
|
329
|
+
existing.set(key, { turnIndex: ti, rowId });
|
|
296
330
|
}
|
|
297
331
|
result.messagesInserted += 1;
|
|
298
332
|
sessionsTouched.add(session);
|
|
@@ -305,6 +339,69 @@ function importLosslessClaw(options) {
|
|
|
305
339
|
} else {
|
|
306
340
|
assignTurnIndices(false);
|
|
307
341
|
}
|
|
342
|
+
const messageParts = listMessageParts(sourceDb);
|
|
343
|
+
const destHasMessageParts = sqliteTableExists(destDb, "lcm_message_parts");
|
|
344
|
+
const existingPartsStmt = destHasMessageParts ? destDb.prepare(
|
|
345
|
+
"SELECT COUNT(*) AS cnt FROM lcm_message_parts WHERE message_id = ?"
|
|
346
|
+
) : void 0;
|
|
347
|
+
const insertPartStmt = destHasMessageParts ? destDb.prepare(
|
|
348
|
+
"INSERT INTO lcm_message_parts (message_id, ordinal, kind, payload, tool_name, file_path, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
349
|
+
) : void 0;
|
|
350
|
+
function processMessageParts(forWrite) {
|
|
351
|
+
const seenDestMessages = /* @__PURE__ */ new Set();
|
|
352
|
+
const blockedDestMessages = /* @__PURE__ */ new Set();
|
|
353
|
+
for (const sourcePart of messageParts) {
|
|
354
|
+
if (!turnIndexByMessageId.has(sourcePart.message_id)) {
|
|
355
|
+
result.messagePartsSkipped += 1;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const destMessageId = destRowIdByMessageId.get(sourcePart.message_id);
|
|
359
|
+
if (destMessageId !== void 0 && destMessageId >= 0) {
|
|
360
|
+
if (blockedDestMessages.has(destMessageId)) {
|
|
361
|
+
result.messagePartsSkipped += 1;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (existingPartsStmt && !seenDestMessages.has(destMessageId)) {
|
|
365
|
+
const existing = existingPartsStmt.get(destMessageId);
|
|
366
|
+
if (existing.cnt > 0) {
|
|
367
|
+
seenDestMessages.add(destMessageId);
|
|
368
|
+
blockedDestMessages.add(destMessageId);
|
|
369
|
+
result.messagePartsSkipped += 1;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
seenDestMessages.add(destMessageId);
|
|
373
|
+
}
|
|
374
|
+
} else if (forWrite) {
|
|
375
|
+
result.messagePartsSkipped += 1;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (forWrite && !insertPartStmt) {
|
|
379
|
+
result.messagePartsSkipped += 1;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (forWrite) {
|
|
383
|
+
const mapped = mapLosslessMessagePart(sourcePart);
|
|
384
|
+
insertPartStmt.run(
|
|
385
|
+
destMessageId,
|
|
386
|
+
mapped.ordinal ?? sourcePart.ordinal,
|
|
387
|
+
mapped.kind,
|
|
388
|
+
JSON.stringify(mapped.payload),
|
|
389
|
+
mapped.toolName ?? null,
|
|
390
|
+
mapped.filePath ?? null,
|
|
391
|
+
mapped.createdAt ?? sourcePart.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
result.messagePartsInserted += 1;
|
|
395
|
+
const session = sessionByMessageId.get(sourcePart.message_id);
|
|
396
|
+
if (session) sessionsTouched.add(session);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (!dryRun) {
|
|
400
|
+
const writeParts = destDb.transaction(() => processMessageParts(true));
|
|
401
|
+
writeParts();
|
|
402
|
+
} else {
|
|
403
|
+
processMessageParts(false);
|
|
404
|
+
}
|
|
308
405
|
const summaries = listSummaries(sourceDb);
|
|
309
406
|
const summaryMessages = listSummaryMessages(sourceDb);
|
|
310
407
|
const summaryParents = listSummaryParents(sourceDb);
|
|
@@ -431,6 +528,30 @@ function importLosslessClaw(options) {
|
|
|
431
528
|
result.sessionsTouched = [...sessionsTouched].sort();
|
|
432
529
|
return result;
|
|
433
530
|
}
|
|
531
|
+
function sqliteTableExists(db, tableName) {
|
|
532
|
+
const row = db.prepare(
|
|
533
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?"
|
|
534
|
+
).get(tableName);
|
|
535
|
+
return row !== void 0;
|
|
536
|
+
}
|
|
537
|
+
function mapLosslessMessagePart(part) {
|
|
538
|
+
const kind = LCM_MESSAGE_PART_KINDS.has(part.kind) ? part.kind : "tool_call";
|
|
539
|
+
let payload;
|
|
540
|
+
try {
|
|
541
|
+
const parsed = JSON.parse(part.payload);
|
|
542
|
+
payload = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { value: parsed };
|
|
543
|
+
} catch {
|
|
544
|
+
payload = { value: part.payload };
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
ordinal: part.ordinal,
|
|
548
|
+
kind,
|
|
549
|
+
payload,
|
|
550
|
+
toolName: part.tool_name,
|
|
551
|
+
filePath: part.file_path,
|
|
552
|
+
createdAt: part.created_at
|
|
553
|
+
};
|
|
554
|
+
}
|
|
434
555
|
export {
|
|
435
556
|
LOSSLESS_CLAW_SOURCE_LABEL,
|
|
436
557
|
assertLosslessClawSchema,
|
|
@@ -439,6 +560,7 @@ export {
|
|
|
439
560
|
indexSummaryDerivations,
|
|
440
561
|
isMultiParent,
|
|
441
562
|
listConversations,
|
|
563
|
+
listMessageParts,
|
|
442
564
|
listMessagesForConversation,
|
|
443
565
|
listSummaries,
|
|
444
566
|
listSummaryMessages,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/source.ts","../src/transform.ts","../src/importer.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Lossless-claw source database access.\n//\n// Reads the schema produced by github.com/martian-engineering/lossless-claw\n// (default location ~/.openclaw/lcm.db). Only the subset of tables that has\n// a clean Remnic-LCM analog is surfaced:\n//\n// conversations → session_id resolution\n// messages → lcm_messages\n// summaries → lcm_summary_nodes\n// summary_messages, summary_parents → derived msg_start/msg_end + parent_id\n//\n// Tables intentionally NOT read: large_files, message_parts,\n// conversation_compaction_telemetry, conversation_compaction_maintenance,\n// lcm_migration_state. None have a Remnic LCM analog and importing them\n// would create dead data.\n// ---------------------------------------------------------------------------\n\nimport { createRequire } from \"node:module\";\n\nimport type Database from \"better-sqlite3\";\n\ntype BetterSqlite3Ctor = typeof import(\"better-sqlite3\");\n\nlet cachedCtor: BetterSqlite3Ctor | null = null;\n\nfunction loadBetterSqlite3(): BetterSqlite3Ctor {\n if (cachedCtor) return cachedCtor;\n const require = createRequire(import.meta.url);\n const loaded = require(\"better-sqlite3\") as\n | BetterSqlite3Ctor\n | { default?: BetterSqlite3Ctor };\n const ctor = typeof loaded === \"function\" ? loaded : loaded.default;\n if (typeof ctor !== \"function\") {\n throw new Error(\n \"better-sqlite3 is unavailable. Install it alongside @remnic/import-lossless-claw \" +\n \"or rebuild from source: `pnpm rebuild better-sqlite3`.\",\n );\n }\n cachedCtor = ctor;\n return ctor;\n}\n\n/**\n * Open a lossless-claw SQLite database file in read-only mode. The CLI uses\n * this so a half-baked source file cannot be written to during import.\n *\n * Tildes in the path are NOT expanded here — callers (CLI, tests) must\n * normalise paths first to keep the boundary explicit (CLAUDE.md gotcha\n * #17).\n */\nexport function openSourceDatabase(filePath: string): Database.Database {\n const Ctor = loadBetterSqlite3();\n return new Ctor(filePath, { readonly: true, fileMustExist: true });\n}\n\n/**\n * Open an in-memory destination database. The caller is expected to apply\n * the Remnic LCM schema via `applyLcmSchema(db)` from `@remnic/core` before\n * passing it to `importLosslessClaw`.\n *\n * Used by the `--dry-run` CLI path as a fallback when no existing on-disk\n * destination exists, so a true write-free run can still compute counts\n * against an empty destination without touching the filesystem.\n */\nexport function openInMemoryDestinationDatabase(): Database.Database {\n const Ctor = loadBetterSqlite3();\n return new Ctor(\":memory:\");\n}\n\n/**\n * Open an existing Remnic LCM database file in read-only mode. Used by the\n * `--dry-run` CLI path so dedup counts reflect the user's real\n * destination state without any write risk (Codex P2 follow-up: a fresh\n * in-memory database makes `messagesSkipped`/`summariesSkipped` always\n * report zero, which is misleading when the user has run a real import\n * before).\n */\nexport function openExistingLcmDatabaseReadOnly(\n filePath: string,\n): Database.Database {\n const Ctor = loadBetterSqlite3();\n return new Ctor(filePath, { readonly: true, fileMustExist: true });\n}\n\nexport interface LosslessClawConversation {\n conversation_id: string;\n session_id: string | null;\n session_key: string | null;\n title: string | null;\n}\n\nexport interface LosslessClawMessage {\n message_id: string;\n conversation_id: string;\n seq: number;\n role: string;\n content: string;\n token_count: number;\n identity_hash: string | null;\n created_at: string;\n}\n\nexport interface LosslessClawSummary {\n summary_id: string;\n kind: string;\n depth: number;\n content: string;\n token_count: number;\n earliest_at: string | null;\n latest_at: string | null;\n}\n\nexport interface LosslessClawSummaryParent {\n summary_id: string;\n parent_summary_id: string;\n ordinal: number;\n}\n\nexport interface LosslessClawSummaryMessage {\n summary_id: string;\n message_id: string;\n}\n\n/**\n * Verify a database handle points at a lossless-claw export by checking for\n * the required tables. Throws a user-facing error on mismatch so callers can\n * surface a clear \"this isn't a lossless-claw database\" message instead of\n * cryptic SQL errors during import.\n */\nexport function assertLosslessClawSchema(db: Database.Database): void {\n const required = [\n \"conversations\",\n \"messages\",\n \"summaries\",\n \"summary_messages\",\n \"summary_parents\",\n ];\n const stmt = db.prepare(\n \"SELECT name FROM sqlite_master WHERE type='table' AND name = ?\",\n );\n const missing: string[] = [];\n for (const name of required) {\n const row = stmt.get(name) as { name: string } | undefined;\n if (!row) missing.push(name);\n }\n if (missing.length > 0) {\n throw new Error(\n `Source database is missing lossless-claw tables: ${missing.join(\", \")}. ` +\n \"Confirm --src points at a lossless-claw lcm.db file.\",\n );\n }\n}\n\nexport function listConversations(\n db: Database.Database,\n): LosslessClawConversation[] {\n return db\n .prepare(\n \"SELECT conversation_id, session_id, session_key, title FROM conversations ORDER BY conversation_id\",\n )\n .all() as LosslessClawConversation[];\n}\n\nexport function listMessagesForConversation(\n db: Database.Database,\n conversationId: string,\n): LosslessClawMessage[] {\n return db\n .prepare(\n \"SELECT message_id, conversation_id, seq, role, content, token_count, identity_hash, created_at \" +\n \"FROM messages WHERE conversation_id = ? ORDER BY seq\",\n )\n .all(conversationId) as LosslessClawMessage[];\n}\n\nexport function listSummaries(db: Database.Database): LosslessClawSummary[] {\n return db\n .prepare(\n \"SELECT summary_id, kind, depth, content, token_count, earliest_at, latest_at \" +\n \"FROM summaries ORDER BY depth, summary_id\",\n )\n .all() as LosslessClawSummary[];\n}\n\nexport function listSummaryParents(\n db: Database.Database,\n): LosslessClawSummaryParent[] {\n return db\n .prepare(\n \"SELECT summary_id, parent_summary_id, ordinal FROM summary_parents ORDER BY summary_id, ordinal\",\n )\n .all() as LosslessClawSummaryParent[];\n}\n\nexport function listSummaryMessages(\n db: Database.Database,\n): LosslessClawSummaryMessage[] {\n return db\n .prepare(\n \"SELECT summary_id, message_id FROM summary_messages\",\n )\n .all() as LosslessClawSummaryMessage[];\n}\n","// ---------------------------------------------------------------------------\n// Pure mapping functions: lossless-claw rows → Remnic LCM rows.\n//\n// Kept side-effect-free so they can be unit-tested without SQLite in the\n// loop. The orchestration in importer.ts handles I/O.\n// ---------------------------------------------------------------------------\n\nimport type {\n LosslessClawConversation,\n LosslessClawMessage,\n LosslessClawSummary,\n LosslessClawSummaryParent,\n LosslessClawSummaryMessage,\n} from \"./source.js\";\n\nexport const LOSSLESS_CLAW_SOURCE_LABEL = \"lossless-claw\";\n\nexport interface MappedMessage {\n session_id: string;\n turn_index: number;\n role: string;\n content: string;\n token_count: number;\n created_at: string;\n metadata: string;\n}\n\nexport interface MappedSummaryNode {\n id: string;\n session_id: string;\n depth: number;\n parent_id: string | null;\n summary_text: string;\n token_count: number;\n msg_start: number;\n msg_end: number;\n escalation: number;\n created_at: string;\n}\n\n/**\n * Resolve a conversation row to a Remnic session_id. Prefer the explicit\n * session_id field; fall back to conversation_id when null/empty so every\n * imported row has a stable session anchor.\n */\nexport function resolveSessionId(\n conversation: LosslessClawConversation,\n): string {\n const candidate = conversation.session_id?.trim();\n if (candidate && candidate.length > 0) return candidate;\n return conversation.conversation_id;\n}\n\n/**\n * Build a JSON metadata blob attached to each imported message. Sorted keys\n * (gotcha #38) so dedup or hashing downstream stays stable across runs.\n *\n * `source_seq` is the original `messages.seq` value from lossless-claw —\n * preserved alongside `conversation_id` so dedup can use a stable source\n * identity. The Remnic LCM `turn_index` is now a session-global running\n * counter (Codex P1: previously equal to `seq`, which collided when\n * multiple source conversations resolved to the same session).\n */\nexport function buildMessageMetadata(\n conversation: LosslessClawConversation,\n message: LosslessClawMessage,\n): string {\n const meta: Record<string, string | number | null> = {\n conversation_id: conversation.conversation_id,\n identity_hash: message.identity_hash ?? null,\n source: LOSSLESS_CLAW_SOURCE_LABEL,\n source_seq: message.seq,\n title: conversation.title ?? null,\n };\n const sorted = Object.keys(meta)\n .sort()\n .reduce<Record<string, string | number | null>>((acc, key) => {\n acc[key] = meta[key] ?? null;\n return acc;\n }, {});\n return JSON.stringify(sorted);\n}\n\n/**\n * Map a source message to a Remnic LCM row. `turnIndex` is supplied by the\n * caller (importer.ts) which assigns a session-global running counter so\n * multiple conversations sharing one session id do not collide on\n * (session_id, turn_index).\n */\nexport function mapMessage(\n conversation: LosslessClawConversation,\n message: LosslessClawMessage,\n turnIndex: number,\n): MappedMessage {\n return {\n session_id: resolveSessionId(conversation),\n turn_index: turnIndex,\n role: message.role,\n content: message.content,\n token_count: message.token_count,\n created_at: message.created_at,\n metadata: buildMessageMetadata(conversation, message),\n };\n}\n\nexport interface SummaryDerivation {\n parents: LosslessClawSummaryParent[];\n messageIds: string[];\n}\n\n/**\n * Pick the canonical parent id from a multi-parent DAG row. lossless-claw\n * supports many-to-many parent edges; Remnic's `lcm_summary_nodes.parent_id`\n * is a single FK. Lowest ordinal wins (tie-break: lexicographic id) so the\n * choice is deterministic. Multi-parent rows are reported by the importer so\n * users have visibility into the lossy edge.\n */\nexport function pickCanonicalParent(\n parents: LosslessClawSummaryParent[],\n): string | null {\n if (parents.length === 0) return null;\n const sorted = [...parents].sort((a, b) => {\n if (a.ordinal !== b.ordinal) return a.ordinal - b.ordinal;\n return a.parent_summary_id.localeCompare(b.parent_summary_id);\n });\n return sorted[0]!.parent_summary_id;\n}\n\nexport interface MapSummaryInput {\n summary: LosslessClawSummary;\n parents: LosslessClawSummaryParent[];\n /** Sequence numbers of messages this summary covers. */\n messageSeqs: number[];\n /** Resolved session id (single value — multi-session summaries error). */\n sessionId: string;\n}\n\nexport function mapSummary(input: MapSummaryInput): MappedSummaryNode {\n if (input.messageSeqs.length === 0) {\n throw new Error(\n `Summary ${input.summary.summary_id} has no message references; ` +\n \"cannot derive msg_start/msg_end. Skip this summary at the caller.\",\n );\n }\n // Iterative min/max — `Math.min(...arr)` / `Math.max(...arr)` push every\n // element onto the call stack via spread and throw `RangeError: Maximum\n // call stack size exceeded` on summaries that cover tens of thousands of\n // messages (Cursor Bugbot review on PR #797).\n let msg_start = input.messageSeqs[0]!;\n let msg_end = msg_start;\n for (let i = 1; i < input.messageSeqs.length; i++) {\n const seq = input.messageSeqs[i]!;\n if (seq < msg_start) msg_start = seq;\n if (seq > msg_end) msg_end = seq;\n }\n return {\n id: input.summary.summary_id,\n session_id: input.sessionId,\n depth: input.summary.depth,\n parent_id: pickCanonicalParent(input.parents),\n summary_text: input.summary.content,\n token_count: input.summary.token_count,\n msg_start,\n msg_end,\n escalation: 0,\n created_at:\n input.summary.latest_at ?? input.summary.earliest_at ?? new Date().toISOString(),\n };\n}\n\n/**\n * Determine if a summary has multiple parents (lossy-collapse signal).\n */\nexport function isMultiParent(parents: LosslessClawSummaryParent[]): boolean {\n return parents.length > 1;\n}\n\n/**\n * Resolve the (probable) single session for a summary by looking at the\n * messages it covers. lossless-claw summaries technically span multiple\n * conversations only via DAG construction, which Remnic's per-session\n * structure cannot represent — return null in that case so the caller can\n * skip the summary with a warning rather than picking a wrong session.\n *\n * Strict on dangling references: if ANY referenced message_id fails to\n * resolve to a session, return null. Silently dropping unresolved IDs\n * would let a summary with mixed valid + dangling refs pass through\n * with msg_start/msg_end computed from only the resolved subset, mis-\n * representing the summary's true coverage (Codex P2 review on PR #797).\n */\nexport function resolveSummarySession(\n messageIds: string[],\n sessionByMessageId: ReadonlyMap<string, string>,\n): string | null {\n if (messageIds.length === 0) return null;\n const sessions = new Set<string>();\n for (const messageId of messageIds) {\n const session = sessionByMessageId.get(messageId);\n if (!session) return null; // dangling reference — refuse to import\n sessions.add(session);\n }\n if (sessions.size !== 1) return null;\n return [...sessions][0]!;\n}\n\n/**\n * Index summary_messages and messages so we can emit per-summary message-id\n * lists and seq lists without N+1 queries. Pure helper.\n */\nexport function indexSummaryDerivations(\n summaryMessages: LosslessClawSummaryMessage[],\n parents: LosslessClawSummaryParent[],\n): Map<string, SummaryDerivation> {\n const out = new Map<string, SummaryDerivation>();\n for (const sm of summaryMessages) {\n const entry = out.get(sm.summary_id) ?? { parents: [], messageIds: [] };\n entry.messageIds.push(sm.message_id);\n out.set(sm.summary_id, entry);\n }\n for (const p of parents) {\n const entry = out.get(p.summary_id) ?? { parents: [], messageIds: [] };\n entry.parents.push(p);\n out.set(p.summary_id, entry);\n }\n return out;\n}\n","// ---------------------------------------------------------------------------\n// lossless-claw → Remnic LCM importer (orchestration)\n//\n// Streams rows from a lossless-claw SQLite export into a Remnic LCM\n// SQLite database opened by the caller. The Remnic database must already\n// have its schema applied (use openLcmDatabase() from @remnic/core).\n//\n// Idempotency: messages are keyed on (session_id, turn_index) — the same\n// natural key Remnic's own indexer uses. Summary nodes are keyed on the\n// preserved primary id.\n//\n// FTS sync: lcm_messages_fts and lcm_summaries_fts are external-content\n// FTS5 tables, so every insert must be mirrored. We do this in the same\n// transaction as the row write to keep the index consistent on crash.\n//\n// Compaction-event boundary: per-session, we insert one row into\n// lcm_compaction_events with tokens_before == tokens_after, marking the\n// post-import state from which Remnic's own compaction will operate.\n// ---------------------------------------------------------------------------\n\nimport type Database from \"better-sqlite3\";\n\nimport {\n assertLosslessClawSchema,\n listConversations,\n listMessagesForConversation,\n listSummaries,\n listSummaryMessages,\n listSummaryParents,\n type LosslessClawConversation,\n type LosslessClawMessage,\n} from \"./source.js\";\nimport {\n indexSummaryDerivations,\n isMultiParent,\n mapMessage,\n mapSummary,\n resolveSessionId,\n resolveSummarySession,\n} from \"./transform.js\";\n\nexport interface ImportLosslessClawOptions {\n /** Open lossless-claw source database (read-only OK). */\n sourceDb: Database.Database;\n /** Open Remnic LCM destination database with schema applied. */\n destDb: Database.Database;\n /** When true, run all reads + transformations but skip writes. */\n dryRun?: boolean;\n /**\n * Optional set of session_ids (post-resolve) to import.\n *\n * `undefined` or an empty Set both mean \"import every session\".\n * Pass a non-empty Set to restrict to specific resolved session ids.\n */\n sessionFilter?: ReadonlySet<string>;\n /** Hook for status output (defaults to no-op). */\n onLog?: (line: string) => void;\n}\n\nexport interface ImportLosslessClawResult {\n conversationsScanned: number;\n sessionsTouched: string[];\n messagesInserted: number;\n messagesSkipped: number;\n summariesInserted: number;\n summariesSkipped: number;\n summariesMultiParentCollapsed: number;\n summariesSkippedNoMessages: number;\n summariesSkippedMultiSession: number;\n compactionEventsInserted: number;\n dryRun: boolean;\n}\n\nconst NOOP_LOG = (_line: string): void => {\n /* default sink */\n};\n\nexport function importLosslessClaw(\n options: ImportLosslessClawOptions,\n): ImportLosslessClawResult {\n const { sourceDb, destDb } = options;\n const dryRun = options.dryRun ?? false;\n // Normalise sessionFilter: an empty Set is truthy in JavaScript, so a\n // raw `sessionFilter && !sessionFilter.has(session)` guard would skip\n // every session if a caller passed `new Set()` expecting \"import all\"\n // (the documented contract on the option). Treat empty-Set the same\n // as undefined here so every guard below is correct (Cursor Bugbot\n // review on PR #797).\n const sessionFilter =\n options.sessionFilter && options.sessionFilter.size > 0\n ? options.sessionFilter\n : undefined;\n const log = options.onLog ?? NOOP_LOG;\n\n assertLosslessClawSchema(sourceDb);\n\n const result: ImportLosslessClawResult = {\n conversationsScanned: 0,\n sessionsTouched: [],\n messagesInserted: 0,\n messagesSkipped: 0,\n summariesInserted: 0,\n summariesSkipped: 0,\n summariesMultiParentCollapsed: 0,\n summariesSkippedNoMessages: 0,\n summariesSkippedMultiSession: 0,\n compactionEventsInserted: 0,\n dryRun,\n };\n\n // ── Pre-resolve session ids per conversation + per message id ──────────\n const conversations = listConversations(sourceDb);\n result.conversationsScanned = conversations.length;\n\n const sessionByConvId = new Map<string, string>();\n const sessionByMessageId = new Map<string, string>();\n\n for (const c of conversations) {\n sessionByConvId.set(c.conversation_id, resolveSessionId(c));\n }\n\n // Materialize messages once per conversation; reused for the write pass\n // and (via sessionByMessageId) for summary mapping.\n const messagesByConv = new Map<\n string,\n ReturnType<typeof listMessagesForConversation>\n >();\n\n for (const c of conversations) {\n const msgs = listMessagesForConversation(sourceDb, c.conversation_id);\n messagesByConv.set(c.conversation_id, msgs);\n const session = sessionByConvId.get(c.conversation_id)!;\n for (const m of msgs) {\n sessionByMessageId.set(m.message_id, session);\n }\n }\n\n // Build a per-session list of (conversation, message) pairs and sort\n // by message.created_at. This handles interleaved conversations\n // correctly: if conv-A has messages at t=0 and t=10 and conv-B has\n // messages at t=5 and t=6, turn_index ends up as t=0, t=5, t=6,\n // t=10 (chronological), not t=0, t=10, t=5, t=6 (which a per-\n // conversation pre-sort produces — Codex P1 follow-up review).\n type SessionEntry = {\n conv: LosslessClawConversation;\n msg: LosslessClawMessage;\n };\n const sessionMessages = new Map<string, SessionEntry[]>();\n const sessionOrder: string[] = [];\n for (const c of conversations) {\n const session = sessionByConvId.get(c.conversation_id)!;\n if (!sessionMessages.has(session)) {\n sessionMessages.set(session, []);\n sessionOrder.push(session);\n }\n const list = sessionMessages.get(session)!;\n for (const m of messagesByConv.get(c.conversation_id) ?? []) {\n list.push({ conv: c, msg: m });\n }\n }\n for (const list of sessionMessages.values()) {\n list.sort((a, b) => {\n if (a.msg.created_at !== b.msg.created_at) {\n return a.msg.created_at < b.msg.created_at ? -1 : 1;\n }\n // Stable tie-breaker chain when timestamps collide: conversation\n // id, then per-conversation seq (preserves intra-conversation\n // order even on identical timestamps).\n const cidCmp = a.conv.conversation_id.localeCompare(\n b.conv.conversation_id,\n );\n if (cidCmp !== 0) return cidCmp;\n return a.msg.seq - b.msg.seq;\n });\n }\n\n // ── Insert messages ────────────────────────────────────────────────────\n // Dedup uses source identity (`metadata.conversation_id` +\n // `metadata.source_seq`) rather than `(session_id, turn_index)` so two\n // source conversations sharing one session can both contribute messages\n // without one's `seq=N` masking the other's `seq=N` (Codex P1 review).\n //\n // To avoid the O(n²) behavior of a per-row `json_extract` lookup with\n // no covering index (Codex P2 review), pre-fetch existing source\n // identities once per affected session into an in-memory Map. The\n // import loop then does O(1) Map lookups for dedup.\n const insertMessageStmt = destDb.prepare(\n \"INSERT INTO lcm_messages (session_id, turn_index, role, content, token_count, created_at, metadata) \" +\n \"VALUES (?, ?, ?, ?, ?, ?, ?)\",\n );\n const insertMessageFtsStmt = destDb.prepare(\n \"INSERT INTO lcm_messages_fts (rowid, content) VALUES (?, ?)\",\n );\n const existingScanStmt = destDb.prepare(\n \"SELECT turn_index, \" +\n \"json_extract(metadata, '$.conversation_id') AS conv, \" +\n \"json_extract(metadata, '$.source_seq') AS source_seq \" +\n \"FROM lcm_messages WHERE session_id = ?\",\n );\n\n // session → \"convId|seq\" → turn_index of the existing row. Lookup is\n // O(1) Map membership instead of a per-row JSON-extract scan.\n const existingBySession = new Map<string, Map<string, number>>();\n // session → max(turn_index) currently in dest (so new rows append).\n const maxTurnBySession = new Map<string, number>();\n for (const session of sessionMessages.keys()) {\n if (sessionFilter && !sessionFilter.has(session)) continue;\n const map = new Map<string, number>();\n let max = -1;\n const rows = existingScanStmt.iterate(session) as Iterable<{\n turn_index: number;\n conv: string | null;\n source_seq: number | null;\n }>;\n for (const row of rows) {\n if (row.turn_index > max) max = row.turn_index;\n if (row.conv != null && row.source_seq != null) {\n map.set(`${row.conv}|${row.source_seq}`, row.turn_index);\n }\n }\n existingBySession.set(session, map);\n maxTurnBySession.set(session, max);\n }\n\n const sessionsTouched = new Set<string>();\n // Mapping from source message_id → assigned (or pre-existing)\n // turn_index. Populated for both inserted rows and dedup-skipped rows\n // so summary mapping (msg_start/msg_end) reflects real turn indices.\n const turnIndexByMessageId = new Map<string, number>();\n\n function assignTurnIndices(forWrite: boolean): void {\n for (const session of sessionOrder) {\n if (sessionFilter && !sessionFilter.has(session)) continue;\n const entries = sessionMessages.get(session) ?? [];\n const existing =\n existingBySession.get(session) ?? new Map<string, number>();\n let nextTurn = (maxTurnBySession.get(session) ?? -1) + 1;\n for (const { conv, msg } of entries) {\n const key = `${conv.conversation_id}|${msg.seq}`;\n const existingTurn = existing.get(key);\n if (existingTurn !== undefined) {\n turnIndexByMessageId.set(msg.message_id, existingTurn);\n result.messagesSkipped += 1;\n continue;\n }\n const ti = nextTurn++;\n turnIndexByMessageId.set(msg.message_id, ti);\n // Update the in-memory dedup map so duplicates within this\n // run also count as skips on subsequent passes (defensive;\n // shouldn't happen with valid source data).\n existing.set(key, ti);\n if (forWrite) {\n const mapped = mapMessage(conv, msg, ti);\n const info = insertMessageStmt.run(\n mapped.session_id,\n mapped.turn_index,\n mapped.role,\n mapped.content,\n mapped.token_count,\n mapped.created_at,\n mapped.metadata,\n );\n insertMessageFtsStmt.run(\n Number(info.lastInsertRowid),\n mapped.content,\n );\n }\n result.messagesInserted += 1;\n sessionsTouched.add(session);\n }\n }\n }\n\n if (!dryRun) {\n const writeMessages = destDb.transaction(() => assignTurnIndices(true));\n writeMessages();\n } else {\n // Dry run: walk the same iteration to populate counters and\n // turnIndexByMessageId without mutating either DB.\n assignTurnIndices(false);\n }\n\n // ── Insert summaries ───────────────────────────────────────────────────\n const summaries = listSummaries(sourceDb);\n const summaryMessages = listSummaryMessages(sourceDb);\n const summaryParents = listSummaryParents(sourceDb);\n const derivations = indexSummaryDerivations(summaryMessages, summaryParents);\n\n const summaryExistsStmt = destDb.prepare(\n \"SELECT 1 AS hit FROM lcm_summary_nodes WHERE id = ? LIMIT 1\",\n );\n const insertSummaryStmt = destDb.prepare(\n \"INSERT INTO lcm_summary_nodes (id, session_id, depth, parent_id, summary_text, token_count, msg_start, msg_end, escalation, created_at) \" +\n \"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\",\n );\n const insertSummaryFtsStmt = destDb.prepare(\n \"INSERT INTO lcm_summaries_fts (rowid, summary_text) VALUES (?, ?)\",\n );\n const lookupSummaryRowidStmt = destDb.prepare(\n \"SELECT rowid AS rowid FROM lcm_summary_nodes WHERE id = ?\",\n );\n\n // Single shared loop body for both write and dry-run paths so summary\n // filter conditions (skip-no-messages, multi-session, dedup, etc.)\n // can never silently diverge between modes (Cursor Bugbot review).\n function processSummaries(forWrite: boolean): void {\n for (const summary of summaries) {\n const derivation = derivations.get(summary.summary_id);\n if (!derivation || derivation.messageIds.length === 0) {\n result.summariesSkippedNoMessages += 1;\n log(\n `skip summary ${summary.summary_id}: no message references in summary_messages`,\n );\n continue;\n }\n const session = resolveSummarySession(\n derivation.messageIds,\n sessionByMessageId,\n );\n if (!session) {\n result.summariesSkippedMultiSession += 1;\n log(\n `skip summary ${summary.summary_id}: covers messages from multiple sessions or has dangling references`,\n );\n continue;\n }\n if (sessionFilter && !sessionFilter.has(session)) continue;\n\n const messageSeqs: number[] = [];\n for (const mid of derivation.messageIds) {\n const seq = turnIndexByMessageId.get(mid);\n if (typeof seq === \"number\") messageSeqs.push(seq);\n }\n if (messageSeqs.length === 0) {\n result.summariesSkippedNoMessages += 1;\n log(\n `skip summary ${summary.summary_id}: message ids exist but seqs unresolved`,\n );\n continue;\n }\n\n const mapped = mapSummary({\n summary,\n parents: derivation.parents,\n messageSeqs,\n sessionId: session,\n });\n\n if (isMultiParent(derivation.parents)) {\n result.summariesMultiParentCollapsed += 1;\n log(\n `summary ${summary.summary_id} has ${derivation.parents.length} parents; ` +\n `keeping ${mapped.parent_id ?? \"(none)\"} (Remnic LCM is single-parent).`,\n );\n }\n\n const existing = summaryExistsStmt.get(mapped.id) as\n | { hit: number }\n | undefined;\n if (existing) {\n result.summariesSkipped += 1;\n continue;\n }\n if (forWrite) {\n insertSummaryStmt.run(\n mapped.id,\n mapped.session_id,\n mapped.depth,\n mapped.parent_id,\n mapped.summary_text,\n mapped.token_count,\n mapped.msg_start,\n mapped.msg_end,\n mapped.escalation,\n mapped.created_at,\n );\n const row = lookupSummaryRowidStmt.get(mapped.id) as\n | { rowid: number }\n | undefined;\n if (row) {\n insertSummaryFtsStmt.run(row.rowid, mapped.summary_text);\n }\n }\n result.summariesInserted += 1;\n sessionsTouched.add(mapped.session_id);\n }\n }\n\n if (!dryRun) {\n const writeSummaries = destDb.transaction(() => processSummaries(true));\n writeSummaries();\n } else {\n processSummaries(false);\n }\n\n // ── Compaction-event boundary ──────────────────────────────────────────\n // Insert one marker row per session that gained data. tokens_before\n // equals tokens_after to encode \"this is an import boundary, not a real\n // compaction event\"; any consumer that needs the distinction can detect\n // the equality.\n //\n // Token totals are queried from the destination at boundary-write time\n // rather than accumulated from this run's newly-inserted rows. That\n // way a session whose only new rows are summaries (e.g. partial retry\n // after a crash between message and summary transactions) still gets\n // a correct anchor reflecting the messages already in the destination\n // (Cursor Bugbot review on PR #797).\n // Always count what compaction events WOULD be written so dry-run\n // output matches the rest of the counters (Cursor Bugbot review on\n // PR #797: dry-run was reporting `Messages inserted: N` but\n // `Compaction events written: 0` despite the documented \"count what\n // would be imported\" contract). Skip the actual INSERTs in dry-run.\n const insertEventStmt = destDb.prepare(\n \"INSERT INTO lcm_compaction_events (session_id, fired_at, msg_before, tokens_before, tokens_after) \" +\n \"VALUES (?, ?, ?, ?, ?)\",\n );\n const maxTurnStmt = destDb.prepare(\n \"SELECT IFNULL(MAX(turn_index), -1) AS max_turn FROM lcm_messages WHERE session_id = ?\",\n );\n const totalTokensStmt = destDb.prepare(\n \"SELECT IFNULL(SUM(token_count), 0) AS total FROM lcm_messages WHERE session_id = ?\",\n );\n\n function processCompactionBoundaries(forWrite: boolean): void {\n const firedAt = new Date().toISOString();\n for (const session of sessionsTouched) {\n const turnRow = maxTurnStmt.get(session) as { max_turn: number };\n const msgBefore = turnRow.max_turn + 1;\n const tokRow = totalTokensStmt.get(session) as { total: number };\n const tokens = tokRow.total;\n if (forWrite) {\n insertEventStmt.run(session, firedAt, msgBefore, tokens, tokens);\n }\n result.compactionEventsInserted += 1;\n }\n }\n\n if (!dryRun) {\n const writeEvents = destDb.transaction(() => processCompactionBoundaries(true));\n writeEvents();\n } else {\n processCompactionBoundaries(false);\n }\n\n result.sessionsTouched = [...sessionsTouched].sort();\n return result;\n}\n"],"mappings":"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","names":["require"]}
|
|
1
|
+
{"version":3,"sources":["../src/source.ts","../src/transform.ts","../src/importer.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Lossless-claw source database access.\n//\n// Reads the schema produced by github.com/martian-engineering/lossless-claw\n// (default location ~/.openclaw/lcm.db). Only the subset of tables that has\n// a clean Remnic-LCM analog is surfaced:\n//\n// conversations → session_id resolution\n// messages → lcm_messages\n// message_parts → lcm_message_parts (when present)\n// summaries → lcm_summary_nodes\n// summary_messages, summary_parents → derived msg_start/msg_end + parent_id\n//\n// Tables intentionally NOT read: large_files,\n// conversation_compaction_telemetry, conversation_compaction_maintenance,\n// lcm_migration_state. None have a Remnic LCM analog and importing them\n// would create dead data.\n// ---------------------------------------------------------------------------\n\nimport { createRequire } from \"node:module\";\n\nimport type Database from \"better-sqlite3\";\n\ntype BetterSqlite3Ctor = typeof import(\"better-sqlite3\");\n\nlet cachedCtor: BetterSqlite3Ctor | null = null;\n\nfunction loadBetterSqlite3(): BetterSqlite3Ctor {\n if (cachedCtor) return cachedCtor;\n const require = createRequire(import.meta.url);\n const loaded = require(\"better-sqlite3\") as\n | BetterSqlite3Ctor\n | { default?: BetterSqlite3Ctor };\n const ctor = typeof loaded === \"function\" ? loaded : loaded.default;\n if (typeof ctor !== \"function\") {\n throw new Error(\n \"better-sqlite3 is unavailable. Install it alongside @remnic/import-lossless-claw \" +\n \"or rebuild from source: `pnpm rebuild better-sqlite3`.\",\n );\n }\n cachedCtor = ctor;\n return ctor;\n}\n\n/**\n * Open a lossless-claw SQLite database file in read-only mode. The CLI uses\n * this so a half-baked source file cannot be written to during import.\n *\n * Tildes in the path are NOT expanded here — callers (CLI, tests) must\n * normalise paths first to keep the boundary explicit (CLAUDE.md gotcha\n * #17).\n */\nexport function openSourceDatabase(filePath: string): Database.Database {\n const Ctor = loadBetterSqlite3();\n return new Ctor(filePath, { readonly: true, fileMustExist: true });\n}\n\n/**\n * Open an in-memory destination database. The caller is expected to apply\n * the Remnic LCM schema via `applyLcmSchema(db)` from `@remnic/core` before\n * passing it to `importLosslessClaw`.\n *\n * Used by the `--dry-run` CLI path as a fallback when no existing on-disk\n * destination exists, so a true write-free run can still compute counts\n * against an empty destination without touching the filesystem.\n */\nexport function openInMemoryDestinationDatabase(): Database.Database {\n const Ctor = loadBetterSqlite3();\n return new Ctor(\":memory:\");\n}\n\n/**\n * Open an existing Remnic LCM database file in read-only mode. Used by the\n * `--dry-run` CLI path so dedup counts reflect the user's real\n * destination state without any write risk (Codex P2 follow-up: a fresh\n * in-memory database makes `messagesSkipped`/`summariesSkipped` always\n * report zero, which is misleading when the user has run a real import\n * before).\n */\nexport function openExistingLcmDatabaseReadOnly(\n filePath: string,\n): Database.Database {\n const Ctor = loadBetterSqlite3();\n return new Ctor(filePath, { readonly: true, fileMustExist: true });\n}\n\nexport interface LosslessClawConversation {\n conversation_id: string;\n session_id: string | null;\n session_key: string | null;\n title: string | null;\n}\n\nexport interface LosslessClawMessage {\n message_id: string;\n conversation_id: string;\n seq: number;\n role: string;\n content: string;\n token_count: number;\n identity_hash: string | null;\n created_at: string;\n}\n\nexport interface LosslessClawMessagePart {\n message_id: string;\n ordinal: number;\n kind: string;\n payload: string;\n tool_name: string | null;\n file_path: string | null;\n created_at: string | null;\n}\n\nexport interface LosslessClawSummary {\n summary_id: string;\n kind: string;\n depth: number;\n content: string;\n token_count: number;\n earliest_at: string | null;\n latest_at: string | null;\n}\n\nexport interface LosslessClawSummaryParent {\n summary_id: string;\n parent_summary_id: string;\n ordinal: number;\n}\n\nexport interface LosslessClawSummaryMessage {\n summary_id: string;\n message_id: string;\n}\n\n/**\n * Verify a database handle points at a lossless-claw export by checking for\n * the required tables. Throws a user-facing error on mismatch so callers can\n * surface a clear \"this isn't a lossless-claw database\" message instead of\n * cryptic SQL errors during import.\n */\nexport function assertLosslessClawSchema(db: Database.Database): void {\n const required = [\n \"conversations\",\n \"messages\",\n \"summaries\",\n \"summary_messages\",\n \"summary_parents\",\n ];\n const stmt = db.prepare(\n \"SELECT name FROM sqlite_master WHERE type='table' AND name = ?\",\n );\n const missing: string[] = [];\n for (const name of required) {\n const row = stmt.get(name) as { name: string } | undefined;\n if (!row) missing.push(name);\n }\n if (missing.length > 0) {\n throw new Error(\n `Source database is missing lossless-claw tables: ${missing.join(\", \")}. ` +\n \"Confirm --src points at a lossless-claw lcm.db file.\",\n );\n }\n}\n\nexport function listConversations(\n db: Database.Database,\n): LosslessClawConversation[] {\n return db\n .prepare(\n \"SELECT conversation_id, session_id, session_key, title FROM conversations ORDER BY conversation_id\",\n )\n .all() as LosslessClawConversation[];\n}\n\nexport function listMessagesForConversation(\n db: Database.Database,\n conversationId: string,\n): LosslessClawMessage[] {\n return db\n .prepare(\n \"SELECT message_id, conversation_id, seq, role, content, token_count, identity_hash, created_at \" +\n \"FROM messages WHERE conversation_id = ? ORDER BY seq\",\n )\n .all(conversationId) as LosslessClawMessage[];\n}\n\nexport function listMessageParts(db: Database.Database): LosslessClawMessagePart[] {\n const hasTable = db\n .prepare(\"SELECT name FROM sqlite_master WHERE type='table' AND name='message_parts'\")\n .get() as { name: string } | undefined;\n if (!hasTable) return [];\n\n const columns = new Set(\n (db.prepare(\"PRAGMA table_info(message_parts)\").all() as Array<{ name: string }>)\n .map((row) => row.name),\n );\n if (!columns.has(\"message_id\")) return [];\n\n const select = (name: string, fallback: string): string =>\n columns.has(name) ? name : `${fallback} AS ${name}`;\n return db\n .prepare(\n \"SELECT \" +\n \"message_id, \" +\n `${select(\"ordinal\", \"0\")}, ` +\n `${select(\"kind\", \"'tool_call'\")}, ` +\n `${select(\"payload\", \"'{}'\")}, ` +\n `${select(\"tool_name\", \"NULL\")}, ` +\n `${select(\"file_path\", \"NULL\")}, ` +\n `${select(\"created_at\", \"NULL\")} ` +\n \"FROM message_parts ORDER BY message_id, ordinal\",\n )\n .all() as LosslessClawMessagePart[];\n}\n\nexport function listSummaries(db: Database.Database): LosslessClawSummary[] {\n return db\n .prepare(\n \"SELECT summary_id, kind, depth, content, token_count, earliest_at, latest_at \" +\n \"FROM summaries ORDER BY depth, summary_id\",\n )\n .all() as LosslessClawSummary[];\n}\n\nexport function listSummaryParents(\n db: Database.Database,\n): LosslessClawSummaryParent[] {\n return db\n .prepare(\n \"SELECT summary_id, parent_summary_id, ordinal FROM summary_parents ORDER BY summary_id, ordinal\",\n )\n .all() as LosslessClawSummaryParent[];\n}\n\nexport function listSummaryMessages(\n db: Database.Database,\n): LosslessClawSummaryMessage[] {\n return db\n .prepare(\n \"SELECT summary_id, message_id FROM summary_messages\",\n )\n .all() as LosslessClawSummaryMessage[];\n}\n","// ---------------------------------------------------------------------------\n// Pure mapping functions: lossless-claw rows → Remnic LCM rows.\n//\n// Kept side-effect-free so they can be unit-tested without SQLite in the\n// loop. The orchestration in importer.ts handles I/O.\n// ---------------------------------------------------------------------------\n\nimport type {\n LosslessClawConversation,\n LosslessClawMessage,\n LosslessClawSummary,\n LosslessClawSummaryParent,\n LosslessClawSummaryMessage,\n} from \"./source.js\";\n\nexport const LOSSLESS_CLAW_SOURCE_LABEL = \"lossless-claw\";\n\nexport interface MappedMessage {\n session_id: string;\n turn_index: number;\n role: string;\n content: string;\n token_count: number;\n created_at: string;\n metadata: string;\n}\n\nexport interface MappedSummaryNode {\n id: string;\n session_id: string;\n depth: number;\n parent_id: string | null;\n summary_text: string;\n token_count: number;\n msg_start: number;\n msg_end: number;\n escalation: number;\n created_at: string;\n}\n\n/**\n * Resolve a conversation row to a Remnic session_id. Prefer the explicit\n * session_id field; fall back to conversation_id when null/empty so every\n * imported row has a stable session anchor.\n */\nexport function resolveSessionId(\n conversation: LosslessClawConversation,\n): string {\n const candidate = conversation.session_id?.trim();\n if (candidate && candidate.length > 0) return candidate;\n return conversation.conversation_id;\n}\n\n/**\n * Build a JSON metadata blob attached to each imported message. Sorted keys\n * (gotcha #38) so dedup or hashing downstream stays stable across runs.\n *\n * `source_seq` is the original `messages.seq` value from lossless-claw —\n * preserved alongside `conversation_id` so dedup can use a stable source\n * identity. The Remnic LCM `turn_index` is now a session-global running\n * counter (Codex P1: previously equal to `seq`, which collided when\n * multiple source conversations resolved to the same session).\n */\nexport function buildMessageMetadata(\n conversation: LosslessClawConversation,\n message: LosslessClawMessage,\n): string {\n const meta: Record<string, string | number | null> = {\n conversation_id: conversation.conversation_id,\n identity_hash: message.identity_hash ?? null,\n source: LOSSLESS_CLAW_SOURCE_LABEL,\n source_seq: message.seq,\n title: conversation.title ?? null,\n };\n const sorted = Object.keys(meta)\n .sort()\n .reduce<Record<string, string | number | null>>((acc, key) => {\n acc[key] = meta[key] ?? null;\n return acc;\n }, {});\n return JSON.stringify(sorted);\n}\n\n/**\n * Map a source message to a Remnic LCM row. `turnIndex` is supplied by the\n * caller (importer.ts) which assigns a session-global running counter so\n * multiple conversations sharing one session id do not collide on\n * (session_id, turn_index).\n */\nexport function mapMessage(\n conversation: LosslessClawConversation,\n message: LosslessClawMessage,\n turnIndex: number,\n): MappedMessage {\n return {\n session_id: resolveSessionId(conversation),\n turn_index: turnIndex,\n role: message.role,\n content: message.content,\n token_count: message.token_count,\n created_at: message.created_at,\n metadata: buildMessageMetadata(conversation, message),\n };\n}\n\nexport interface SummaryDerivation {\n parents: LosslessClawSummaryParent[];\n messageIds: string[];\n}\n\n/**\n * Pick the canonical parent id from a multi-parent DAG row. lossless-claw\n * supports many-to-many parent edges; Remnic's `lcm_summary_nodes.parent_id`\n * is a single FK. Lowest ordinal wins (tie-break: lexicographic id) so the\n * choice is deterministic. Multi-parent rows are reported by the importer so\n * users have visibility into the lossy edge.\n */\nexport function pickCanonicalParent(\n parents: LosslessClawSummaryParent[],\n): string | null {\n if (parents.length === 0) return null;\n const sorted = [...parents].sort((a, b) => {\n if (a.ordinal !== b.ordinal) return a.ordinal - b.ordinal;\n return a.parent_summary_id.localeCompare(b.parent_summary_id);\n });\n return sorted[0]!.parent_summary_id;\n}\n\nexport interface MapSummaryInput {\n summary: LosslessClawSummary;\n parents: LosslessClawSummaryParent[];\n /** Sequence numbers of messages this summary covers. */\n messageSeqs: number[];\n /** Resolved session id (single value — multi-session summaries error). */\n sessionId: string;\n}\n\nexport function mapSummary(input: MapSummaryInput): MappedSummaryNode {\n if (input.messageSeqs.length === 0) {\n throw new Error(\n `Summary ${input.summary.summary_id} has no message references; ` +\n \"cannot derive msg_start/msg_end. Skip this summary at the caller.\",\n );\n }\n // Iterative min/max — `Math.min(...arr)` / `Math.max(...arr)` push every\n // element onto the call stack via spread and throw `RangeError: Maximum\n // call stack size exceeded` on summaries that cover tens of thousands of\n // messages (Cursor Bugbot review on PR #797).\n let msg_start = input.messageSeqs[0]!;\n let msg_end = msg_start;\n for (let i = 1; i < input.messageSeqs.length; i++) {\n const seq = input.messageSeqs[i]!;\n if (seq < msg_start) msg_start = seq;\n if (seq > msg_end) msg_end = seq;\n }\n return {\n id: input.summary.summary_id,\n session_id: input.sessionId,\n depth: input.summary.depth,\n parent_id: pickCanonicalParent(input.parents),\n summary_text: input.summary.content,\n token_count: input.summary.token_count,\n msg_start,\n msg_end,\n escalation: 0,\n created_at:\n input.summary.latest_at ?? input.summary.earliest_at ?? new Date().toISOString(),\n };\n}\n\n/**\n * Determine if a summary has multiple parents (lossy-collapse signal).\n */\nexport function isMultiParent(parents: LosslessClawSummaryParent[]): boolean {\n return parents.length > 1;\n}\n\n/**\n * Resolve the (probable) single session for a summary by looking at the\n * messages it covers. lossless-claw summaries technically span multiple\n * conversations only via DAG construction, which Remnic's per-session\n * structure cannot represent — return null in that case so the caller can\n * skip the summary with a warning rather than picking a wrong session.\n *\n * Strict on dangling references: if ANY referenced message_id fails to\n * resolve to a session, return null. Silently dropping unresolved IDs\n * would let a summary with mixed valid + dangling refs pass through\n * with msg_start/msg_end computed from only the resolved subset, mis-\n * representing the summary's true coverage (Codex P2 review on PR #797).\n */\nexport function resolveSummarySession(\n messageIds: string[],\n sessionByMessageId: ReadonlyMap<string, string>,\n): string | null {\n if (messageIds.length === 0) return null;\n const sessions = new Set<string>();\n for (const messageId of messageIds) {\n const session = sessionByMessageId.get(messageId);\n if (!session) return null; // dangling reference — refuse to import\n sessions.add(session);\n }\n if (sessions.size !== 1) return null;\n return [...sessions][0]!;\n}\n\n/**\n * Index summary_messages and messages so we can emit per-summary message-id\n * lists and seq lists without N+1 queries. Pure helper.\n */\nexport function indexSummaryDerivations(\n summaryMessages: LosslessClawSummaryMessage[],\n parents: LosslessClawSummaryParent[],\n): Map<string, SummaryDerivation> {\n const out = new Map<string, SummaryDerivation>();\n for (const sm of summaryMessages) {\n const entry = out.get(sm.summary_id) ?? { parents: [], messageIds: [] };\n entry.messageIds.push(sm.message_id);\n out.set(sm.summary_id, entry);\n }\n for (const p of parents) {\n const entry = out.get(p.summary_id) ?? { parents: [], messageIds: [] };\n entry.parents.push(p);\n out.set(p.summary_id, entry);\n }\n return out;\n}\n","// ---------------------------------------------------------------------------\n// lossless-claw → Remnic LCM importer (orchestration)\n//\n// Streams rows from a lossless-claw SQLite export into a Remnic LCM\n// SQLite database opened by the caller. The Remnic database must already\n// have its schema applied (use openLcmDatabase() from @remnic/core).\n//\n// Idempotency: messages are keyed on (session_id, turn_index) — the same\n// natural key Remnic's own indexer uses. Summary nodes are keyed on the\n// preserved primary id.\n//\n// FTS sync: lcm_messages_fts and lcm_summaries_fts are external-content\n// FTS5 tables, so every insert must be mirrored. We do this in the same\n// transaction as the row write to keep the index consistent on crash.\n//\n// Compaction-event boundary: per-session, we insert one row into\n// lcm_compaction_events with tokens_before == tokens_after, marking the\n// post-import state from which Remnic's own compaction will operate.\n// ---------------------------------------------------------------------------\n\nimport type Database from \"better-sqlite3\";\n\nimport {\n assertLosslessClawSchema,\n listConversations,\n listMessageParts,\n listMessagesForConversation,\n listSummaries,\n listSummaryMessages,\n listSummaryParents,\n type LosslessClawConversation,\n type LosslessClawMessage,\n type LosslessClawMessagePart,\n} from \"./source.js\";\nimport {\n indexSummaryDerivations,\n isMultiParent,\n mapMessage,\n mapSummary,\n resolveSessionId,\n resolveSummarySession,\n} from \"./transform.js\";\n\nexport interface ImportLosslessClawOptions {\n /** Open lossless-claw source database (read-only OK). */\n sourceDb: Database.Database;\n /** Open Remnic LCM destination database with schema applied. */\n destDb: Database.Database;\n /** When true, run all reads + transformations but skip writes. */\n dryRun?: boolean;\n /**\n * Optional set of session_ids (post-resolve) to import.\n *\n * `undefined` or an empty Set both mean \"import every session\".\n * Pass a non-empty Set to restrict to specific resolved session ids.\n */\n sessionFilter?: ReadonlySet<string>;\n /** Hook for status output (defaults to no-op). */\n onLog?: (line: string) => void;\n}\n\nexport interface ImportLosslessClawResult {\n conversationsScanned: number;\n sessionsTouched: string[];\n messagesInserted: number;\n messagesSkipped: number;\n messagePartsInserted: number;\n messagePartsSkipped: number;\n summariesInserted: number;\n summariesSkipped: number;\n summariesMultiParentCollapsed: number;\n summariesSkippedNoMessages: number;\n summariesSkippedMultiSession: number;\n compactionEventsInserted: number;\n dryRun: boolean;\n}\n\nconst NOOP_LOG = (_line: string): void => {\n /* default sink */\n};\n\ntype LcmMessagePartKind =\n | \"text\"\n | \"tool_call\"\n | \"tool_result\"\n | \"patch\"\n | \"file_read\"\n | \"file_write\"\n | \"step_start\"\n | \"step_finish\"\n | \"snapshot\"\n | \"retry\";\n\ninterface LcmMessagePartInput {\n ordinal?: number;\n kind: LcmMessagePartKind;\n payload: Record<string, unknown>;\n toolName?: string | null;\n filePath?: string | null;\n createdAt?: string | null;\n}\n\nconst LCM_MESSAGE_PART_KINDS: ReadonlySet<string> = new Set([\n \"text\",\n \"tool_call\",\n \"tool_result\",\n \"patch\",\n \"file_read\",\n \"file_write\",\n \"step_start\",\n \"step_finish\",\n \"snapshot\",\n \"retry\",\n]);\n\nexport function importLosslessClaw(\n options: ImportLosslessClawOptions,\n): ImportLosslessClawResult {\n const { sourceDb, destDb } = options;\n const dryRun = options.dryRun ?? false;\n // Normalise sessionFilter: an empty Set is truthy in JavaScript, so a\n // raw `sessionFilter && !sessionFilter.has(session)` guard would skip\n // every session if a caller passed `new Set()` expecting \"import all\"\n // (the documented contract on the option). Treat empty-Set the same\n // as undefined here so every guard below is correct (Cursor Bugbot\n // review on PR #797).\n const sessionFilter =\n options.sessionFilter && options.sessionFilter.size > 0\n ? options.sessionFilter\n : undefined;\n const log = options.onLog ?? NOOP_LOG;\n\n assertLosslessClawSchema(sourceDb);\n\n const result: ImportLosslessClawResult = {\n conversationsScanned: 0,\n sessionsTouched: [],\n messagesInserted: 0,\n messagesSkipped: 0,\n messagePartsInserted: 0,\n messagePartsSkipped: 0,\n summariesInserted: 0,\n summariesSkipped: 0,\n summariesMultiParentCollapsed: 0,\n summariesSkippedNoMessages: 0,\n summariesSkippedMultiSession: 0,\n compactionEventsInserted: 0,\n dryRun,\n };\n\n // ── Pre-resolve session ids per conversation + per message id ──────────\n const conversations = listConversations(sourceDb);\n result.conversationsScanned = conversations.length;\n\n const sessionByConvId = new Map<string, string>();\n const sessionByMessageId = new Map<string, string>();\n\n for (const c of conversations) {\n sessionByConvId.set(c.conversation_id, resolveSessionId(c));\n }\n\n // Materialize messages once per conversation; reused for the write pass\n // and (via sessionByMessageId) for summary mapping.\n const messagesByConv = new Map<\n string,\n ReturnType<typeof listMessagesForConversation>\n >();\n\n for (const c of conversations) {\n const msgs = listMessagesForConversation(sourceDb, c.conversation_id);\n messagesByConv.set(c.conversation_id, msgs);\n const session = sessionByConvId.get(c.conversation_id)!;\n for (const m of msgs) {\n sessionByMessageId.set(m.message_id, session);\n }\n }\n\n // Build a per-session list of (conversation, message) pairs and sort\n // by message.created_at. This handles interleaved conversations\n // correctly: if conv-A has messages at t=0 and t=10 and conv-B has\n // messages at t=5 and t=6, turn_index ends up as t=0, t=5, t=6,\n // t=10 (chronological), not t=0, t=10, t=5, t=6 (which a per-\n // conversation pre-sort produces — Codex P1 follow-up review).\n type SessionEntry = {\n conv: LosslessClawConversation;\n msg: LosslessClawMessage;\n };\n const sessionMessages = new Map<string, SessionEntry[]>();\n const sessionOrder: string[] = [];\n for (const c of conversations) {\n const session = sessionByConvId.get(c.conversation_id)!;\n if (!sessionMessages.has(session)) {\n sessionMessages.set(session, []);\n sessionOrder.push(session);\n }\n const list = sessionMessages.get(session)!;\n for (const m of messagesByConv.get(c.conversation_id) ?? []) {\n list.push({ conv: c, msg: m });\n }\n }\n for (const list of sessionMessages.values()) {\n list.sort((a, b) => {\n if (a.msg.created_at !== b.msg.created_at) {\n return a.msg.created_at < b.msg.created_at ? -1 : 1;\n }\n // Stable tie-breaker chain when timestamps collide: conversation\n // id, then per-conversation seq (preserves intra-conversation\n // order even on identical timestamps).\n const cidCmp = a.conv.conversation_id.localeCompare(\n b.conv.conversation_id,\n );\n if (cidCmp !== 0) return cidCmp;\n return a.msg.seq - b.msg.seq;\n });\n }\n\n // ── Insert messages ────────────────────────────────────────────────────\n // Dedup uses source identity (`metadata.conversation_id` +\n // `metadata.source_seq`) rather than `(session_id, turn_index)` so two\n // source conversations sharing one session can both contribute messages\n // without one's `seq=N` masking the other's `seq=N` (Codex P1 review).\n //\n // To avoid the O(n²) behavior of a per-row `json_extract` lookup with\n // no covering index (Codex P2 review), pre-fetch existing source\n // identities once per affected session into an in-memory Map. The\n // import loop then does O(1) Map lookups for dedup.\n const insertMessageStmt = destDb.prepare(\n \"INSERT INTO lcm_messages (session_id, turn_index, role, content, token_count, created_at, metadata) \" +\n \"VALUES (?, ?, ?, ?, ?, ?, ?)\",\n );\n const insertMessageFtsStmt = destDb.prepare(\n \"INSERT INTO lcm_messages_fts (rowid, content) VALUES (?, ?)\",\n );\n const existingScanStmt = destDb.prepare(\n \"SELECT id, turn_index, \" +\n \"json_extract(metadata, '$.conversation_id') AS conv, \" +\n \"json_extract(metadata, '$.source_seq') AS source_seq \" +\n \"FROM lcm_messages WHERE session_id = ?\",\n );\n\n // session → \"convId|seq\" → destination row identity. Lookup is\n // O(1) Map membership instead of a per-row JSON-extract scan.\n const existingBySession = new Map<\n string,\n Map<string, { turnIndex: number; rowId: number }>\n >();\n // session → max(turn_index) currently in dest (so new rows append).\n const maxTurnBySession = new Map<string, number>();\n for (const session of sessionMessages.keys()) {\n if (sessionFilter && !sessionFilter.has(session)) continue;\n const map = new Map<string, { turnIndex: number; rowId: number }>();\n let max = -1;\n const rows = existingScanStmt.iterate(session) as Iterable<{\n id: number;\n turn_index: number;\n conv: string | null;\n source_seq: number | null;\n }>;\n for (const row of rows) {\n if (row.turn_index > max) max = row.turn_index;\n if (row.conv != null && row.source_seq != null) {\n map.set(`${row.conv}|${row.source_seq}`, {\n turnIndex: row.turn_index,\n rowId: row.id,\n });\n }\n }\n existingBySession.set(session, map);\n maxTurnBySession.set(session, max);\n }\n\n const sessionsTouched = new Set<string>();\n // Mapping from source message_id → assigned (or pre-existing)\n // turn_index. Populated for both inserted rows and dedup-skipped rows\n // so summary mapping (msg_start/msg_end) reflects real turn indices.\n const turnIndexByMessageId = new Map<string, number>();\n const destRowIdByMessageId = new Map<string, number>();\n\n function assignTurnIndices(forWrite: boolean): void {\n for (const session of sessionOrder) {\n if (sessionFilter && !sessionFilter.has(session)) continue;\n const entries = sessionMessages.get(session) ?? [];\n const existing =\n existingBySession.get(session) ??\n new Map<string, { turnIndex: number; rowId: number }>();\n let nextTurn = (maxTurnBySession.get(session) ?? -1) + 1;\n for (const { conv, msg } of entries) {\n const key = `${conv.conversation_id}|${msg.seq}`;\n const existingTurn = existing.get(key);\n if (existingTurn !== undefined) {\n turnIndexByMessageId.set(msg.message_id, existingTurn.turnIndex);\n destRowIdByMessageId.set(msg.message_id, existingTurn.rowId);\n result.messagesSkipped += 1;\n continue;\n }\n const ti = nextTurn++;\n turnIndexByMessageId.set(msg.message_id, ti);\n // Update the in-memory dedup map so duplicates within this\n // run also count as skips on subsequent passes (defensive;\n // shouldn't happen with valid source data).\n existing.set(key, { turnIndex: ti, rowId: -1 });\n if (forWrite) {\n const mapped = mapMessage(conv, msg, ti);\n const info = insertMessageStmt.run(\n mapped.session_id,\n mapped.turn_index,\n mapped.role,\n mapped.content,\n mapped.token_count,\n mapped.created_at,\n mapped.metadata,\n );\n insertMessageFtsStmt.run(\n Number(info.lastInsertRowid),\n mapped.content,\n );\n const rowId = Number(info.lastInsertRowid);\n destRowIdByMessageId.set(msg.message_id, rowId);\n existing.set(key, { turnIndex: ti, rowId });\n }\n result.messagesInserted += 1;\n sessionsTouched.add(session);\n }\n }\n }\n\n if (!dryRun) {\n const writeMessages = destDb.transaction(() => assignTurnIndices(true));\n writeMessages();\n } else {\n // Dry run: walk the same iteration to populate counters and\n // turnIndexByMessageId without mutating either DB.\n assignTurnIndices(false);\n }\n\n // ── Insert message parts ────────────────────────────────────────────────\n const messageParts = listMessageParts(sourceDb);\n const destHasMessageParts = sqliteTableExists(destDb, \"lcm_message_parts\");\n const existingPartsStmt = destHasMessageParts\n ? destDb.prepare(\n \"SELECT COUNT(*) AS cnt FROM lcm_message_parts WHERE message_id = ?\",\n )\n : undefined;\n const insertPartStmt = destHasMessageParts\n ? destDb.prepare(\n \"INSERT INTO lcm_message_parts (message_id, ordinal, kind, payload, tool_name, file_path, created_at) \" +\n \"VALUES (?, ?, ?, ?, ?, ?, ?)\",\n )\n : undefined;\n\n function processMessageParts(forWrite: boolean): void {\n const seenDestMessages = new Set<number>();\n const blockedDestMessages = new Set<number>();\n for (const sourcePart of messageParts) {\n if (!turnIndexByMessageId.has(sourcePart.message_id)) {\n result.messagePartsSkipped += 1;\n continue;\n }\n const destMessageId = destRowIdByMessageId.get(sourcePart.message_id);\n if (destMessageId !== undefined && destMessageId >= 0) {\n if (blockedDestMessages.has(destMessageId)) {\n result.messagePartsSkipped += 1;\n continue;\n }\n if (existingPartsStmt && !seenDestMessages.has(destMessageId)) {\n const existing = existingPartsStmt.get(destMessageId) as { cnt: number };\n if (existing.cnt > 0) {\n seenDestMessages.add(destMessageId);\n blockedDestMessages.add(destMessageId);\n result.messagePartsSkipped += 1;\n continue;\n }\n seenDestMessages.add(destMessageId);\n }\n } else if (forWrite) {\n result.messagePartsSkipped += 1;\n continue;\n }\n if (forWrite && !insertPartStmt) {\n result.messagePartsSkipped += 1;\n continue;\n }\n if (forWrite) {\n const mapped = mapLosslessMessagePart(sourcePart);\n insertPartStmt!.run(\n destMessageId,\n mapped.ordinal ?? sourcePart.ordinal,\n mapped.kind,\n JSON.stringify(mapped.payload),\n mapped.toolName ?? null,\n mapped.filePath ?? null,\n mapped.createdAt ?? sourcePart.created_at ?? new Date().toISOString(),\n );\n }\n result.messagePartsInserted += 1;\n const session = sessionByMessageId.get(sourcePart.message_id);\n if (session) sessionsTouched.add(session);\n }\n }\n\n if (!dryRun) {\n const writeParts = destDb.transaction(() => processMessageParts(true));\n writeParts();\n } else {\n processMessageParts(false);\n }\n\n // ── Insert summaries ───────────────────────────────────────────────────\n const summaries = listSummaries(sourceDb);\n const summaryMessages = listSummaryMessages(sourceDb);\n const summaryParents = listSummaryParents(sourceDb);\n const derivations = indexSummaryDerivations(summaryMessages, summaryParents);\n\n const summaryExistsStmt = destDb.prepare(\n \"SELECT 1 AS hit FROM lcm_summary_nodes WHERE id = ? LIMIT 1\",\n );\n const insertSummaryStmt = destDb.prepare(\n \"INSERT INTO lcm_summary_nodes (id, session_id, depth, parent_id, summary_text, token_count, msg_start, msg_end, escalation, created_at) \" +\n \"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\",\n );\n const insertSummaryFtsStmt = destDb.prepare(\n \"INSERT INTO lcm_summaries_fts (rowid, summary_text) VALUES (?, ?)\",\n );\n const lookupSummaryRowidStmt = destDb.prepare(\n \"SELECT rowid AS rowid FROM lcm_summary_nodes WHERE id = ?\",\n );\n\n // Single shared loop body for both write and dry-run paths so summary\n // filter conditions (skip-no-messages, multi-session, dedup, etc.)\n // can never silently diverge between modes (Cursor Bugbot review).\n function processSummaries(forWrite: boolean): void {\n for (const summary of summaries) {\n const derivation = derivations.get(summary.summary_id);\n if (!derivation || derivation.messageIds.length === 0) {\n result.summariesSkippedNoMessages += 1;\n log(\n `skip summary ${summary.summary_id}: no message references in summary_messages`,\n );\n continue;\n }\n const session = resolveSummarySession(\n derivation.messageIds,\n sessionByMessageId,\n );\n if (!session) {\n result.summariesSkippedMultiSession += 1;\n log(\n `skip summary ${summary.summary_id}: covers messages from multiple sessions or has dangling references`,\n );\n continue;\n }\n if (sessionFilter && !sessionFilter.has(session)) continue;\n\n const messageSeqs: number[] = [];\n for (const mid of derivation.messageIds) {\n const seq = turnIndexByMessageId.get(mid);\n if (typeof seq === \"number\") messageSeqs.push(seq);\n }\n if (messageSeqs.length === 0) {\n result.summariesSkippedNoMessages += 1;\n log(\n `skip summary ${summary.summary_id}: message ids exist but seqs unresolved`,\n );\n continue;\n }\n\n const mapped = mapSummary({\n summary,\n parents: derivation.parents,\n messageSeqs,\n sessionId: session,\n });\n\n if (isMultiParent(derivation.parents)) {\n result.summariesMultiParentCollapsed += 1;\n log(\n `summary ${summary.summary_id} has ${derivation.parents.length} parents; ` +\n `keeping ${mapped.parent_id ?? \"(none)\"} (Remnic LCM is single-parent).`,\n );\n }\n\n const existing = summaryExistsStmt.get(mapped.id) as\n | { hit: number }\n | undefined;\n if (existing) {\n result.summariesSkipped += 1;\n continue;\n }\n if (forWrite) {\n insertSummaryStmt.run(\n mapped.id,\n mapped.session_id,\n mapped.depth,\n mapped.parent_id,\n mapped.summary_text,\n mapped.token_count,\n mapped.msg_start,\n mapped.msg_end,\n mapped.escalation,\n mapped.created_at,\n );\n const row = lookupSummaryRowidStmt.get(mapped.id) as\n | { rowid: number }\n | undefined;\n if (row) {\n insertSummaryFtsStmt.run(row.rowid, mapped.summary_text);\n }\n }\n result.summariesInserted += 1;\n sessionsTouched.add(mapped.session_id);\n }\n }\n\n if (!dryRun) {\n const writeSummaries = destDb.transaction(() => processSummaries(true));\n writeSummaries();\n } else {\n processSummaries(false);\n }\n\n // ── Compaction-event boundary ──────────────────────────────────────────\n // Insert one marker row per session that gained data. tokens_before\n // equals tokens_after to encode \"this is an import boundary, not a real\n // compaction event\"; any consumer that needs the distinction can detect\n // the equality.\n //\n // Token totals are queried from the destination at boundary-write time\n // rather than accumulated from this run's newly-inserted rows. That\n // way a session whose only new rows are summaries (e.g. partial retry\n // after a crash between message and summary transactions) still gets\n // a correct anchor reflecting the messages already in the destination\n // (Cursor Bugbot review on PR #797).\n // Always count what compaction events WOULD be written so dry-run\n // output matches the rest of the counters (Cursor Bugbot review on\n // PR #797: dry-run was reporting `Messages inserted: N` but\n // `Compaction events written: 0` despite the documented \"count what\n // would be imported\" contract). Skip the actual INSERTs in dry-run.\n const insertEventStmt = destDb.prepare(\n \"INSERT INTO lcm_compaction_events (session_id, fired_at, msg_before, tokens_before, tokens_after) \" +\n \"VALUES (?, ?, ?, ?, ?)\",\n );\n const maxTurnStmt = destDb.prepare(\n \"SELECT IFNULL(MAX(turn_index), -1) AS max_turn FROM lcm_messages WHERE session_id = ?\",\n );\n const totalTokensStmt = destDb.prepare(\n \"SELECT IFNULL(SUM(token_count), 0) AS total FROM lcm_messages WHERE session_id = ?\",\n );\n\n function processCompactionBoundaries(forWrite: boolean): void {\n const firedAt = new Date().toISOString();\n for (const session of sessionsTouched) {\n const turnRow = maxTurnStmt.get(session) as { max_turn: number };\n const msgBefore = turnRow.max_turn + 1;\n const tokRow = totalTokensStmt.get(session) as { total: number };\n const tokens = tokRow.total;\n if (forWrite) {\n insertEventStmt.run(session, firedAt, msgBefore, tokens, tokens);\n }\n result.compactionEventsInserted += 1;\n }\n }\n\n if (!dryRun) {\n const writeEvents = destDb.transaction(() => processCompactionBoundaries(true));\n writeEvents();\n } else {\n processCompactionBoundaries(false);\n }\n\n result.sessionsTouched = [...sessionsTouched].sort();\n return result;\n}\n\nfunction sqliteTableExists(db: Database.Database, tableName: string): boolean {\n const row = db\n .prepare(\n \"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?\",\n )\n .get(tableName) as { name: string } | undefined;\n return row !== undefined;\n}\n\nfunction mapLosslessMessagePart(\n part: LosslessClawMessagePart,\n): LcmMessagePartInput {\n const kind = LCM_MESSAGE_PART_KINDS.has(part.kind)\n ? (part.kind as LcmMessagePartKind)\n : \"tool_call\";\n let payload: Record<string, unknown>;\n try {\n const parsed = JSON.parse(part.payload);\n payload =\n parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : { value: parsed };\n } catch {\n payload = { value: part.payload };\n }\n return {\n ordinal: part.ordinal,\n kind,\n payload,\n toolName: part.tool_name,\n filePath: part.file_path,\n createdAt: part.created_at,\n };\n}\n"],"mappings":"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","names":["require"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/import-lossless-claw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Import lossless-claw (LCM) SQLite databases into Remnic's LCM mode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,13 +11,9 @@
|
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"check-types": "tsc --noEmit",
|
|
18
|
-
"test": "tsx --test src/transform.test.ts src/source.test.ts src/importer.test.ts",
|
|
19
|
-
"prepublishOnly": "npm run build"
|
|
20
|
-
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
21
17
|
"publishConfig": {
|
|
22
18
|
"access": "public",
|
|
23
19
|
"provenance": true
|
|
@@ -26,14 +22,14 @@
|
|
|
26
22
|
"better-sqlite3": "^12.6.2"
|
|
27
23
|
},
|
|
28
24
|
"peerDependencies": {
|
|
29
|
-
"@remnic/core": "
|
|
25
|
+
"@remnic/core": "^1.1.5"
|
|
30
26
|
},
|
|
31
27
|
"devDependencies": {
|
|
32
|
-
"@remnic/core": "workspace:*",
|
|
33
28
|
"@types/better-sqlite3": "^7.6.0",
|
|
34
29
|
"tsup": "^8.0.0",
|
|
35
30
|
"tsx": "^4.0.0",
|
|
36
|
-
"typescript": "^5.7.0"
|
|
31
|
+
"typescript": "^5.7.0",
|
|
32
|
+
"@remnic/core": "1.1.5"
|
|
37
33
|
},
|
|
38
34
|
"license": "MIT",
|
|
39
35
|
"repository": {
|
|
@@ -41,5 +37,17 @@
|
|
|
41
37
|
"url": "https://github.com/joshuaswarren/remnic.git",
|
|
42
38
|
"directory": "packages/import-lossless-claw"
|
|
43
39
|
},
|
|
44
|
-
"keywords": [
|
|
45
|
-
|
|
40
|
+
"keywords": [
|
|
41
|
+
"remnic",
|
|
42
|
+
"memory",
|
|
43
|
+
"lcm",
|
|
44
|
+
"lossless-claw",
|
|
45
|
+
"import",
|
|
46
|
+
"openclaw"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
50
|
+
"check-types": "tsc --noEmit",
|
|
51
|
+
"test": "tsx --test src/transform.test.ts src/source.test.ts src/importer.test.ts"
|
|
52
|
+
}
|
|
53
|
+
}
|