engrm 0.4.12 → 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.
@@ -569,6 +569,18 @@ var MIGRATIONS = [
569
569
  CREATE INDEX IF NOT EXISTS idx_tool_events_created
570
570
  ON tool_events(created_at_epoch DESC, id DESC);
571
571
  `
572
+ },
573
+ {
574
+ version: 11,
575
+ description: "Add observation provenance from tool and prompt chronology",
576
+ sql: `
577
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
578
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
579
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
580
+ ON observations(source_tool, created_at_epoch DESC);
581
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
582
+ ON observations(session_id, source_prompt_number DESC);
583
+ `
572
584
  }
573
585
  ];
574
586
  function isVecExtensionLoaded(db) {
@@ -622,6 +634,8 @@ function inferLegacySchemaVersion(db) {
622
634
  version = Math.max(version, 9);
623
635
  if (tableExists(db, "tool_events"))
624
636
  version = Math.max(version, 10);
637
+ if (columnExists(db, "observations", "source_tool"))
638
+ version = Math.max(version, 11);
625
639
  return version;
626
640
  }
627
641
  function runMigrations(db) {
@@ -704,6 +718,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
704
718
 
705
719
  // src/storage/sqlite.ts
706
720
  import { createHash as createHash2 } from "node:crypto";
721
+
722
+ // src/intelligence/summary-sections.ts
723
+ function extractSummaryItems(section, limit) {
724
+ if (!section || !section.trim())
725
+ return [];
726
+ const rawLines = section.split(`
727
+ `).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
728
+ const items = [];
729
+ const seen = new Set;
730
+ let heading = null;
731
+ for (const rawLine of rawLines) {
732
+ const line = stripSectionPrefix(rawLine);
733
+ if (!line)
734
+ continue;
735
+ const headingOnly = parseHeading(line);
736
+ if (headingOnly) {
737
+ heading = headingOnly;
738
+ continue;
739
+ }
740
+ const isBullet = /^[-*•]\s+/.test(line);
741
+ const stripped = line.replace(/^[-*•]\s+/, "").trim();
742
+ if (!stripped)
743
+ continue;
744
+ const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
745
+ const normalized = normalizeItem(item);
746
+ if (!normalized || seen.has(normalized))
747
+ continue;
748
+ seen.add(normalized);
749
+ items.push(item);
750
+ if (limit && items.length >= limit)
751
+ break;
752
+ }
753
+ return items;
754
+ }
755
+ function formatSummaryItems(section, maxLen) {
756
+ const items = extractSummaryItems(section);
757
+ if (items.length === 0)
758
+ return null;
759
+ const cleaned = items.map((item) => `- ${item}`).join(`
760
+ `);
761
+ if (cleaned.length <= maxLen)
762
+ return cleaned;
763
+ const truncated = cleaned.slice(0, maxLen).trimEnd();
764
+ const lastBreak = Math.max(truncated.lastIndexOf(`
765
+ `), truncated.lastIndexOf(" "));
766
+ const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
767
+ return `${safe.trimEnd()}…`;
768
+ }
769
+ function normalizeSummarySection(section) {
770
+ const items = extractSummaryItems(section);
771
+ if (items.length === 0) {
772
+ const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
773
+ return cleaned || null;
774
+ }
775
+ return items.map((item) => `- ${item}`).join(`
776
+ `);
777
+ }
778
+ function normalizeSummaryRequest(value) {
779
+ const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
780
+ return cleaned || null;
781
+ }
782
+ function stripSectionPrefix(value) {
783
+ return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
784
+ }
785
+ function parseHeading(value) {
786
+ const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
787
+ if (boldMatch?.[1]) {
788
+ return boldMatch[1].trim().replace(/\s+/g, " ");
789
+ }
790
+ const plainMatch = value.match(/^(.+?):$/);
791
+ if (plainMatch?.[1]) {
792
+ return plainMatch[1].trim().replace(/\s+/g, " ");
793
+ }
794
+ return null;
795
+ }
796
+ function normalizeItem(value) {
797
+ return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
798
+ }
799
+
800
+ // src/storage/sqlite.ts
707
801
  var IS_BUN = typeof globalThis.Bun !== "undefined";
708
802
  function openDatabase(dbPath) {
709
803
  if (IS_BUN) {
@@ -819,8 +913,9 @@ class MemDatabase {
819
913
  const result = this.db.query(`INSERT INTO observations (
820
914
  session_id, project_id, type, title, narrative, facts, concepts,
821
915
  files_read, files_modified, quality, lifecycle, sensitivity,
822
- user_id, device_id, agent, created_at, created_at_epoch
823
- ) 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);
916
+ user_id, device_id, agent, source_tool, source_prompt_number,
917
+ created_at, created_at_epoch
918
+ ) 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);
824
919
  const id = Number(result.lastInsertRowid);
825
920
  const row = this.getObservationById(id);
826
921
  this.ftsInsert(row);
@@ -1061,6 +1156,13 @@ class MemDatabase {
1061
1156
  ORDER BY prompt_number ASC
1062
1157
  LIMIT ?`).all(sessionId, limit);
1063
1158
  }
1159
+ getLatestSessionPromptNumber(sessionId) {
1160
+ const row = this.db.query(`SELECT prompt_number FROM user_prompts
1161
+ WHERE session_id = ?
1162
+ ORDER BY prompt_number DESC
1163
+ LIMIT 1`).get(sessionId);
1164
+ return row?.prompt_number ?? null;
1165
+ }
1064
1166
  insertToolEvent(input) {
1065
1167
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1066
1168
  const result = this.db.query(`INSERT INTO tool_events (
@@ -1170,8 +1272,15 @@ class MemDatabase {
1170
1272
  }
1171
1273
  insertSessionSummary(summary) {
1172
1274
  const now = Math.floor(Date.now() / 1000);
1275
+ const normalized = {
1276
+ request: normalizeSummaryRequest(summary.request),
1277
+ investigated: normalizeSummarySection(summary.investigated),
1278
+ learned: normalizeSummarySection(summary.learned),
1279
+ completed: normalizeSummarySection(summary.completed),
1280
+ next_steps: normalizeSummarySection(summary.next_steps)
1281
+ };
1173
1282
  const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
1174
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
1283
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1175
1284
  const id = Number(result.lastInsertRowid);
1176
1285
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1177
1286
  }
@@ -2043,23 +2152,7 @@ function chooseMeaningfulSessionHeadline(request, completed) {
2043
2152
  return request ?? completed ?? "(no summary)";
2044
2153
  }
2045
2154
  function formatSummarySection(value, maxLen) {
2046
- if (!value)
2047
- return null;
2048
- const cleaned = value.split(`
2049
- `).map((line) => line.trim()).filter(Boolean).map((line) => line.startsWith("-") ? line : `- ${line}`).join(`
2050
- `);
2051
- if (!cleaned)
2052
- return null;
2053
- return truncateMultilineText(cleaned, maxLen);
2054
- }
2055
- function truncateMultilineText(text, maxLen) {
2056
- if (text.length <= maxLen)
2057
- return text;
2058
- const truncated = text.slice(0, maxLen).trimEnd();
2059
- const lastBreak = Math.max(truncated.lastIndexOf(`
2060
- `), truncated.lastIndexOf(" "));
2061
- const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
2062
- return `${safe.trimEnd()}…`;
2155
+ return formatSummaryItems(value, maxLen);
2063
2156
  }
2064
2157
  function truncateText(text, maxLen) {
2065
2158
  if (text.length <= maxLen)
@@ -2083,8 +2176,7 @@ function stripInlineSectionLabel(value) {
2083
2176
  function extractMeaningfulLines(value, limit) {
2084
2177
  if (!value)
2085
2178
  return [];
2086
- return value.split(`
2087
- `).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);
2179
+ return extractSummaryItems(value).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle(line)).slice(0, limit);
2088
2180
  }
2089
2181
  function formatObservationDetailFromContext(obs) {
2090
2182
  if (obs.facts) {
@@ -645,6 +645,18 @@ var MIGRATIONS = [
645
645
  CREATE INDEX IF NOT EXISTS idx_tool_events_created
646
646
  ON tool_events(created_at_epoch DESC, id DESC);
647
647
  `
648
+ },
649
+ {
650
+ version: 11,
651
+ description: "Add observation provenance from tool and prompt chronology",
652
+ sql: `
653
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
654
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
655
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
656
+ ON observations(source_tool, created_at_epoch DESC);
657
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
658
+ ON observations(session_id, source_prompt_number DESC);
659
+ `
648
660
  }
649
661
  ];
650
662
  function isVecExtensionLoaded(db) {
@@ -698,6 +710,8 @@ function inferLegacySchemaVersion(db) {
698
710
  version = Math.max(version, 9);
699
711
  if (tableExists(db, "tool_events"))
700
712
  version = Math.max(version, 10);
713
+ if (columnExists(db, "observations", "source_tool"))
714
+ version = Math.max(version, 11);
701
715
  return version;
702
716
  }
703
717
  function runMigrations(db) {
@@ -780,6 +794,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
780
794
 
781
795
  // src/storage/sqlite.ts
782
796
  import { createHash as createHash2 } from "node:crypto";
797
+
798
+ // src/intelligence/summary-sections.ts
799
+ function extractSummaryItems(section, limit) {
800
+ if (!section || !section.trim())
801
+ return [];
802
+ const rawLines = section.split(`
803
+ `).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
804
+ const items = [];
805
+ const seen = new Set;
806
+ let heading = null;
807
+ for (const rawLine of rawLines) {
808
+ const line = stripSectionPrefix(rawLine);
809
+ if (!line)
810
+ continue;
811
+ const headingOnly = parseHeading(line);
812
+ if (headingOnly) {
813
+ heading = headingOnly;
814
+ continue;
815
+ }
816
+ const isBullet = /^[-*•]\s+/.test(line);
817
+ const stripped = line.replace(/^[-*•]\s+/, "").trim();
818
+ if (!stripped)
819
+ continue;
820
+ const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
821
+ const normalized = normalizeItem(item);
822
+ if (!normalized || seen.has(normalized))
823
+ continue;
824
+ seen.add(normalized);
825
+ items.push(item);
826
+ if (limit && items.length >= limit)
827
+ break;
828
+ }
829
+ return items;
830
+ }
831
+ function formatSummaryItems(section, maxLen) {
832
+ const items = extractSummaryItems(section);
833
+ if (items.length === 0)
834
+ return null;
835
+ const cleaned = items.map((item) => `- ${item}`).join(`
836
+ `);
837
+ if (cleaned.length <= maxLen)
838
+ return cleaned;
839
+ const truncated = cleaned.slice(0, maxLen).trimEnd();
840
+ const lastBreak = Math.max(truncated.lastIndexOf(`
841
+ `), truncated.lastIndexOf(" "));
842
+ const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
843
+ return `${safe.trimEnd()}…`;
844
+ }
845
+ function normalizeSummarySection(section) {
846
+ const items = extractSummaryItems(section);
847
+ if (items.length === 0) {
848
+ const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
849
+ return cleaned || null;
850
+ }
851
+ return items.map((item) => `- ${item}`).join(`
852
+ `);
853
+ }
854
+ function normalizeSummaryRequest(value) {
855
+ const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
856
+ return cleaned || null;
857
+ }
858
+ function stripSectionPrefix(value) {
859
+ return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
860
+ }
861
+ function parseHeading(value) {
862
+ const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
863
+ if (boldMatch?.[1]) {
864
+ return boldMatch[1].trim().replace(/\s+/g, " ");
865
+ }
866
+ const plainMatch = value.match(/^(.+?):$/);
867
+ if (plainMatch?.[1]) {
868
+ return plainMatch[1].trim().replace(/\s+/g, " ");
869
+ }
870
+ return null;
871
+ }
872
+ function normalizeItem(value) {
873
+ return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
874
+ }
875
+
876
+ // src/storage/sqlite.ts
783
877
  var IS_BUN = typeof globalThis.Bun !== "undefined";
784
878
  function openDatabase(dbPath) {
785
879
  if (IS_BUN) {
@@ -895,8 +989,9 @@ class MemDatabase {
895
989
  const result = this.db.query(`INSERT INTO observations (
896
990
  session_id, project_id, type, title, narrative, facts, concepts,
897
991
  files_read, files_modified, quality, lifecycle, sensitivity,
898
- user_id, device_id, agent, created_at, created_at_epoch
899
- ) 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);
992
+ user_id, device_id, agent, source_tool, source_prompt_number,
993
+ created_at, created_at_epoch
994
+ ) 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);
900
995
  const id = Number(result.lastInsertRowid);
901
996
  const row = this.getObservationById(id);
902
997
  this.ftsInsert(row);
@@ -1137,6 +1232,13 @@ class MemDatabase {
1137
1232
  ORDER BY prompt_number ASC
1138
1233
  LIMIT ?`).all(sessionId, limit);
1139
1234
  }
1235
+ getLatestSessionPromptNumber(sessionId) {
1236
+ const row = this.db.query(`SELECT prompt_number FROM user_prompts
1237
+ WHERE session_id = ?
1238
+ ORDER BY prompt_number DESC
1239
+ LIMIT 1`).get(sessionId);
1240
+ return row?.prompt_number ?? null;
1241
+ }
1140
1242
  insertToolEvent(input) {
1141
1243
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1142
1244
  const result = this.db.query(`INSERT INTO tool_events (
@@ -1246,8 +1348,15 @@ class MemDatabase {
1246
1348
  }
1247
1349
  insertSessionSummary(summary) {
1248
1350
  const now = Math.floor(Date.now() / 1000);
1351
+ const normalized = {
1352
+ request: normalizeSummaryRequest(summary.request),
1353
+ investigated: normalizeSummarySection(summary.investigated),
1354
+ learned: normalizeSummarySection(summary.learned),
1355
+ completed: normalizeSummarySection(summary.completed),
1356
+ next_steps: normalizeSummarySection(summary.next_steps)
1357
+ };
1249
1358
  const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
1250
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
1359
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1251
1360
  const id = Number(result.lastInsertRowid);
1252
1361
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1253
1362
  }
@@ -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) {
@@ -2406,8 +2481,9 @@ class MemDatabase {
2406
2481
  const result = this.db.query(`INSERT INTO observations (
2407
2482
  session_id, project_id, type, title, narrative, facts, concepts,
2408
2483
  files_read, files_modified, quality, lifecycle, sensitivity,
2409
- user_id, device_id, agent, created_at, created_at_epoch
2410
- ) 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);
2411
2487
  const id = Number(result.lastInsertRowid);
2412
2488
  const row = this.getObservationById(id);
2413
2489
  this.ftsInsert(row);
@@ -2648,6 +2724,13 @@ class MemDatabase {
2648
2724
  ORDER BY prompt_number ASC
2649
2725
  LIMIT ?`).all(sessionId, limit);
2650
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
+ }
2651
2734
  insertToolEvent(input) {
2652
2735
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
2653
2736
  const result = this.db.query(`INSERT INTO tool_events (
@@ -2757,8 +2840,15 @@ class MemDatabase {
2757
2840
  }
2758
2841
  insertSessionSummary(summary) {
2759
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
+ };
2760
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)
2761
- 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);
2762
2852
  const id = Number(result.lastInsertRowid);
2763
2853
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
2764
2854
  }
@@ -2964,7 +3054,8 @@ async function main() {
2964
3054
  securityFindings: context.securityFindings?.length ?? 0,
2965
3055
  unreadMessages: msgCount,
2966
3056
  synced: syncedCount,
2967
- context
3057
+ context,
3058
+ estimatedReadTokens: estimateTokens(formatContextForInjection(context))
2968
3059
  });
2969
3060
  let packLine = "";
2970
3061
  try {
@@ -3049,6 +3140,20 @@ function formatSplashScreen(data) {
3049
3140
  lines.push(` ${line}`);
3050
3141
  }
3051
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
+ }
3052
3157
  lines.push("");
3053
3158
  return lines.join(`
3054
3159
  `);
@@ -3157,6 +3262,36 @@ function formatVisibleStartupBrief(context) {
3157
3262
  }
3158
3263
  return lines.slice(0, 14);
3159
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
+ }
3160
3295
  function rememberShownItem(shown, value) {
3161
3296
  if (!value)
3162
3297
  return;
@@ -3502,6 +3637,7 @@ function capitalize(value) {
3502
3637
  return value.charAt(0).toUpperCase() + value.slice(1);
3503
3638
  }
3504
3639
  var __testables = {
3640
+ formatSplashScreen,
3505
3641
  formatVisibleStartupBrief
3506
3642
  };
3507
3643
  runHook("session-start", main);