engrm 0.4.11 → 0.4.13

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.
@@ -395,6 +395,84 @@ function computeObservationPriority(obs, nowEpoch) {
395
395
  return computeBlendedScore(obs.quality, obs.created_at_epoch, nowEpoch) + observationTypeBoost(obs.type);
396
396
  }
397
397
 
398
+ // src/intelligence/summary-sections.ts
399
+ function extractSummaryItems(section, limit) {
400
+ if (!section || !section.trim())
401
+ return [];
402
+ const rawLines = section.split(`
403
+ `).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
404
+ const items = [];
405
+ const seen = new Set;
406
+ let heading = null;
407
+ for (const rawLine of rawLines) {
408
+ const line = stripSectionPrefix(rawLine);
409
+ if (!line)
410
+ continue;
411
+ const headingOnly = parseHeading(line);
412
+ if (headingOnly) {
413
+ heading = headingOnly;
414
+ continue;
415
+ }
416
+ const isBullet = /^[-*•]\s+/.test(line);
417
+ const stripped = line.replace(/^[-*•]\s+/, "").trim();
418
+ if (!stripped)
419
+ continue;
420
+ const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
421
+ const normalized = normalizeItem(item);
422
+ if (!normalized || seen.has(normalized))
423
+ continue;
424
+ seen.add(normalized);
425
+ items.push(item);
426
+ if (limit && items.length >= limit)
427
+ break;
428
+ }
429
+ return items;
430
+ }
431
+ function formatSummaryItems(section, maxLen) {
432
+ const items = extractSummaryItems(section);
433
+ if (items.length === 0)
434
+ return null;
435
+ const cleaned = items.map((item) => `- ${item}`).join(`
436
+ `);
437
+ if (cleaned.length <= maxLen)
438
+ return cleaned;
439
+ const truncated = cleaned.slice(0, maxLen).trimEnd();
440
+ const lastBreak = Math.max(truncated.lastIndexOf(`
441
+ `), truncated.lastIndexOf(" "));
442
+ const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
443
+ return `${safe.trimEnd()}…`;
444
+ }
445
+ function normalizeSummarySection(section) {
446
+ const items = extractSummaryItems(section);
447
+ if (items.length === 0) {
448
+ const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
449
+ return cleaned || null;
450
+ }
451
+ return items.map((item) => `- ${item}`).join(`
452
+ `);
453
+ }
454
+ function normalizeSummaryRequest(value) {
455
+ const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
456
+ return cleaned || null;
457
+ }
458
+ function stripSectionPrefix(value) {
459
+ return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
460
+ }
461
+ function parseHeading(value) {
462
+ const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
463
+ if (boldMatch?.[1]) {
464
+ return boldMatch[1].trim().replace(/\s+/g, " ");
465
+ }
466
+ const plainMatch = value.match(/^(.+?):$/);
467
+ if (plainMatch?.[1]) {
468
+ return plainMatch[1].trim().replace(/\s+/g, " ");
469
+ }
470
+ return null;
471
+ }
472
+ function normalizeItem(value) {
473
+ return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
474
+ }
475
+
398
476
  // src/context/inject.ts
399
477
  function tokenizeProjectHint(text) {
400
478
  return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
@@ -786,23 +864,7 @@ function chooseMeaningfulSessionHeadline(request, completed) {
786
864
  return request ?? completed ?? "(no summary)";
787
865
  }
788
866
  function formatSummarySection(value, maxLen) {
789
- if (!value)
790
- return null;
791
- const cleaned = value.split(`
792
- `).map((line) => line.trim()).filter(Boolean).map((line) => line.startsWith("-") ? line : `- ${line}`).join(`
793
- `);
794
- if (!cleaned)
795
- return null;
796
- return truncateMultilineText(cleaned, maxLen);
797
- }
798
- function truncateMultilineText(text, maxLen) {
799
- if (text.length <= maxLen)
800
- return text;
801
- const truncated = text.slice(0, maxLen).trimEnd();
802
- const lastBreak = Math.max(truncated.lastIndexOf(`
803
- `), truncated.lastIndexOf(" "));
804
- const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
805
- return `${safe.trimEnd()}…`;
867
+ return formatSummaryItems(value, maxLen);
806
868
  }
807
869
  function truncateText(text, maxLen) {
808
870
  if (text.length <= maxLen)
@@ -826,8 +888,7 @@ function stripInlineSectionLabel(value) {
826
888
  function extractMeaningfulLines(value, limit) {
827
889
  if (!value)
828
890
  return [];
829
- return value.split(`
830
- `).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle(line)).slice(0, limit);
891
+ return extractSummaryItems(value).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle(line)).slice(0, limit);
831
892
  }
832
893
  function formatObservationDetailFromContext(obs) {
833
894
  if (obs.facts) {
@@ -1093,7 +1154,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
1093
1154
  import { join as join3 } from "node:path";
1094
1155
  import { homedir } from "node:os";
1095
1156
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
1096
- var CLIENT_VERSION = "0.4.0";
1157
+ var CLIENT_VERSION = "0.4.13";
1097
1158
  function hashFile(filePath) {
1098
1159
  try {
1099
1160
  if (!existsSync3(filePath))
@@ -2156,6 +2217,18 @@ var MIGRATIONS = [
2156
2217
  CREATE INDEX IF NOT EXISTS idx_tool_events_created
2157
2218
  ON tool_events(created_at_epoch DESC, id DESC);
2158
2219
  `
2220
+ },
2221
+ {
2222
+ version: 11,
2223
+ description: "Add observation provenance from tool and prompt chronology",
2224
+ sql: `
2225
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
2226
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
2227
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
2228
+ ON observations(source_tool, created_at_epoch DESC);
2229
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
2230
+ ON observations(session_id, source_prompt_number DESC);
2231
+ `
2159
2232
  }
2160
2233
  ];
2161
2234
  function isVecExtensionLoaded(db) {
@@ -2209,6 +2282,8 @@ function inferLegacySchemaVersion(db) {
2209
2282
  version = Math.max(version, 9);
2210
2283
  if (tableExists(db, "tool_events"))
2211
2284
  version = Math.max(version, 10);
2285
+ if (columnExists(db, "observations", "source_tool"))
2286
+ version = Math.max(version, 11);
2212
2287
  return version;
2213
2288
  }
2214
2289
  function runMigrations(db) {
@@ -2327,6 +2402,7 @@ function openNodeDatabase(dbPath) {
2327
2402
  const BetterSqlite3 = __require("better-sqlite3");
2328
2403
  const raw = new BetterSqlite3(dbPath);
2329
2404
  return {
2405
+ __raw: raw,
2330
2406
  query(sql) {
2331
2407
  const stmt = raw.prepare(sql);
2332
2408
  return {
@@ -2364,7 +2440,7 @@ class MemDatabase {
2364
2440
  loadVecExtension() {
2365
2441
  try {
2366
2442
  const sqliteVec = __require("sqlite-vec");
2367
- sqliteVec.load(this.db);
2443
+ sqliteVec.load(this.db.__raw ?? this.db);
2368
2444
  return true;
2369
2445
  } catch {
2370
2446
  return false;
@@ -2405,8 +2481,9 @@ class MemDatabase {
2405
2481
  const result = this.db.query(`INSERT INTO observations (
2406
2482
  session_id, project_id, type, title, narrative, facts, concepts,
2407
2483
  files_read, files_modified, quality, lifecycle, sensitivity,
2408
- user_id, device_id, agent, created_at, created_at_epoch
2409
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(obs.session_id ?? null, obs.project_id, obs.type, obs.title, obs.narrative ?? null, obs.facts ?? null, obs.concepts ?? null, obs.files_read ?? null, obs.files_modified ?? null, obs.quality, obs.lifecycle ?? "active", obs.sensitivity ?? "shared", obs.user_id, obs.device_id, obs.agent ?? "claude-code", createdAt, now);
2484
+ user_id, device_id, agent, source_tool, source_prompt_number,
2485
+ created_at, created_at_epoch
2486
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(obs.session_id ?? null, obs.project_id, obs.type, obs.title, obs.narrative ?? null, obs.facts ?? null, obs.concepts ?? null, obs.files_read ?? null, obs.files_modified ?? null, obs.quality, obs.lifecycle ?? "active", obs.sensitivity ?? "shared", obs.user_id, obs.device_id, obs.agent ?? "claude-code", obs.source_tool ?? null, obs.source_prompt_number ?? null, createdAt, now);
2410
2487
  const id = Number(result.lastInsertRowid);
2411
2488
  const row = this.getObservationById(id);
2412
2489
  this.ftsInsert(row);
@@ -2647,6 +2724,13 @@ class MemDatabase {
2647
2724
  ORDER BY prompt_number ASC
2648
2725
  LIMIT ?`).all(sessionId, limit);
2649
2726
  }
2727
+ getLatestSessionPromptNumber(sessionId) {
2728
+ const row = this.db.query(`SELECT prompt_number FROM user_prompts
2729
+ WHERE session_id = ?
2730
+ ORDER BY prompt_number DESC
2731
+ LIMIT 1`).get(sessionId);
2732
+ return row?.prompt_number ?? null;
2733
+ }
2650
2734
  insertToolEvent(input) {
2651
2735
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
2652
2736
  const result = this.db.query(`INSERT INTO tool_events (
@@ -2756,8 +2840,15 @@ class MemDatabase {
2756
2840
  }
2757
2841
  insertSessionSummary(summary) {
2758
2842
  const now = Math.floor(Date.now() / 1000);
2843
+ const normalized = {
2844
+ request: normalizeSummaryRequest(summary.request),
2845
+ investigated: normalizeSummarySection(summary.investigated),
2846
+ learned: normalizeSummarySection(summary.learned),
2847
+ completed: normalizeSummarySection(summary.completed),
2848
+ next_steps: normalizeSummarySection(summary.next_steps)
2849
+ };
2759
2850
  const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
2760
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
2851
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
2761
2852
  const id = Number(result.lastInsertRowid);
2762
2853
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
2763
2854
  }
@@ -2963,7 +3054,8 @@ async function main() {
2963
3054
  securityFindings: context.securityFindings?.length ?? 0,
2964
3055
  unreadMessages: msgCount,
2965
3056
  synced: syncedCount,
2966
- context
3057
+ context,
3058
+ estimatedReadTokens: estimateTokens(formatContextForInjection(context))
2967
3059
  });
2968
3060
  let packLine = "";
2969
3061
  try {
@@ -3048,6 +3140,20 @@ function formatSplashScreen(data) {
3048
3140
  lines.push(` ${line}`);
3049
3141
  }
3050
3142
  }
3143
+ const economics = formatContextEconomics(data);
3144
+ if (economics.length > 0) {
3145
+ lines.push("");
3146
+ for (const line of economics) {
3147
+ lines.push(` ${line}`);
3148
+ }
3149
+ }
3150
+ const inspectHints = formatInspectHints(data.context);
3151
+ if (inspectHints.length > 0) {
3152
+ lines.push("");
3153
+ for (const line of inspectHints) {
3154
+ lines.push(` ${line}`);
3155
+ }
3156
+ }
3051
3157
  lines.push("");
3052
3158
  return lines.join(`
3053
3159
  `);
@@ -3156,6 +3262,36 @@ function formatVisibleStartupBrief(context) {
3156
3262
  }
3157
3263
  return lines.slice(0, 14);
3158
3264
  }
3265
+ function formatContextEconomics(data) {
3266
+ const totalMemories = Math.max(0, data.loaded + data.available);
3267
+ const parts = [];
3268
+ if (totalMemories > 0) {
3269
+ parts.push(`${totalMemories.toLocaleString()} total memories`);
3270
+ }
3271
+ if (data.estimatedReadTokens > 0) {
3272
+ parts.push(`read now ~${data.estimatedReadTokens.toLocaleString()}t`);
3273
+ }
3274
+ if (parts.length === 0)
3275
+ return [];
3276
+ return [`${c2.dim}Context economics:${c2.reset} ${parts.join(" \xB7 ")}`];
3277
+ }
3278
+ function formatInspectHints(context) {
3279
+ const hints = [];
3280
+ if ((context.recentSessions?.length ?? 0) > 0) {
3281
+ hints.push("recent_sessions");
3282
+ hints.push("session_story");
3283
+ }
3284
+ if ((context.recentPrompts?.length ?? 0) > 0 || (context.recentToolEvents?.length ?? 0) > 0) {
3285
+ hints.push("activity_feed");
3286
+ }
3287
+ if (context.observations.length > 0) {
3288
+ hints.push("memory_console");
3289
+ }
3290
+ const unique = Array.from(new Set(hints)).slice(0, 4);
3291
+ if (unique.length === 0)
3292
+ return [];
3293
+ return [`${c2.dim}Inspect:${c2.reset} ${unique.join(" \xB7 ")}`];
3294
+ }
3159
3295
  function rememberShownItem(shown, value) {
3160
3296
  if (!value)
3161
3297
  return;
@@ -3501,6 +3637,7 @@ function capitalize(value) {
3501
3637
  return value.charAt(0).toUpperCase() + value.slice(1);
3502
3638
  }
3503
3639
  var __testables = {
3640
+ formatSplashScreen,
3504
3641
  formatVisibleStartupBrief
3505
3642
  };
3506
3643
  runHook("session-start", main);
@@ -804,6 +804,18 @@ var MIGRATIONS = [
804
804
  CREATE INDEX IF NOT EXISTS idx_tool_events_created
805
805
  ON tool_events(created_at_epoch DESC, id DESC);
806
806
  `
807
+ },
808
+ {
809
+ version: 11,
810
+ description: "Add observation provenance from tool and prompt chronology",
811
+ sql: `
812
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
813
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
814
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
815
+ ON observations(source_tool, created_at_epoch DESC);
816
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
817
+ ON observations(session_id, source_prompt_number DESC);
818
+ `
807
819
  }
808
820
  ];
809
821
  function isVecExtensionLoaded(db) {
@@ -857,6 +869,8 @@ function inferLegacySchemaVersion(db) {
857
869
  version = Math.max(version, 9);
858
870
  if (tableExists(db, "tool_events"))
859
871
  version = Math.max(version, 10);
872
+ if (columnExists(db, "observations", "source_tool"))
873
+ version = Math.max(version, 11);
860
874
  return version;
861
875
  }
862
876
  function runMigrations(db) {
@@ -939,6 +953,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
939
953
 
940
954
  // src/storage/sqlite.ts
941
955
  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
942
1036
  var IS_BUN = typeof globalThis.Bun !== "undefined";
943
1037
  function openDatabase(dbPath) {
944
1038
  if (IS_BUN) {
@@ -975,6 +1069,7 @@ function openNodeDatabase(dbPath) {
975
1069
  const BetterSqlite3 = __require("better-sqlite3");
976
1070
  const raw = new BetterSqlite3(dbPath);
977
1071
  return {
1072
+ __raw: raw,
978
1073
  query(sql) {
979
1074
  const stmt = raw.prepare(sql);
980
1075
  return {
@@ -1012,7 +1107,7 @@ class MemDatabase {
1012
1107
  loadVecExtension() {
1013
1108
  try {
1014
1109
  const sqliteVec = __require("sqlite-vec");
1015
- sqliteVec.load(this.db);
1110
+ sqliteVec.load(this.db.__raw ?? this.db);
1016
1111
  return true;
1017
1112
  } catch {
1018
1113
  return false;
@@ -1053,8 +1148,9 @@ class MemDatabase {
1053
1148
  const result = this.db.query(`INSERT INTO observations (
1054
1149
  session_id, project_id, type, title, narrative, facts, concepts,
1055
1150
  files_read, files_modified, quality, lifecycle, sensitivity,
1056
- user_id, device_id, agent, created_at, created_at_epoch
1057
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(obs.session_id ?? null, obs.project_id, obs.type, obs.title, obs.narrative ?? null, obs.facts ?? null, obs.concepts ?? null, obs.files_read ?? null, obs.files_modified ?? null, obs.quality, obs.lifecycle ?? "active", obs.sensitivity ?? "shared", obs.user_id, obs.device_id, obs.agent ?? "claude-code", createdAt, now);
1151
+ user_id, device_id, agent, source_tool, source_prompt_number,
1152
+ created_at, created_at_epoch
1153
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(obs.session_id ?? null, obs.project_id, obs.type, obs.title, obs.narrative ?? null, obs.facts ?? null, obs.concepts ?? null, obs.files_read ?? null, obs.files_modified ?? null, obs.quality, obs.lifecycle ?? "active", obs.sensitivity ?? "shared", obs.user_id, obs.device_id, obs.agent ?? "claude-code", obs.source_tool ?? null, obs.source_prompt_number ?? null, createdAt, now);
1058
1154
  const id = Number(result.lastInsertRowid);
1059
1155
  const row = this.getObservationById(id);
1060
1156
  this.ftsInsert(row);
@@ -1295,6 +1391,13 @@ class MemDatabase {
1295
1391
  ORDER BY prompt_number ASC
1296
1392
  LIMIT ?`).all(sessionId, limit);
1297
1393
  }
1394
+ getLatestSessionPromptNumber(sessionId) {
1395
+ const row = this.db.query(`SELECT prompt_number FROM user_prompts
1396
+ WHERE session_id = ?
1397
+ ORDER BY prompt_number DESC
1398
+ LIMIT 1`).get(sessionId);
1399
+ return row?.prompt_number ?? null;
1400
+ }
1298
1401
  insertToolEvent(input) {
1299
1402
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1300
1403
  const result = this.db.query(`INSERT INTO tool_events (
@@ -1404,8 +1507,15 @@ class MemDatabase {
1404
1507
  }
1405
1508
  insertSessionSummary(summary) {
1406
1509
  const now = Math.floor(Date.now() / 1000);
1510
+ const normalized = {
1511
+ request: normalizeSummaryRequest(summary.request),
1512
+ investigated: normalizeSummarySection(summary.investigated),
1513
+ learned: normalizeSummarySection(summary.learned),
1514
+ completed: normalizeSummarySection(summary.completed),
1515
+ next_steps: normalizeSummarySection(summary.next_steps)
1516
+ };
1407
1517
  const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
1408
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
1518
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1409
1519
  const id = Number(result.lastInsertRowid);
1410
1520
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1411
1521
  }
@@ -1879,6 +1989,8 @@ function buildVectorDocument(obs, config, project) {
1879
1989
  concepts: obs.concepts ? JSON.parse(obs.concepts) : [],
1880
1990
  files_read: obs.files_read ? JSON.parse(obs.files_read) : [],
1881
1991
  files_modified: obs.files_modified ? JSON.parse(obs.files_modified) : [],
1992
+ source_tool: obs.source_tool,
1993
+ source_prompt_number: obs.source_prompt_number,
1882
1994
  session_id: obs.session_id,
1883
1995
  created_at_epoch: obs.created_at_epoch,
1884
1996
  created_at: obs.created_at,
@@ -1932,6 +2044,8 @@ function buildSummaryVectorDocument(summary, config, project, observations = [],
1932
2044
  recent_tool_commands: captureContext?.recent_tool_commands ?? [],
1933
2045
  hot_files: captureContext?.hot_files ?? [],
1934
2046
  recent_outcomes: captureContext?.recent_outcomes ?? [],
2047
+ observation_source_tools: captureContext?.observation_source_tools ?? [],
2048
+ latest_observation_prompt_number: captureContext?.latest_observation_prompt_number ?? null,
1935
2049
  decisions_count: valueSignals.decisions_count,
1936
2050
  lessons_count: valueSignals.lessons_count,
1937
2051
  discoveries_count: valueSignals.discoveries_count,
@@ -2044,10 +2158,7 @@ function countPresentSections(summary) {
2044
2158
  ].filter((value) => Boolean(value && value.trim())).length;
2045
2159
  }
2046
2160
  function extractSectionItems(section) {
2047
- if (!section)
2048
- return [];
2049
- return section.split(`
2050
- `).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).slice(0, 4);
2161
+ return extractSummaryItems(section, 4);
2051
2162
  }
2052
2163
  function buildSummaryCaptureContext(prompts, toolEvents, observations) {
2053
2164
  const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
@@ -2060,6 +2171,13 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
2060
2171
  ]).filter(Boolean))].slice(0, 6);
2061
2172
  const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0).slice(0, 6);
2062
2173
  const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
2174
+ const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
2175
+ if (!obs.source_tool)
2176
+ return acc;
2177
+ acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
2178
+ return acc;
2179
+ }, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
2180
+ const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
2063
2181
  return {
2064
2182
  prompt_count: prompts.length,
2065
2183
  tool_event_count: toolEvents.length,
@@ -2069,7 +2187,9 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
2069
2187
  recent_tool_commands: recentToolCommands,
2070
2188
  capture_state: captureState,
2071
2189
  hot_files: hotFiles,
2072
- recent_outcomes: recentOutcomes
2190
+ recent_outcomes: recentOutcomes,
2191
+ observation_source_tools: observationSourceTools,
2192
+ latest_observation_prompt_number: latestObservationPromptNumber
2073
2193
  };
2074
2194
  }
2075
2195
  function parseJsonArray2(value) {
@@ -2297,10 +2417,7 @@ function countPresentSections2(summary) {
2297
2417
  ].filter(hasContent).length;
2298
2418
  }
2299
2419
  function extractSectionItems2(section) {
2300
- if (!hasContent(section))
2301
- return [];
2302
- return section.split(`
2303
- `).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
2420
+ return extractSummaryItems(section);
2304
2421
  }
2305
2422
  function extractObservationTitles(observations, types) {
2306
2423
  const typeSet = new Set(types);
@@ -2418,7 +2535,7 @@ function buildBeacon(db, config, sessionId, metrics) {
2418
2535
  sentinel_used: valueSignals.security_findings_count > 0,
2419
2536
  risk_score: riskScore,
2420
2537
  stacks_detected: stacks,
2421
- client_version: "0.4.0",
2538
+ client_version: "0.4.13",
2422
2539
  context_observations_injected: metrics?.contextObsInjected ?? 0,
2423
2540
  context_total_available: metrics?.contextTotalAvailable ?? 0,
2424
2541
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -3202,6 +3319,7 @@ async function saveObservation(db, config, input) {
3202
3319
  reason: `Merged into existing observation #${duplicate.id}`
3203
3320
  };
3204
3321
  }
3322
+ const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
3205
3323
  const obs = db.insertObservation({
3206
3324
  session_id: input.session_id ?? null,
3207
3325
  project_id: project.id,
@@ -3217,7 +3335,9 @@ async function saveObservation(db, config, input) {
3217
3335
  sensitivity,
3218
3336
  user_id: config.user_id,
3219
3337
  device_id: config.device_id,
3220
- agent: input.agent ?? "claude-code"
3338
+ agent: input.agent ?? "claude-code",
3339
+ source_tool: input.source_tool ?? null,
3340
+ source_prompt_number: sourcePromptNumber
3221
3341
  });
3222
3342
  db.addToOutbox("observation", obs.id);
3223
3343
  if (db.vecAvailable) {
@@ -3469,6 +3589,11 @@ async function main() {
3469
3589
  }
3470
3590
  }
3471
3591
  if (event.last_assistant_message) {
3592
+ if (event.session_id) {
3593
+ try {
3594
+ createAssistantCheckpoint(db, event.session_id, event.cwd, event.last_assistant_message);
3595
+ } catch {}
3596
+ }
3472
3597
  const unsaved = detectUnsavedPlans(event.last_assistant_message);
3473
3598
  if (unsaved.length > 0) {
3474
3599
  console.error("");
@@ -3608,6 +3733,71 @@ ${sections.join(`
3608
3733
  });
3609
3734
  db.addToOutbox("observation", digestObs.id);
3610
3735
  }
3736
+ function createAssistantCheckpoint(db, sessionId, cwd, message) {
3737
+ const checkpoint = extractAssistantCheckpoint(message);
3738
+ if (!checkpoint)
3739
+ return;
3740
+ const existing = db.getObservationsBySession(sessionId).find((obs) => obs.source_tool === "assistant-stop" && obs.title === checkpoint.title);
3741
+ if (existing)
3742
+ return;
3743
+ const detected = detectProject(cwd);
3744
+ const project = db.getProjectByCanonicalId(detected.canonical_id);
3745
+ if (!project)
3746
+ return;
3747
+ const promptNumber = db.getLatestSessionPromptNumber(sessionId);
3748
+ const row = db.insertObservation({
3749
+ session_id: sessionId,
3750
+ project_id: project.id,
3751
+ type: checkpoint.type,
3752
+ title: checkpoint.title,
3753
+ narrative: checkpoint.narrative,
3754
+ facts: checkpoint.facts.length > 0 ? JSON.stringify(checkpoint.facts.slice(0, 8)) : null,
3755
+ quality: checkpoint.quality,
3756
+ lifecycle: "active",
3757
+ sensitivity: "shared",
3758
+ user_id: db.getSessionById(sessionId)?.user_id ?? "unknown",
3759
+ device_id: db.getSessionById(sessionId)?.device_id ?? "unknown",
3760
+ agent: db.getSessionById(sessionId)?.agent ?? "claude-code",
3761
+ source_tool: "assistant-stop",
3762
+ source_prompt_number: promptNumber
3763
+ });
3764
+ db.addToOutbox("observation", row.id);
3765
+ }
3766
+ function extractAssistantCheckpoint(message) {
3767
+ const compact = message.replace(/\r/g, "").trim();
3768
+ if (compact.length < 180)
3769
+ return null;
3770
+ const normalizedLines = compact.split(`
3771
+ `).map((line) => line.trim()).filter(Boolean);
3772
+ const bulletLines = compact.split(`
3773
+ `).map((line) => line.trim()).filter(Boolean).filter((line) => /^[-*]\s+/.test(line)).map((line) => line.replace(/^[-*]\s+/, "").trim()).filter((line) => line.length > 20).slice(0, 8);
3774
+ const substantiveLines = compact.split(`
3775
+ `).map((line) => line.trim()).filter(Boolean).filter((line) => !/^#+\s*/.test(line)).filter((line) => !/^[-*]\s*$/.test(line));
3776
+ const title = pickAssistantCheckpointTitle(substantiveLines, bulletLines);
3777
+ if (!title)
3778
+ return null;
3779
+ const lowered = compact.toLowerCase();
3780
+ const headingText = normalizedLines.filter((line) => /^[A-Za-z][A-Za-z /_-]{2,}:$/.test(line)).join(" ").toLowerCase();
3781
+ const hasNextSteps = normalizedLines.some((line) => /^Next Steps?:/i.test(line));
3782
+ const deploymentSignals = /\bdeploy|deployment|ansible|rolled out|released to staging|pushed commit|shipped to staging|launched\b/.test(lowered) || /\bdeployment\b/.test(headingText);
3783
+ const decisionSignals = /\bdecid|recommend|strategy|pricing|trade.?off|agreed|approach|direction\b/.test(lowered) || /\bdecision\b/.test(headingText);
3784
+ const featureSignals = /\bimplemented|introduced|exposed|added|built|created|enabled|wired\b/.test(lowered) || /\bfeature\b/.test(headingText);
3785
+ const type = decisionSignals && !deploymentSignals ? "decision" : deploymentSignals || featureSignals ? "feature" : hasNextSteps ? "decision" : "change";
3786
+ const facts = bulletLines.filter((line) => line !== title);
3787
+ const narrative = substantiveLines.slice(0, 6).join(`
3788
+ `);
3789
+ return {
3790
+ type,
3791
+ title,
3792
+ narrative,
3793
+ facts,
3794
+ quality: 0.72
3795
+ };
3796
+ }
3797
+ function pickAssistantCheckpointTitle(substantiveLines, bulletLines) {
3798
+ 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));
3799
+ return candidates[0] ?? null;
3800
+ }
3611
3801
  function detectUnsavedPlans(message) {
3612
3802
  const hints = [];
3613
3803
  const lower = message.toLowerCase();
@@ -3677,4 +3867,10 @@ function readSessionMetrics(sessionId) {
3677
3867
  } catch {}
3678
3868
  return result;
3679
3869
  }
3870
+ var __testables = {
3871
+ extractAssistantCheckpoint
3872
+ };
3680
3873
  runHook("stop", main);
3874
+ export {
3875
+ __testables
3876
+ };