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.
@@ -1138,7 +1138,7 @@ var MIGRATIONS = [
1138
1138
  -- Sync outbox (offline-first queue)
1139
1139
  CREATE TABLE sync_outbox (
1140
1140
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1141
- record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
1141
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
1142
1142
  record_id INTEGER NOT NULL,
1143
1143
  status TEXT DEFAULT 'pending' CHECK (status IN (
1144
1144
  'pending', 'syncing', 'synced', 'failed'
@@ -1433,6 +1433,18 @@ var MIGRATIONS = [
1433
1433
  },
1434
1434
  {
1435
1435
  version: 11,
1436
+ description: "Add observation provenance from tool and prompt chronology",
1437
+ sql: `
1438
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
1439
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
1440
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
1441
+ ON observations(source_tool, created_at_epoch DESC);
1442
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
1443
+ ON observations(session_id, source_prompt_number DESC);
1444
+ `
1445
+ },
1446
+ {
1447
+ version: 12,
1436
1448
  description: "Add synced handoff metadata to session summaries",
1437
1449
  sql: `
1438
1450
  ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
@@ -1442,15 +1454,79 @@ var MIGRATIONS = [
1442
1454
  `
1443
1455
  },
1444
1456
  {
1445
- version: 11,
1446
- description: "Add observation provenance from tool and prompt chronology",
1457
+ version: 13,
1458
+ description: "Add current_thread to session summaries",
1447
1459
  sql: `
1448
- ALTER TABLE observations ADD COLUMN source_tool TEXT;
1449
- ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
1450
- CREATE INDEX IF NOT EXISTS idx_observations_source_tool
1451
- ON observations(source_tool, created_at_epoch DESC);
1452
- CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
1453
- ON observations(session_id, source_prompt_number DESC);
1460
+ ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
1461
+ `
1462
+ },
1463
+ {
1464
+ version: 14,
1465
+ description: "Add chat_messages lane for raw conversation recall",
1466
+ sql: `
1467
+ CREATE TABLE IF NOT EXISTS chat_messages (
1468
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1469
+ session_id TEXT NOT NULL,
1470
+ project_id INTEGER REFERENCES projects(id),
1471
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
1472
+ content TEXT NOT NULL,
1473
+ user_id TEXT NOT NULL,
1474
+ device_id TEXT NOT NULL,
1475
+ agent TEXT DEFAULT 'claude-code',
1476
+ created_at_epoch INTEGER NOT NULL
1477
+ );
1478
+
1479
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_session
1480
+ ON chat_messages(session_id, created_at_epoch DESC, id DESC);
1481
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_project
1482
+ ON chat_messages(project_id, created_at_epoch DESC, id DESC);
1483
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_created
1484
+ ON chat_messages(created_at_epoch DESC, id DESC);
1485
+ `
1486
+ },
1487
+ {
1488
+ version: 15,
1489
+ description: "Add remote_source_id for chat message sync deduplication",
1490
+ sql: `
1491
+ ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
1492
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
1493
+ ON chat_messages(remote_source_id)
1494
+ WHERE remote_source_id IS NOT NULL;
1495
+ `
1496
+ },
1497
+ {
1498
+ version: 16,
1499
+ description: "Allow chat_message records in sync_outbox",
1500
+ sql: `
1501
+ CREATE TABLE sync_outbox_new (
1502
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1503
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
1504
+ record_id INTEGER NOT NULL,
1505
+ status TEXT DEFAULT 'pending' CHECK (status IN (
1506
+ 'pending', 'syncing', 'synced', 'failed'
1507
+ )),
1508
+ retry_count INTEGER DEFAULT 0,
1509
+ max_retries INTEGER DEFAULT 10,
1510
+ last_error TEXT,
1511
+ created_at_epoch INTEGER NOT NULL,
1512
+ synced_at_epoch INTEGER,
1513
+ next_retry_epoch INTEGER
1514
+ );
1515
+
1516
+ INSERT INTO sync_outbox_new (
1517
+ id, record_type, record_id, status, retry_count, max_retries,
1518
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
1519
+ )
1520
+ SELECT
1521
+ id, record_type, record_id, status, retry_count, max_retries,
1522
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
1523
+ FROM sync_outbox;
1524
+
1525
+ DROP TABLE sync_outbox;
1526
+ ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
1527
+
1528
+ CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
1529
+ CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
1454
1530
  `
1455
1531
  }
1456
1532
  ];
@@ -1507,6 +1583,21 @@ function inferLegacySchemaVersion(db) {
1507
1583
  version = Math.max(version, 10);
1508
1584
  if (columnExists(db, "observations", "source_tool"))
1509
1585
  version = Math.max(version, 11);
1586
+ 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")) {
1587
+ version = Math.max(version, 12);
1588
+ }
1589
+ if (columnExists(db, "session_summaries", "current_thread")) {
1590
+ version = Math.max(version, 13);
1591
+ }
1592
+ if (tableExists(db, "chat_messages")) {
1593
+ version = Math.max(version, 14);
1594
+ }
1595
+ if (columnExists(db, "chat_messages", "remote_source_id")) {
1596
+ version = Math.max(version, 15);
1597
+ }
1598
+ if (syncOutboxSupportsChatMessages(db)) {
1599
+ version = Math.max(version, 16);
1600
+ }
1510
1601
  return version;
1511
1602
  }
1512
1603
  function runMigrations(db) {
@@ -1585,6 +1676,93 @@ function ensureObservationTypes(db) {
1585
1676
  }
1586
1677
  }
1587
1678
  }
1679
+ function ensureSessionSummaryColumns(db) {
1680
+ const required = [
1681
+ "capture_state",
1682
+ "recent_tool_names",
1683
+ "hot_files",
1684
+ "recent_outcomes",
1685
+ "current_thread"
1686
+ ];
1687
+ for (const column of required) {
1688
+ if (columnExists(db, "session_summaries", column))
1689
+ continue;
1690
+ db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
1691
+ }
1692
+ const current = getSchemaVersion(db);
1693
+ if (current < 13) {
1694
+ db.exec("PRAGMA user_version = 13");
1695
+ }
1696
+ }
1697
+ function ensureChatMessageColumns(db) {
1698
+ if (!tableExists(db, "chat_messages"))
1699
+ return;
1700
+ if (!columnExists(db, "chat_messages", "remote_source_id")) {
1701
+ db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
1702
+ }
1703
+ 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");
1704
+ const current = getSchemaVersion(db);
1705
+ if (current < 15) {
1706
+ db.exec("PRAGMA user_version = 15");
1707
+ }
1708
+ }
1709
+ function ensureSyncOutboxSupportsChatMessages(db) {
1710
+ if (syncOutboxSupportsChatMessages(db)) {
1711
+ const current = getSchemaVersion(db);
1712
+ if (current < 16) {
1713
+ db.exec("PRAGMA user_version = 16");
1714
+ }
1715
+ return;
1716
+ }
1717
+ db.exec("BEGIN TRANSACTION");
1718
+ try {
1719
+ db.exec(`
1720
+ CREATE TABLE sync_outbox_new (
1721
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1722
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
1723
+ record_id INTEGER NOT NULL,
1724
+ status TEXT DEFAULT 'pending' CHECK (status IN (
1725
+ 'pending', 'syncing', 'synced', 'failed'
1726
+ )),
1727
+ retry_count INTEGER DEFAULT 0,
1728
+ max_retries INTEGER DEFAULT 10,
1729
+ last_error TEXT,
1730
+ created_at_epoch INTEGER NOT NULL,
1731
+ synced_at_epoch INTEGER,
1732
+ next_retry_epoch INTEGER
1733
+ );
1734
+
1735
+ INSERT INTO sync_outbox_new (
1736
+ id, record_type, record_id, status, retry_count, max_retries,
1737
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
1738
+ )
1739
+ SELECT
1740
+ id, record_type, record_id, status, retry_count, max_retries,
1741
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
1742
+ FROM sync_outbox;
1743
+
1744
+ DROP TABLE sync_outbox;
1745
+ ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
1746
+
1747
+ CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
1748
+ CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
1749
+ `);
1750
+ db.exec("PRAGMA user_version = 16");
1751
+ db.exec("COMMIT");
1752
+ } catch (error) {
1753
+ db.exec("ROLLBACK");
1754
+ throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
1755
+ }
1756
+ }
1757
+ function syncOutboxSupportsChatMessages(db) {
1758
+ const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
1759
+ const sql = row?.sql ?? "";
1760
+ return sql.includes("'chat_message'");
1761
+ }
1762
+ function getSchemaVersion(db) {
1763
+ const result = db.query("PRAGMA user_version").get();
1764
+ return result.user_version;
1765
+ }
1588
1766
  var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
1589
1767
 
1590
1768
  // src/storage/sqlite.ts
@@ -1739,6 +1917,9 @@ class MemDatabase {
1739
1917
  this.vecAvailable = this.loadVecExtension();
1740
1918
  runMigrations(this.db);
1741
1919
  ensureObservationTypes(this.db);
1920
+ ensureSessionSummaryColumns(this.db);
1921
+ ensureChatMessageColumns(this.db);
1922
+ ensureSyncOutboxSupportsChatMessages(this.db);
1742
1923
  }
1743
1924
  loadVecExtension() {
1744
1925
  try {
@@ -1964,6 +2145,7 @@ class MemDatabase {
1964
2145
  p.name AS project_name,
1965
2146
  ss.request AS request,
1966
2147
  ss.completed AS completed,
2148
+ ss.current_thread AS current_thread,
1967
2149
  ss.capture_state AS capture_state,
1968
2150
  ss.recent_tool_names AS recent_tool_names,
1969
2151
  ss.hot_files AS hot_files,
@@ -1982,6 +2164,7 @@ class MemDatabase {
1982
2164
  p.name AS project_name,
1983
2165
  ss.request AS request,
1984
2166
  ss.completed AS completed,
2167
+ ss.current_thread AS current_thread,
1985
2168
  ss.capture_state AS capture_state,
1986
2169
  ss.recent_tool_names AS recent_tool_names,
1987
2170
  ss.hot_files AS hot_files,
@@ -2072,6 +2255,54 @@ class MemDatabase {
2072
2255
  ORDER BY created_at_epoch DESC, id DESC
2073
2256
  LIMIT ?`).all(...userId ? [userId] : [], limit);
2074
2257
  }
2258
+ insertChatMessage(input) {
2259
+ const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
2260
+ const content = input.content.trim();
2261
+ const result = this.db.query(`INSERT INTO chat_messages (
2262
+ session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
2263
+ ) 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);
2264
+ return this.getChatMessageById(Number(result.lastInsertRowid));
2265
+ }
2266
+ getChatMessageById(id) {
2267
+ return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
2268
+ }
2269
+ getChatMessageByRemoteSourceId(remoteSourceId) {
2270
+ return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
2271
+ }
2272
+ getSessionChatMessages(sessionId, limit = 50) {
2273
+ return this.db.query(`SELECT * FROM chat_messages
2274
+ WHERE session_id = ?
2275
+ ORDER BY created_at_epoch ASC, id ASC
2276
+ LIMIT ?`).all(sessionId, limit);
2277
+ }
2278
+ getRecentChatMessages(projectId, limit = 20, userId) {
2279
+ const visibilityClause = userId ? " AND user_id = ?" : "";
2280
+ if (projectId !== null) {
2281
+ return this.db.query(`SELECT * FROM chat_messages
2282
+ WHERE project_id = ?${visibilityClause}
2283
+ ORDER BY created_at_epoch DESC, id DESC
2284
+ LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
2285
+ }
2286
+ return this.db.query(`SELECT * FROM chat_messages
2287
+ WHERE 1 = 1${visibilityClause}
2288
+ ORDER BY created_at_epoch DESC, id DESC
2289
+ LIMIT ?`).all(...userId ? [userId] : [], limit);
2290
+ }
2291
+ searchChatMessages(query, projectId, limit = 20, userId) {
2292
+ const needle = `%${query.toLowerCase()}%`;
2293
+ const visibilityClause = userId ? " AND user_id = ?" : "";
2294
+ if (projectId !== null) {
2295
+ return this.db.query(`SELECT * FROM chat_messages
2296
+ WHERE project_id = ?
2297
+ AND lower(content) LIKE ?${visibilityClause}
2298
+ ORDER BY created_at_epoch DESC, id DESC
2299
+ LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
2300
+ }
2301
+ return this.db.query(`SELECT * FROM chat_messages
2302
+ WHERE lower(content) LIKE ?${visibilityClause}
2303
+ ORDER BY created_at_epoch DESC, id DESC
2304
+ LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
2305
+ }
2075
2306
  addToOutbox(recordType, recordId) {
2076
2307
  const now = Math.floor(Date.now() / 1000);
2077
2308
  this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
@@ -2160,9 +2391,9 @@ class MemDatabase {
2160
2391
  };
2161
2392
  const result = this.db.query(`INSERT INTO session_summaries (
2162
2393
  session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
2163
- capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
2394
+ current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
2164
2395
  )
2165
- 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);
2396
+ 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);
2166
2397
  const id = Number(result.lastInsertRowid);
2167
2398
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
2168
2399
  }
@@ -2178,6 +2409,7 @@ class MemDatabase {
2178
2409
  learned: normalizeSummarySection(summary.learned ?? existing.learned),
2179
2410
  completed: normalizeSummarySection(summary.completed ?? existing.completed),
2180
2411
  next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
2412
+ current_thread: summary.current_thread ?? existing.current_thread,
2181
2413
  capture_state: summary.capture_state ?? existing.capture_state,
2182
2414
  recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
2183
2415
  hot_files: summary.hot_files ?? existing.hot_files,
@@ -2191,12 +2423,13 @@ class MemDatabase {
2191
2423
  learned = ?,
2192
2424
  completed = ?,
2193
2425
  next_steps = ?,
2426
+ current_thread = ?,
2194
2427
  capture_state = ?,
2195
2428
  recent_tool_names = ?,
2196
2429
  hot_files = ?,
2197
2430
  recent_outcomes = ?,
2198
2431
  created_at_epoch = ?
2199
- 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);
2432
+ 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);
2200
2433
  return this.getSessionSummary(summary.session_id);
2201
2434
  }
2202
2435
  getSessionSummary(sessionId) {