engrm 0.4.21 → 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'
@@ -648,6 +648,18 @@ var MIGRATIONS = [
648
648
  },
649
649
  {
650
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
+ },
661
+ {
662
+ version: 12,
651
663
  description: "Add synced handoff metadata to session summaries",
652
664
  sql: `
653
665
  ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
@@ -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
  ];
@@ -722,6 +798,21 @@ function inferLegacySchemaVersion(db) {
722
798
  version = Math.max(version, 10);
723
799
  if (columnExists(db, "observations", "source_tool"))
724
800
  version = Math.max(version, 11);
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")) {
802
+ version = Math.max(version, 12);
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
+ }
725
816
  return version;
726
817
  }
727
818
  function runMigrations(db) {
@@ -800,6 +891,93 @@ function ensureObservationTypes(db) {
800
891
  }
801
892
  }
802
893
  }
894
+ function ensureSessionSummaryColumns(db) {
895
+ const required = [
896
+ "capture_state",
897
+ "recent_tool_names",
898
+ "hot_files",
899
+ "recent_outcomes",
900
+ "current_thread"
901
+ ];
902
+ for (const column of required) {
903
+ if (columnExists(db, "session_summaries", column))
904
+ continue;
905
+ db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
906
+ }
907
+ const current = getSchemaVersion(db);
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)}`);
970
+ }
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
+ }
977
+ function getSchemaVersion(db) {
978
+ const result = db.query("PRAGMA user_version").get();
979
+ return result.user_version;
980
+ }
803
981
  var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
804
982
 
805
983
  // src/storage/sqlite.ts
@@ -954,6 +1132,9 @@ class MemDatabase {
954
1132
  this.vecAvailable = this.loadVecExtension();
955
1133
  runMigrations(this.db);
956
1134
  ensureObservationTypes(this.db);
1135
+ ensureSessionSummaryColumns(this.db);
1136
+ ensureChatMessageColumns(this.db);
1137
+ ensureSyncOutboxSupportsChatMessages(this.db);
957
1138
  }
958
1139
  loadVecExtension() {
959
1140
  try {
@@ -1179,6 +1360,7 @@ class MemDatabase {
1179
1360
  p.name AS project_name,
1180
1361
  ss.request AS request,
1181
1362
  ss.completed AS completed,
1363
+ ss.current_thread AS current_thread,
1182
1364
  ss.capture_state AS capture_state,
1183
1365
  ss.recent_tool_names AS recent_tool_names,
1184
1366
  ss.hot_files AS hot_files,
@@ -1197,6 +1379,7 @@ class MemDatabase {
1197
1379
  p.name AS project_name,
1198
1380
  ss.request AS request,
1199
1381
  ss.completed AS completed,
1382
+ ss.current_thread AS current_thread,
1200
1383
  ss.capture_state AS capture_state,
1201
1384
  ss.recent_tool_names AS recent_tool_names,
1202
1385
  ss.hot_files AS hot_files,
@@ -1287,6 +1470,54 @@ class MemDatabase {
1287
1470
  ORDER BY created_at_epoch DESC, id DESC
1288
1471
  LIMIT ?`).all(...userId ? [userId] : [], limit);
1289
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
+ }
1290
1521
  addToOutbox(recordType, recordId) {
1291
1522
  const now = Math.floor(Date.now() / 1000);
1292
1523
  this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
@@ -1375,9 +1606,9 @@ class MemDatabase {
1375
1606
  };
1376
1607
  const result = this.db.query(`INSERT INTO session_summaries (
1377
1608
  session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
1378
- 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
1379
1610
  )
1380
- 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);
1381
1612
  const id = Number(result.lastInsertRowid);
1382
1613
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1383
1614
  }
@@ -1393,6 +1624,7 @@ class MemDatabase {
1393
1624
  learned: normalizeSummarySection(summary.learned ?? existing.learned),
1394
1625
  completed: normalizeSummarySection(summary.completed ?? existing.completed),
1395
1626
  next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
1627
+ current_thread: summary.current_thread ?? existing.current_thread,
1396
1628
  capture_state: summary.capture_state ?? existing.capture_state,
1397
1629
  recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
1398
1630
  hot_files: summary.hot_files ?? existing.hot_files,
@@ -1406,12 +1638,13 @@ class MemDatabase {
1406
1638
  learned = ?,
1407
1639
  completed = ?,
1408
1640
  next_steps = ?,
1641
+ current_thread = ?,
1409
1642
  capture_state = ?,
1410
1643
  recent_tool_names = ?,
1411
1644
  hot_files = ?,
1412
1645
  recent_outcomes = ?,
1413
1646
  created_at_epoch = ?
1414
- 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);
1415
1648
  return this.getSessionSummary(summary.session_id);
1416
1649
  }
1417
1650
  getSessionSummary(sessionId) {