engrm 0.4.17 → 0.4.19

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
@@ -1316,6 +1316,31 @@ class MemDatabase {
1316
1316
  const id = Number(result.lastInsertRowid);
1317
1317
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1318
1318
  }
1319
+ upsertSessionSummary(summary) {
1320
+ const existing = this.getSessionSummary(summary.session_id);
1321
+ if (!existing) {
1322
+ return this.insertSessionSummary(summary);
1323
+ }
1324
+ const now = Math.floor(Date.now() / 1000);
1325
+ const normalized = {
1326
+ request: normalizeSummaryRequest(summary.request ?? existing.request),
1327
+ investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
1328
+ learned: normalizeSummarySection(summary.learned ?? existing.learned),
1329
+ completed: normalizeSummarySection(summary.completed ?? existing.completed),
1330
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
1331
+ };
1332
+ this.db.query(`UPDATE session_summaries
1333
+ SET project_id = ?,
1334
+ user_id = ?,
1335
+ request = ?,
1336
+ investigated = ?,
1337
+ learned = ?,
1338
+ completed = ?,
1339
+ next_steps = ?,
1340
+ created_at_epoch = ?
1341
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
1342
+ return this.getSessionSummary(summary.session_id);
1343
+ }
1319
1344
  getSessionSummary(sessionId) {
1320
1345
  return this.db.query("SELECT * FROM session_summaries WHERE session_id = ?").get(sessionId) ?? null;
1321
1346
  }
@@ -2145,6 +2145,31 @@ class MemDatabase {
2145
2145
  const id = Number(result.lastInsertRowid);
2146
2146
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
2147
2147
  }
2148
+ upsertSessionSummary(summary) {
2149
+ const existing = this.getSessionSummary(summary.session_id);
2150
+ if (!existing) {
2151
+ return this.insertSessionSummary(summary);
2152
+ }
2153
+ const now = Math.floor(Date.now() / 1000);
2154
+ const normalized = {
2155
+ request: normalizeSummaryRequest(summary.request ?? existing.request),
2156
+ investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
2157
+ learned: normalizeSummarySection(summary.learned ?? existing.learned),
2158
+ completed: normalizeSummarySection(summary.completed ?? existing.completed),
2159
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
2160
+ };
2161
+ this.db.query(`UPDATE session_summaries
2162
+ SET project_id = ?,
2163
+ user_id = ?,
2164
+ request = ?,
2165
+ investigated = ?,
2166
+ learned = ?,
2167
+ completed = ?,
2168
+ next_steps = ?,
2169
+ created_at_epoch = ?
2170
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
2171
+ return this.getSessionSummary(summary.session_id);
2172
+ }
2148
2173
  getSessionSummary(sessionId) {
2149
2174
  return this.db.query("SELECT * FROM session_summaries WHERE session_id = ?").get(sessionId) ?? null;
2150
2175
  }
@@ -1490,6 +1490,31 @@ class MemDatabase {
1490
1490
  const id = Number(result.lastInsertRowid);
1491
1491
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1492
1492
  }
1493
+ upsertSessionSummary(summary) {
1494
+ const existing = this.getSessionSummary(summary.session_id);
1495
+ if (!existing) {
1496
+ return this.insertSessionSummary(summary);
1497
+ }
1498
+ const now = Math.floor(Date.now() / 1000);
1499
+ const normalized = {
1500
+ request: normalizeSummaryRequest(summary.request ?? existing.request),
1501
+ investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
1502
+ learned: normalizeSummarySection(summary.learned ?? existing.learned),
1503
+ completed: normalizeSummarySection(summary.completed ?? existing.completed),
1504
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
1505
+ };
1506
+ this.db.query(`UPDATE session_summaries
1507
+ SET project_id = ?,
1508
+ user_id = ?,
1509
+ request = ?,
1510
+ investigated = ?,
1511
+ learned = ?,
1512
+ completed = ?,
1513
+ next_steps = ?,
1514
+ created_at_epoch = ?
1515
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
1516
+ return this.getSessionSummary(summary.session_id);
1517
+ }
1493
1518
  getSessionSummary(sessionId) {
1494
1519
  return this.db.query("SELECT * FROM session_summaries WHERE session_id = ?").get(sessionId) ?? null;
1495
1520
  }
@@ -1284,6 +1284,31 @@ class MemDatabase {
1284
1284
  const id = Number(result.lastInsertRowid);
1285
1285
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1286
1286
  }
1287
+ upsertSessionSummary(summary) {
1288
+ const existing = this.getSessionSummary(summary.session_id);
1289
+ if (!existing) {
1290
+ return this.insertSessionSummary(summary);
1291
+ }
1292
+ const now = Math.floor(Date.now() / 1000);
1293
+ const normalized = {
1294
+ request: normalizeSummaryRequest(summary.request ?? existing.request),
1295
+ investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
1296
+ learned: normalizeSummarySection(summary.learned ?? existing.learned),
1297
+ completed: normalizeSummarySection(summary.completed ?? existing.completed),
1298
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
1299
+ };
1300
+ this.db.query(`UPDATE session_summaries
1301
+ SET project_id = ?,
1302
+ user_id = ?,
1303
+ request = ?,
1304
+ investigated = ?,
1305
+ learned = ?,
1306
+ completed = ?,
1307
+ next_steps = ?,
1308
+ created_at_epoch = ?
1309
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
1310
+ return this.getSessionSummary(summary.session_id);
1311
+ }
1287
1312
  getSessionSummary(sessionId) {
1288
1313
  return this.db.query("SELECT * FROM session_summaries WHERE session_id = ?").get(sessionId) ?? null;
1289
1314
  }
@@ -1360,6 +1360,31 @@ class MemDatabase {
1360
1360
  const id = Number(result.lastInsertRowid);
1361
1361
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1362
1362
  }
1363
+ upsertSessionSummary(summary) {
1364
+ const existing = this.getSessionSummary(summary.session_id);
1365
+ if (!existing) {
1366
+ return this.insertSessionSummary(summary);
1367
+ }
1368
+ const now = Math.floor(Date.now() / 1000);
1369
+ const normalized = {
1370
+ request: normalizeSummaryRequest(summary.request ?? existing.request),
1371
+ investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
1372
+ learned: normalizeSummarySection(summary.learned ?? existing.learned),
1373
+ completed: normalizeSummarySection(summary.completed ?? existing.completed),
1374
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
1375
+ };
1376
+ this.db.query(`UPDATE session_summaries
1377
+ SET project_id = ?,
1378
+ user_id = ?,
1379
+ request = ?,
1380
+ investigated = ?,
1381
+ learned = ?,
1382
+ completed = ?,
1383
+ next_steps = ?,
1384
+ created_at_epoch = ?
1385
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
1386
+ return this.getSessionSummary(summary.session_id);
1387
+ }
1363
1388
  getSessionSummary(sessionId) {
1364
1389
  return this.db.query("SELECT * FROM session_summaries WHERE session_id = ?").get(sessionId) ?? null;
1365
1390
  }
@@ -1154,7 +1154,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
1154
1154
  import { join as join3 } from "node:path";
1155
1155
  import { homedir } from "node:os";
1156
1156
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
1157
- var CLIENT_VERSION = "0.4.17";
1157
+ var CLIENT_VERSION = "0.4.19";
1158
1158
  function hashFile(filePath) {
1159
1159
  try {
1160
1160
  if (!existsSync3(filePath))
@@ -1620,15 +1620,11 @@ function mergeChanges(db, config, changes) {
1620
1620
  let skipped = 0;
1621
1621
  for (const change of changes) {
1622
1622
  const parsed = parseSourceId(change.source_id);
1623
+ const remoteSummary = isRemoteSummary(change);
1623
1624
  if (parsed && parsed.deviceId === config.device_id) {
1624
1625
  skipped++;
1625
1626
  continue;
1626
1627
  }
1627
- const existing = db.db.query("SELECT id FROM observations WHERE remote_source_id = ?").get(change.source_id);
1628
- if (existing) {
1629
- skipped++;
1630
- continue;
1631
- }
1632
1628
  const projectCanonical = change.metadata?.project_canonical ?? null;
1633
1629
  if (!projectCanonical) {
1634
1630
  skipped++;
@@ -1641,6 +1637,18 @@ function mergeChanges(db, config, changes) {
1641
1637
  name: change.metadata?.project_name ?? projectCanonical.split("/").pop() ?? "unknown"
1642
1638
  });
1643
1639
  }
1640
+ if (remoteSummary) {
1641
+ const mergedSummary = mergeRemoteSummary(db, config, change, project.id);
1642
+ if (mergedSummary) {
1643
+ merged++;
1644
+ }
1645
+ }
1646
+ const existing = db.db.query("SELECT id FROM observations WHERE remote_source_id = ?").get(change.source_id);
1647
+ if (existing) {
1648
+ if (!remoteSummary)
1649
+ skipped++;
1650
+ continue;
1651
+ }
1644
1652
  const normalizedType = normalizeRemoteObservationType(change.metadata?.type, change.source_id);
1645
1653
  if (!normalizedType) {
1646
1654
  skipped++;
@@ -1672,6 +1680,26 @@ function mergeChanges(db, config, changes) {
1672
1680
  }
1673
1681
  return { merged, skipped };
1674
1682
  }
1683
+ function isRemoteSummary(change) {
1684
+ const rawType = typeof change.metadata?.type === "string" ? change.metadata.type.toLowerCase() : "";
1685
+ return rawType === "summary" || change.source_id.includes("-summary-");
1686
+ }
1687
+ function mergeRemoteSummary(db, config, change, projectId) {
1688
+ const sessionId = typeof change.metadata?.session_id === "string" ? change.metadata.session_id : null;
1689
+ if (!sessionId)
1690
+ return false;
1691
+ const summary = db.upsertSessionSummary({
1692
+ session_id: sessionId,
1693
+ project_id: projectId,
1694
+ user_id: (typeof change.metadata?.user_id === "string" ? change.metadata.user_id : null) ?? config.user_id,
1695
+ request: typeof change.metadata?.request === "string" ? change.metadata.request : null,
1696
+ investigated: typeof change.metadata?.investigated === "string" ? change.metadata.investigated : null,
1697
+ learned: typeof change.metadata?.learned === "string" ? change.metadata.learned : null,
1698
+ completed: typeof change.metadata?.completed === "string" ? change.metadata.completed : null,
1699
+ next_steps: typeof change.metadata?.next_steps === "string" ? change.metadata.next_steps : null
1700
+ });
1701
+ return Boolean(summary);
1702
+ }
1675
1703
  function normalizeRemoteObservationType(rawType, sourceId) {
1676
1704
  const type = typeof rawType === "string" ? rawType.trim().toLowerCase() : "";
1677
1705
  if (type === "bugfix" || type === "discovery" || type === "decision" || type === "pattern" || type === "change" || type === "feature" || type === "refactor" || type === "digest" || type === "standard" || type === "message") {
@@ -2852,6 +2880,31 @@ class MemDatabase {
2852
2880
  const id = Number(result.lastInsertRowid);
2853
2881
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
2854
2882
  }
2883
+ upsertSessionSummary(summary) {
2884
+ const existing = this.getSessionSummary(summary.session_id);
2885
+ if (!existing) {
2886
+ return this.insertSessionSummary(summary);
2887
+ }
2888
+ const now = Math.floor(Date.now() / 1000);
2889
+ const normalized = {
2890
+ request: normalizeSummaryRequest(summary.request ?? existing.request),
2891
+ investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
2892
+ learned: normalizeSummarySection(summary.learned ?? existing.learned),
2893
+ completed: normalizeSummarySection(summary.completed ?? existing.completed),
2894
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
2895
+ };
2896
+ this.db.query(`UPDATE session_summaries
2897
+ SET project_id = ?,
2898
+ user_id = ?,
2899
+ request = ?,
2900
+ investigated = ?,
2901
+ learned = ?,
2902
+ completed = ?,
2903
+ next_steps = ?,
2904
+ created_at_epoch = ?
2905
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
2906
+ return this.getSessionSummary(summary.session_id);
2907
+ }
2855
2908
  getSessionSummary(sessionId) {
2856
2909
  return this.db.query("SELECT * FROM session_summaries WHERE session_id = ?").get(sessionId) ?? null;
2857
2910
  }
@@ -3157,13 +3210,13 @@ function formatSplashScreen(data) {
3157
3210
  }
3158
3211
  }
3159
3212
  const contextIndex = formatContextIndex(data.context, handoffShownItems);
3160
- if (contextIndex.length > 0) {
3213
+ if (contextIndex.lines.length > 0) {
3161
3214
  lines.push("");
3162
- for (const line of contextIndex) {
3215
+ for (const line of contextIndex.lines) {
3163
3216
  lines.push(` ${line}`);
3164
3217
  }
3165
3218
  }
3166
- const inspectHints = formatInspectHints(data.context);
3219
+ const inspectHints = formatInspectHints(data.context, contextIndex.observationIds);
3167
3220
  if (inspectHints.length > 0) {
3168
3221
  lines.push("");
3169
3222
  for (const line of inspectHints) {
@@ -3300,19 +3353,23 @@ function formatLegend() {
3300
3353
  ];
3301
3354
  }
3302
3355
  function formatContextIndex(context, shownItems) {
3303
- const rows = pickContextIndexObservations(context, shownItems).map((obs) => {
3356
+ const selected = pickContextIndexObservations(context, shownItems);
3357
+ const rows = selected.map((obs) => {
3304
3358
  const icon = observationIcon(obs.type);
3305
3359
  const fileHint = extractPrimaryFileHint(obs);
3306
3360
  return `${icon} #${obs.id} ${truncateInline(obs.title, 110)}${fileHint ? ` ${c2.dim}(${fileHint})${c2.reset}` : ""}`;
3307
3361
  });
3308
3362
  if (rows.length === 0)
3309
- return [];
3310
- return [
3311
- `${c2.dim}Handoff index:${c2.reset} use IDs when you want the deeper thread`,
3312
- ...rows
3313
- ];
3363
+ return { lines: [], observationIds: [] };
3364
+ return {
3365
+ lines: [
3366
+ `${c2.dim}Handoff index:${c2.reset} use IDs when you want the deeper thread`,
3367
+ ...rows
3368
+ ],
3369
+ observationIds: selected.map((obs) => obs.id)
3370
+ };
3314
3371
  }
3315
- function formatInspectHints(context) {
3372
+ function formatInspectHints(context, visibleObservationIds = []) {
3316
3373
  const hints = [];
3317
3374
  if ((context.recentSessions?.length ?? 0) > 0) {
3318
3375
  hints.push("recent_sessions");
@@ -3327,7 +3384,7 @@ function formatInspectHints(context) {
3327
3384
  const unique = Array.from(new Set(hints)).slice(0, 4);
3328
3385
  if (unique.length === 0)
3329
3386
  return [];
3330
- const ids = context.observations.slice(0, 5).map((obs) => obs.id);
3387
+ const ids = visibleObservationIds.slice(0, 5);
3331
3388
  const fetchHint = ids.length > 0 ? `get_observations([${ids.join(", ")}])` : null;
3332
3389
  return [
3333
3390
  `${c2.dim}Next look:${c2.reset} ${unique.join(" \xB7 ")}`,
@@ -237,6 +237,84 @@ function normalizeObservationKey(value) {
237
237
  return value.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\b(modified|updated|edited|touched|changed)\b/g, "").replace(/\s+/g, " ").trim();
238
238
  }
239
239
 
240
+ // src/intelligence/summary-sections.ts
241
+ function extractSummaryItems(section, limit) {
242
+ if (!section || !section.trim())
243
+ return [];
244
+ const rawLines = section.split(`
245
+ `).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
246
+ const items = [];
247
+ const seen = new Set;
248
+ let heading = null;
249
+ for (const rawLine of rawLines) {
250
+ const line = stripSectionPrefix(rawLine);
251
+ if (!line)
252
+ continue;
253
+ const headingOnly = parseHeading(line);
254
+ if (headingOnly) {
255
+ heading = headingOnly;
256
+ continue;
257
+ }
258
+ const isBullet = /^[-*•]\s+/.test(line);
259
+ const stripped = line.replace(/^[-*•]\s+/, "").trim();
260
+ if (!stripped)
261
+ continue;
262
+ const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
263
+ const normalized = normalizeItem(item);
264
+ if (!normalized || seen.has(normalized))
265
+ continue;
266
+ seen.add(normalized);
267
+ items.push(item);
268
+ if (limit && items.length >= limit)
269
+ break;
270
+ }
271
+ return items;
272
+ }
273
+ function formatSummaryItems(section, maxLen) {
274
+ const items = extractSummaryItems(section);
275
+ if (items.length === 0)
276
+ return null;
277
+ const cleaned = items.map((item) => `- ${item}`).join(`
278
+ `);
279
+ if (cleaned.length <= maxLen)
280
+ return cleaned;
281
+ const truncated = cleaned.slice(0, maxLen).trimEnd();
282
+ const lastBreak = Math.max(truncated.lastIndexOf(`
283
+ `), truncated.lastIndexOf(" "));
284
+ const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
285
+ return `${safe.trimEnd()}…`;
286
+ }
287
+ function normalizeSummarySection(section) {
288
+ const items = extractSummaryItems(section);
289
+ if (items.length === 0) {
290
+ const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
291
+ return cleaned || null;
292
+ }
293
+ return items.map((item) => `- ${item}`).join(`
294
+ `);
295
+ }
296
+ function normalizeSummaryRequest(value) {
297
+ const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
298
+ return cleaned || null;
299
+ }
300
+ function stripSectionPrefix(value) {
301
+ return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
302
+ }
303
+ function parseHeading(value) {
304
+ const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
305
+ if (boldMatch?.[1]) {
306
+ return boldMatch[1].trim().replace(/\s+/g, " ");
307
+ }
308
+ const plainMatch = value.match(/^(.+?):$/);
309
+ if (plainMatch?.[1]) {
310
+ return plainMatch[1].trim().replace(/\s+/g, " ");
311
+ }
312
+ return null;
313
+ }
314
+ function normalizeItem(value) {
315
+ return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
316
+ }
317
+
240
318
  // src/config.ts
241
319
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
242
320
  import { homedir, hostname, networkInterfaces } from "node:os";
@@ -953,86 +1031,6 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
953
1031
 
954
1032
  // src/storage/sqlite.ts
955
1033
  import { createHash as createHash2 } from "node:crypto";
956
-
957
- // src/intelligence/summary-sections.ts
958
- function extractSummaryItems(section, limit) {
959
- if (!section || !section.trim())
960
- return [];
961
- const rawLines = section.split(`
962
- `).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
963
- const items = [];
964
- const seen = new Set;
965
- let heading = null;
966
- for (const rawLine of rawLines) {
967
- const line = stripSectionPrefix(rawLine);
968
- if (!line)
969
- continue;
970
- const headingOnly = parseHeading(line);
971
- if (headingOnly) {
972
- heading = headingOnly;
973
- continue;
974
- }
975
- const isBullet = /^[-*•]\s+/.test(line);
976
- const stripped = line.replace(/^[-*•]\s+/, "").trim();
977
- if (!stripped)
978
- continue;
979
- const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
980
- const normalized = normalizeItem(item);
981
- if (!normalized || seen.has(normalized))
982
- continue;
983
- seen.add(normalized);
984
- items.push(item);
985
- if (limit && items.length >= limit)
986
- break;
987
- }
988
- return items;
989
- }
990
- function formatSummaryItems(section, maxLen) {
991
- const items = extractSummaryItems(section);
992
- if (items.length === 0)
993
- return null;
994
- const cleaned = items.map((item) => `- ${item}`).join(`
995
- `);
996
- if (cleaned.length <= maxLen)
997
- return cleaned;
998
- const truncated = cleaned.slice(0, maxLen).trimEnd();
999
- const lastBreak = Math.max(truncated.lastIndexOf(`
1000
- `), truncated.lastIndexOf(" "));
1001
- const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
1002
- return `${safe.trimEnd()}…`;
1003
- }
1004
- function normalizeSummarySection(section) {
1005
- const items = extractSummaryItems(section);
1006
- if (items.length === 0) {
1007
- const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
1008
- return cleaned || null;
1009
- }
1010
- return items.map((item) => `- ${item}`).join(`
1011
- `);
1012
- }
1013
- function normalizeSummaryRequest(value) {
1014
- const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
1015
- return cleaned || null;
1016
- }
1017
- function stripSectionPrefix(value) {
1018
- return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
1019
- }
1020
- function parseHeading(value) {
1021
- const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
1022
- if (boldMatch?.[1]) {
1023
- return boldMatch[1].trim().replace(/\s+/g, " ");
1024
- }
1025
- const plainMatch = value.match(/^(.+?):$/);
1026
- if (plainMatch?.[1]) {
1027
- return plainMatch[1].trim().replace(/\s+/g, " ");
1028
- }
1029
- return null;
1030
- }
1031
- function normalizeItem(value) {
1032
- return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
1033
- }
1034
-
1035
- // src/storage/sqlite.ts
1036
1034
  var IS_BUN = typeof globalThis.Bun !== "undefined";
1037
1035
  function openDatabase(dbPath) {
1038
1036
  if (IS_BUN) {
@@ -1519,6 +1517,31 @@ class MemDatabase {
1519
1517
  const id = Number(result.lastInsertRowid);
1520
1518
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1521
1519
  }
1520
+ upsertSessionSummary(summary) {
1521
+ const existing = this.getSessionSummary(summary.session_id);
1522
+ if (!existing) {
1523
+ return this.insertSessionSummary(summary);
1524
+ }
1525
+ const now = Math.floor(Date.now() / 1000);
1526
+ const normalized = {
1527
+ request: normalizeSummaryRequest(summary.request ?? existing.request),
1528
+ investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
1529
+ learned: normalizeSummarySection(summary.learned ?? existing.learned),
1530
+ completed: normalizeSummarySection(summary.completed ?? existing.completed),
1531
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
1532
+ };
1533
+ this.db.query(`UPDATE session_summaries
1534
+ SET project_id = ?,
1535
+ user_id = ?,
1536
+ request = ?,
1537
+ investigated = ?,
1538
+ learned = ?,
1539
+ completed = ?,
1540
+ next_steps = ?,
1541
+ created_at_epoch = ?
1542
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
1543
+ return this.getSessionSummary(summary.session_id);
1544
+ }
1522
1545
  getSessionSummary(sessionId) {
1523
1546
  return this.db.query("SELECT * FROM session_summaries WHERE session_id = ?").get(sessionId) ?? null;
1524
1547
  }
@@ -2535,7 +2558,7 @@ function buildBeacon(db, config, sessionId, metrics) {
2535
2558
  sentinel_used: valueSignals.security_findings_count > 0,
2536
2559
  risk_score: riskScore,
2537
2560
  stacks_detected: stacks,
2538
- client_version: "0.4.17",
2561
+ client_version: "0.4.19",
2539
2562
  context_observations_injected: metrics?.contextObsInjected ?? 0,
2540
2563
  context_total_available: metrics?.contextTotalAvailable ?? 0,
2541
2564
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -3567,7 +3590,9 @@ async function main() {
3567
3590
  if (!existing) {
3568
3591
  const observations = db.getObservationsBySession(event.session_id);
3569
3592
  const session = db.getSessionMetrics(event.session_id);
3570
- const summary = extractRetrospective(observations, event.session_id, session?.project_id ?? null, config.user_id) ?? buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message);
3593
+ const retrospective = extractRetrospective(observations, event.session_id, session?.project_id ?? null, config.user_id);
3594
+ const assistantSections = extractAssistantSummarySections(event.last_assistant_message);
3595
+ const summary = mergeSessionSummary(retrospective, assistantSections, event.session_id, session?.project_id ?? null, config.user_id) ?? mergeSessionSummary(buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message), assistantSections, event.session_id, session?.project_id ?? null, config.user_id) ?? buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message);
3571
3596
  if (summary) {
3572
3597
  const row = db.insertSessionSummary(summary);
3573
3598
  db.addToOutbox("summary", row.id);
@@ -3676,6 +3701,85 @@ function buildCheckpointCompleted(checkpoint) {
3676
3701
  return lines.join(`
3677
3702
  `);
3678
3703
  }
3704
+ function mergeSessionSummary(base, extra, sessionId, projectId, userId) {
3705
+ if (!base && !extra)
3706
+ return null;
3707
+ return {
3708
+ session_id: sessionId,
3709
+ project_id: projectId,
3710
+ user_id: userId,
3711
+ request: chooseRicherSummaryValue(base?.request ?? null, extra?.request ?? null, true),
3712
+ investigated: chooseRicherSummaryValue(base?.investigated ?? null, extra?.investigated ?? null, false),
3713
+ learned: chooseRicherSummaryValue(base?.learned ?? null, extra?.learned ?? null, false),
3714
+ completed: chooseRicherSummaryValue(base?.completed ?? null, extra?.completed ?? null, false),
3715
+ next_steps: chooseRicherSummaryValue(base?.next_steps ?? null, extra?.next_steps ?? null, false)
3716
+ };
3717
+ }
3718
+ function chooseRicherSummaryValue(base, extra, isRequest) {
3719
+ const normalizedBase = isRequest ? normalizeSummaryRequest(base) : normalizeSummarySection(base);
3720
+ const normalizedExtra = isRequest ? normalizeSummaryRequest(extra) : normalizeSummarySection(extra);
3721
+ if (!normalizedBase)
3722
+ return normalizedExtra;
3723
+ if (!normalizedExtra)
3724
+ return normalizedBase;
3725
+ if (normalizedExtra.length > normalizedBase.length + 24)
3726
+ return normalizedExtra;
3727
+ if (isRequest && isGenericCheckpointLine(normalizedBase) && !isGenericCheckpointLine(normalizedExtra)) {
3728
+ return normalizedExtra;
3729
+ }
3730
+ return normalizedBase;
3731
+ }
3732
+ function extractAssistantSummarySections(message) {
3733
+ const compact = message?.replace(/\r/g, "").trim();
3734
+ if (!compact || compact.length < 80)
3735
+ return null;
3736
+ const sections = new Map;
3737
+ let current = null;
3738
+ for (const rawLine of compact.split(`
3739
+ `)) {
3740
+ const line = rawLine.trim();
3741
+ if (!line)
3742
+ continue;
3743
+ const heading = parseAssistantSectionHeading(line);
3744
+ if (heading) {
3745
+ current = heading;
3746
+ if (!sections.has(current))
3747
+ sections.set(current, []);
3748
+ continue;
3749
+ }
3750
+ if (!current)
3751
+ continue;
3752
+ if (current === "request" && isGenericCheckpointLine(line))
3753
+ continue;
3754
+ sections.get(current)?.push(line);
3755
+ }
3756
+ const request = normalizeSummaryRequest(sections.get("request")?.join(" ") ?? null);
3757
+ const investigated = normalizeSummarySection(sections.get("investigated")?.join(`
3758
+ `) ?? null);
3759
+ const learned = normalizeSummarySection(sections.get("learned")?.join(`
3760
+ `) ?? null);
3761
+ const completed = normalizeSummarySection(sections.get("completed")?.join(`
3762
+ `) ?? null);
3763
+ const next_steps = normalizeSummarySection(sections.get("next_steps")?.join(`
3764
+ `) ?? null);
3765
+ if (!request && !investigated && !learned && !completed && !next_steps)
3766
+ return null;
3767
+ return { request, investigated, learned, completed, next_steps };
3768
+ }
3769
+ function parseAssistantSectionHeading(value) {
3770
+ const normalized = value.toLowerCase().replace(/\*+/g, "").trim();
3771
+ if (/^request:/.test(normalized))
3772
+ return "request";
3773
+ if (/^investigated:/.test(normalized))
3774
+ return "investigated";
3775
+ if (/^learned:/.test(normalized))
3776
+ return "learned";
3777
+ if (/^completed:/.test(normalized))
3778
+ return "completed";
3779
+ if (/^next steps?:/.test(normalized))
3780
+ return "next_steps";
3781
+ return null;
3782
+ }
3679
3783
  function createSessionDigest(db, sessionId, cwd) {
3680
3784
  const observations = db.getObservationsBySession(sessionId);
3681
3785
  if (observations.length < 2)
@@ -3831,9 +3935,13 @@ function extractAssistantCheckpoint(message) {
3831
3935
  };
3832
3936
  }
3833
3937
  function pickAssistantCheckpointTitle(substantiveLines, bulletLines) {
3834
- const candidates = [...bulletLines, ...substantiveLines].map((line) => line.replace(/^Completed:\s*/i, "").trim()).filter((line) => line.length > 20).filter((line) => !/^Next Steps?:/i.test(line)).filter((line) => !/^Investigated:/i.test(line)).filter((line) => !/^Learned:/i.test(line));
3938
+ const candidates = [...bulletLines, ...substantiveLines].map((line) => line.replace(/^Completed:\s*/i, "").trim()).filter((line) => line.length > 20).filter((line) => !isGenericCheckpointLine(line)).filter((line) => !/^Next Steps?:/i.test(line)).filter((line) => !/^Investigated:/i.test(line)).filter((line) => !/^Learned:/i.test(line));
3835
3939
  return candidates[0] ?? null;
3836
3940
  }
3941
+ function isGenericCheckpointLine(value) {
3942
+ const normalized = value.toLowerCase().replace(/\s+/g, " ").trim();
3943
+ return normalized === "here's where things stand:" || normalized === "here's where things stand" || normalized === "where things stand:" || normalized === "where things stand" || normalized === "current status:" || normalized === "current status" || normalized === "status update:" || normalized === "status update";
3944
+ }
3837
3945
  function detectUnsavedPlans(message) {
3838
3946
  const hints = [];
3839
3947
  const lower = message.toLowerCase();
@@ -1428,6 +1428,31 @@ class MemDatabase {
1428
1428
  const id = Number(result.lastInsertRowid);
1429
1429
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1430
1430
  }
1431
+ upsertSessionSummary(summary) {
1432
+ const existing = this.getSessionSummary(summary.session_id);
1433
+ if (!existing) {
1434
+ return this.insertSessionSummary(summary);
1435
+ }
1436
+ const now = Math.floor(Date.now() / 1000);
1437
+ const normalized = {
1438
+ request: normalizeSummaryRequest(summary.request ?? existing.request),
1439
+ investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
1440
+ learned: normalizeSummarySection(summary.learned ?? existing.learned),
1441
+ completed: normalizeSummarySection(summary.completed ?? existing.completed),
1442
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
1443
+ };
1444
+ this.db.query(`UPDATE session_summaries
1445
+ SET project_id = ?,
1446
+ user_id = ?,
1447
+ request = ?,
1448
+ investigated = ?,
1449
+ learned = ?,
1450
+ completed = ?,
1451
+ next_steps = ?,
1452
+ created_at_epoch = ?
1453
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
1454
+ return this.getSessionSummary(summary.session_id);
1455
+ }
1431
1456
  getSessionSummary(sessionId) {
1432
1457
  return this.db.query("SELECT * FROM session_summaries WHERE session_id = ?").get(sessionId) ?? null;
1433
1458
  }
@@ -1599,6 +1624,20 @@ async function main() {
1599
1624
  device_id: config.device_id,
1600
1625
  agent: "claude-code"
1601
1626
  });
1627
+ const compactPrompt = event.prompt.replace(/\s+/g, " ").trim();
1628
+ if (compactPrompt.length >= 8) {
1629
+ const summary = db.upsertSessionSummary({
1630
+ session_id: event.session_id,
1631
+ project_id: project.id,
1632
+ user_id: config.user_id,
1633
+ request: compactPrompt,
1634
+ investigated: null,
1635
+ learned: null,
1636
+ completed: null,
1637
+ next_steps: null
1638
+ });
1639
+ db.addToOutbox("summary", summary.id);
1640
+ }
1602
1641
  } finally {
1603
1642
  db.close();
1604
1643
  }
package/dist/server.js CHANGED
@@ -14838,6 +14838,31 @@ class MemDatabase {
14838
14838
  const id = Number(result.lastInsertRowid);
14839
14839
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
14840
14840
  }
14841
+ upsertSessionSummary(summary) {
14842
+ const existing = this.getSessionSummary(summary.session_id);
14843
+ if (!existing) {
14844
+ return this.insertSessionSummary(summary);
14845
+ }
14846
+ const now = Math.floor(Date.now() / 1000);
14847
+ const normalized = {
14848
+ request: normalizeSummaryRequest(summary.request ?? existing.request),
14849
+ investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
14850
+ learned: normalizeSummarySection(summary.learned ?? existing.learned),
14851
+ completed: normalizeSummarySection(summary.completed ?? existing.completed),
14852
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
14853
+ };
14854
+ this.db.query(`UPDATE session_summaries
14855
+ SET project_id = ?,
14856
+ user_id = ?,
14857
+ request = ?,
14858
+ investigated = ?,
14859
+ learned = ?,
14860
+ completed = ?,
14861
+ next_steps = ?,
14862
+ created_at_epoch = ?
14863
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
14864
+ return this.getSessionSummary(summary.session_id);
14865
+ }
14841
14866
  getSessionSummary(sessionId) {
14842
14867
  return this.db.query("SELECT * FROM session_summaries WHERE session_id = ?").get(sessionId) ?? null;
14843
14868
  }
@@ -18910,15 +18935,11 @@ function mergeChanges(db, config2, changes) {
18910
18935
  let skipped = 0;
18911
18936
  for (const change of changes) {
18912
18937
  const parsed = parseSourceId(change.source_id);
18938
+ const remoteSummary = isRemoteSummary(change);
18913
18939
  if (parsed && parsed.deviceId === config2.device_id) {
18914
18940
  skipped++;
18915
18941
  continue;
18916
18942
  }
18917
- const existing = db.db.query("SELECT id FROM observations WHERE remote_source_id = ?").get(change.source_id);
18918
- if (existing) {
18919
- skipped++;
18920
- continue;
18921
- }
18922
18943
  const projectCanonical = change.metadata?.project_canonical ?? null;
18923
18944
  if (!projectCanonical) {
18924
18945
  skipped++;
@@ -18931,6 +18952,18 @@ function mergeChanges(db, config2, changes) {
18931
18952
  name: change.metadata?.project_name ?? projectCanonical.split("/").pop() ?? "unknown"
18932
18953
  });
18933
18954
  }
18955
+ if (remoteSummary) {
18956
+ const mergedSummary = mergeRemoteSummary(db, config2, change, project.id);
18957
+ if (mergedSummary) {
18958
+ merged++;
18959
+ }
18960
+ }
18961
+ const existing = db.db.query("SELECT id FROM observations WHERE remote_source_id = ?").get(change.source_id);
18962
+ if (existing) {
18963
+ if (!remoteSummary)
18964
+ skipped++;
18965
+ continue;
18966
+ }
18934
18967
  const normalizedType = normalizeRemoteObservationType(change.metadata?.type, change.source_id);
18935
18968
  if (!normalizedType) {
18936
18969
  skipped++;
@@ -18962,6 +18995,26 @@ function mergeChanges(db, config2, changes) {
18962
18995
  }
18963
18996
  return { merged, skipped };
18964
18997
  }
18998
+ function isRemoteSummary(change) {
18999
+ const rawType = typeof change.metadata?.type === "string" ? change.metadata.type.toLowerCase() : "";
19000
+ return rawType === "summary" || change.source_id.includes("-summary-");
19001
+ }
19002
+ function mergeRemoteSummary(db, config2, change, projectId) {
19003
+ const sessionId = typeof change.metadata?.session_id === "string" ? change.metadata.session_id : null;
19004
+ if (!sessionId)
19005
+ return false;
19006
+ const summary = db.upsertSessionSummary({
19007
+ session_id: sessionId,
19008
+ project_id: projectId,
19009
+ user_id: (typeof change.metadata?.user_id === "string" ? change.metadata.user_id : null) ?? config2.user_id,
19010
+ request: typeof change.metadata?.request === "string" ? change.metadata.request : null,
19011
+ investigated: typeof change.metadata?.investigated === "string" ? change.metadata.investigated : null,
19012
+ learned: typeof change.metadata?.learned === "string" ? change.metadata.learned : null,
19013
+ completed: typeof change.metadata?.completed === "string" ? change.metadata.completed : null,
19014
+ next_steps: typeof change.metadata?.next_steps === "string" ? change.metadata.next_steps : null
19015
+ });
19016
+ return Boolean(summary);
19017
+ }
18965
19018
  function normalizeRemoteObservationType(rawType, sourceId) {
18966
19019
  const type = typeof rawType === "string" ? rawType.trim().toLowerCase() : "";
18967
19020
  if (type === "bugfix" || type === "discovery" || type === "decision" || type === "pattern" || type === "change" || type === "feature" || type === "refactor" || type === "digest" || type === "standard" || type === "message") {
@@ -19764,7 +19817,7 @@ process.on("SIGTERM", () => {
19764
19817
  });
19765
19818
  var server = new McpServer({
19766
19819
  name: "engrm",
19767
- version: "0.4.17"
19820
+ version: "0.4.19"
19768
19821
  });
19769
19822
  server.tool("save_observation", "Save an observation to memory", {
19770
19823
  type: exports_external.enum([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engrm",
3
- "version": "0.4.17",
3
+ "version": "0.4.19",
4
4
  "description": "Shared memory across devices, sessions, and coding agents",
5
5
  "mcpName": "io.github.dr12hes/engrm",
6
6
  "type": "module",