engrm 0.4.46 → 0.4.48

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/dist/cli.js CHANGED
@@ -1005,6 +1005,20 @@ function ensureChatMessageColumns(db) {
1005
1005
  db.exec("PRAGMA user_version = 17");
1006
1006
  }
1007
1007
  }
1008
+ function ensureObservationVectorTable(db) {
1009
+ if (!isVecExtensionLoaded(db))
1010
+ return;
1011
+ db.exec(`
1012
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
1013
+ observation_id INTEGER PRIMARY KEY,
1014
+ embedding FLOAT[384]
1015
+ );
1016
+ `);
1017
+ const current = getSchemaVersion(db);
1018
+ if (current < 4) {
1019
+ db.exec("PRAGMA user_version = 4");
1020
+ }
1021
+ }
1008
1022
  function ensureChatVectorTable(db) {
1009
1023
  if (!isVecExtensionLoaded(db))
1010
1024
  return;
@@ -1233,6 +1247,7 @@ class MemDatabase {
1233
1247
  ensureObservationTypes(this.db);
1234
1248
  ensureSessionSummaryColumns(this.db);
1235
1249
  ensureChatMessageColumns(this.db);
1250
+ ensureObservationVectorTable(this.db);
1236
1251
  ensureChatVectorTable(this.db);
1237
1252
  ensureSyncOutboxSupportsChatMessages(this.db);
1238
1253
  }
@@ -1879,6 +1879,20 @@ function ensureChatMessageColumns(db) {
1879
1879
  db.exec("PRAGMA user_version = 17");
1880
1880
  }
1881
1881
  }
1882
+ function ensureObservationVectorTable(db) {
1883
+ if (!isVecExtensionLoaded(db))
1884
+ return;
1885
+ db.exec(`
1886
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
1887
+ observation_id INTEGER PRIMARY KEY,
1888
+ embedding FLOAT[384]
1889
+ );
1890
+ `);
1891
+ const current = getSchemaVersion(db);
1892
+ if (current < 4) {
1893
+ db.exec("PRAGMA user_version = 4");
1894
+ }
1895
+ }
1882
1896
  function ensureChatVectorTable(db) {
1883
1897
  if (!isVecExtensionLoaded(db))
1884
1898
  return;
@@ -2107,6 +2121,7 @@ class MemDatabase {
2107
2121
  ensureObservationTypes(this.db);
2108
2122
  ensureSessionSummaryColumns(this.db);
2109
2123
  ensureChatMessageColumns(this.db);
2124
+ ensureObservationVectorTable(this.db);
2110
2125
  ensureChatVectorTable(this.db);
2111
2126
  ensureSyncOutboxSupportsChatMessages(this.db);
2112
2127
  }
@@ -1183,6 +1183,20 @@ function ensureChatMessageColumns(db) {
1183
1183
  db.exec("PRAGMA user_version = 17");
1184
1184
  }
1185
1185
  }
1186
+ function ensureObservationVectorTable(db) {
1187
+ if (!isVecExtensionLoaded(db))
1188
+ return;
1189
+ db.exec(`
1190
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
1191
+ observation_id INTEGER PRIMARY KEY,
1192
+ embedding FLOAT[384]
1193
+ );
1194
+ `);
1195
+ const current = getSchemaVersion(db);
1196
+ if (current < 4) {
1197
+ db.exec("PRAGMA user_version = 4");
1198
+ }
1199
+ }
1186
1200
  function ensureChatVectorTable(db) {
1187
1201
  if (!isVecExtensionLoaded(db))
1188
1202
  return;
@@ -1411,6 +1425,7 @@ class MemDatabase {
1411
1425
  ensureObservationTypes(this.db);
1412
1426
  ensureSessionSummaryColumns(this.db);
1413
1427
  ensureChatMessageColumns(this.db);
1428
+ ensureObservationVectorTable(this.db);
1414
1429
  ensureChatVectorTable(this.db);
1415
1430
  ensureSyncOutboxSupportsChatMessages(this.db);
1416
1431
  }
@@ -4260,80 +4275,6 @@ function findTranscriptPathBySessionId(sessionId) {
4260
4275
  }
4261
4276
  return null;
4262
4277
  }
4263
- function truncateTranscript(messages, maxBytes = 50000) {
4264
- const lines = [];
4265
- for (const msg of messages) {
4266
- lines.push(`[${msg.role}]: ${msg.text}`);
4267
- }
4268
- const full = lines.join(`
4269
- `);
4270
- if (Buffer.byteLength(full, "utf-8") <= maxBytes)
4271
- return full;
4272
- let result = "";
4273
- for (let i = lines.length - 1;i >= 0; i--) {
4274
- const candidate = lines[i] + `
4275
- ` + result;
4276
- if (Buffer.byteLength(candidate, "utf-8") > maxBytes)
4277
- break;
4278
- result = candidate;
4279
- }
4280
- return result.trim();
4281
- }
4282
- async function analyzeTranscript(config, transcript, sessionId) {
4283
- if (!config.candengo_url || !config.candengo_api_key)
4284
- return null;
4285
- const url = `${config.candengo_url}/v1/mem/transcript-analysis`;
4286
- const controller = new AbortController;
4287
- const timeout = setTimeout(() => controller.abort(), 30000);
4288
- try {
4289
- const response = await fetch(url, {
4290
- method: "POST",
4291
- headers: {
4292
- "Content-Type": "application/json",
4293
- Authorization: `Bearer ${config.candengo_api_key}`
4294
- },
4295
- body: JSON.stringify({
4296
- transcript,
4297
- session_id: sessionId
4298
- }),
4299
- signal: controller.signal
4300
- });
4301
- if (!response.ok)
4302
- return null;
4303
- const data = await response.json();
4304
- if (!Array.isArray(data.plans) || !Array.isArray(data.decisions) || !Array.isArray(data.insights)) {
4305
- return null;
4306
- }
4307
- return data;
4308
- } catch {
4309
- return null;
4310
- } finally {
4311
- clearTimeout(timeout);
4312
- }
4313
- }
4314
- async function saveTranscriptResults(db, config, results, sessionId, cwd) {
4315
- let saved = 0;
4316
- const items = [
4317
- ...results.plans.map((item) => ({ item, type: "decision" })),
4318
- ...results.decisions.map((item) => ({ item, type: "decision" })),
4319
- ...results.insights.map((item) => ({ item, type: "discovery" }))
4320
- ];
4321
- for (const { item, type } of items) {
4322
- if (!item.title || item.title.trim().length === 0)
4323
- continue;
4324
- const result = await saveObservation(db, config, {
4325
- type,
4326
- title: item.title.slice(0, 80),
4327
- narrative: item.narrative,
4328
- concepts: item.concepts,
4329
- session_id: sessionId,
4330
- cwd
4331
- });
4332
- if (result.success)
4333
- saved++;
4334
- }
4335
- return saved;
4336
- }
4337
4278
 
4338
4279
  // src/tools/recent-chat.ts
4339
4280
  function summarizeChatSources(messages) {
@@ -977,6 +977,20 @@ function ensureChatMessageColumns(db) {
977
977
  db.exec("PRAGMA user_version = 17");
978
978
  }
979
979
  }
980
+ function ensureObservationVectorTable(db) {
981
+ if (!isVecExtensionLoaded(db))
982
+ return;
983
+ db.exec(`
984
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
985
+ observation_id INTEGER PRIMARY KEY,
986
+ embedding FLOAT[384]
987
+ );
988
+ `);
989
+ const current = getSchemaVersion(db);
990
+ if (current < 4) {
991
+ db.exec("PRAGMA user_version = 4");
992
+ }
993
+ }
980
994
  function ensureChatVectorTable(db) {
981
995
  if (!isVecExtensionLoaded(db))
982
996
  return;
@@ -1205,6 +1219,7 @@ class MemDatabase {
1205
1219
  ensureObservationTypes(this.db);
1206
1220
  ensureSessionSummaryColumns(this.db);
1207
1221
  ensureChatMessageColumns(this.db);
1222
+ ensureObservationVectorTable(this.db);
1208
1223
  ensureChatVectorTable(this.db);
1209
1224
  ensureSyncOutboxSupportsChatMessages(this.db);
1210
1225
  }
@@ -4548,80 +4563,6 @@ function findTranscriptPathBySessionId(sessionId) {
4548
4563
  }
4549
4564
  return null;
4550
4565
  }
4551
- function truncateTranscript(messages, maxBytes = 50000) {
4552
- const lines = [];
4553
- for (const msg of messages) {
4554
- lines.push(`[${msg.role}]: ${msg.text}`);
4555
- }
4556
- const full = lines.join(`
4557
- `);
4558
- if (Buffer.byteLength(full, "utf-8") <= maxBytes)
4559
- return full;
4560
- let result = "";
4561
- for (let i = lines.length - 1;i >= 0; i--) {
4562
- const candidate = lines[i] + `
4563
- ` + result;
4564
- if (Buffer.byteLength(candidate, "utf-8") > maxBytes)
4565
- break;
4566
- result = candidate;
4567
- }
4568
- return result.trim();
4569
- }
4570
- async function analyzeTranscript(config, transcript, sessionId) {
4571
- if (!config.candengo_url || !config.candengo_api_key)
4572
- return null;
4573
- const url = `${config.candengo_url}/v1/mem/transcript-analysis`;
4574
- const controller = new AbortController;
4575
- const timeout = setTimeout(() => controller.abort(), 30000);
4576
- try {
4577
- const response = await fetch(url, {
4578
- method: "POST",
4579
- headers: {
4580
- "Content-Type": "application/json",
4581
- Authorization: `Bearer ${config.candengo_api_key}`
4582
- },
4583
- body: JSON.stringify({
4584
- transcript,
4585
- session_id: sessionId
4586
- }),
4587
- signal: controller.signal
4588
- });
4589
- if (!response.ok)
4590
- return null;
4591
- const data = await response.json();
4592
- if (!Array.isArray(data.plans) || !Array.isArray(data.decisions) || !Array.isArray(data.insights)) {
4593
- return null;
4594
- }
4595
- return data;
4596
- } catch {
4597
- return null;
4598
- } finally {
4599
- clearTimeout(timeout);
4600
- }
4601
- }
4602
- async function saveTranscriptResults(db, config, results, sessionId, cwd) {
4603
- let saved = 0;
4604
- const items = [
4605
- ...results.plans.map((item) => ({ item, type: "decision" })),
4606
- ...results.decisions.map((item) => ({ item, type: "decision" })),
4607
- ...results.insights.map((item) => ({ item, type: "discovery" }))
4608
- ];
4609
- for (const { item, type } of items) {
4610
- if (!item.title || item.title.trim().length === 0)
4611
- continue;
4612
- const result = await saveObservation(db, config, {
4613
- type,
4614
- title: item.title.slice(0, 80),
4615
- narrative: item.narrative,
4616
- concepts: item.concepts,
4617
- session_id: sessionId,
4618
- cwd
4619
- });
4620
- if (result.success)
4621
- saved++;
4622
- }
4623
- return saved;
4624
- }
4625
4566
 
4626
4567
  // hooks/pre-compact.ts
4627
4568
  function formatCurrentSessionContext(observations) {
@@ -1053,6 +1053,20 @@ function ensureChatMessageColumns(db) {
1053
1053
  db.exec("PRAGMA user_version = 17");
1054
1054
  }
1055
1055
  }
1056
+ function ensureObservationVectorTable(db) {
1057
+ if (!isVecExtensionLoaded(db))
1058
+ return;
1059
+ db.exec(`
1060
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
1061
+ observation_id INTEGER PRIMARY KEY,
1062
+ embedding FLOAT[384]
1063
+ );
1064
+ `);
1065
+ const current = getSchemaVersion(db);
1066
+ if (current < 4) {
1067
+ db.exec("PRAGMA user_version = 4");
1068
+ }
1069
+ }
1056
1070
  function ensureChatVectorTable(db) {
1057
1071
  if (!isVecExtensionLoaded(db))
1058
1072
  return;
@@ -1281,6 +1295,7 @@ class MemDatabase {
1281
1295
  ensureObservationTypes(this.db);
1282
1296
  ensureSessionSummaryColumns(this.db);
1283
1297
  ensureChatMessageColumns(this.db);
1298
+ ensureObservationVectorTable(this.db);
1284
1299
  ensureChatVectorTable(this.db);
1285
1300
  ensureSyncOutboxSupportsChatMessages(this.db);
1286
1301
  }
@@ -3280,7 +3280,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
3280
3280
  import { join as join3 } from "node:path";
3281
3281
  import { homedir } from "node:os";
3282
3282
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
3283
- var CLIENT_VERSION = "0.4.46";
3283
+ var CLIENT_VERSION = "0.4.48";
3284
3284
  function hashFile(filePath) {
3285
3285
  try {
3286
3286
  if (!existsSync3(filePath))
@@ -4920,6 +4920,20 @@ function ensureChatMessageColumns(db) {
4920
4920
  db.exec("PRAGMA user_version = 17");
4921
4921
  }
4922
4922
  }
4923
+ function ensureObservationVectorTable(db) {
4924
+ if (!isVecExtensionLoaded(db))
4925
+ return;
4926
+ db.exec(`
4927
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
4928
+ observation_id INTEGER PRIMARY KEY,
4929
+ embedding FLOAT[384]
4930
+ );
4931
+ `);
4932
+ const current = getSchemaVersion(db);
4933
+ if (current < 4) {
4934
+ db.exec("PRAGMA user_version = 4");
4935
+ }
4936
+ }
4923
4937
  function ensureChatVectorTable(db) {
4924
4938
  if (!isVecExtensionLoaded(db))
4925
4939
  return;
@@ -5068,6 +5082,7 @@ class MemDatabase {
5068
5082
  ensureObservationTypes(this.db);
5069
5083
  ensureSessionSummaryColumns(this.db);
5070
5084
  ensureChatMessageColumns(this.db);
5085
+ ensureObservationVectorTable(this.db);
5071
5086
  ensureChatVectorTable(this.db);
5072
5087
  ensureSyncOutboxSupportsChatMessages(this.db);
5073
5088
  }
@@ -2,6 +2,13 @@
2
2
  import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
+ // hooks/stop.ts
6
+ import { spawn } from "node:child_process";
7
+ import { mkdtempSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { dirname as dirname2, join as join6 } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
5
12
  // src/intelligence/observation-priority.ts
6
13
  var RECENCY_WINDOW_SECONDS = 30 * 86400;
7
14
  function computeBlendedScore(quality, createdAtEpoch, nowEpoch) {
@@ -1290,6 +1297,20 @@ function ensureChatMessageColumns(db) {
1290
1297
  db.exec("PRAGMA user_version = 17");
1291
1298
  }
1292
1299
  }
1300
+ function ensureObservationVectorTable(db) {
1301
+ if (!isVecExtensionLoaded(db))
1302
+ return;
1303
+ db.exec(`
1304
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
1305
+ observation_id INTEGER PRIMARY KEY,
1306
+ embedding FLOAT[384]
1307
+ );
1308
+ `);
1309
+ const current = getSchemaVersion(db);
1310
+ if (current < 4) {
1311
+ db.exec("PRAGMA user_version = 4");
1312
+ }
1313
+ }
1293
1314
  function ensureChatVectorTable(db) {
1294
1315
  if (!isVecExtensionLoaded(db))
1295
1316
  return;
@@ -1438,6 +1459,7 @@ class MemDatabase {
1438
1459
  ensureObservationTypes(this.db);
1439
1460
  ensureSessionSummaryColumns(this.db);
1440
1461
  ensureChatMessageColumns(this.db);
1462
+ ensureObservationVectorTable(this.db);
1441
1463
  ensureChatVectorTable(this.db);
1442
1464
  ensureSyncOutboxSupportsChatMessages(this.db);
1443
1465
  }
@@ -3492,7 +3514,7 @@ function buildBeacon(db, config, sessionId, metrics) {
3492
3514
  sentinel_used: valueSignals.security_findings_count > 0,
3493
3515
  risk_score: riskScore,
3494
3516
  stacks_detected: stacks,
3495
- client_version: "0.4.46",
3517
+ client_version: "0.4.48",
3496
3518
  context_observations_injected: metrics?.contextObsInjected ?? 0,
3497
3519
  context_total_available: metrics?.contextTotalAvailable ?? 0,
3498
3520
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -4641,533 +4663,11 @@ function findTranscriptPathBySessionId(sessionId) {
4641
4663
  }
4642
4664
  return null;
4643
4665
  }
4644
- function truncateTranscript(messages, maxBytes = 50000) {
4645
- const lines = [];
4646
- for (const msg of messages) {
4647
- lines.push(`[${msg.role}]: ${msg.text}`);
4648
- }
4649
- const full = lines.join(`
4650
- `);
4651
- if (Buffer.byteLength(full, "utf-8") <= maxBytes)
4652
- return full;
4653
- let result = "";
4654
- for (let i = lines.length - 1;i >= 0; i--) {
4655
- const candidate = lines[i] + `
4656
- ` + result;
4657
- if (Buffer.byteLength(candidate, "utf-8") > maxBytes)
4658
- break;
4659
- result = candidate;
4660
- }
4661
- return result.trim();
4662
- }
4663
- async function analyzeTranscript(config, transcript, sessionId) {
4664
- if (!config.candengo_url || !config.candengo_api_key)
4665
- return null;
4666
- const url = `${config.candengo_url}/v1/mem/transcript-analysis`;
4667
- const controller = new AbortController;
4668
- const timeout = setTimeout(() => controller.abort(), 30000);
4669
- try {
4670
- const response = await fetch(url, {
4671
- method: "POST",
4672
- headers: {
4673
- "Content-Type": "application/json",
4674
- Authorization: `Bearer ${config.candengo_api_key}`
4675
- },
4676
- body: JSON.stringify({
4677
- transcript,
4678
- session_id: sessionId
4679
- }),
4680
- signal: controller.signal
4681
- });
4682
- if (!response.ok)
4683
- return null;
4684
- const data = await response.json();
4685
- if (!Array.isArray(data.plans) || !Array.isArray(data.decisions) || !Array.isArray(data.insights)) {
4686
- return null;
4687
- }
4688
- return data;
4689
- } catch {
4690
- return null;
4691
- } finally {
4692
- clearTimeout(timeout);
4693
- }
4694
- }
4695
- async function saveTranscriptResults(db, config, results, sessionId, cwd) {
4696
- let saved = 0;
4697
- const items = [
4698
- ...results.plans.map((item) => ({ item, type: "decision" })),
4699
- ...results.decisions.map((item) => ({ item, type: "decision" })),
4700
- ...results.insights.map((item) => ({ item, type: "discovery" }))
4701
- ];
4702
- for (const { item, type } of items) {
4703
- if (!item.title || item.title.trim().length === 0)
4704
- continue;
4705
- const result = await saveObservation(db, config, {
4706
- type,
4707
- title: item.title.slice(0, 80),
4708
- narrative: item.narrative,
4709
- concepts: item.concepts,
4710
- session_id: sessionId,
4711
- cwd
4712
- });
4713
- if (result.success)
4714
- saved++;
4715
- }
4716
- return saved;
4717
- }
4718
-
4719
- // src/tools/recent-chat.ts
4720
- function summarizeChatSources(messages) {
4721
- return messages.reduce((summary, message) => {
4722
- summary[getChatCaptureOrigin(message)] += 1;
4723
- return summary;
4724
- }, { transcript: 0, history: 0, hook: 0 });
4725
- }
4726
- function getChatCoverageState(messagesOrSummary) {
4727
- const summary = Array.isArray(messagesOrSummary) ? summarizeChatSources(messagesOrSummary) : messagesOrSummary;
4728
- if (summary.transcript > 0)
4729
- return "transcript-backed";
4730
- if (summary.history > 0)
4731
- return "history-backed";
4732
- if (summary.hook > 0)
4733
- return "hook-only";
4734
- return "none";
4735
- }
4736
- function getChatCaptureOrigin(message) {
4737
- if (message.source_kind === "transcript")
4738
- return "transcript";
4739
- if (typeof message.remote_source_id === "string" && message.remote_source_id.startsWith("history:")) {
4740
- return "history";
4741
- }
4742
- return "hook";
4743
- }
4744
-
4745
- // src/tools/session-story.ts
4746
- function getSessionStory(db, input) {
4747
- const session = db.getSessionById(input.session_id);
4748
- const summary = db.getSessionSummary(input.session_id);
4749
- const prompts = db.getSessionUserPrompts(input.session_id, 50);
4750
- const chatMessages = db.getSessionChatMessages(input.session_id, 50);
4751
- const toolEvents = db.getSessionToolEvents(input.session_id, 100);
4752
- const allObservations = db.getObservationsBySession(input.session_id);
4753
- const handoffs = allObservations.filter((obs) => looksLikeHandoff(obs));
4754
- const rollingHandoffDrafts = handoffs.filter((obs) => isDraftHandoff(obs));
4755
- const savedHandoffs = handoffs.filter((obs) => !isDraftHandoff(obs));
4756
- const observations = allObservations.filter((obs) => !looksLikeHandoff(obs));
4757
- const metrics = db.getSessionMetrics(input.session_id);
4758
- const projectName = session?.project_id !== null && session?.project_id !== undefined ? db.getProjectById(session.project_id)?.name ?? null : null;
4759
- const latestRequest = prompts[prompts.length - 1]?.prompt?.trim() || summary?.request?.trim() || null;
4760
- return {
4761
- session,
4762
- project_name: projectName,
4763
- summary,
4764
- prompts,
4765
- chat_messages: chatMessages,
4766
- chat_source_summary: summarizeChatSources(chatMessages),
4767
- chat_coverage_state: getChatCoverageState(chatMessages),
4768
- tool_events: toolEvents,
4769
- observations,
4770
- handoffs,
4771
- saved_handoffs: savedHandoffs,
4772
- rolling_handoff_drafts: rollingHandoffDrafts,
4773
- metrics,
4774
- capture_state: classifyCaptureState({
4775
- hasSummary: Boolean(summary?.request || summary?.completed),
4776
- promptCount: prompts.length,
4777
- toolEventCount: toolEvents.length
4778
- }),
4779
- capture_gaps: buildCaptureGaps({
4780
- promptCount: prompts.length,
4781
- toolEventCount: toolEvents.length,
4782
- toolCallsCount: metrics?.tool_calls_count ?? 0,
4783
- observationCount: observations.length,
4784
- hasSummary: Boolean(summary?.request || summary?.completed)
4785
- }),
4786
- latest_request: latestRequest,
4787
- recent_outcomes: collectRecentOutcomes(observations),
4788
- hot_files: collectHotFiles(observations),
4789
- provenance_summary: collectProvenanceSummary(observations)
4790
- };
4791
- }
4792
- function classifyCaptureState(input) {
4793
- if (input.promptCount > 0 && input.toolEventCount > 0)
4794
- return "rich";
4795
- if (input.promptCount > 0 || input.toolEventCount > 0)
4796
- return "partial";
4797
- if (input.hasSummary)
4798
- return "summary-only";
4799
- return "legacy";
4800
- }
4801
- function buildCaptureGaps(input) {
4802
- const gaps = [];
4803
- if (input.promptCount === 0)
4804
- gaps.push("missing prompts");
4805
- if (input.toolCallsCount > 0 && input.toolEventCount === 0) {
4806
- gaps.push("missing raw tool chronology");
4807
- } else if (input.toolEventCount === 0) {
4808
- gaps.push("no tool events");
4809
- }
4810
- if (input.observationCount === 0 && input.hasSummary) {
4811
- gaps.push("summary without reusable observations");
4812
- }
4813
- return gaps;
4814
- }
4815
- function collectRecentOutcomes(observations) {
4816
- const seen = new Set;
4817
- const outcomes = [];
4818
- for (const obs of observations) {
4819
- if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
4820
- continue;
4821
- const title = obs.title.trim();
4822
- if (!title || looksLikeFileOperationTitle(title))
4823
- continue;
4824
- const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
4825
- if (seen.has(normalized))
4826
- continue;
4827
- seen.add(normalized);
4828
- outcomes.push(title);
4829
- if (outcomes.length >= 6)
4830
- break;
4831
- }
4832
- return outcomes;
4833
- }
4834
- function collectHotFiles(observations) {
4835
- const counts = new Map;
4836
- for (const obs of observations) {
4837
- for (const path of [...parseJsonArray3(obs.files_modified), ...parseJsonArray3(obs.files_read)]) {
4838
- counts.set(path, (counts.get(path) ?? 0) + 1);
4839
- }
4840
- }
4841
- return Array.from(counts.entries()).map(([path, count]) => ({ path, count })).sort((a, b) => b.count - a.count || a.path.localeCompare(b.path)).slice(0, 8);
4842
- }
4843
- function parseJsonArray3(value) {
4844
- if (!value)
4845
- return [];
4846
- try {
4847
- const parsed = JSON.parse(value);
4848
- return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
4849
- } catch {
4850
- return [];
4851
- }
4852
- }
4853
- function looksLikeFileOperationTitle(value) {
4854
- return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
4855
- }
4856
- function collectProvenanceSummary(observations) {
4857
- const counts = new Map;
4858
- for (const obs of observations) {
4859
- if (!obs.source_tool)
4860
- continue;
4861
- counts.set(obs.source_tool, (counts.get(obs.source_tool) ?? 0) + 1);
4862
- }
4863
- 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);
4864
- }
4865
-
4866
- // src/tools/handoffs.ts
4867
- async function upsertRollingHandoff(db, config, input) {
4868
- const resolved = resolveTargetSession(db, input.cwd, config.user_id, input.session_id);
4869
- if (!resolved.session) {
4870
- return {
4871
- success: false,
4872
- reason: "No recent session found to draft a handoff yet"
4873
- };
4874
- }
4875
- const story = getSessionStory(db, { session_id: resolved.session.session_id });
4876
- if (!story.session) {
4877
- return {
4878
- success: false,
4879
- reason: `Session ${resolved.session.session_id} not found`
4880
- };
4881
- }
4882
- const includeChat = input.include_chat === true || input.include_chat !== false && shouldAutoIncludeChat(story);
4883
- const chatLimit = Math.max(1, Math.min(input.chat_limit ?? 3, 6));
4884
- const title = `Handoff Draft: ${buildHandoffTitle(story.summary, story.latest_request)}`;
4885
- const narrative = buildHandoffNarrative(story.summary, story, {
4886
- includeChat,
4887
- chatLimit
4888
- });
4889
- const facts = buildHandoffFacts(story.summary, story);
4890
- const concepts = buildDraftHandoffConcepts(story.project_name, story.capture_state);
4891
- const existing = getSessionRollingHandoff(db, story.session.session_id);
4892
- const now = Math.floor(Date.now() / 1000);
4893
- if (existing) {
4894
- const nextFacts = JSON.stringify(facts);
4895
- const nextConcepts = JSON.stringify(concepts);
4896
- const shouldRefresh = existing.title !== title || (existing.narrative ?? null) !== narrative || (existing.facts ?? null) !== nextFacts || (existing.concepts ?? null) !== nextConcepts || now - existing.created_at_epoch >= 120;
4897
- if (!shouldRefresh) {
4898
- return {
4899
- success: true,
4900
- observation_id: existing.id,
4901
- session_id: story.session.session_id,
4902
- title: existing.title
4903
- };
4904
- }
4905
- const updated = db.updateObservationContent(existing.id, {
4906
- title,
4907
- narrative,
4908
- facts: nextFacts,
4909
- concepts: nextConcepts,
4910
- created_at_epoch: now
4911
- });
4912
- if (!updated) {
4913
- return {
4914
- success: false,
4915
- reason: "Failed to update rolling handoff draft"
4916
- };
4917
- }
4918
- db.addToOutbox("observation", updated.id);
4919
- return {
4920
- success: true,
4921
- observation_id: updated.id,
4922
- session_id: story.session.session_id,
4923
- title: updated.title
4924
- };
4925
- }
4926
- const result = await saveObservation(db, config, {
4927
- type: "message",
4928
- title,
4929
- narrative,
4930
- facts,
4931
- concepts,
4932
- session_id: story.session.session_id,
4933
- cwd: input.cwd,
4934
- agent: "engrm-handoff",
4935
- source_tool: "rolling_handoff"
4936
- });
4937
- return {
4938
- success: result.success,
4939
- observation_id: result.observation_id,
4940
- session_id: story.session.session_id,
4941
- title,
4942
- reason: result.reason
4943
- };
4944
- }
4945
- function getRecentHandoffs(db, input) {
4946
- const limit = Math.max(1, Math.min(input.limit ?? 10, 25));
4947
- const queryLimit = input.current_device_id ? Math.max(limit, Math.min(limit * 5, 50)) : limit;
4948
- const projectScoped = input.project_scoped !== false;
4949
- let projectId = null;
4950
- let projectName;
4951
- if (projectScoped) {
4952
- const cwd = input.cwd ?? process.cwd();
4953
- const detected = detectProject(cwd);
4954
- const project = db.getProjectByCanonicalId(detected.canonical_id);
4955
- if (project) {
4956
- projectId = project.id;
4957
- projectName = project.name;
4958
- }
4959
- }
4960
- const conditions = [
4961
- "o.type = 'message'",
4962
- "o.lifecycle IN ('active', 'aging', 'pinned')",
4963
- "o.superseded_by IS NULL",
4964
- `(o.title LIKE 'Handoff:%' OR o.concepts LIKE '%"handoff"%')`
4965
- ];
4966
- const params = [];
4967
- if (input.user_id) {
4968
- conditions.push("(o.sensitivity != 'personal' OR o.user_id = ?)");
4969
- params.push(input.user_id);
4970
- }
4971
- if (projectId !== null) {
4972
- conditions.push("o.project_id = ?");
4973
- params.push(projectId);
4974
- }
4975
- params.push(queryLimit);
4976
- const handoffs = db.db.query(`SELECT o.*, p.name AS project_name
4977
- FROM observations o
4978
- LEFT JOIN projects p ON p.id = o.project_id
4979
- WHERE ${conditions.join(" AND ")}
4980
- ORDER BY o.created_at_epoch DESC, o.id DESC
4981
- LIMIT ?`).all(...params);
4982
- handoffs.sort((a, b) => compareHandoffs(a, b, input.current_device_id));
4983
- return {
4984
- handoffs: handoffs.slice(0, limit),
4985
- project: projectName
4986
- };
4987
- }
4988
- function formatHandoffSource(handoff) {
4989
- const ageSeconds = Math.max(0, Math.floor(Date.now() / 1000) - handoff.created_at_epoch);
4990
- const ageLabel = ageSeconds < 3600 ? `${Math.max(1, Math.floor(ageSeconds / 60) || 1)}m ago` : ageSeconds < 86400 ? `${Math.floor(ageSeconds / 3600)}h ago` : `${Math.floor(ageSeconds / 86400)}d ago`;
4991
- return `from ${handoff.device_id} · ${ageLabel}`;
4992
- }
4993
- function isDraftHandoff(obs) {
4994
- if (obs.title.startsWith("Handoff Draft:"))
4995
- return true;
4996
- const concepts = parseJsonArray4(obs.concepts);
4997
- return concepts.includes("draft-handoff") || concepts.includes("auto-handoff");
4998
- }
4999
- function getSessionRollingHandoff(db, sessionId) {
5000
- return db.db.query(`SELECT o.*, p.name AS project_name
5001
- FROM observations o
5002
- LEFT JOIN projects p ON p.id = o.project_id
5003
- WHERE o.session_id = ?
5004
- AND o.type = 'message'
5005
- AND o.lifecycle IN ('active', 'aging', 'pinned')
5006
- AND o.superseded_by IS NULL
5007
- AND (o.title LIKE 'Handoff Draft:%' OR o.concepts LIKE '%"draft-handoff"%')
5008
- ORDER BY o.created_at_epoch DESC, o.id DESC
5009
- LIMIT 1`).get(sessionId) ?? null;
5010
- }
5011
- function compareHandoffs(a, b, currentDeviceId) {
5012
- const aDraft = isDraftHandoff(a) ? 1 : 0;
5013
- const bDraft = isDraftHandoff(b) ? 1 : 0;
5014
- if (aDraft !== bDraft)
5015
- return aDraft - bDraft;
5016
- if (currentDeviceId) {
5017
- const aOther = a.device_id !== currentDeviceId ? 1 : 0;
5018
- const bOther = b.device_id !== currentDeviceId ? 1 : 0;
5019
- if (aOther !== bOther)
5020
- return bOther - aOther;
5021
- }
5022
- if (b.created_at_epoch !== a.created_at_epoch) {
5023
- return b.created_at_epoch - a.created_at_epoch;
5024
- }
5025
- return b.id - a.id;
5026
- }
5027
- function resolveTargetSession(db, cwd, userId, sessionId) {
5028
- if (sessionId) {
5029
- const session = db.getSessionById(sessionId);
5030
- if (!session)
5031
- return { session: null };
5032
- const projectName = session.project_id ? db.getProjectById(session.project_id)?.name : undefined;
5033
- return {
5034
- session: {
5035
- ...session,
5036
- project_name: projectName ?? null,
5037
- request: db.getSessionSummary(sessionId)?.request ?? null,
5038
- completed: db.getSessionSummary(sessionId)?.completed ?? null,
5039
- current_thread: db.getSessionSummary(sessionId)?.current_thread ?? null,
5040
- capture_state: db.getSessionSummary(sessionId)?.capture_state ?? null,
5041
- recent_tool_names: db.getSessionSummary(sessionId)?.recent_tool_names ?? null,
5042
- hot_files: db.getSessionSummary(sessionId)?.hot_files ?? null,
5043
- recent_outcomes: db.getSessionSummary(sessionId)?.recent_outcomes ?? null,
5044
- prompt_count: db.getSessionUserPrompts(sessionId, 200).length,
5045
- tool_event_count: db.getSessionToolEvents(sessionId, 200).length
5046
- },
5047
- projectName: projectName ?? undefined
5048
- };
5049
- }
5050
- const detected = detectProject(cwd ?? process.cwd());
5051
- const project = db.getProjectByCanonicalId(detected.canonical_id);
5052
- const sessions = db.getRecentSessions(project?.id ?? null, 10, userId);
5053
- return {
5054
- session: sessions[0] ?? null,
5055
- projectName: project?.name
5056
- };
5057
- }
5058
- function buildHandoffTitle(summary, latestRequest, explicit) {
5059
- const chosen = explicit?.trim() || summary?.current_thread?.trim() || summary?.completed?.trim() || latestRequest?.trim() || "Current work";
5060
- return compactLine2(chosen) ?? "Current work";
5061
- }
5062
- function buildHandoffNarrative(summary, story, options) {
5063
- const sections = [];
5064
- if (summary?.request || story.latest_request) {
5065
- sections.push(`Request: ${summary?.request ?? story.latest_request}`);
5066
- }
5067
- if (summary?.current_thread) {
5068
- sections.push(`Current thread: ${summary.current_thread}`);
5069
- }
5070
- if (summary?.investigated) {
5071
- sections.push(`Investigated: ${summary.investigated}`);
5072
- }
5073
- if (summary?.learned) {
5074
- sections.push(`Learned: ${summary.learned}`);
5075
- }
5076
- if (summary?.completed) {
5077
- sections.push(`Completed: ${summary.completed}`);
5078
- }
5079
- if (summary?.next_steps) {
5080
- sections.push(`Next Steps: ${summary.next_steps}`);
5081
- }
5082
- if (story.recent_outcomes.length > 0) {
5083
- sections.push(`Recent outcomes:
5084
- ${story.recent_outcomes.slice(0, 5).map((item) => `- ${item}`).join(`
5085
- `)}`);
5086
- }
5087
- if (story.hot_files.length > 0) {
5088
- sections.push(`Hot files:
5089
- ${story.hot_files.slice(0, 5).map((file) => `- ${file.path}`).join(`
5090
- `)}`);
5091
- }
5092
- if (story.provenance_summary.length > 0) {
5093
- sections.push(`Tool trail:
5094
- ${story.provenance_summary.slice(0, 5).map((item) => `- ${item.tool}: ${item.count}`).join(`
5095
- `)}`);
5096
- }
5097
- if (options.includeChat && story.chat_messages.length > 0) {
5098
- const chatLines = story.chat_messages.slice(-options.chatLimit).map((msg) => `- [${msg.role}] ${compactLine2(msg.content) ?? msg.content.slice(0, 120)}`);
5099
- sections.push(`Chat snippets:
5100
- ${chatLines.join(`
5101
- `)}`);
5102
- }
5103
- return sections.filter(Boolean).join(`
5104
-
5105
- `);
5106
- }
5107
- function shouldAutoIncludeChat(story) {
5108
- if (story.chat_messages.length === 0)
5109
- return false;
5110
- const summary = story.summary;
5111
- const thinSummary = !summary?.completed && !summary?.current_thread && story.recent_outcomes.length < 2;
5112
- const thinChronology = story.capture_state !== "rich" || story.tool_events.length === 0;
5113
- return thinSummary || thinChronology;
5114
- }
5115
- function buildHandoffFacts(summary, story) {
5116
- const facts = [
5117
- `session_id=${story.session?.session_id ?? "unknown"}`,
5118
- `capture_state=${story.capture_state}`,
5119
- story.project_name ? `project=${story.project_name}` : null,
5120
- summary?.current_thread ? `current_thread=${summary.current_thread}` : null,
5121
- story.hot_files[0] ? `hot_file=${story.hot_files[0].path}` : null,
5122
- story.provenance_summary[0] ? `primary_tool=${story.provenance_summary[0].tool}` : null
5123
- ];
5124
- return facts.filter((item) => Boolean(item));
5125
- }
5126
- function buildDraftHandoffConcepts(projectName, captureState) {
5127
- return [
5128
- "handoff",
5129
- "draft-handoff",
5130
- "auto-handoff",
5131
- `capture:${captureState}`,
5132
- ...projectName ? [projectName] : []
5133
- ];
5134
- }
5135
- function looksLikeHandoff(obs) {
5136
- if (obs.title.startsWith("Handoff:") || obs.title.startsWith("Handoff Draft:"))
5137
- return true;
5138
- const concepts = parseJsonArray4(obs.concepts);
5139
- return concepts.includes("handoff") || concepts.includes("session-handoff") || concepts.includes("draft-handoff");
5140
- }
5141
- function parseJsonArray4(value) {
5142
- if (!value)
5143
- return [];
5144
- try {
5145
- const parsed = JSON.parse(value);
5146
- return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
5147
- } catch {
5148
- return [];
5149
- }
5150
- }
5151
- function compactLine2(value) {
5152
- if (value === null || value === undefined)
5153
- return null;
5154
- let text;
5155
- if (typeof value === "string") {
5156
- text = value;
5157
- } else {
5158
- try {
5159
- text = JSON.stringify(value);
5160
- } catch {
5161
- text = String(value);
5162
- }
5163
- }
5164
- const trimmed = text.replace(/\s+/g, " ").trim();
5165
- if (!trimmed)
5166
- return null;
5167
- return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
5168
- }
5169
4666
 
5170
4667
  // hooks/stop.ts
4668
+ var thisFile = fileURLToPath(import.meta.url);
4669
+ var thisDir = dirname2(thisFile);
4670
+ var isWorker = process.env.ENGRM_STOP_WORKER === "1";
5171
4671
  function printRetrospective(summary) {
5172
4672
  const lines = [];
5173
4673
  lines.push("");
@@ -5202,9 +4702,53 @@ function printRetrospective(summary) {
5202
4702
  `));
5203
4703
  }
5204
4704
  async function main() {
5205
- const event = await parseStdinJson();
5206
- if (!event)
4705
+ if (isWorker) {
4706
+ await runWorkerFromFile(process.argv[2]);
5207
4707
  process.exit(0);
4708
+ }
4709
+ const raw = await readStdin();
4710
+ if (!raw.trim())
4711
+ process.exit(0);
4712
+ spawnDetachedWorker(raw);
4713
+ process.exit(0);
4714
+ }
4715
+ function spawnDetachedWorker(raw) {
4716
+ const tempDir = mkdtempSync(join6(tmpdir(), "engrm-stop-"));
4717
+ const payloadPath = join6(tempDir, "event.json");
4718
+ writeFileSync2(payloadPath, raw, "utf-8");
4719
+ const isBun = process.execPath.endsWith("bun");
4720
+ const childArgs = isBun ? ["run", thisFile, payloadPath] : [thisFile, payloadPath];
4721
+ const child = spawn(process.execPath, childArgs, {
4722
+ detached: true,
4723
+ stdio: "ignore",
4724
+ env: {
4725
+ ...process.env,
4726
+ ENGRM_STOP_WORKER: "1"
4727
+ }
4728
+ });
4729
+ child.unref();
4730
+ }
4731
+ async function runWorkerFromFile(payloadPath) {
4732
+ if (!payloadPath)
4733
+ return;
4734
+ let raw = "";
4735
+ try {
4736
+ raw = readFileSync5(payloadPath, "utf-8");
4737
+ } catch {
4738
+ return;
4739
+ } finally {
4740
+ try {
4741
+ rmSync(dirname2(payloadPath), { recursive: true, force: true });
4742
+ } catch {}
4743
+ }
4744
+ let event = null;
4745
+ try {
4746
+ event = JSON.parse(raw);
4747
+ } catch {
4748
+ return;
4749
+ }
4750
+ if (!event)
4751
+ return;
5208
4752
  if (event.stop_hook_active)
5209
4753
  process.exit(0);
5210
4754
  const boot = bootstrapHook("stop");
@@ -5238,12 +4782,6 @@ async function main() {
5238
4782
  source_kind: "hook"
5239
4783
  });
5240
4784
  db.addToOutbox("chat_message", chatMessage.id);
5241
- if (db.vecAvailable) {
5242
- const chatEmbedding = await embedText(composeChatEmbeddingText(event.last_assistant_message));
5243
- if (chatEmbedding) {
5244
- db.vecChatInsert(chatMessage.id, chatEmbedding);
5245
- }
5246
- }
5247
4785
  }
5248
4786
  createAssistantCheckpoint(db, event.session_id, event.cwd, event.last_assistant_message);
5249
4787
  } catch {}
@@ -5258,10 +4796,6 @@ async function main() {
5258
4796
  if (summary) {
5259
4797
  const row = db.upsertSessionSummary(summary);
5260
4798
  db.addToOutbox("summary", row.id);
5261
- await upsertRollingHandoff(db, config, {
5262
- session_id: event.session_id,
5263
- cwd: event.cwd
5264
- });
5265
4799
  let securityFindings = [];
5266
4800
  try {
5267
4801
  if (session?.project_id) {
@@ -5298,36 +4832,19 @@ async function main() {
5298
4832
  createSessionDigest(db, event.session_id, event.cwd);
5299
4833
  } catch {}
5300
4834
  }
5301
- if (config.transcript_analysis?.enabled && event.session_id) {
5302
- try {
5303
- const messages = readTranscript(event.session_id, event.cwd, event.transcript_path);
5304
- if (messages.length > 10) {
5305
- const transcript = truncateTranscript(messages);
5306
- const results = await analyzeTranscript(config, transcript, event.session_id);
5307
- if (results) {
5308
- const saved = await saveTranscriptResults(db, config, results, event.session_id, event.cwd);
5309
- if (saved > 0) {
5310
- console.error(`
5311
- \uD83D\uDCA1 Engrm: Extracted ${saved} insight(s) from session transcript.`);
5312
- }
5313
- }
5314
- }
5315
- } catch {}
5316
- }
5317
- await pushOnce(db, config, { timeoutMs: 1500 });
4835
+ await withTimeout(pushOnce(db, config, { timeoutMs: 250 }), 350);
5318
4836
  try {
5319
4837
  if (event.session_id) {
5320
4838
  const metrics = readSessionMetrics(event.session_id);
5321
4839
  const beacon = buildBeacon(db, config, event.session_id, metrics);
5322
4840
  if (beacon) {
5323
- await sendBeacon(config, beacon);
4841
+ await withTimeout(sendBeacon(config, beacon), 150);
5324
4842
  }
5325
4843
  }
5326
4844
  } catch {}
5327
4845
  } finally {
5328
4846
  db.close();
5329
4847
  }
5330
- process.exit(0);
5331
4848
  }
5332
4849
  function buildFallbackSessionSummary(db, sessionId, projectId, userId, lastAssistantMessage) {
5333
4850
  const prompts = db.getSessionUserPrompts(sessionId, 10).filter((prompt) => isMeaningfulSummaryPrompt(prompt));
@@ -5539,6 +5056,12 @@ ${sections.join(`
5539
5056
  });
5540
5057
  db.addToOutbox("observation", digestObs.id);
5541
5058
  }
5059
+ async function withTimeout(promise, timeoutMs) {
5060
+ return await Promise.race([
5061
+ promise,
5062
+ new Promise((resolve2) => setTimeout(() => resolve2(null), timeoutMs))
5063
+ ]);
5064
+ }
5542
5065
  function createAssistantCheckpoint(db, sessionId, cwd, message) {
5543
5066
  const checkpoint = extractAssistantCheckpoint(message);
5544
5067
  if (!checkpoint)
@@ -5629,14 +5152,14 @@ function detectUnsavedPlans(message) {
5629
5152
  return hints;
5630
5153
  }
5631
5154
  function readSessionMetrics(sessionId) {
5632
- const { existsSync: existsSync5, readFileSync: readFileSync5, unlinkSync } = __require("node:fs");
5633
- const { join: join6 } = __require("node:path");
5155
+ const { existsSync: existsSync5, readFileSync: readFileSync6, unlinkSync } = __require("node:fs");
5156
+ const { join: join7 } = __require("node:path");
5634
5157
  const { homedir: homedir4 } = __require("node:os");
5635
5158
  const result = {};
5636
5159
  try {
5637
- const obsPath = join6(homedir4(), ".engrm", "observer-sessions", `${sessionId}.json`);
5160
+ const obsPath = join7(homedir4(), ".engrm", "observer-sessions", `${sessionId}.json`);
5638
5161
  if (existsSync5(obsPath)) {
5639
- const state = JSON.parse(readFileSync5(obsPath, "utf-8"));
5162
+ const state = JSON.parse(readFileSync6(obsPath, "utf-8"));
5640
5163
  if (typeof state.recallAttempts === "number")
5641
5164
  result.recallAttempts = state.recallAttempts;
5642
5165
  if (typeof state.recallHits === "number")
@@ -5644,9 +5167,9 @@ function readSessionMetrics(sessionId) {
5644
5167
  }
5645
5168
  } catch {}
5646
5169
  try {
5647
- const hookPath = join6(homedir4(), ".engrm", "hook-session-metrics.json");
5170
+ const hookPath = join7(homedir4(), ".engrm", "hook-session-metrics.json");
5648
5171
  if (existsSync5(hookPath)) {
5649
- const hookMetrics = JSON.parse(readFileSync5(hookPath, "utf-8"));
5172
+ const hookMetrics = JSON.parse(readFileSync6(hookPath, "utf-8"));
5650
5173
  if (typeof hookMetrics.contextObsInjected === "number")
5651
5174
  result.contextObsInjected = hookMetrics.contextObsInjected;
5652
5175
  if (typeof hookMetrics.contextTotalAvailable === "number")
@@ -5657,9 +5180,9 @@ function readSessionMetrics(sessionId) {
5657
5180
  }
5658
5181
  } catch {}
5659
5182
  try {
5660
- const mcpPath = join6(homedir4(), ".engrm", "mcp-session-metrics.json");
5183
+ const mcpPath = join7(homedir4(), ".engrm", "mcp-session-metrics.json");
5661
5184
  if (existsSync5(mcpPath)) {
5662
- const metrics = JSON.parse(readFileSync5(mcpPath, "utf-8"));
5185
+ const metrics = JSON.parse(readFileSync6(mcpPath, "utf-8"));
5663
5186
  if (typeof metrics.contextObsInjected === "number" && metrics.contextObsInjected > 0) {
5664
5187
  result.contextObsInjected = metrics.contextObsInjected;
5665
5188
  }
@@ -1123,6 +1123,20 @@ function ensureChatMessageColumns(db) {
1123
1123
  db.exec("PRAGMA user_version = 17");
1124
1124
  }
1125
1125
  }
1126
+ function ensureObservationVectorTable(db) {
1127
+ if (!isVecExtensionLoaded(db))
1128
+ return;
1129
+ db.exec(`
1130
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
1131
+ observation_id INTEGER PRIMARY KEY,
1132
+ embedding FLOAT[384]
1133
+ );
1134
+ `);
1135
+ const current = getSchemaVersion(db);
1136
+ if (current < 4) {
1137
+ db.exec("PRAGMA user_version = 4");
1138
+ }
1139
+ }
1126
1140
  function ensureChatVectorTable(db) {
1127
1141
  if (!isVecExtensionLoaded(db))
1128
1142
  return;
@@ -1351,6 +1365,7 @@ class MemDatabase {
1351
1365
  ensureObservationTypes(this.db);
1352
1366
  ensureSessionSummaryColumns(this.db);
1353
1367
  ensureChatMessageColumns(this.db);
1368
+ ensureObservationVectorTable(this.db);
1354
1369
  ensureChatVectorTable(this.db);
1355
1370
  ensureSyncOutboxSupportsChatMessages(this.db);
1356
1371
  }
package/dist/server.js CHANGED
@@ -14534,6 +14534,20 @@ function ensureChatMessageColumns(db) {
14534
14534
  db.exec("PRAGMA user_version = 17");
14535
14535
  }
14536
14536
  }
14537
+ function ensureObservationVectorTable(db) {
14538
+ if (!isVecExtensionLoaded(db))
14539
+ return;
14540
+ db.exec(`
14541
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_observations USING vec0(
14542
+ observation_id INTEGER PRIMARY KEY,
14543
+ embedding FLOAT[384]
14544
+ );
14545
+ `);
14546
+ const current = getSchemaVersion(db);
14547
+ if (current < 4) {
14548
+ db.exec("PRAGMA user_version = 4");
14549
+ }
14550
+ }
14537
14551
  function ensureChatVectorTable(db) {
14538
14552
  if (!isVecExtensionLoaded(db))
14539
14553
  return;
@@ -14762,6 +14776,7 @@ class MemDatabase {
14762
14776
  ensureObservationTypes(this.db);
14763
14777
  ensureSessionSummaryColumns(this.db);
14764
14778
  ensureChatMessageColumns(this.db);
14779
+ ensureObservationVectorTable(this.db);
14765
14780
  ensureChatVectorTable(this.db);
14766
14781
  ensureSyncOutboxSupportsChatMessages(this.db);
14767
14782
  }
@@ -23449,7 +23464,7 @@ function installStdioLivenessGuards() {
23449
23464
  function buildServer() {
23450
23465
  const server = new McpServer({
23451
23466
  name: "engrm",
23452
- version: "0.4.46"
23467
+ version: "0.4.48"
23453
23468
  });
23454
23469
  const enabledToolNames = getEnabledToolNames(config2.tool_profile);
23455
23470
  const originalTool = server.tool.bind(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.46",
3
+ "version": "0.4.48",
4
4
  "description": "Shared memory across devices, sessions, and agents, with thin MCP tools for durable capture, live continuity, and Hermes-ready remote MCP support",
5
5
  "mcpName": "io.github.dr12hes/engrm",
6
6
  "type": "module",