engrm 0.4.26 → 0.4.28

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.
@@ -756,6 +756,17 @@ var MIGRATIONS = [
756
756
  ON chat_messages(session_id, transcript_index)
757
757
  WHERE transcript_index IS NOT NULL;
758
758
  `
759
+ },
760
+ {
761
+ version: 18,
762
+ description: "Add sqlite-vec semantic search for chat recall",
763
+ condition: (db) => isVecExtensionLoaded(db),
764
+ sql: `
765
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
766
+ chat_message_id INTEGER PRIMARY KEY,
767
+ embedding FLOAT[384]
768
+ );
769
+ `
759
770
  }
760
771
  ];
761
772
  function isVecExtensionLoaded(db) {
@@ -829,6 +840,9 @@ function inferLegacySchemaVersion(db) {
829
840
  if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
830
841
  version = Math.max(version, 17);
831
842
  }
843
+ if (tableExists(db, "vec_chat_messages")) {
844
+ version = Math.max(version, 18);
845
+ }
832
846
  return version;
833
847
  }
834
848
  function runMigrations(db) {
@@ -945,6 +959,20 @@ function ensureChatMessageColumns(db) {
945
959
  db.exec("PRAGMA user_version = 17");
946
960
  }
947
961
  }
962
+ function ensureChatVectorTable(db) {
963
+ if (!isVecExtensionLoaded(db))
964
+ return;
965
+ db.exec(`
966
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
967
+ chat_message_id INTEGER PRIMARY KEY,
968
+ embedding FLOAT[384]
969
+ );
970
+ `);
971
+ const current = getSchemaVersion(db);
972
+ if (current < 18) {
973
+ db.exec("PRAGMA user_version = 18");
974
+ }
975
+ }
948
976
  function ensureSyncOutboxSupportsChatMessages(db) {
949
977
  if (syncOutboxSupportsChatMessages(db)) {
950
978
  const current = getSchemaVersion(db);
@@ -1158,6 +1186,7 @@ class MemDatabase {
1158
1186
  ensureObservationTypes(this.db);
1159
1187
  ensureSessionSummaryColumns(this.db);
1160
1188
  ensureChatMessageColumns(this.db);
1189
+ ensureChatVectorTable(this.db);
1161
1190
  ensureSyncOutboxSupportsChatMessages(this.db);
1162
1191
  }
1163
1192
  loadVecExtension() {
@@ -1524,6 +1553,14 @@ class MemDatabase {
1524
1553
  getChatMessageByRemoteSourceId(remoteSourceId) {
1525
1554
  return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
1526
1555
  }
1556
+ getChatMessagesByIds(ids) {
1557
+ if (ids.length === 0)
1558
+ return [];
1559
+ const placeholders = ids.map(() => "?").join(",");
1560
+ const rows = this.db.query(`SELECT * FROM chat_messages WHERE id IN (${placeholders})`).all(...ids);
1561
+ const order = new Map(ids.map((id, index) => [id, index]));
1562
+ return rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
1563
+ }
1527
1564
  getSessionChatMessages(sessionId, limit = 50) {
1528
1565
  return this.db.query(`SELECT * FROM chat_messages
1529
1566
  WHERE session_id = ?
@@ -1600,6 +1637,39 @@ class MemDatabase {
1600
1637
  ORDER BY created_at_epoch DESC, id DESC
1601
1638
  LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
1602
1639
  }
1640
+ vecChatInsert(chatMessageId, embedding) {
1641
+ if (!this.vecAvailable)
1642
+ return;
1643
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
1644
+ }
1645
+ searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
1646
+ if (!this.vecAvailable)
1647
+ return [];
1648
+ const embeddingBlob = new Uint8Array(queryEmbedding.buffer);
1649
+ const visibilityClause = userId ? " AND c.user_id = ?" : "";
1650
+ const transcriptPreference = `
1651
+ AND (
1652
+ c.source_kind = 'transcript'
1653
+ OR NOT EXISTS (
1654
+ SELECT 1 FROM chat_messages t2
1655
+ WHERE t2.session_id = c.session_id
1656
+ AND t2.source_kind = 'transcript'
1657
+ )
1658
+ )`;
1659
+ if (projectId !== null) {
1660
+ return this.db.query(`SELECT v.chat_message_id, v.distance
1661
+ FROM vec_chat_messages v
1662
+ JOIN chat_messages c ON c.id = v.chat_message_id
1663
+ WHERE v.embedding MATCH ?
1664
+ AND k = ?
1665
+ AND c.project_id = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, projectId, ...userId ? [userId] : []);
1666
+ }
1667
+ return this.db.query(`SELECT v.chat_message_id, v.distance
1668
+ FROM vec_chat_messages v
1669
+ JOIN chat_messages c ON c.id = v.chat_message_id
1670
+ WHERE v.embedding MATCH ?
1671
+ AND k = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, ...userId ? [userId] : []);
1672
+ }
1603
1673
  getTranscriptChatMessage(sessionId, transcriptIndex) {
1604
1674
  return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
1605
1675
  }
@@ -494,6 +494,8 @@ function getSessionStory(db, input) {
494
494
  summary,
495
495
  prompts,
496
496
  chat_messages: chatMessages,
497
+ chat_source_summary: summarizeChatSources(chatMessages),
498
+ chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
497
499
  tool_events: toolEvents,
498
500
  observations,
499
501
  handoffs,
@@ -591,6 +593,12 @@ function collectProvenanceSummary(observations) {
591
593
  }
592
594
  return Array.from(counts.entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
593
595
  }
596
+ function summarizeChatSources(messages) {
597
+ return messages.reduce((summary, message) => {
598
+ summary[message.source_kind] += 1;
599
+ return summary;
600
+ }, { transcript: 0, hook: 0 });
601
+ }
594
602
 
595
603
  // src/tools/save.ts
596
604
  import { relative, isAbsolute } from "node:path";
@@ -919,6 +927,9 @@ function composeEmbeddingText(obs) {
919
927
 
920
928
  `);
921
929
  }
930
+ function composeChatEmbeddingText(text) {
931
+ return text.replace(/\s+/g, " ").trim().slice(0, 2000);
932
+ }
922
933
  async function getPipeline() {
923
934
  if (_pipeline)
924
935
  return _pipeline;
@@ -3049,7 +3060,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3049
3060
  import { join as join3 } from "node:path";
3050
3061
  import { homedir } from "node:os";
3051
3062
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3052
- var CLIENT_VERSION = "0.4.26";
3063
+ var CLIENT_VERSION = "0.4.28";
3053
3064
  function hashFile(filePath) {
3054
3065
  try {
3055
3066
  if (!existsSync3(filePath))
@@ -4244,6 +4255,17 @@ var MIGRATIONS = [
4244
4255
  ON chat_messages(session_id, transcript_index)
4245
4256
  WHERE transcript_index IS NOT NULL;
4246
4257
  `
4258
+ },
4259
+ {
4260
+ version: 18,
4261
+ description: "Add sqlite-vec semantic search for chat recall",
4262
+ condition: (db) => isVecExtensionLoaded(db),
4263
+ sql: `
4264
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
4265
+ chat_message_id INTEGER PRIMARY KEY,
4266
+ embedding FLOAT[384]
4267
+ );
4268
+ `
4247
4269
  }
4248
4270
  ];
4249
4271
  function isVecExtensionLoaded(db) {
@@ -4317,6 +4339,9 @@ function inferLegacySchemaVersion(db) {
4317
4339
  if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
4318
4340
  version = Math.max(version, 17);
4319
4341
  }
4342
+ if (tableExists(db, "vec_chat_messages")) {
4343
+ version = Math.max(version, 18);
4344
+ }
4320
4345
  return version;
4321
4346
  }
4322
4347
  function runMigrations(db) {
@@ -4433,6 +4458,20 @@ function ensureChatMessageColumns(db) {
4433
4458
  db.exec("PRAGMA user_version = 17");
4434
4459
  }
4435
4460
  }
4461
+ function ensureChatVectorTable(db) {
4462
+ if (!isVecExtensionLoaded(db))
4463
+ return;
4464
+ db.exec(`
4465
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
4466
+ chat_message_id INTEGER PRIMARY KEY,
4467
+ embedding FLOAT[384]
4468
+ );
4469
+ `);
4470
+ const current = getSchemaVersion(db);
4471
+ if (current < 18) {
4472
+ db.exec("PRAGMA user_version = 18");
4473
+ }
4474
+ }
4436
4475
  function ensureSyncOutboxSupportsChatMessages(db) {
4437
4476
  if (syncOutboxSupportsChatMessages(db)) {
4438
4477
  const current = getSchemaVersion(db);
@@ -4566,6 +4605,7 @@ class MemDatabase {
4566
4605
  ensureObservationTypes(this.db);
4567
4606
  ensureSessionSummaryColumns(this.db);
4568
4607
  ensureChatMessageColumns(this.db);
4608
+ ensureChatVectorTable(this.db);
4569
4609
  ensureSyncOutboxSupportsChatMessages(this.db);
4570
4610
  }
4571
4611
  loadVecExtension() {
@@ -4932,6 +4972,14 @@ class MemDatabase {
4932
4972
  getChatMessageByRemoteSourceId(remoteSourceId) {
4933
4973
  return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
4934
4974
  }
4975
+ getChatMessagesByIds(ids) {
4976
+ if (ids.length === 0)
4977
+ return [];
4978
+ const placeholders = ids.map(() => "?").join(",");
4979
+ const rows = this.db.query(`SELECT * FROM chat_messages WHERE id IN (${placeholders})`).all(...ids);
4980
+ const order = new Map(ids.map((id, index) => [id, index]));
4981
+ return rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
4982
+ }
4935
4983
  getSessionChatMessages(sessionId, limit = 50) {
4936
4984
  return this.db.query(`SELECT * FROM chat_messages
4937
4985
  WHERE session_id = ?
@@ -5008,6 +5056,39 @@ class MemDatabase {
5008
5056
  ORDER BY created_at_epoch DESC, id DESC
5009
5057
  LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
5010
5058
  }
5059
+ vecChatInsert(chatMessageId, embedding) {
5060
+ if (!this.vecAvailable)
5061
+ return;
5062
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
5063
+ }
5064
+ searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
5065
+ if (!this.vecAvailable)
5066
+ return [];
5067
+ const embeddingBlob = new Uint8Array(queryEmbedding.buffer);
5068
+ const visibilityClause = userId ? " AND c.user_id = ?" : "";
5069
+ const transcriptPreference = `
5070
+ AND (
5071
+ c.source_kind = 'transcript'
5072
+ OR NOT EXISTS (
5073
+ SELECT 1 FROM chat_messages t2
5074
+ WHERE t2.session_id = c.session_id
5075
+ AND t2.source_kind = 'transcript'
5076
+ )
5077
+ )`;
5078
+ if (projectId !== null) {
5079
+ return this.db.query(`SELECT v.chat_message_id, v.distance
5080
+ FROM vec_chat_messages v
5081
+ JOIN chat_messages c ON c.id = v.chat_message_id
5082
+ WHERE v.embedding MATCH ?
5083
+ AND k = ?
5084
+ AND c.project_id = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, projectId, ...userId ? [userId] : []);
5085
+ }
5086
+ return this.db.query(`SELECT v.chat_message_id, v.distance
5087
+ FROM vec_chat_messages v
5088
+ JOIN chat_messages c ON c.id = v.chat_message_id
5089
+ WHERE v.embedding MATCH ?
5090
+ AND k = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, ...userId ? [userId] : []);
5091
+ }
5011
5092
  getTranscriptChatMessage(sessionId, transcriptIndex) {
5012
5093
  return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
5013
5094
  }
@@ -5661,6 +5742,9 @@ function formatInspectHints(context, visibleObservationIds = []) {
5661
5742
  if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0) {
5662
5743
  hints.push("activity_feed");
5663
5744
  }
5745
+ if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentChatMessages?.length ?? 0) > 0 || context.observations.length > 0) {
5746
+ hints.push("search_recall");
5747
+ }
5664
5748
  if (context.observations.length > 0) {
5665
5749
  hints.push("memory_console");
5666
5750
  }
@@ -5671,10 +5755,12 @@ function formatInspectHints(context, visibleObservationIds = []) {
5671
5755
  if ((context.recentChatMessages?.length ?? 0) > 0) {
5672
5756
  hints.push("recent_chat");
5673
5757
  }
5758
+ if (hasHookOnlyRecentChat(context)) {
5759
+ hints.push("refresh_chat_recall");
5760
+ }
5674
5761
  if (continuityState !== "fresh") {
5675
5762
  hints.push("recent_chat");
5676
5763
  hints.push("recent_handoffs");
5677
- hints.push("refresh_chat_recall");
5678
5764
  }
5679
5765
  const unique = Array.from(new Set(hints)).slice(0, 4);
5680
5766
  if (unique.length === 0)
@@ -6144,6 +6230,10 @@ function hasFreshContinuitySignal(context) {
6144
6230
  function getStartupContinuityState(context) {
6145
6231
  return classifyContinuityState(context.recentPrompts?.length ?? 0, context.recentToolEvents?.length ?? 0, context.recentHandoffs?.length ?? 0, context.recentChatMessages?.length ?? 0, context.recentSessions ?? [], context.recentOutcomes?.length ?? 0);
6146
6232
  }
6233
+ function hasHookOnlyRecentChat(context) {
6234
+ const recentChat = context.recentChatMessages ?? [];
6235
+ return recentChat.length > 0 && !recentChat.some((message) => message.source_kind === "transcript");
6236
+ }
6147
6237
  function observationAgeDays3(obs) {
6148
6238
  const createdAt = new Date(obs.created_at).getTime();
6149
6239
  if (!Number.isFinite(createdAt))
@@ -993,6 +993,17 @@ var MIGRATIONS = [
993
993
  ON chat_messages(session_id, transcript_index)
994
994
  WHERE transcript_index IS NOT NULL;
995
995
  `
996
+ },
997
+ {
998
+ version: 18,
999
+ description: "Add sqlite-vec semantic search for chat recall",
1000
+ condition: (db) => isVecExtensionLoaded(db),
1001
+ sql: `
1002
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
1003
+ chat_message_id INTEGER PRIMARY KEY,
1004
+ embedding FLOAT[384]
1005
+ );
1006
+ `
996
1007
  }
997
1008
  ];
998
1009
  function isVecExtensionLoaded(db) {
@@ -1066,6 +1077,9 @@ function inferLegacySchemaVersion(db) {
1066
1077
  if (columnExists(db, "chat_messages", "source_kind") && columnExists(db, "chat_messages", "transcript_index")) {
1067
1078
  version = Math.max(version, 17);
1068
1079
  }
1080
+ if (tableExists(db, "vec_chat_messages")) {
1081
+ version = Math.max(version, 18);
1082
+ }
1069
1083
  return version;
1070
1084
  }
1071
1085
  function runMigrations(db) {
@@ -1182,6 +1196,20 @@ function ensureChatMessageColumns(db) {
1182
1196
  db.exec("PRAGMA user_version = 17");
1183
1197
  }
1184
1198
  }
1199
+ function ensureChatVectorTable(db) {
1200
+ if (!isVecExtensionLoaded(db))
1201
+ return;
1202
+ db.exec(`
1203
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_chat_messages USING vec0(
1204
+ chat_message_id INTEGER PRIMARY KEY,
1205
+ embedding FLOAT[384]
1206
+ );
1207
+ `);
1208
+ const current = getSchemaVersion(db);
1209
+ if (current < 18) {
1210
+ db.exec("PRAGMA user_version = 18");
1211
+ }
1212
+ }
1185
1213
  function ensureSyncOutboxSupportsChatMessages(db) {
1186
1214
  if (syncOutboxSupportsChatMessages(db)) {
1187
1215
  const current = getSchemaVersion(db);
@@ -1315,6 +1343,7 @@ class MemDatabase {
1315
1343
  ensureObservationTypes(this.db);
1316
1344
  ensureSessionSummaryColumns(this.db);
1317
1345
  ensureChatMessageColumns(this.db);
1346
+ ensureChatVectorTable(this.db);
1318
1347
  ensureSyncOutboxSupportsChatMessages(this.db);
1319
1348
  }
1320
1349
  loadVecExtension() {
@@ -1681,6 +1710,14 @@ class MemDatabase {
1681
1710
  getChatMessageByRemoteSourceId(remoteSourceId) {
1682
1711
  return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
1683
1712
  }
1713
+ getChatMessagesByIds(ids) {
1714
+ if (ids.length === 0)
1715
+ return [];
1716
+ const placeholders = ids.map(() => "?").join(",");
1717
+ const rows = this.db.query(`SELECT * FROM chat_messages WHERE id IN (${placeholders})`).all(...ids);
1718
+ const order = new Map(ids.map((id, index) => [id, index]));
1719
+ return rows.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
1720
+ }
1684
1721
  getSessionChatMessages(sessionId, limit = 50) {
1685
1722
  return this.db.query(`SELECT * FROM chat_messages
1686
1723
  WHERE session_id = ?
@@ -1757,6 +1794,39 @@ class MemDatabase {
1757
1794
  ORDER BY created_at_epoch DESC, id DESC
1758
1795
  LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
1759
1796
  }
1797
+ vecChatInsert(chatMessageId, embedding) {
1798
+ if (!this.vecAvailable)
1799
+ return;
1800
+ this.db.query("INSERT OR REPLACE INTO vec_chat_messages (chat_message_id, embedding) VALUES (?, ?)").run(chatMessageId, new Uint8Array(embedding.buffer));
1801
+ }
1802
+ searchChatVec(queryEmbedding, projectId, limit = 20, userId) {
1803
+ if (!this.vecAvailable)
1804
+ return [];
1805
+ const embeddingBlob = new Uint8Array(queryEmbedding.buffer);
1806
+ const visibilityClause = userId ? " AND c.user_id = ?" : "";
1807
+ const transcriptPreference = `
1808
+ AND (
1809
+ c.source_kind = 'transcript'
1810
+ OR NOT EXISTS (
1811
+ SELECT 1 FROM chat_messages t2
1812
+ WHERE t2.session_id = c.session_id
1813
+ AND t2.source_kind = 'transcript'
1814
+ )
1815
+ )`;
1816
+ if (projectId !== null) {
1817
+ return this.db.query(`SELECT v.chat_message_id, v.distance
1818
+ FROM vec_chat_messages v
1819
+ JOIN chat_messages c ON c.id = v.chat_message_id
1820
+ WHERE v.embedding MATCH ?
1821
+ AND k = ?
1822
+ AND c.project_id = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, projectId, ...userId ? [userId] : []);
1823
+ }
1824
+ return this.db.query(`SELECT v.chat_message_id, v.distance
1825
+ FROM vec_chat_messages v
1826
+ JOIN chat_messages c ON c.id = v.chat_message_id
1827
+ WHERE v.embedding MATCH ?
1828
+ AND k = ?` + visibilityClause + transcriptPreference).all(embeddingBlob, limit, ...userId ? [userId] : []);
1829
+ }
1760
1830
  getTranscriptChatMessage(sessionId, transcriptIndex) {
1761
1831
  return this.db.query("SELECT * FROM chat_messages WHERE session_id = ? AND transcript_index = ?").get(sessionId, transcriptIndex) ?? null;
1762
1832
  }
@@ -2719,6 +2789,9 @@ function composeEmbeddingText(obs) {
2719
2789
 
2720
2790
  `);
2721
2791
  }
2792
+ function composeChatEmbeddingText(text) {
2793
+ return text.replace(/\s+/g, " ").trim().slice(0, 2000);
2794
+ }
2722
2795
  async function getPipeline() {
2723
2796
  if (_pipeline)
2724
2797
  return _pipeline;
@@ -3009,7 +3082,7 @@ function buildBeacon(db, config, sessionId, metrics) {
3009
3082
  sentinel_used: valueSignals.security_findings_count > 0,
3010
3083
  risk_score: riskScore,
3011
3084
  stacks_detected: stacks,
3012
- client_version: "0.4.26",
3085
+ client_version: "0.4.28",
3013
3086
  context_observations_injected: metrics?.contextObsInjected ?? 0,
3014
3087
  context_total_available: metrics?.contextTotalAvailable ?? 0,
3015
3088
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -3910,7 +3983,7 @@ function readTranscript(sessionId, cwd, transcriptPath) {
3910
3983
  }
3911
3984
  return messages;
3912
3985
  }
3913
- function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
3986
+ async function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
3914
3987
  const messages = readTranscript(sessionId, cwd, transcriptPath).map((message) => ({
3915
3988
  ...message,
3916
3989
  text: message.text.trim()
@@ -3940,6 +4013,12 @@ function syncTranscriptChat(db, config, sessionId, cwd, transcriptPath) {
3940
4013
  transcript_index: transcriptIndex
3941
4014
  });
3942
4015
  db.addToOutbox("chat_message", row.id);
4016
+ if (db.vecAvailable) {
4017
+ const embedding = await embedText(composeChatEmbeddingText(message.text));
4018
+ if (embedding) {
4019
+ db.vecChatInsert(row.id, embedding);
4020
+ }
4021
+ }
3943
4022
  imported++;
3944
4023
  }
3945
4024
  return { imported, total: messages.length };
@@ -4040,6 +4119,8 @@ function getSessionStory(db, input) {
4040
4119
  summary,
4041
4120
  prompts,
4042
4121
  chat_messages: chatMessages,
4122
+ chat_source_summary: summarizeChatSources(chatMessages),
4123
+ chat_coverage_state: chatMessages.some((message) => message.source_kind === "transcript") ? "transcript-backed" : chatMessages.length > 0 ? "hook-only" : "none",
4043
4124
  tool_events: toolEvents,
4044
4125
  observations,
4045
4126
  handoffs,
@@ -4137,6 +4218,12 @@ function collectProvenanceSummary(observations) {
4137
4218
  }
4138
4219
  return Array.from(counts.entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
4139
4220
  }
4221
+ function summarizeChatSources(messages) {
4222
+ return messages.reduce((summary, message) => {
4223
+ summary[message.source_kind] += 1;
4224
+ return summary;
4225
+ }, { transcript: 0, hook: 0 });
4226
+ }
4140
4227
 
4141
4228
  // src/tools/handoffs.ts
4142
4229
  async function upsertRollingHandoff(db, config, input) {
@@ -4477,7 +4564,7 @@ async function main() {
4477
4564
  try {
4478
4565
  if (event.session_id) {
4479
4566
  db.completeSession(event.session_id);
4480
- syncTranscriptChat(db, config, event.session_id, event.cwd, event.transcript_path);
4567
+ await syncTranscriptChat(db, config, event.session_id, event.cwd, event.transcript_path);
4481
4568
  if (event.last_assistant_message) {
4482
4569
  try {
4483
4570
  const detected = detectProject(event.cwd);
@@ -4500,6 +4587,12 @@ async function main() {
4500
4587
  source_kind: "hook"
4501
4588
  });
4502
4589
  db.addToOutbox("chat_message", chatMessage.id);
4590
+ if (db.vecAvailable) {
4591
+ const chatEmbedding = await embedText(composeChatEmbeddingText(event.last_assistant_message));
4592
+ if (chatEmbedding) {
4593
+ db.vecChatInsert(chatMessage.id, chatEmbedding);
4594
+ }
4595
+ }
4503
4596
  }
4504
4597
  createAssistantCheckpoint(db, event.session_id, event.cwd, event.last_assistant_message);
4505
4598
  } catch {}