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.
@@ -353,7 +353,7 @@ var MIGRATIONS = [
353
353
  -- Sync outbox (offline-first queue)
354
354
  CREATE TABLE sync_outbox (
355
355
  id INTEGER PRIMARY KEY AUTOINCREMENT,
356
- record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
356
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
357
357
  record_id INTEGER NOT NULL,
358
358
  status TEXT DEFAULT 'pending' CHECK (status IN (
359
359
  'pending', 'syncing', 'synced', 'failed'
@@ -646,6 +646,18 @@ var MIGRATIONS = [
646
646
  ON tool_events(created_at_epoch DESC, id DESC);
647
647
  `
648
648
  },
649
+ {
650
+ version: 11,
651
+ description: "Add observation provenance from tool and prompt chronology",
652
+ sql: `
653
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
654
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
655
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
656
+ ON observations(source_tool, created_at_epoch DESC);
657
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
658
+ ON observations(session_id, source_prompt_number DESC);
659
+ `
660
+ },
649
661
  {
650
662
  version: 12,
651
663
  description: "Add synced handoff metadata to session summaries",
@@ -657,15 +669,79 @@ var MIGRATIONS = [
657
669
  `
658
670
  },
659
671
  {
660
- version: 11,
661
- description: "Add observation provenance from tool and prompt chronology",
672
+ version: 13,
673
+ description: "Add current_thread to session summaries",
662
674
  sql: `
663
- ALTER TABLE observations ADD COLUMN source_tool TEXT;
664
- ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
665
- CREATE INDEX IF NOT EXISTS idx_observations_source_tool
666
- ON observations(source_tool, created_at_epoch DESC);
667
- CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
668
- ON observations(session_id, source_prompt_number DESC);
675
+ ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
676
+ `
677
+ },
678
+ {
679
+ version: 14,
680
+ description: "Add chat_messages lane for raw conversation recall",
681
+ sql: `
682
+ CREATE TABLE IF NOT EXISTS chat_messages (
683
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
684
+ session_id TEXT NOT NULL,
685
+ project_id INTEGER REFERENCES projects(id),
686
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
687
+ content TEXT NOT NULL,
688
+ user_id TEXT NOT NULL,
689
+ device_id TEXT NOT NULL,
690
+ agent TEXT DEFAULT 'claude-code',
691
+ created_at_epoch INTEGER NOT NULL
692
+ );
693
+
694
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_session
695
+ ON chat_messages(session_id, created_at_epoch DESC, id DESC);
696
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_project
697
+ ON chat_messages(project_id, created_at_epoch DESC, id DESC);
698
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_created
699
+ ON chat_messages(created_at_epoch DESC, id DESC);
700
+ `
701
+ },
702
+ {
703
+ version: 15,
704
+ description: "Add remote_source_id for chat message sync deduplication",
705
+ sql: `
706
+ ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
707
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
708
+ ON chat_messages(remote_source_id)
709
+ WHERE remote_source_id IS NOT NULL;
710
+ `
711
+ },
712
+ {
713
+ version: 16,
714
+ description: "Allow chat_message records in sync_outbox",
715
+ sql: `
716
+ CREATE TABLE sync_outbox_new (
717
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
718
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
719
+ record_id INTEGER NOT NULL,
720
+ status TEXT DEFAULT 'pending' CHECK (status IN (
721
+ 'pending', 'syncing', 'synced', 'failed'
722
+ )),
723
+ retry_count INTEGER DEFAULT 0,
724
+ max_retries INTEGER DEFAULT 10,
725
+ last_error TEXT,
726
+ created_at_epoch INTEGER NOT NULL,
727
+ synced_at_epoch INTEGER,
728
+ next_retry_epoch INTEGER
729
+ );
730
+
731
+ INSERT INTO sync_outbox_new (
732
+ id, record_type, record_id, status, retry_count, max_retries,
733
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
734
+ )
735
+ SELECT
736
+ id, record_type, record_id, status, retry_count, max_retries,
737
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
738
+ FROM sync_outbox;
739
+
740
+ DROP TABLE sync_outbox;
741
+ ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
742
+
743
+ CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
744
+ CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
669
745
  `
670
746
  }
671
747
  ];
@@ -725,6 +801,18 @@ function inferLegacySchemaVersion(db) {
725
801
  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")) {
726
802
  version = Math.max(version, 12);
727
803
  }
804
+ if (columnExists(db, "session_summaries", "current_thread")) {
805
+ version = Math.max(version, 13);
806
+ }
807
+ if (tableExists(db, "chat_messages")) {
808
+ version = Math.max(version, 14);
809
+ }
810
+ if (columnExists(db, "chat_messages", "remote_source_id")) {
811
+ version = Math.max(version, 15);
812
+ }
813
+ if (syncOutboxSupportsChatMessages(db)) {
814
+ version = Math.max(version, 16);
815
+ }
728
816
  return version;
729
817
  }
730
818
  function runMigrations(db) {
@@ -808,7 +896,8 @@ function ensureSessionSummaryColumns(db) {
808
896
  "capture_state",
809
897
  "recent_tool_names",
810
898
  "hot_files",
811
- "recent_outcomes"
899
+ "recent_outcomes",
900
+ "current_thread"
812
901
  ];
813
902
  for (const column of required) {
814
903
  if (columnExists(db, "session_summaries", column))
@@ -816,10 +905,75 @@ function ensureSessionSummaryColumns(db) {
816
905
  db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
817
906
  }
818
907
  const current = getSchemaVersion(db);
819
- if (current < 12) {
820
- db.exec("PRAGMA user_version = 12");
908
+ if (current < 13) {
909
+ db.exec("PRAGMA user_version = 13");
910
+ }
911
+ }
912
+ function ensureChatMessageColumns(db) {
913
+ if (!tableExists(db, "chat_messages"))
914
+ return;
915
+ if (!columnExists(db, "chat_messages", "remote_source_id")) {
916
+ db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
917
+ }
918
+ 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");
919
+ const current = getSchemaVersion(db);
920
+ if (current < 15) {
921
+ db.exec("PRAGMA user_version = 15");
922
+ }
923
+ }
924
+ function ensureSyncOutboxSupportsChatMessages(db) {
925
+ if (syncOutboxSupportsChatMessages(db)) {
926
+ const current = getSchemaVersion(db);
927
+ if (current < 16) {
928
+ db.exec("PRAGMA user_version = 16");
929
+ }
930
+ return;
931
+ }
932
+ db.exec("BEGIN TRANSACTION");
933
+ try {
934
+ db.exec(`
935
+ CREATE TABLE sync_outbox_new (
936
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
937
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
938
+ record_id INTEGER NOT NULL,
939
+ status TEXT DEFAULT 'pending' CHECK (status IN (
940
+ 'pending', 'syncing', 'synced', 'failed'
941
+ )),
942
+ retry_count INTEGER DEFAULT 0,
943
+ max_retries INTEGER DEFAULT 10,
944
+ last_error TEXT,
945
+ created_at_epoch INTEGER NOT NULL,
946
+ synced_at_epoch INTEGER,
947
+ next_retry_epoch INTEGER
948
+ );
949
+
950
+ INSERT INTO sync_outbox_new (
951
+ id, record_type, record_id, status, retry_count, max_retries,
952
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
953
+ )
954
+ SELECT
955
+ id, record_type, record_id, status, retry_count, max_retries,
956
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
957
+ FROM sync_outbox;
958
+
959
+ DROP TABLE sync_outbox;
960
+ ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
961
+
962
+ CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
963
+ CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
964
+ `);
965
+ db.exec("PRAGMA user_version = 16");
966
+ db.exec("COMMIT");
967
+ } catch (error) {
968
+ db.exec("ROLLBACK");
969
+ throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
821
970
  }
822
971
  }
972
+ function syncOutboxSupportsChatMessages(db) {
973
+ const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
974
+ const sql = row?.sql ?? "";
975
+ return sql.includes("'chat_message'");
976
+ }
823
977
  function getSchemaVersion(db) {
824
978
  const result = db.query("PRAGMA user_version").get();
825
979
  return result.user_version;
@@ -979,6 +1133,8 @@ class MemDatabase {
979
1133
  runMigrations(this.db);
980
1134
  ensureObservationTypes(this.db);
981
1135
  ensureSessionSummaryColumns(this.db);
1136
+ ensureChatMessageColumns(this.db);
1137
+ ensureSyncOutboxSupportsChatMessages(this.db);
982
1138
  }
983
1139
  loadVecExtension() {
984
1140
  try {
@@ -1204,6 +1360,7 @@ class MemDatabase {
1204
1360
  p.name AS project_name,
1205
1361
  ss.request AS request,
1206
1362
  ss.completed AS completed,
1363
+ ss.current_thread AS current_thread,
1207
1364
  ss.capture_state AS capture_state,
1208
1365
  ss.recent_tool_names AS recent_tool_names,
1209
1366
  ss.hot_files AS hot_files,
@@ -1222,6 +1379,7 @@ class MemDatabase {
1222
1379
  p.name AS project_name,
1223
1380
  ss.request AS request,
1224
1381
  ss.completed AS completed,
1382
+ ss.current_thread AS current_thread,
1225
1383
  ss.capture_state AS capture_state,
1226
1384
  ss.recent_tool_names AS recent_tool_names,
1227
1385
  ss.hot_files AS hot_files,
@@ -1312,6 +1470,54 @@ class MemDatabase {
1312
1470
  ORDER BY created_at_epoch DESC, id DESC
1313
1471
  LIMIT ?`).all(...userId ? [userId] : [], limit);
1314
1472
  }
1473
+ insertChatMessage(input) {
1474
+ const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1475
+ const content = input.content.trim();
1476
+ const result = this.db.query(`INSERT INTO chat_messages (
1477
+ session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
1478
+ ) 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);
1479
+ return this.getChatMessageById(Number(result.lastInsertRowid));
1480
+ }
1481
+ getChatMessageById(id) {
1482
+ return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
1483
+ }
1484
+ getChatMessageByRemoteSourceId(remoteSourceId) {
1485
+ return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
1486
+ }
1487
+ getSessionChatMessages(sessionId, limit = 50) {
1488
+ return this.db.query(`SELECT * FROM chat_messages
1489
+ WHERE session_id = ?
1490
+ ORDER BY created_at_epoch ASC, id ASC
1491
+ LIMIT ?`).all(sessionId, limit);
1492
+ }
1493
+ getRecentChatMessages(projectId, limit = 20, userId) {
1494
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1495
+ if (projectId !== null) {
1496
+ return this.db.query(`SELECT * FROM chat_messages
1497
+ WHERE project_id = ?${visibilityClause}
1498
+ ORDER BY created_at_epoch DESC, id DESC
1499
+ LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
1500
+ }
1501
+ return this.db.query(`SELECT * FROM chat_messages
1502
+ WHERE 1 = 1${visibilityClause}
1503
+ ORDER BY created_at_epoch DESC, id DESC
1504
+ LIMIT ?`).all(...userId ? [userId] : [], limit);
1505
+ }
1506
+ searchChatMessages(query, projectId, limit = 20, userId) {
1507
+ const needle = `%${query.toLowerCase()}%`;
1508
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1509
+ if (projectId !== null) {
1510
+ return this.db.query(`SELECT * FROM chat_messages
1511
+ WHERE project_id = ?
1512
+ AND lower(content) LIKE ?${visibilityClause}
1513
+ ORDER BY created_at_epoch DESC, id DESC
1514
+ LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
1515
+ }
1516
+ return this.db.query(`SELECT * FROM chat_messages
1517
+ WHERE lower(content) LIKE ?${visibilityClause}
1518
+ ORDER BY created_at_epoch DESC, id DESC
1519
+ LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
1520
+ }
1315
1521
  addToOutbox(recordType, recordId) {
1316
1522
  const now = Math.floor(Date.now() / 1000);
1317
1523
  this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
@@ -1400,9 +1606,9 @@ class MemDatabase {
1400
1606
  };
1401
1607
  const result = this.db.query(`INSERT INTO session_summaries (
1402
1608
  session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
1403
- capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
1609
+ current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
1404
1610
  )
1405
- 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);
1611
+ 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);
1406
1612
  const id = Number(result.lastInsertRowid);
1407
1613
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1408
1614
  }
@@ -1418,6 +1624,7 @@ class MemDatabase {
1418
1624
  learned: normalizeSummarySection(summary.learned ?? existing.learned),
1419
1625
  completed: normalizeSummarySection(summary.completed ?? existing.completed),
1420
1626
  next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
1627
+ current_thread: summary.current_thread ?? existing.current_thread,
1421
1628
  capture_state: summary.capture_state ?? existing.capture_state,
1422
1629
  recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
1423
1630
  hot_files: summary.hot_files ?? existing.hot_files,
@@ -1431,12 +1638,13 @@ class MemDatabase {
1431
1638
  learned = ?,
1432
1639
  completed = ?,
1433
1640
  next_steps = ?,
1641
+ current_thread = ?,
1434
1642
  capture_state = ?,
1435
1643
  recent_tool_names = ?,
1436
1644
  hot_files = ?,
1437
1645
  recent_outcomes = ?,
1438
1646
  created_at_epoch = ?
1439
- 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);
1647
+ 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);
1440
1648
  return this.getSessionSummary(summary.session_id);
1441
1649
  }
1442
1650
  getSessionSummary(sessionId) {