@remnic/import-lossless-claw 0.1.1 → 9.3.517
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/README.md +6 -2
- package/dist/index.js +54 -18
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -62,8 +62,12 @@ destDb.close();
|
|
|
62
62
|
|
|
63
63
|
## Idempotency
|
|
64
64
|
|
|
65
|
-
Re-running the importer inserts zero new rows. Messages dedupe on
|
|
66
|
-
|
|
65
|
+
Re-running the importer inserts zero new rows. Messages dedupe on the
|
|
66
|
+
source identity stored in metadata: `(session_id, conversation_id,
|
|
67
|
+
source_seq)`. Destination `turn_index` values are assigned
|
|
68
|
+
session-globally as rows are appended, so they remain stable for already
|
|
69
|
+
imported source messages but are not the dedupe key. Summary nodes dedupe
|
|
70
|
+
on `id`.
|
|
67
71
|
|
|
68
72
|
## What's lossy
|
|
69
73
|
|
package/dist/index.js
CHANGED
|
@@ -186,18 +186,6 @@ function indexSummaryDerivations(summaryMessages, parents) {
|
|
|
186
186
|
// src/importer.ts
|
|
187
187
|
var NOOP_LOG = (_line) => {
|
|
188
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
|
-
]);
|
|
201
189
|
function importLosslessClaw(options) {
|
|
202
190
|
const { sourceDb, destDb } = options;
|
|
203
191
|
const dryRun = options.dryRun ?? false;
|
|
@@ -288,6 +276,20 @@ function importLosslessClaw(options) {
|
|
|
288
276
|
existingBySession.set(session, map);
|
|
289
277
|
maxTurnBySession.set(session, max);
|
|
290
278
|
}
|
|
279
|
+
const importBoundaryExistsStmt = destDb.prepare(
|
|
280
|
+
"SELECT 1 AS hit FROM lcm_compaction_events WHERE session_id = ? AND tokens_before = tokens_after LIMIT 1"
|
|
281
|
+
);
|
|
282
|
+
const importBoundaryCache = /* @__PURE__ */ new Map();
|
|
283
|
+
function hasImportBoundary(session) {
|
|
284
|
+
const cached = importBoundaryCache.get(session);
|
|
285
|
+
if (cached !== void 0) {
|
|
286
|
+
return cached;
|
|
287
|
+
}
|
|
288
|
+
const row = importBoundaryExistsStmt.get(session);
|
|
289
|
+
const exists = row !== void 0;
|
|
290
|
+
importBoundaryCache.set(session, exists);
|
|
291
|
+
return exists;
|
|
292
|
+
}
|
|
291
293
|
const sessionsTouched = /* @__PURE__ */ new Set();
|
|
292
294
|
const turnIndexByMessageId = /* @__PURE__ */ new Map();
|
|
293
295
|
const destRowIdByMessageId = /* @__PURE__ */ new Map();
|
|
@@ -304,6 +306,9 @@ function importLosslessClaw(options) {
|
|
|
304
306
|
turnIndexByMessageId.set(msg.message_id, existingTurn.turnIndex);
|
|
305
307
|
destRowIdByMessageId.set(msg.message_id, existingTurn.rowId);
|
|
306
308
|
result.messagesSkipped += 1;
|
|
309
|
+
if (!hasImportBoundary(session)) {
|
|
310
|
+
sessionsTouched.add(session);
|
|
311
|
+
}
|
|
307
312
|
continue;
|
|
308
313
|
}
|
|
309
314
|
const ti = nextTurn++;
|
|
@@ -407,7 +412,7 @@ function importLosslessClaw(options) {
|
|
|
407
412
|
const summaryParents = listSummaryParents(sourceDb);
|
|
408
413
|
const derivations = indexSummaryDerivations(summaryMessages, summaryParents);
|
|
409
414
|
const summaryExistsStmt = destDb.prepare(
|
|
410
|
-
"SELECT
|
|
415
|
+
"SELECT session_id FROM lcm_summary_nodes WHERE id = ? LIMIT 1"
|
|
411
416
|
);
|
|
412
417
|
const insertSummaryStmt = destDb.prepare(
|
|
413
418
|
"INSERT INTO lcm_summary_nodes (id, session_id, depth, parent_id, summary_text, token_count, msg_start, msg_end, escalation, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
@@ -418,6 +423,22 @@ function importLosslessClaw(options) {
|
|
|
418
423
|
const lookupSummaryRowidStmt = destDb.prepare(
|
|
419
424
|
"SELECT rowid AS rowid FROM lcm_summary_nodes WHERE id = ?"
|
|
420
425
|
);
|
|
426
|
+
const importableSummarySessions = /* @__PURE__ */ new Map();
|
|
427
|
+
for (const summary of summaries) {
|
|
428
|
+
const derivation = derivations.get(summary.summary_id);
|
|
429
|
+
if (!derivation || derivation.messageIds.length === 0) continue;
|
|
430
|
+
const session = resolveSummarySession(
|
|
431
|
+
derivation.messageIds,
|
|
432
|
+
sessionByMessageId
|
|
433
|
+
);
|
|
434
|
+
if (!session) continue;
|
|
435
|
+
if (sessionFilter && !sessionFilter.has(session)) continue;
|
|
436
|
+
if (derivation.messageIds.some(
|
|
437
|
+
(mid) => typeof turnIndexByMessageId.get(mid) === "number"
|
|
438
|
+
)) {
|
|
439
|
+
importableSummarySessions.set(summary.summary_id, session);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
421
442
|
function processSummaries(forWrite) {
|
|
422
443
|
for (const summary of summaries) {
|
|
423
444
|
const derivation = derivations.get(summary.summary_id);
|
|
@@ -452,21 +473,37 @@ function importLosslessClaw(options) {
|
|
|
452
473
|
);
|
|
453
474
|
continue;
|
|
454
475
|
}
|
|
476
|
+
const validParents = derivation.parents.filter((parent) => {
|
|
477
|
+
const importableParentSession = importableSummarySessions.get(parent.parent_summary_id);
|
|
478
|
+
if (importableParentSession !== void 0) {
|
|
479
|
+
return importableParentSession === session;
|
|
480
|
+
}
|
|
481
|
+
const existingParent = summaryExistsStmt.get(parent.parent_summary_id);
|
|
482
|
+
return existingParent !== void 0 && existingParent.session_id === session;
|
|
483
|
+
});
|
|
484
|
+
if (validParents.length !== derivation.parents.length) {
|
|
485
|
+
log(
|
|
486
|
+
`summary ${summary.summary_id}: dropped ${derivation.parents.length - validParents.length} parent link(s) to skipped or missing summaries`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
455
489
|
const mapped = mapSummary({
|
|
456
490
|
summary,
|
|
457
|
-
parents:
|
|
491
|
+
parents: validParents,
|
|
458
492
|
messageSeqs,
|
|
459
493
|
sessionId: session
|
|
460
494
|
});
|
|
461
|
-
if (isMultiParent(
|
|
495
|
+
if (isMultiParent(validParents)) {
|
|
462
496
|
result.summariesMultiParentCollapsed += 1;
|
|
463
497
|
log(
|
|
464
|
-
`summary ${summary.summary_id} has ${
|
|
498
|
+
`summary ${summary.summary_id} has ${validParents.length} parents; keeping ${mapped.parent_id ?? "(none)"} (Remnic LCM is single-parent).`
|
|
465
499
|
);
|
|
466
500
|
}
|
|
467
501
|
const existing = summaryExistsStmt.get(mapped.id);
|
|
468
502
|
if (existing) {
|
|
469
503
|
result.summariesSkipped += 1;
|
|
504
|
+
if (!hasImportBoundary(mapped.session_id)) {
|
|
505
|
+
sessionsTouched.add(mapped.session_id);
|
|
506
|
+
}
|
|
470
507
|
continue;
|
|
471
508
|
}
|
|
472
509
|
if (forWrite) {
|
|
@@ -535,7 +572,6 @@ function sqliteTableExists(db, tableName) {
|
|
|
535
572
|
return row !== void 0;
|
|
536
573
|
}
|
|
537
574
|
function mapLosslessMessagePart(part) {
|
|
538
|
-
const kind = LCM_MESSAGE_PART_KINDS.has(part.kind) ? part.kind : "tool_call";
|
|
539
575
|
let payload;
|
|
540
576
|
try {
|
|
541
577
|
const parsed = JSON.parse(part.payload);
|
|
@@ -545,7 +581,7 @@ function mapLosslessMessagePart(part) {
|
|
|
545
581
|
}
|
|
546
582
|
return {
|
|
547
583
|
ordinal: part.ordinal,
|
|
548
|
-
kind,
|
|
584
|
+
kind: part.kind,
|
|
549
585
|
payload,
|
|
550
586
|
toolName: part.tool_name,
|
|
551
587
|
filePath: part.file_path,
|
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// 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"]}
|
|
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\ninterface LcmMessagePartInput {\n ordinal?: number;\n kind: string;\n payload: Record<string, unknown>;\n toolName?: string | null;\n filePath?: string | null;\n createdAt?: string | null;\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 importBoundaryExistsStmt = destDb.prepare(\n \"SELECT 1 AS hit FROM lcm_compaction_events \" +\n \"WHERE session_id = ? AND tokens_before = tokens_after LIMIT 1\",\n );\n const importBoundaryCache = new Map<string, boolean>();\n\n function hasImportBoundary(session: string): boolean {\n const cached = importBoundaryCache.get(session);\n if (cached !== undefined) {\n return cached;\n }\n const row = importBoundaryExistsStmt.get(session) as\n | { hit: number }\n | undefined;\n const exists = row !== undefined;\n importBoundaryCache.set(session, exists);\n return exists;\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 if (!hasImportBoundary(session)) {\n sessionsTouched.add(session);\n }\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 session_id 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 const importableSummarySessions = new Map<string, string>();\n for (const summary of summaries) {\n const derivation = derivations.get(summary.summary_id);\n if (!derivation || derivation.messageIds.length === 0) continue;\n const session = resolveSummarySession(\n derivation.messageIds,\n sessionByMessageId,\n );\n if (!session) continue;\n if (sessionFilter && !sessionFilter.has(session)) continue;\n if (\n derivation.messageIds.some(\n (mid) => typeof turnIndexByMessageId.get(mid) === \"number\",\n )\n ) {\n importableSummarySessions.set(summary.summary_id, session);\n }\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 validParents = derivation.parents.filter((parent) => {\n const importableParentSession = importableSummarySessions.get(parent.parent_summary_id);\n if (importableParentSession !== undefined) {\n return importableParentSession === session;\n }\n const existingParent = summaryExistsStmt.get(parent.parent_summary_id) as\n | { session_id: string }\n | undefined;\n return existingParent !== undefined && existingParent.session_id === session;\n });\n if (validParents.length !== derivation.parents.length) {\n log(\n `summary ${summary.summary_id}: dropped ${derivation.parents.length - validParents.length} ` +\n \"parent link(s) to skipped or missing summaries\",\n );\n }\n\n const mapped = mapSummary({\n summary,\n parents: validParents,\n messageSeqs,\n sessionId: session,\n });\n\n if (isMultiParent(validParents)) {\n result.summariesMultiParentCollapsed += 1;\n log(\n `summary ${summary.summary_id} has ${validParents.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 | { session_id: string }\n | undefined;\n if (existing) {\n result.summariesSkipped += 1;\n if (!hasImportBoundary(mapped.session_id)) {\n sessionsTouched.add(mapped.session_id);\n }\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 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: part.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": "
|
|
3
|
+
"version": "9.3.517",
|
|
4
4
|
"description": "Import lossless-claw (LCM) SQLite databases into Remnic's LCM mode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
"better-sqlite3": "^12.6.2"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@remnic/core": "^
|
|
25
|
+
"@remnic/core": "^9.3.517"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/better-sqlite3": "^7.6.0",
|
|
29
29
|
"tsup": "^8.0.0",
|
|
30
30
|
"tsx": "^4.0.0",
|
|
31
31
|
"typescript": "^5.7.0",
|
|
32
|
-
"@remnic/core": "
|
|
32
|
+
"@remnic/core": "9.3.517"
|
|
33
33
|
},
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"repository": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
],
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsup src/index.ts --format esm --dts",
|
|
50
|
+
"precheck-types": "node ../../scripts/ensure-bench-build-deps.mjs",
|
|
50
51
|
"check-types": "tsc --noEmit",
|
|
51
52
|
"test": "tsx --test src/transform.test.ts src/source.test.ts src/importer.test.ts"
|
|
52
53
|
}
|