engrm 0.4.22 → 0.4.23

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 CHANGED
@@ -221,6 +221,9 @@ The MCP server exposes tools that supported agents can call directly:
221
221
  | `recent_tools` | Inspect captured raw tool chronology |
222
222
  | `recent_sessions` | List recent local sessions to inspect further |
223
223
  | `session_story` | Show prompts, tools, observations, and summary for one session |
224
+ | `create_handoff` | Save an explicit syncable handoff so you can resume work on another device |
225
+ | `recent_handoffs` | List recent saved handoffs for the current project or workspace |
226
+ | `load_handoff` | Open a saved handoff as a resume point for a new session |
224
227
  | `plugin_catalog` | Inspect Engrm plugin manifests for memory-aware integrations |
225
228
  | `save_plugin_memory` | Save reduced plugin output with stable Engrm provenance |
226
229
  | `capture_git_diff` | Reduce a git diff into a durable memory object and save it |
@@ -273,6 +276,21 @@ These tools are intentionally small:
273
276
  - reduced durable memory output
274
277
  - visible in Engrm's local inspection tools so we can judge tool value honestly
275
278
 
279
+ ### Explicit Handoffs
280
+
281
+ For long-running work across devices, Engrm now has an explicit handoff flow:
282
+
283
+ - `create_handoff`
284
+ - snapshot the active thread into a syncable handoff message
285
+ - `recent_handoffs`
286
+ - list the latest saved handoffs
287
+ - `load_handoff`
288
+ - reopen a saved handoff as a clear resume point in a new session
289
+
290
+ This is the deliberate version of multi-device continuity: useful when you want to move from laptop to home machine without waiting for an end-of-session summary.
291
+
292
+ The separate chat lane is still kept distinct from durable observations, but it can now sync too, so recent user/assistant conversation is recoverable on another machine without polluting the main memory feed.
293
+
276
294
  ### Local Memory Inspection
277
295
 
278
296
  For local testing, Engrm now exposes a small inspection set that lets you see
package/dist/cli.js CHANGED
@@ -305,7 +305,7 @@ var MIGRATIONS = [
305
305
  -- Sync outbox (offline-first queue)
306
306
  CREATE TABLE sync_outbox (
307
307
  id INTEGER PRIMARY KEY AUTOINCREMENT,
308
- record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
308
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
309
309
  record_id INTEGER NOT NULL,
310
310
  status TEXT DEFAULT 'pending' CHECK (status IN (
311
311
  'pending', 'syncing', 'synced', 'failed'
@@ -598,6 +598,18 @@ var MIGRATIONS = [
598
598
  ON tool_events(created_at_epoch DESC, id DESC);
599
599
  `
600
600
  },
601
+ {
602
+ version: 11,
603
+ description: "Add observation provenance from tool and prompt chronology",
604
+ sql: `
605
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
606
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
607
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
608
+ ON observations(source_tool, created_at_epoch DESC);
609
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
610
+ ON observations(session_id, source_prompt_number DESC);
611
+ `
612
+ },
601
613
  {
602
614
  version: 12,
603
615
  description: "Add synced handoff metadata to session summaries",
@@ -609,15 +621,79 @@ var MIGRATIONS = [
609
621
  `
610
622
  },
611
623
  {
612
- version: 11,
613
- description: "Add observation provenance from tool and prompt chronology",
624
+ version: 13,
625
+ description: "Add current_thread to session summaries",
614
626
  sql: `
615
- ALTER TABLE observations ADD COLUMN source_tool TEXT;
616
- ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
617
- CREATE INDEX IF NOT EXISTS idx_observations_source_tool
618
- ON observations(source_tool, created_at_epoch DESC);
619
- CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
620
- ON observations(session_id, source_prompt_number DESC);
627
+ ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
628
+ `
629
+ },
630
+ {
631
+ version: 14,
632
+ description: "Add chat_messages lane for raw conversation recall",
633
+ sql: `
634
+ CREATE TABLE IF NOT EXISTS chat_messages (
635
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
636
+ session_id TEXT NOT NULL,
637
+ project_id INTEGER REFERENCES projects(id),
638
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
639
+ content TEXT NOT NULL,
640
+ user_id TEXT NOT NULL,
641
+ device_id TEXT NOT NULL,
642
+ agent TEXT DEFAULT 'claude-code',
643
+ created_at_epoch INTEGER NOT NULL
644
+ );
645
+
646
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_session
647
+ ON chat_messages(session_id, created_at_epoch DESC, id DESC);
648
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_project
649
+ ON chat_messages(project_id, created_at_epoch DESC, id DESC);
650
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_created
651
+ ON chat_messages(created_at_epoch DESC, id DESC);
652
+ `
653
+ },
654
+ {
655
+ version: 15,
656
+ description: "Add remote_source_id for chat message sync deduplication",
657
+ sql: `
658
+ ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
659
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
660
+ ON chat_messages(remote_source_id)
661
+ WHERE remote_source_id IS NOT NULL;
662
+ `
663
+ },
664
+ {
665
+ version: 16,
666
+ description: "Allow chat_message records in sync_outbox",
667
+ sql: `
668
+ CREATE TABLE sync_outbox_new (
669
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
670
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
671
+ record_id INTEGER NOT NULL,
672
+ status TEXT DEFAULT 'pending' CHECK (status IN (
673
+ 'pending', 'syncing', 'synced', 'failed'
674
+ )),
675
+ retry_count INTEGER DEFAULT 0,
676
+ max_retries INTEGER DEFAULT 10,
677
+ last_error TEXT,
678
+ created_at_epoch INTEGER NOT NULL,
679
+ synced_at_epoch INTEGER,
680
+ next_retry_epoch INTEGER
681
+ );
682
+
683
+ INSERT INTO sync_outbox_new (
684
+ id, record_type, record_id, status, retry_count, max_retries,
685
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
686
+ )
687
+ SELECT
688
+ id, record_type, record_id, status, retry_count, max_retries,
689
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
690
+ FROM sync_outbox;
691
+
692
+ DROP TABLE sync_outbox;
693
+ ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
694
+
695
+ CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
696
+ CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
621
697
  `
622
698
  }
623
699
  ];
@@ -677,6 +753,18 @@ function inferLegacySchemaVersion(db) {
677
753
  if (columnExists(db, "session_summaries", "capture_state") && columnExists(db, "session_summaries", "recent_tool_names") && columnExists(db, "session_summaries", "hot_files") && columnExists(db, "session_summaries", "recent_outcomes")) {
678
754
  version = Math.max(version, 12);
679
755
  }
756
+ if (columnExists(db, "session_summaries", "current_thread")) {
757
+ version = Math.max(version, 13);
758
+ }
759
+ if (tableExists(db, "chat_messages")) {
760
+ version = Math.max(version, 14);
761
+ }
762
+ if (columnExists(db, "chat_messages", "remote_source_id")) {
763
+ version = Math.max(version, 15);
764
+ }
765
+ if (syncOutboxSupportsChatMessages(db)) {
766
+ version = Math.max(version, 16);
767
+ }
680
768
  return version;
681
769
  }
682
770
  function runMigrations(db) {
@@ -760,7 +848,8 @@ function ensureSessionSummaryColumns(db) {
760
848
  "capture_state",
761
849
  "recent_tool_names",
762
850
  "hot_files",
763
- "recent_outcomes"
851
+ "recent_outcomes",
852
+ "current_thread"
764
853
  ];
765
854
  for (const column of required) {
766
855
  if (columnExists(db, "session_summaries", column))
@@ -768,10 +857,75 @@ function ensureSessionSummaryColumns(db) {
768
857
  db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
769
858
  }
770
859
  const current = getSchemaVersion(db);
771
- if (current < 12) {
772
- db.exec("PRAGMA user_version = 12");
860
+ if (current < 13) {
861
+ db.exec("PRAGMA user_version = 13");
862
+ }
863
+ }
864
+ function ensureChatMessageColumns(db) {
865
+ if (!tableExists(db, "chat_messages"))
866
+ return;
867
+ if (!columnExists(db, "chat_messages", "remote_source_id")) {
868
+ db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
869
+ }
870
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source ON chat_messages(remote_source_id) WHERE remote_source_id IS NOT NULL");
871
+ const current = getSchemaVersion(db);
872
+ if (current < 15) {
873
+ db.exec("PRAGMA user_version = 15");
874
+ }
875
+ }
876
+ function ensureSyncOutboxSupportsChatMessages(db) {
877
+ if (syncOutboxSupportsChatMessages(db)) {
878
+ const current = getSchemaVersion(db);
879
+ if (current < 16) {
880
+ db.exec("PRAGMA user_version = 16");
881
+ }
882
+ return;
883
+ }
884
+ db.exec("BEGIN TRANSACTION");
885
+ try {
886
+ db.exec(`
887
+ CREATE TABLE sync_outbox_new (
888
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
889
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
890
+ record_id INTEGER NOT NULL,
891
+ status TEXT DEFAULT 'pending' CHECK (status IN (
892
+ 'pending', 'syncing', 'synced', 'failed'
893
+ )),
894
+ retry_count INTEGER DEFAULT 0,
895
+ max_retries INTEGER DEFAULT 10,
896
+ last_error TEXT,
897
+ created_at_epoch INTEGER NOT NULL,
898
+ synced_at_epoch INTEGER,
899
+ next_retry_epoch INTEGER
900
+ );
901
+
902
+ INSERT INTO sync_outbox_new (
903
+ id, record_type, record_id, status, retry_count, max_retries,
904
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
905
+ )
906
+ SELECT
907
+ id, record_type, record_id, status, retry_count, max_retries,
908
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
909
+ FROM sync_outbox;
910
+
911
+ DROP TABLE sync_outbox;
912
+ ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
913
+
914
+ CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
915
+ CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
916
+ `);
917
+ db.exec("PRAGMA user_version = 16");
918
+ db.exec("COMMIT");
919
+ } catch (error) {
920
+ db.exec("ROLLBACK");
921
+ throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
773
922
  }
774
923
  }
924
+ function syncOutboxSupportsChatMessages(db) {
925
+ const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
926
+ const sql = row?.sql ?? "";
927
+ return sql.includes("'chat_message'");
928
+ }
775
929
  function getSchemaVersion(db) {
776
930
  const result = db.query("PRAGMA user_version").get();
777
931
  return result.user_version;
@@ -931,6 +1085,8 @@ class MemDatabase {
931
1085
  runMigrations(this.db);
932
1086
  ensureObservationTypes(this.db);
933
1087
  ensureSessionSummaryColumns(this.db);
1088
+ ensureChatMessageColumns(this.db);
1089
+ ensureSyncOutboxSupportsChatMessages(this.db);
934
1090
  }
935
1091
  loadVecExtension() {
936
1092
  try {
@@ -1156,6 +1312,7 @@ class MemDatabase {
1156
1312
  p.name AS project_name,
1157
1313
  ss.request AS request,
1158
1314
  ss.completed AS completed,
1315
+ ss.current_thread AS current_thread,
1159
1316
  ss.capture_state AS capture_state,
1160
1317
  ss.recent_tool_names AS recent_tool_names,
1161
1318
  ss.hot_files AS hot_files,
@@ -1174,6 +1331,7 @@ class MemDatabase {
1174
1331
  p.name AS project_name,
1175
1332
  ss.request AS request,
1176
1333
  ss.completed AS completed,
1334
+ ss.current_thread AS current_thread,
1177
1335
  ss.capture_state AS capture_state,
1178
1336
  ss.recent_tool_names AS recent_tool_names,
1179
1337
  ss.hot_files AS hot_files,
@@ -1264,6 +1422,54 @@ class MemDatabase {
1264
1422
  ORDER BY created_at_epoch DESC, id DESC
1265
1423
  LIMIT ?`).all(...userId ? [userId] : [], limit);
1266
1424
  }
1425
+ insertChatMessage(input) {
1426
+ const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1427
+ const content = input.content.trim();
1428
+ const result = this.db.query(`INSERT INTO chat_messages (
1429
+ session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
1430
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.session_id, input.project_id, input.role, content, input.user_id, input.device_id, input.agent ?? "claude-code", createdAt, input.remote_source_id ?? null);
1431
+ return this.getChatMessageById(Number(result.lastInsertRowid));
1432
+ }
1433
+ getChatMessageById(id) {
1434
+ return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
1435
+ }
1436
+ getChatMessageByRemoteSourceId(remoteSourceId) {
1437
+ return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
1438
+ }
1439
+ getSessionChatMessages(sessionId, limit = 50) {
1440
+ return this.db.query(`SELECT * FROM chat_messages
1441
+ WHERE session_id = ?
1442
+ ORDER BY created_at_epoch ASC, id ASC
1443
+ LIMIT ?`).all(sessionId, limit);
1444
+ }
1445
+ getRecentChatMessages(projectId, limit = 20, userId) {
1446
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1447
+ if (projectId !== null) {
1448
+ return this.db.query(`SELECT * FROM chat_messages
1449
+ WHERE project_id = ?${visibilityClause}
1450
+ ORDER BY created_at_epoch DESC, id DESC
1451
+ LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
1452
+ }
1453
+ return this.db.query(`SELECT * FROM chat_messages
1454
+ WHERE 1 = 1${visibilityClause}
1455
+ ORDER BY created_at_epoch DESC, id DESC
1456
+ LIMIT ?`).all(...userId ? [userId] : [], limit);
1457
+ }
1458
+ searchChatMessages(query, projectId, limit = 20, userId) {
1459
+ const needle = `%${query.toLowerCase()}%`;
1460
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1461
+ if (projectId !== null) {
1462
+ return this.db.query(`SELECT * FROM chat_messages
1463
+ WHERE project_id = ?
1464
+ AND lower(content) LIKE ?${visibilityClause}
1465
+ ORDER BY created_at_epoch DESC, id DESC
1466
+ LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
1467
+ }
1468
+ return this.db.query(`SELECT * FROM chat_messages
1469
+ WHERE lower(content) LIKE ?${visibilityClause}
1470
+ ORDER BY created_at_epoch DESC, id DESC
1471
+ LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
1472
+ }
1267
1473
  addToOutbox(recordType, recordId) {
1268
1474
  const now = Math.floor(Date.now() / 1000);
1269
1475
  this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
@@ -1352,9 +1558,9 @@ class MemDatabase {
1352
1558
  };
1353
1559
  const result = this.db.query(`INSERT INTO session_summaries (
1354
1560
  session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
1355
- capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
1561
+ current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
1356
1562
  )
1357
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, summary.capture_state ?? null, summary.recent_tool_names ?? null, summary.hot_files ?? null, summary.recent_outcomes ?? null, now);
1563
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, summary.current_thread ?? null, summary.capture_state ?? null, summary.recent_tool_names ?? null, summary.hot_files ?? null, summary.recent_outcomes ?? null, now);
1358
1564
  const id = Number(result.lastInsertRowid);
1359
1565
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1360
1566
  }
@@ -1370,6 +1576,7 @@ class MemDatabase {
1370
1576
  learned: normalizeSummarySection(summary.learned ?? existing.learned),
1371
1577
  completed: normalizeSummarySection(summary.completed ?? existing.completed),
1372
1578
  next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
1579
+ current_thread: summary.current_thread ?? existing.current_thread,
1373
1580
  capture_state: summary.capture_state ?? existing.capture_state,
1374
1581
  recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
1375
1582
  hot_files: summary.hot_files ?? existing.hot_files,
@@ -1383,12 +1590,13 @@ class MemDatabase {
1383
1590
  learned = ?,
1384
1591
  completed = ?,
1385
1592
  next_steps = ?,
1593
+ current_thread = ?,
1386
1594
  capture_state = ?,
1387
1595
  recent_tool_names = ?,
1388
1596
  hot_files = ?,
1389
1597
  recent_outcomes = ?,
1390
1598
  created_at_epoch = ?
1391
- WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, normalized.capture_state, normalized.recent_tool_names, normalized.hot_files, normalized.recent_outcomes, now, summary.session_id);
1599
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, normalized.current_thread, normalized.capture_state, normalized.recent_tool_names, normalized.hot_files, normalized.recent_outcomes, now, summary.session_id);
1392
1600
  return this.getSessionSummary(summary.session_id);
1393
1601
  }
1394
1602
  getSessionSummary(sessionId) {
@@ -1600,7 +1808,7 @@ var MIGRATIONS2 = [
1600
1808
  -- Sync outbox (offline-first queue)
1601
1809
  CREATE TABLE sync_outbox (
1602
1810
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1603
- record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
1811
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
1604
1812
  record_id INTEGER NOT NULL,
1605
1813
  status TEXT DEFAULT 'pending' CHECK (status IN (
1606
1814
  'pending', 'syncing', 'synced', 'failed'
@@ -1893,6 +2101,18 @@ var MIGRATIONS2 = [
1893
2101
  ON tool_events(created_at_epoch DESC, id DESC);
1894
2102
  `
1895
2103
  },
2104
+ {
2105
+ version: 11,
2106
+ description: "Add observation provenance from tool and prompt chronology",
2107
+ sql: `
2108
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
2109
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
2110
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
2111
+ ON observations(source_tool, created_at_epoch DESC);
2112
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
2113
+ ON observations(session_id, source_prompt_number DESC);
2114
+ `
2115
+ },
1896
2116
  {
1897
2117
  version: 12,
1898
2118
  description: "Add synced handoff metadata to session summaries",
@@ -1904,15 +2124,79 @@ var MIGRATIONS2 = [
1904
2124
  `
1905
2125
  },
1906
2126
  {
1907
- version: 11,
1908
- description: "Add observation provenance from tool and prompt chronology",
2127
+ version: 13,
2128
+ description: "Add current_thread to session summaries",
1909
2129
  sql: `
1910
- ALTER TABLE observations ADD COLUMN source_tool TEXT;
1911
- ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
1912
- CREATE INDEX IF NOT EXISTS idx_observations_source_tool
1913
- ON observations(source_tool, created_at_epoch DESC);
1914
- CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
1915
- ON observations(session_id, source_prompt_number DESC);
2130
+ ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
2131
+ `
2132
+ },
2133
+ {
2134
+ version: 14,
2135
+ description: "Add chat_messages lane for raw conversation recall",
2136
+ sql: `
2137
+ CREATE TABLE IF NOT EXISTS chat_messages (
2138
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2139
+ session_id TEXT NOT NULL,
2140
+ project_id INTEGER REFERENCES projects(id),
2141
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
2142
+ content TEXT NOT NULL,
2143
+ user_id TEXT NOT NULL,
2144
+ device_id TEXT NOT NULL,
2145
+ agent TEXT DEFAULT 'claude-code',
2146
+ created_at_epoch INTEGER NOT NULL
2147
+ );
2148
+
2149
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_session
2150
+ ON chat_messages(session_id, created_at_epoch DESC, id DESC);
2151
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_project
2152
+ ON chat_messages(project_id, created_at_epoch DESC, id DESC);
2153
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_created
2154
+ ON chat_messages(created_at_epoch DESC, id DESC);
2155
+ `
2156
+ },
2157
+ {
2158
+ version: 15,
2159
+ description: "Add remote_source_id for chat message sync deduplication",
2160
+ sql: `
2161
+ ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
2162
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
2163
+ ON chat_messages(remote_source_id)
2164
+ WHERE remote_source_id IS NOT NULL;
2165
+ `
2166
+ },
2167
+ {
2168
+ version: 16,
2169
+ description: "Allow chat_message records in sync_outbox",
2170
+ sql: `
2171
+ CREATE TABLE sync_outbox_new (
2172
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2173
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
2174
+ record_id INTEGER NOT NULL,
2175
+ status TEXT DEFAULT 'pending' CHECK (status IN (
2176
+ 'pending', 'syncing', 'synced', 'failed'
2177
+ )),
2178
+ retry_count INTEGER DEFAULT 0,
2179
+ max_retries INTEGER DEFAULT 10,
2180
+ last_error TEXT,
2181
+ created_at_epoch INTEGER NOT NULL,
2182
+ synced_at_epoch INTEGER,
2183
+ next_retry_epoch INTEGER
2184
+ );
2185
+
2186
+ INSERT INTO sync_outbox_new (
2187
+ id, record_type, record_id, status, retry_count, max_retries,
2188
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
2189
+ )
2190
+ SELECT
2191
+ id, record_type, record_id, status, retry_count, max_retries,
2192
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
2193
+ FROM sync_outbox;
2194
+
2195
+ DROP TABLE sync_outbox;
2196
+ ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
2197
+
2198
+ CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
2199
+ CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
1916
2200
  `
1917
2201
  }
1918
2202
  ];