@remnic/import-lossless-claw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @remnic/import-lossless-claw
2
+
3
+ Migrate a [lossless-claw](https://github.com/martian-engineering/lossless-claw)
4
+ LCM SQLite database into Remnic's LCM mode.
5
+
6
+ ## Why this exists
7
+
8
+ Remnic ships its own *lossless context management* mode whose schema is
9
+ near-isomorphic to lossless-claw's. This package is a SQLite→SQLite
10
+ importer for users who want to switch from lossless-claw to Remnic without
11
+ losing session history.
12
+
13
+ For coexistence (running both side-by-side) and the full migration story,
14
+ see [`docs/lcm-to-remnic-migration.md`](../../docs/lcm-to-remnic-migration.md).
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install -g @remnic/import-lossless-claw
20
+ # or
21
+ pnpm add @remnic/import-lossless-claw
22
+ ```
23
+
24
+ The CLI command lives in `@remnic/cli`; this package is loaded lazily on
25
+ demand via the à-la-carte loader (CLAUDE.md gotcha #57).
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ remnic import-lossless-claw --src ~/.openclaw/lcm.db
31
+ remnic import-lossless-claw --src ~/.openclaw/lcm.db --dry-run
32
+ remnic import-lossless-claw --src ~/.openclaw/lcm.db --session-filter sess-A
33
+ ```
34
+
35
+ The destination is `<memoryDir>/state/lcm.sqlite`, which Remnic creates
36
+ automatically when `lcmEnabled: true` is set in plugin config.
37
+
38
+ ## Programmatic API
39
+
40
+ ```ts
41
+ import {
42
+ importLosslessClaw,
43
+ openSourceDatabase,
44
+ } from "@remnic/import-lossless-claw";
45
+ import { ensureLcmStateDir, openLcmDatabase } from "@remnic/core";
46
+
47
+ const sourceDb = openSourceDatabase("/path/to/lcm.db");
48
+ await ensureLcmStateDir("/path/to/memoryDir");
49
+ const destDb = openLcmDatabase("/path/to/memoryDir");
50
+
51
+ const result = importLosslessClaw({
52
+ sourceDb,
53
+ destDb,
54
+ dryRun: false,
55
+ sessionFilter: new Set(["sess-A"]),
56
+ onLog: (line) => console.log(line),
57
+ });
58
+
59
+ sourceDb.close();
60
+ destDb.close();
61
+ ```
62
+
63
+ ## Idempotency
64
+
65
+ Re-running the importer inserts zero new rows. Messages dedupe on
66
+ `(session_id, turn_index)`; summary nodes dedupe on `id`.
67
+
68
+ ## What's lossy
69
+
70
+ - Multi-parent summary DAG → single-parent (lowest `ordinal` wins,
71
+ lexicographic tie-break). Count reported in result.
72
+ - `message_parts`, `large_files`, compaction telemetry — no Remnic LCM
73
+ analog, skipped silently.
74
+
75
+ See the migration doc for the full mapping table.
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,201 @@
1
+ import Database from 'better-sqlite3';
2
+
3
+ interface ImportLosslessClawOptions {
4
+ /** Open lossless-claw source database (read-only OK). */
5
+ sourceDb: Database.Database;
6
+ /** Open Remnic LCM destination database with schema applied. */
7
+ destDb: Database.Database;
8
+ /** When true, run all reads + transformations but skip writes. */
9
+ dryRun?: boolean;
10
+ /**
11
+ * Optional set of session_ids (post-resolve) to import.
12
+ *
13
+ * `undefined` or an empty Set both mean "import every session".
14
+ * Pass a non-empty Set to restrict to specific resolved session ids.
15
+ */
16
+ sessionFilter?: ReadonlySet<string>;
17
+ /** Hook for status output (defaults to no-op). */
18
+ onLog?: (line: string) => void;
19
+ }
20
+ interface ImportLosslessClawResult {
21
+ conversationsScanned: number;
22
+ sessionsTouched: string[];
23
+ messagesInserted: number;
24
+ messagesSkipped: number;
25
+ summariesInserted: number;
26
+ summariesSkipped: number;
27
+ summariesMultiParentCollapsed: number;
28
+ summariesSkippedNoMessages: number;
29
+ summariesSkippedMultiSession: number;
30
+ compactionEventsInserted: number;
31
+ dryRun: boolean;
32
+ }
33
+ declare function importLosslessClaw(options: ImportLosslessClawOptions): ImportLosslessClawResult;
34
+
35
+ /**
36
+ * Open a lossless-claw SQLite database file in read-only mode. The CLI uses
37
+ * this so a half-baked source file cannot be written to during import.
38
+ *
39
+ * Tildes in the path are NOT expanded here — callers (CLI, tests) must
40
+ * normalise paths first to keep the boundary explicit (CLAUDE.md gotcha
41
+ * #17).
42
+ */
43
+ declare function openSourceDatabase(filePath: string): Database.Database;
44
+ /**
45
+ * Open an in-memory destination database. The caller is expected to apply
46
+ * the Remnic LCM schema via `applyLcmSchema(db)` from `@remnic/core` before
47
+ * passing it to `importLosslessClaw`.
48
+ *
49
+ * Used by the `--dry-run` CLI path as a fallback when no existing on-disk
50
+ * destination exists, so a true write-free run can still compute counts
51
+ * against an empty destination without touching the filesystem.
52
+ */
53
+ declare function openInMemoryDestinationDatabase(): Database.Database;
54
+ /**
55
+ * Open an existing Remnic LCM database file in read-only mode. Used by the
56
+ * `--dry-run` CLI path so dedup counts reflect the user's real
57
+ * destination state without any write risk (Codex P2 follow-up: a fresh
58
+ * in-memory database makes `messagesSkipped`/`summariesSkipped` always
59
+ * report zero, which is misleading when the user has run a real import
60
+ * before).
61
+ */
62
+ declare function openExistingLcmDatabaseReadOnly(filePath: string): Database.Database;
63
+ interface LosslessClawConversation {
64
+ conversation_id: string;
65
+ session_id: string | null;
66
+ session_key: string | null;
67
+ title: string | null;
68
+ }
69
+ interface LosslessClawMessage {
70
+ message_id: string;
71
+ conversation_id: string;
72
+ seq: number;
73
+ role: string;
74
+ content: string;
75
+ token_count: number;
76
+ identity_hash: string | null;
77
+ created_at: string;
78
+ }
79
+ interface LosslessClawSummary {
80
+ summary_id: string;
81
+ kind: string;
82
+ depth: number;
83
+ content: string;
84
+ token_count: number;
85
+ earliest_at: string | null;
86
+ latest_at: string | null;
87
+ }
88
+ interface LosslessClawSummaryParent {
89
+ summary_id: string;
90
+ parent_summary_id: string;
91
+ ordinal: number;
92
+ }
93
+ interface LosslessClawSummaryMessage {
94
+ summary_id: string;
95
+ message_id: string;
96
+ }
97
+ /**
98
+ * Verify a database handle points at a lossless-claw export by checking for
99
+ * the required tables. Throws a user-facing error on mismatch so callers can
100
+ * surface a clear "this isn't a lossless-claw database" message instead of
101
+ * cryptic SQL errors during import.
102
+ */
103
+ declare function assertLosslessClawSchema(db: Database.Database): void;
104
+ declare function listConversations(db: Database.Database): LosslessClawConversation[];
105
+ declare function listMessagesForConversation(db: Database.Database, conversationId: string): LosslessClawMessage[];
106
+ declare function listSummaries(db: Database.Database): LosslessClawSummary[];
107
+ declare function listSummaryParents(db: Database.Database): LosslessClawSummaryParent[];
108
+ declare function listSummaryMessages(db: Database.Database): LosslessClawSummaryMessage[];
109
+
110
+ declare const LOSSLESS_CLAW_SOURCE_LABEL = "lossless-claw";
111
+ interface MappedMessage {
112
+ session_id: string;
113
+ turn_index: number;
114
+ role: string;
115
+ content: string;
116
+ token_count: number;
117
+ created_at: string;
118
+ metadata: string;
119
+ }
120
+ interface MappedSummaryNode {
121
+ id: string;
122
+ session_id: string;
123
+ depth: number;
124
+ parent_id: string | null;
125
+ summary_text: string;
126
+ token_count: number;
127
+ msg_start: number;
128
+ msg_end: number;
129
+ escalation: number;
130
+ created_at: string;
131
+ }
132
+ /**
133
+ * Resolve a conversation row to a Remnic session_id. Prefer the explicit
134
+ * session_id field; fall back to conversation_id when null/empty so every
135
+ * imported row has a stable session anchor.
136
+ */
137
+ declare function resolveSessionId(conversation: LosslessClawConversation): string;
138
+ /**
139
+ * Build a JSON metadata blob attached to each imported message. Sorted keys
140
+ * (gotcha #38) so dedup or hashing downstream stays stable across runs.
141
+ *
142
+ * `source_seq` is the original `messages.seq` value from lossless-claw —
143
+ * preserved alongside `conversation_id` so dedup can use a stable source
144
+ * identity. The Remnic LCM `turn_index` is now a session-global running
145
+ * counter (Codex P1: previously equal to `seq`, which collided when
146
+ * multiple source conversations resolved to the same session).
147
+ */
148
+ declare function buildMessageMetadata(conversation: LosslessClawConversation, message: LosslessClawMessage): string;
149
+ /**
150
+ * Map a source message to a Remnic LCM row. `turnIndex` is supplied by the
151
+ * caller (importer.ts) which assigns a session-global running counter so
152
+ * multiple conversations sharing one session id do not collide on
153
+ * (session_id, turn_index).
154
+ */
155
+ declare function mapMessage(conversation: LosslessClawConversation, message: LosslessClawMessage, turnIndex: number): MappedMessage;
156
+ interface SummaryDerivation {
157
+ parents: LosslessClawSummaryParent[];
158
+ messageIds: string[];
159
+ }
160
+ /**
161
+ * Pick the canonical parent id from a multi-parent DAG row. lossless-claw
162
+ * supports many-to-many parent edges; Remnic's `lcm_summary_nodes.parent_id`
163
+ * is a single FK. Lowest ordinal wins (tie-break: lexicographic id) so the
164
+ * choice is deterministic. Multi-parent rows are reported by the importer so
165
+ * users have visibility into the lossy edge.
166
+ */
167
+ declare function pickCanonicalParent(parents: LosslessClawSummaryParent[]): string | null;
168
+ interface MapSummaryInput {
169
+ summary: LosslessClawSummary;
170
+ parents: LosslessClawSummaryParent[];
171
+ /** Sequence numbers of messages this summary covers. */
172
+ messageSeqs: number[];
173
+ /** Resolved session id (single value — multi-session summaries error). */
174
+ sessionId: string;
175
+ }
176
+ declare function mapSummary(input: MapSummaryInput): MappedSummaryNode;
177
+ /**
178
+ * Determine if a summary has multiple parents (lossy-collapse signal).
179
+ */
180
+ declare function isMultiParent(parents: LosslessClawSummaryParent[]): boolean;
181
+ /**
182
+ * Resolve the (probable) single session for a summary by looking at the
183
+ * messages it covers. lossless-claw summaries technically span multiple
184
+ * conversations only via DAG construction, which Remnic's per-session
185
+ * structure cannot represent — return null in that case so the caller can
186
+ * skip the summary with a warning rather than picking a wrong session.
187
+ *
188
+ * Strict on dangling references: if ANY referenced message_id fails to
189
+ * resolve to a session, return null. Silently dropping unresolved IDs
190
+ * would let a summary with mixed valid + dangling refs pass through
191
+ * with msg_start/msg_end computed from only the resolved subset, mis-
192
+ * representing the summary's true coverage (Codex P2 review on PR #797).
193
+ */
194
+ declare function resolveSummarySession(messageIds: string[], sessionByMessageId: ReadonlyMap<string, string>): string | null;
195
+ /**
196
+ * Index summary_messages and messages so we can emit per-summary message-id
197
+ * lists and seq lists without N+1 queries. Pure helper.
198
+ */
199
+ declare function indexSummaryDerivations(summaryMessages: LosslessClawSummaryMessage[], parents: LosslessClawSummaryParent[]): Map<string, SummaryDerivation>;
200
+
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 };
package/dist/index.js ADDED
@@ -0,0 +1,455 @@
1
+ // openclaw-engram: Local-first memory plugin
2
+
3
+ // src/source.ts
4
+ import { createRequire } from "module";
5
+ var cachedCtor = null;
6
+ function loadBetterSqlite3() {
7
+ if (cachedCtor) return cachedCtor;
8
+ const require2 = createRequire(import.meta.url);
9
+ const loaded = require2("better-sqlite3");
10
+ const ctor = typeof loaded === "function" ? loaded : loaded.default;
11
+ if (typeof ctor !== "function") {
12
+ throw new Error(
13
+ "better-sqlite3 is unavailable. Install it alongside @remnic/import-lossless-claw or rebuild from source: `pnpm rebuild better-sqlite3`."
14
+ );
15
+ }
16
+ cachedCtor = ctor;
17
+ return ctor;
18
+ }
19
+ function openSourceDatabase(filePath) {
20
+ const Ctor = loadBetterSqlite3();
21
+ return new Ctor(filePath, { readonly: true, fileMustExist: true });
22
+ }
23
+ function openInMemoryDestinationDatabase() {
24
+ const Ctor = loadBetterSqlite3();
25
+ return new Ctor(":memory:");
26
+ }
27
+ function openExistingLcmDatabaseReadOnly(filePath) {
28
+ const Ctor = loadBetterSqlite3();
29
+ return new Ctor(filePath, { readonly: true, fileMustExist: true });
30
+ }
31
+ function assertLosslessClawSchema(db) {
32
+ const required = [
33
+ "conversations",
34
+ "messages",
35
+ "summaries",
36
+ "summary_messages",
37
+ "summary_parents"
38
+ ];
39
+ const stmt = db.prepare(
40
+ "SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
41
+ );
42
+ const missing = [];
43
+ for (const name of required) {
44
+ const row = stmt.get(name);
45
+ if (!row) missing.push(name);
46
+ }
47
+ if (missing.length > 0) {
48
+ throw new Error(
49
+ `Source database is missing lossless-claw tables: ${missing.join(", ")}. Confirm --src points at a lossless-claw lcm.db file.`
50
+ );
51
+ }
52
+ }
53
+ function listConversations(db) {
54
+ return db.prepare(
55
+ "SELECT conversation_id, session_id, session_key, title FROM conversations ORDER BY conversation_id"
56
+ ).all();
57
+ }
58
+ function listMessagesForConversation(db, conversationId) {
59
+ return db.prepare(
60
+ "SELECT message_id, conversation_id, seq, role, content, token_count, identity_hash, created_at FROM messages WHERE conversation_id = ? ORDER BY seq"
61
+ ).all(conversationId);
62
+ }
63
+ function listSummaries(db) {
64
+ return db.prepare(
65
+ "SELECT summary_id, kind, depth, content, token_count, earliest_at, latest_at FROM summaries ORDER BY depth, summary_id"
66
+ ).all();
67
+ }
68
+ function listSummaryParents(db) {
69
+ return db.prepare(
70
+ "SELECT summary_id, parent_summary_id, ordinal FROM summary_parents ORDER BY summary_id, ordinal"
71
+ ).all();
72
+ }
73
+ function listSummaryMessages(db) {
74
+ return db.prepare(
75
+ "SELECT summary_id, message_id FROM summary_messages"
76
+ ).all();
77
+ }
78
+
79
+ // src/transform.ts
80
+ var LOSSLESS_CLAW_SOURCE_LABEL = "lossless-claw";
81
+ function resolveSessionId(conversation) {
82
+ const candidate = conversation.session_id?.trim();
83
+ if (candidate && candidate.length > 0) return candidate;
84
+ return conversation.conversation_id;
85
+ }
86
+ function buildMessageMetadata(conversation, message) {
87
+ const meta = {
88
+ conversation_id: conversation.conversation_id,
89
+ identity_hash: message.identity_hash ?? null,
90
+ source: LOSSLESS_CLAW_SOURCE_LABEL,
91
+ source_seq: message.seq,
92
+ title: conversation.title ?? null
93
+ };
94
+ const sorted = Object.keys(meta).sort().reduce((acc, key) => {
95
+ acc[key] = meta[key] ?? null;
96
+ return acc;
97
+ }, {});
98
+ return JSON.stringify(sorted);
99
+ }
100
+ function mapMessage(conversation, message, turnIndex) {
101
+ return {
102
+ session_id: resolveSessionId(conversation),
103
+ turn_index: turnIndex,
104
+ role: message.role,
105
+ content: message.content,
106
+ token_count: message.token_count,
107
+ created_at: message.created_at,
108
+ metadata: buildMessageMetadata(conversation, message)
109
+ };
110
+ }
111
+ function pickCanonicalParent(parents) {
112
+ if (parents.length === 0) return null;
113
+ const sorted = [...parents].sort((a, b) => {
114
+ if (a.ordinal !== b.ordinal) return a.ordinal - b.ordinal;
115
+ return a.parent_summary_id.localeCompare(b.parent_summary_id);
116
+ });
117
+ return sorted[0].parent_summary_id;
118
+ }
119
+ function mapSummary(input) {
120
+ if (input.messageSeqs.length === 0) {
121
+ throw new Error(
122
+ `Summary ${input.summary.summary_id} has no message references; cannot derive msg_start/msg_end. Skip this summary at the caller.`
123
+ );
124
+ }
125
+ let msg_start = input.messageSeqs[0];
126
+ let msg_end = msg_start;
127
+ for (let i = 1; i < input.messageSeqs.length; i++) {
128
+ const seq = input.messageSeqs[i];
129
+ if (seq < msg_start) msg_start = seq;
130
+ if (seq > msg_end) msg_end = seq;
131
+ }
132
+ return {
133
+ id: input.summary.summary_id,
134
+ session_id: input.sessionId,
135
+ depth: input.summary.depth,
136
+ parent_id: pickCanonicalParent(input.parents),
137
+ summary_text: input.summary.content,
138
+ token_count: input.summary.token_count,
139
+ msg_start,
140
+ msg_end,
141
+ escalation: 0,
142
+ created_at: input.summary.latest_at ?? input.summary.earliest_at ?? (/* @__PURE__ */ new Date()).toISOString()
143
+ };
144
+ }
145
+ function isMultiParent(parents) {
146
+ return parents.length > 1;
147
+ }
148
+ function resolveSummarySession(messageIds, sessionByMessageId) {
149
+ if (messageIds.length === 0) return null;
150
+ const sessions = /* @__PURE__ */ new Set();
151
+ for (const messageId of messageIds) {
152
+ const session = sessionByMessageId.get(messageId);
153
+ if (!session) return null;
154
+ sessions.add(session);
155
+ }
156
+ if (sessions.size !== 1) return null;
157
+ return [...sessions][0];
158
+ }
159
+ function indexSummaryDerivations(summaryMessages, parents) {
160
+ const out = /* @__PURE__ */ new Map();
161
+ for (const sm of summaryMessages) {
162
+ const entry = out.get(sm.summary_id) ?? { parents: [], messageIds: [] };
163
+ entry.messageIds.push(sm.message_id);
164
+ out.set(sm.summary_id, entry);
165
+ }
166
+ for (const p of parents) {
167
+ const entry = out.get(p.summary_id) ?? { parents: [], messageIds: [] };
168
+ entry.parents.push(p);
169
+ out.set(p.summary_id, entry);
170
+ }
171
+ return out;
172
+ }
173
+
174
+ // src/importer.ts
175
+ var NOOP_LOG = (_line) => {
176
+ };
177
+ function importLosslessClaw(options) {
178
+ const { sourceDb, destDb } = options;
179
+ const dryRun = options.dryRun ?? false;
180
+ const sessionFilter = options.sessionFilter && options.sessionFilter.size > 0 ? options.sessionFilter : void 0;
181
+ const log = options.onLog ?? NOOP_LOG;
182
+ assertLosslessClawSchema(sourceDb);
183
+ const result = {
184
+ conversationsScanned: 0,
185
+ sessionsTouched: [],
186
+ messagesInserted: 0,
187
+ messagesSkipped: 0,
188
+ summariesInserted: 0,
189
+ summariesSkipped: 0,
190
+ summariesMultiParentCollapsed: 0,
191
+ summariesSkippedNoMessages: 0,
192
+ summariesSkippedMultiSession: 0,
193
+ compactionEventsInserted: 0,
194
+ dryRun
195
+ };
196
+ const conversations = listConversations(sourceDb);
197
+ result.conversationsScanned = conversations.length;
198
+ const sessionByConvId = /* @__PURE__ */ new Map();
199
+ const sessionByMessageId = /* @__PURE__ */ new Map();
200
+ for (const c of conversations) {
201
+ sessionByConvId.set(c.conversation_id, resolveSessionId(c));
202
+ }
203
+ const messagesByConv = /* @__PURE__ */ new Map();
204
+ for (const c of conversations) {
205
+ const msgs = listMessagesForConversation(sourceDb, c.conversation_id);
206
+ messagesByConv.set(c.conversation_id, msgs);
207
+ const session = sessionByConvId.get(c.conversation_id);
208
+ for (const m of msgs) {
209
+ sessionByMessageId.set(m.message_id, session);
210
+ }
211
+ }
212
+ const sessionMessages = /* @__PURE__ */ new Map();
213
+ const sessionOrder = [];
214
+ for (const c of conversations) {
215
+ const session = sessionByConvId.get(c.conversation_id);
216
+ if (!sessionMessages.has(session)) {
217
+ sessionMessages.set(session, []);
218
+ sessionOrder.push(session);
219
+ }
220
+ const list = sessionMessages.get(session);
221
+ for (const m of messagesByConv.get(c.conversation_id) ?? []) {
222
+ list.push({ conv: c, msg: m });
223
+ }
224
+ }
225
+ for (const list of sessionMessages.values()) {
226
+ list.sort((a, b) => {
227
+ if (a.msg.created_at !== b.msg.created_at) {
228
+ return a.msg.created_at < b.msg.created_at ? -1 : 1;
229
+ }
230
+ const cidCmp = a.conv.conversation_id.localeCompare(
231
+ b.conv.conversation_id
232
+ );
233
+ if (cidCmp !== 0) return cidCmp;
234
+ return a.msg.seq - b.msg.seq;
235
+ });
236
+ }
237
+ const insertMessageStmt = destDb.prepare(
238
+ "INSERT INTO lcm_messages (session_id, turn_index, role, content, token_count, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)"
239
+ );
240
+ const insertMessageFtsStmt = destDb.prepare(
241
+ "INSERT INTO lcm_messages_fts (rowid, content) VALUES (?, ?)"
242
+ );
243
+ 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 = ?"
245
+ );
246
+ const existingBySession = /* @__PURE__ */ new Map();
247
+ const maxTurnBySession = /* @__PURE__ */ new Map();
248
+ for (const session of sessionMessages.keys()) {
249
+ if (sessionFilter && !sessionFilter.has(session)) continue;
250
+ const map = /* @__PURE__ */ new Map();
251
+ let max = -1;
252
+ const rows = existingScanStmt.iterate(session);
253
+ for (const row of rows) {
254
+ if (row.turn_index > max) max = row.turn_index;
255
+ if (row.conv != null && row.source_seq != null) {
256
+ map.set(`${row.conv}|${row.source_seq}`, row.turn_index);
257
+ }
258
+ }
259
+ existingBySession.set(session, map);
260
+ maxTurnBySession.set(session, max);
261
+ }
262
+ const sessionsTouched = /* @__PURE__ */ new Set();
263
+ const turnIndexByMessageId = /* @__PURE__ */ new Map();
264
+ function assignTurnIndices(forWrite) {
265
+ for (const session of sessionOrder) {
266
+ if (sessionFilter && !sessionFilter.has(session)) continue;
267
+ const entries = sessionMessages.get(session) ?? [];
268
+ const existing = existingBySession.get(session) ?? /* @__PURE__ */ new Map();
269
+ let nextTurn = (maxTurnBySession.get(session) ?? -1) + 1;
270
+ for (const { conv, msg } of entries) {
271
+ const key = `${conv.conversation_id}|${msg.seq}`;
272
+ const existingTurn = existing.get(key);
273
+ if (existingTurn !== void 0) {
274
+ turnIndexByMessageId.set(msg.message_id, existingTurn);
275
+ result.messagesSkipped += 1;
276
+ continue;
277
+ }
278
+ const ti = nextTurn++;
279
+ turnIndexByMessageId.set(msg.message_id, ti);
280
+ existing.set(key, ti);
281
+ if (forWrite) {
282
+ const mapped = mapMessage(conv, msg, ti);
283
+ const info = insertMessageStmt.run(
284
+ mapped.session_id,
285
+ mapped.turn_index,
286
+ mapped.role,
287
+ mapped.content,
288
+ mapped.token_count,
289
+ mapped.created_at,
290
+ mapped.metadata
291
+ );
292
+ insertMessageFtsStmt.run(
293
+ Number(info.lastInsertRowid),
294
+ mapped.content
295
+ );
296
+ }
297
+ result.messagesInserted += 1;
298
+ sessionsTouched.add(session);
299
+ }
300
+ }
301
+ }
302
+ if (!dryRun) {
303
+ const writeMessages = destDb.transaction(() => assignTurnIndices(true));
304
+ writeMessages();
305
+ } else {
306
+ assignTurnIndices(false);
307
+ }
308
+ const summaries = listSummaries(sourceDb);
309
+ const summaryMessages = listSummaryMessages(sourceDb);
310
+ const summaryParents = listSummaryParents(sourceDb);
311
+ const derivations = indexSummaryDerivations(summaryMessages, summaryParents);
312
+ const summaryExistsStmt = destDb.prepare(
313
+ "SELECT 1 AS hit FROM lcm_summary_nodes WHERE id = ? LIMIT 1"
314
+ );
315
+ const insertSummaryStmt = destDb.prepare(
316
+ "INSERT INTO lcm_summary_nodes (id, session_id, depth, parent_id, summary_text, token_count, msg_start, msg_end, escalation, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
317
+ );
318
+ const insertSummaryFtsStmt = destDb.prepare(
319
+ "INSERT INTO lcm_summaries_fts (rowid, summary_text) VALUES (?, ?)"
320
+ );
321
+ const lookupSummaryRowidStmt = destDb.prepare(
322
+ "SELECT rowid AS rowid FROM lcm_summary_nodes WHERE id = ?"
323
+ );
324
+ function processSummaries(forWrite) {
325
+ for (const summary of summaries) {
326
+ const derivation = derivations.get(summary.summary_id);
327
+ if (!derivation || derivation.messageIds.length === 0) {
328
+ result.summariesSkippedNoMessages += 1;
329
+ log(
330
+ `skip summary ${summary.summary_id}: no message references in summary_messages`
331
+ );
332
+ continue;
333
+ }
334
+ const session = resolveSummarySession(
335
+ derivation.messageIds,
336
+ sessionByMessageId
337
+ );
338
+ if (!session) {
339
+ result.summariesSkippedMultiSession += 1;
340
+ log(
341
+ `skip summary ${summary.summary_id}: covers messages from multiple sessions or has dangling references`
342
+ );
343
+ continue;
344
+ }
345
+ if (sessionFilter && !sessionFilter.has(session)) continue;
346
+ const messageSeqs = [];
347
+ for (const mid of derivation.messageIds) {
348
+ const seq = turnIndexByMessageId.get(mid);
349
+ if (typeof seq === "number") messageSeqs.push(seq);
350
+ }
351
+ if (messageSeqs.length === 0) {
352
+ result.summariesSkippedNoMessages += 1;
353
+ log(
354
+ `skip summary ${summary.summary_id}: message ids exist but seqs unresolved`
355
+ );
356
+ continue;
357
+ }
358
+ const mapped = mapSummary({
359
+ summary,
360
+ parents: derivation.parents,
361
+ messageSeqs,
362
+ sessionId: session
363
+ });
364
+ if (isMultiParent(derivation.parents)) {
365
+ result.summariesMultiParentCollapsed += 1;
366
+ log(
367
+ `summary ${summary.summary_id} has ${derivation.parents.length} parents; keeping ${mapped.parent_id ?? "(none)"} (Remnic LCM is single-parent).`
368
+ );
369
+ }
370
+ const existing = summaryExistsStmt.get(mapped.id);
371
+ if (existing) {
372
+ result.summariesSkipped += 1;
373
+ continue;
374
+ }
375
+ if (forWrite) {
376
+ insertSummaryStmt.run(
377
+ mapped.id,
378
+ mapped.session_id,
379
+ mapped.depth,
380
+ mapped.parent_id,
381
+ mapped.summary_text,
382
+ mapped.token_count,
383
+ mapped.msg_start,
384
+ mapped.msg_end,
385
+ mapped.escalation,
386
+ mapped.created_at
387
+ );
388
+ const row = lookupSummaryRowidStmt.get(mapped.id);
389
+ if (row) {
390
+ insertSummaryFtsStmt.run(row.rowid, mapped.summary_text);
391
+ }
392
+ }
393
+ result.summariesInserted += 1;
394
+ sessionsTouched.add(mapped.session_id);
395
+ }
396
+ }
397
+ if (!dryRun) {
398
+ const writeSummaries = destDb.transaction(() => processSummaries(true));
399
+ writeSummaries();
400
+ } else {
401
+ processSummaries(false);
402
+ }
403
+ const insertEventStmt = destDb.prepare(
404
+ "INSERT INTO lcm_compaction_events (session_id, fired_at, msg_before, tokens_before, tokens_after) VALUES (?, ?, ?, ?, ?)"
405
+ );
406
+ const maxTurnStmt = destDb.prepare(
407
+ "SELECT IFNULL(MAX(turn_index), -1) AS max_turn FROM lcm_messages WHERE session_id = ?"
408
+ );
409
+ const totalTokensStmt = destDb.prepare(
410
+ "SELECT IFNULL(SUM(token_count), 0) AS total FROM lcm_messages WHERE session_id = ?"
411
+ );
412
+ function processCompactionBoundaries(forWrite) {
413
+ const firedAt = (/* @__PURE__ */ new Date()).toISOString();
414
+ for (const session of sessionsTouched) {
415
+ const turnRow = maxTurnStmt.get(session);
416
+ const msgBefore = turnRow.max_turn + 1;
417
+ const tokRow = totalTokensStmt.get(session);
418
+ const tokens = tokRow.total;
419
+ if (forWrite) {
420
+ insertEventStmt.run(session, firedAt, msgBefore, tokens, tokens);
421
+ }
422
+ result.compactionEventsInserted += 1;
423
+ }
424
+ }
425
+ if (!dryRun) {
426
+ const writeEvents = destDb.transaction(() => processCompactionBoundaries(true));
427
+ writeEvents();
428
+ } else {
429
+ processCompactionBoundaries(false);
430
+ }
431
+ result.sessionsTouched = [...sessionsTouched].sort();
432
+ return result;
433
+ }
434
+ export {
435
+ LOSSLESS_CLAW_SOURCE_LABEL,
436
+ assertLosslessClawSchema,
437
+ buildMessageMetadata,
438
+ importLosslessClaw,
439
+ indexSummaryDerivations,
440
+ isMultiParent,
441
+ listConversations,
442
+ listMessagesForConversation,
443
+ listSummaries,
444
+ listSummaryMessages,
445
+ listSummaryParents,
446
+ mapMessage,
447
+ mapSummary,
448
+ openExistingLcmDatabaseReadOnly,
449
+ openInMemoryDestinationDatabase,
450
+ openSourceDatabase,
451
+ pickCanonicalParent,
452
+ resolveSessionId,
453
+ resolveSummarySession
454
+ };
455
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@remnic/import-lossless-claw",
3
+ "version": "0.1.0",
4
+ "description": "Import lossless-claw (LCM) SQLite databases into Remnic's LCM mode",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": ["dist"],
15
+ "scripts": {
16
+ "build": "tsup src/index.ts --format esm --dts",
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
+ },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "provenance": true
24
+ },
25
+ "dependencies": {
26
+ "better-sqlite3": "^12.6.2"
27
+ },
28
+ "peerDependencies": {
29
+ "@remnic/core": "workspace:^"
30
+ },
31
+ "devDependencies": {
32
+ "@remnic/core": "workspace:*",
33
+ "@types/better-sqlite3": "^7.6.0",
34
+ "tsup": "^8.0.0",
35
+ "tsx": "^4.0.0",
36
+ "typescript": "^5.7.0"
37
+ },
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/joshuaswarren/remnic.git",
42
+ "directory": "packages/import-lossless-claw"
43
+ },
44
+ "keywords": ["remnic", "memory", "lcm", "lossless-claw", "import", "openclaw"]
45
+ }