engrm 0.4.12 → 0.4.14

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/README.md CHANGED
@@ -215,6 +215,8 @@ The MCP server exposes tools that supported agents can call directly:
215
215
  | `memory_console` | Show a high-signal local memory console for the current project |
216
216
  | `project_memory_index` | Show typed local memory by project, including hot files and recent sessions |
217
217
  | `workspace_memory_index` | Show cross-project local memory coverage across the whole workspace |
218
+ | `tool_memory_index` | Show which source tools and plugins are creating durable memory |
219
+ | `session_tool_memory` | Show which tools in one session produced reusable memory and which produced none |
218
220
  | `recent_requests` | Inspect captured raw user prompt chronology |
219
221
  | `recent_tools` | Inspect captured raw tool chronology |
220
222
  | `recent_sessions` | List recent local sessions to inspect further |
@@ -222,6 +224,32 @@ The MCP server exposes tools that supported agents can call directly:
222
224
  | `plugin_catalog` | Inspect Engrm plugin manifests for memory-aware integrations |
223
225
  | `save_plugin_memory` | Save reduced plugin output with stable Engrm provenance |
224
226
  | `capture_git_diff` | Reduce a git diff into a durable memory object and save it |
227
+ | `capture_git_worktree` | Read the current git worktree diff and save reduced memory directly |
228
+ | `capture_repo_scan` | Run a lightweight repo scan and save reduced findings as memory |
229
+ | `capture_openclaw_content` | Save OpenClaw content, research, and follow-up work as plugin memory |
230
+
231
+ ### Thin Tools, Thick Memory
232
+
233
+ Engrm now has a real thin-tool layer, not just a plugin spec.
234
+
235
+ Current first-party thin tools:
236
+
237
+ - `capture_git_worktree`
238
+ - reads the current repo diff directly
239
+ - reduces it through `engrm.git-diff`
240
+ - `capture_repo_scan`
241
+ - runs a lightweight repo scan
242
+ - reduces it through `engrm.repo-scan`
243
+ - `capture_openclaw_content`
244
+ - captures posted/researched/outcome-style OpenClaw work
245
+ - reduces it through `engrm.openclaw-content`
246
+
247
+ These tools are intentionally small:
248
+
249
+ - tiny input surface
250
+ - local-first execution
251
+ - reduced durable memory output
252
+ - visible in Engrm's local inspection tools so we can judge tool value honestly
225
253
 
226
254
  ### Local Memory Inspection
227
255
 
@@ -236,8 +264,10 @@ Recommended flow:
236
264
  3. activity_feed
237
265
  4. recent_sessions
238
266
  5. session_story
239
- 6. project_memory_index
240
- 7. workspace_memory_index
267
+ 6. tool_memory_index
268
+ 7. session_tool_memory
269
+ 8. project_memory_index
270
+ 9. workspace_memory_index
241
271
  ```
242
272
 
243
273
  What each tool is good for:
@@ -247,9 +277,30 @@ What each tool is good for:
247
277
  - `activity_feed` shows the merged chronology across prompts, tools, observations, and summaries
248
278
  - `recent_sessions` helps you pick a session worth opening
249
279
  - `session_story` reconstructs one session in detail
280
+ - `tool_memory_index` shows which tools and plugins are actually producing durable memory
281
+ - `session_tool_memory` shows which tool calls in one session turned into reusable memory and which did not
250
282
  - `project_memory_index` shows typed memory by repo
251
283
  - `workspace_memory_index` shows coverage across all repos on the machine
252
284
 
285
+ ### Thin Tool Workflow
286
+
287
+ The current practical flow for thin tools is:
288
+
289
+ ```text
290
+ 1. memory_console / project_memory_index
291
+ 2. tool_memory_index
292
+ 3. capture_git_worktree or capture_repo_scan
293
+ 4. session_tool_memory
294
+ 5. session_story
295
+ ```
296
+
297
+ That lets you:
298
+
299
+ - see what Engrm already knows
300
+ - see which tools/plugins are producing value
301
+ - capture the current repo state with a thin tool
302
+ - verify whether that tool produced reusable memory
303
+
253
304
  ### Observation Types
254
305
 
255
306
  | Type | What it captures |
package/dist/cli.js CHANGED
@@ -597,6 +597,18 @@ var MIGRATIONS = [
597
597
  CREATE INDEX IF NOT EXISTS idx_tool_events_created
598
598
  ON tool_events(created_at_epoch DESC, id DESC);
599
599
  `
600
+ },
601
+ {
602
+ version: 11,
603
+ description: "Add observation provenance from tool and prompt chronology",
604
+ sql: `
605
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
606
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
607
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
608
+ ON observations(source_tool, created_at_epoch DESC);
609
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
610
+ ON observations(session_id, source_prompt_number DESC);
611
+ `
600
612
  }
601
613
  ];
602
614
  function isVecExtensionLoaded(db) {
@@ -650,6 +662,8 @@ function inferLegacySchemaVersion(db) {
650
662
  version = Math.max(version, 9);
651
663
  if (tableExists(db, "tool_events"))
652
664
  version = Math.max(version, 10);
665
+ if (columnExists(db, "observations", "source_tool"))
666
+ version = Math.max(version, 11);
653
667
  return version;
654
668
  }
655
669
  function runMigrations(db) {
@@ -736,6 +750,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
736
750
 
737
751
  // src/storage/sqlite.ts
738
752
  import { createHash as createHash2 } from "node:crypto";
753
+
754
+ // src/intelligence/summary-sections.ts
755
+ function extractSummaryItems(section, limit) {
756
+ if (!section || !section.trim())
757
+ return [];
758
+ const rawLines = section.split(`
759
+ `).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
760
+ const items = [];
761
+ const seen = new Set;
762
+ let heading = null;
763
+ for (const rawLine of rawLines) {
764
+ const line = stripSectionPrefix(rawLine);
765
+ if (!line)
766
+ continue;
767
+ const headingOnly = parseHeading(line);
768
+ if (headingOnly) {
769
+ heading = headingOnly;
770
+ continue;
771
+ }
772
+ const isBullet = /^[-*•]\s+/.test(line);
773
+ const stripped = line.replace(/^[-*•]\s+/, "").trim();
774
+ if (!stripped)
775
+ continue;
776
+ const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
777
+ const normalized = normalizeItem(item);
778
+ if (!normalized || seen.has(normalized))
779
+ continue;
780
+ seen.add(normalized);
781
+ items.push(item);
782
+ if (limit && items.length >= limit)
783
+ break;
784
+ }
785
+ return items;
786
+ }
787
+ function formatSummaryItems(section, maxLen) {
788
+ const items = extractSummaryItems(section);
789
+ if (items.length === 0)
790
+ return null;
791
+ const cleaned = items.map((item) => `- ${item}`).join(`
792
+ `);
793
+ if (cleaned.length <= maxLen)
794
+ return cleaned;
795
+ const truncated = cleaned.slice(0, maxLen).trimEnd();
796
+ const lastBreak = Math.max(truncated.lastIndexOf(`
797
+ `), truncated.lastIndexOf(" "));
798
+ const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
799
+ return `${safe.trimEnd()}…`;
800
+ }
801
+ function normalizeSummarySection(section) {
802
+ const items = extractSummaryItems(section);
803
+ if (items.length === 0) {
804
+ const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
805
+ return cleaned || null;
806
+ }
807
+ return items.map((item) => `- ${item}`).join(`
808
+ `);
809
+ }
810
+ function normalizeSummaryRequest(value) {
811
+ const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
812
+ return cleaned || null;
813
+ }
814
+ function stripSectionPrefix(value) {
815
+ return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
816
+ }
817
+ function parseHeading(value) {
818
+ const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
819
+ if (boldMatch?.[1]) {
820
+ return boldMatch[1].trim().replace(/\s+/g, " ");
821
+ }
822
+ const plainMatch = value.match(/^(.+?):$/);
823
+ if (plainMatch?.[1]) {
824
+ return plainMatch[1].trim().replace(/\s+/g, " ");
825
+ }
826
+ return null;
827
+ }
828
+ function normalizeItem(value) {
829
+ return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
830
+ }
831
+
832
+ // src/storage/sqlite.ts
739
833
  var IS_BUN = typeof globalThis.Bun !== "undefined";
740
834
  function openDatabase(dbPath) {
741
835
  if (IS_BUN) {
@@ -851,8 +945,9 @@ class MemDatabase {
851
945
  const result = this.db.query(`INSERT INTO observations (
852
946
  session_id, project_id, type, title, narrative, facts, concepts,
853
947
  files_read, files_modified, quality, lifecycle, sensitivity,
854
- user_id, device_id, agent, created_at, created_at_epoch
855
- ) 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);
948
+ user_id, device_id, agent, source_tool, source_prompt_number,
949
+ created_at, created_at_epoch
950
+ ) 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);
856
951
  const id = Number(result.lastInsertRowid);
857
952
  const row = this.getObservationById(id);
858
953
  this.ftsInsert(row);
@@ -1093,6 +1188,13 @@ class MemDatabase {
1093
1188
  ORDER BY prompt_number ASC
1094
1189
  LIMIT ?`).all(sessionId, limit);
1095
1190
  }
1191
+ getLatestSessionPromptNumber(sessionId) {
1192
+ const row = this.db.query(`SELECT prompt_number FROM user_prompts
1193
+ WHERE session_id = ?
1194
+ ORDER BY prompt_number DESC
1195
+ LIMIT 1`).get(sessionId);
1196
+ return row?.prompt_number ?? null;
1197
+ }
1096
1198
  insertToolEvent(input) {
1097
1199
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1098
1200
  const result = this.db.query(`INSERT INTO tool_events (
@@ -1202,8 +1304,15 @@ class MemDatabase {
1202
1304
  }
1203
1305
  insertSessionSummary(summary) {
1204
1306
  const now = Math.floor(Date.now() / 1000);
1307
+ const normalized = {
1308
+ request: normalizeSummaryRequest(summary.request),
1309
+ investigated: normalizeSummarySection(summary.investigated),
1310
+ learned: normalizeSummarySection(summary.learned),
1311
+ completed: normalizeSummarySection(summary.completed),
1312
+ next_steps: normalizeSummarySection(summary.next_steps)
1313
+ };
1205
1314
  const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
1206
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
1315
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1207
1316
  const id = Number(result.lastInsertRowid);
1208
1317
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1209
1318
  }
@@ -1708,6 +1817,18 @@ var MIGRATIONS2 = [
1708
1817
  CREATE INDEX IF NOT EXISTS idx_tool_events_created
1709
1818
  ON tool_events(created_at_epoch DESC, id DESC);
1710
1819
  `
1820
+ },
1821
+ {
1822
+ version: 11,
1823
+ description: "Add observation provenance from tool and prompt chronology",
1824
+ sql: `
1825
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
1826
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
1827
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
1828
+ ON observations(source_tool, created_at_epoch DESC);
1829
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
1830
+ ON observations(session_id, source_prompt_number DESC);
1831
+ `
1711
1832
  }
1712
1833
  ];
1713
1834
  function isVecExtensionLoaded2(db) {
@@ -2987,6 +3108,7 @@ async function saveObservation(db, config, input) {
2987
3108
  reason: `Merged into existing observation #${duplicate.id}`
2988
3109
  };
2989
3110
  }
3111
+ const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
2990
3112
  const obs = db.insertObservation({
2991
3113
  session_id: input.session_id ?? null,
2992
3114
  project_id: project.id,
@@ -3002,7 +3124,9 @@ async function saveObservation(db, config, input) {
3002
3124
  sensitivity,
3003
3125
  user_id: config.user_id,
3004
3126
  device_id: config.device_id,
3005
- agent: input.agent ?? "claude-code"
3127
+ agent: input.agent ?? "claude-code",
3128
+ source_tool: input.source_tool ?? null,
3129
+ source_prompt_number: sourcePromptNumber
3006
3130
  });
3007
3131
  db.addToOutbox("observation", obs.id);
3008
3132
  if (db.vecAvailable) {
@@ -4001,7 +4125,12 @@ function handleUpdate() {
4001
4125
  console.log(`Updating Engrm to latest version...
4002
4126
  `);
4003
4127
  try {
4004
- execSync2("npm install -g engrm@latest", { stdio: "inherit" });
4128
+ const latest = execSync2("npm view engrm version", { encoding: "utf-8" }).trim();
4129
+ if (!latest)
4130
+ throw new Error("Could not resolve latest engrm version from npm");
4131
+ console.log(`Installing engrm@${latest}...
4132
+ `);
4133
+ execSync2(`npm install -g engrm@${latest}`, { stdio: "inherit" });
4005
4134
  console.log(`
4006
4135
  Update complete. Re-registering integrations...`);
4007
4136
  const result = registerAll();
@@ -4012,7 +4141,8 @@ Update complete. Re-registering integrations...`);
4012
4141
  console.log(`
4013
4142
  Restart Claude Code or Codex to use the new version.`);
4014
4143
  } catch (error) {
4015
- console.error("Update failed. Try manually: npm install -g engrm@latest");
4144
+ console.error(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
4145
+ console.error("Try manually: npm install -g engrm@<version>");
4016
4146
  }
4017
4147
  }
4018
4148
  async function handleDoctor() {
@@ -797,6 +797,7 @@ async function saveObservation(db, config, input) {
797
797
  reason: `Merged into existing observation #${duplicate.id}`
798
798
  };
799
799
  }
800
+ const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
800
801
  const obs = db.insertObservation({
801
802
  session_id: input.session_id ?? null,
802
803
  project_id: project.id,
@@ -812,7 +813,9 @@ async function saveObservation(db, config, input) {
812
813
  sensitivity,
813
814
  user_id: config.user_id,
814
815
  device_id: config.device_id,
815
- agent: input.agent ?? "claude-code"
816
+ agent: input.agent ?? "claude-code",
817
+ source_tool: input.source_tool ?? null,
818
+ source_prompt_number: sourcePromptNumber
816
819
  });
817
820
  db.addToOutbox("observation", obs.id);
818
821
  if (db.vecAvailable) {
@@ -1427,6 +1430,18 @@ var MIGRATIONS = [
1427
1430
  CREATE INDEX IF NOT EXISTS idx_tool_events_created
1428
1431
  ON tool_events(created_at_epoch DESC, id DESC);
1429
1432
  `
1433
+ },
1434
+ {
1435
+ version: 11,
1436
+ description: "Add observation provenance from tool and prompt chronology",
1437
+ sql: `
1438
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
1439
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
1440
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
1441
+ ON observations(source_tool, created_at_epoch DESC);
1442
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
1443
+ ON observations(session_id, source_prompt_number DESC);
1444
+ `
1430
1445
  }
1431
1446
  ];
1432
1447
  function isVecExtensionLoaded(db) {
@@ -1480,6 +1495,8 @@ function inferLegacySchemaVersion(db) {
1480
1495
  version = Math.max(version, 9);
1481
1496
  if (tableExists(db, "tool_events"))
1482
1497
  version = Math.max(version, 10);
1498
+ if (columnExists(db, "observations", "source_tool"))
1499
+ version = Math.max(version, 11);
1483
1500
  return version;
1484
1501
  }
1485
1502
  function runMigrations(db) {
@@ -1562,6 +1579,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
1562
1579
 
1563
1580
  // src/storage/sqlite.ts
1564
1581
  import { createHash as createHash2 } from "node:crypto";
1582
+
1583
+ // src/intelligence/summary-sections.ts
1584
+ function extractSummaryItems(section, limit) {
1585
+ if (!section || !section.trim())
1586
+ return [];
1587
+ const rawLines = section.split(`
1588
+ `).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
1589
+ const items = [];
1590
+ const seen = new Set;
1591
+ let heading = null;
1592
+ for (const rawLine of rawLines) {
1593
+ const line = stripSectionPrefix(rawLine);
1594
+ if (!line)
1595
+ continue;
1596
+ const headingOnly = parseHeading(line);
1597
+ if (headingOnly) {
1598
+ heading = headingOnly;
1599
+ continue;
1600
+ }
1601
+ const isBullet = /^[-*•]\s+/.test(line);
1602
+ const stripped = line.replace(/^[-*•]\s+/, "").trim();
1603
+ if (!stripped)
1604
+ continue;
1605
+ const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
1606
+ const normalized = normalizeItem(item);
1607
+ if (!normalized || seen.has(normalized))
1608
+ continue;
1609
+ seen.add(normalized);
1610
+ items.push(item);
1611
+ if (limit && items.length >= limit)
1612
+ break;
1613
+ }
1614
+ return items;
1615
+ }
1616
+ function formatSummaryItems(section, maxLen) {
1617
+ const items = extractSummaryItems(section);
1618
+ if (items.length === 0)
1619
+ return null;
1620
+ const cleaned = items.map((item) => `- ${item}`).join(`
1621
+ `);
1622
+ if (cleaned.length <= maxLen)
1623
+ return cleaned;
1624
+ const truncated = cleaned.slice(0, maxLen).trimEnd();
1625
+ const lastBreak = Math.max(truncated.lastIndexOf(`
1626
+ `), truncated.lastIndexOf(" "));
1627
+ const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
1628
+ return `${safe.trimEnd()}…`;
1629
+ }
1630
+ function normalizeSummarySection(section) {
1631
+ const items = extractSummaryItems(section);
1632
+ if (items.length === 0) {
1633
+ const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
1634
+ return cleaned || null;
1635
+ }
1636
+ return items.map((item) => `- ${item}`).join(`
1637
+ `);
1638
+ }
1639
+ function normalizeSummaryRequest(value) {
1640
+ const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
1641
+ return cleaned || null;
1642
+ }
1643
+ function stripSectionPrefix(value) {
1644
+ return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
1645
+ }
1646
+ function parseHeading(value) {
1647
+ const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
1648
+ if (boldMatch?.[1]) {
1649
+ return boldMatch[1].trim().replace(/\s+/g, " ");
1650
+ }
1651
+ const plainMatch = value.match(/^(.+?):$/);
1652
+ if (plainMatch?.[1]) {
1653
+ return plainMatch[1].trim().replace(/\s+/g, " ");
1654
+ }
1655
+ return null;
1656
+ }
1657
+ function normalizeItem(value) {
1658
+ return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
1659
+ }
1660
+
1661
+ // src/storage/sqlite.ts
1565
1662
  var IS_BUN = typeof globalThis.Bun !== "undefined";
1566
1663
  function openDatabase(dbPath) {
1567
1664
  if (IS_BUN) {
@@ -1677,8 +1774,9 @@ class MemDatabase {
1677
1774
  const result = this.db.query(`INSERT INTO observations (
1678
1775
  session_id, project_id, type, title, narrative, facts, concepts,
1679
1776
  files_read, files_modified, quality, lifecycle, sensitivity,
1680
- user_id, device_id, agent, created_at, created_at_epoch
1681
- ) 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);
1777
+ user_id, device_id, agent, source_tool, source_prompt_number,
1778
+ created_at, created_at_epoch
1779
+ ) 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);
1682
1780
  const id = Number(result.lastInsertRowid);
1683
1781
  const row = this.getObservationById(id);
1684
1782
  this.ftsInsert(row);
@@ -1919,6 +2017,13 @@ class MemDatabase {
1919
2017
  ORDER BY prompt_number ASC
1920
2018
  LIMIT ?`).all(sessionId, limit);
1921
2019
  }
2020
+ getLatestSessionPromptNumber(sessionId) {
2021
+ const row = this.db.query(`SELECT prompt_number FROM user_prompts
2022
+ WHERE session_id = ?
2023
+ ORDER BY prompt_number DESC
2024
+ LIMIT 1`).get(sessionId);
2025
+ return row?.prompt_number ?? null;
2026
+ }
1922
2027
  insertToolEvent(input) {
1923
2028
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1924
2029
  const result = this.db.query(`INSERT INTO tool_events (
@@ -2028,8 +2133,15 @@ class MemDatabase {
2028
2133
  }
2029
2134
  insertSessionSummary(summary) {
2030
2135
  const now = Math.floor(Date.now() / 1000);
2136
+ const normalized = {
2137
+ request: normalizeSummaryRequest(summary.request),
2138
+ investigated: normalizeSummarySection(summary.investigated),
2139
+ learned: normalizeSummarySection(summary.learned),
2140
+ completed: normalizeSummarySection(summary.completed),
2141
+ next_steps: normalizeSummarySection(summary.next_steps)
2142
+ };
2031
2143
  const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
2032
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
2144
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
2033
2145
  const id = Number(result.lastInsertRowid);
2034
2146
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
2035
2147
  }
@@ -775,6 +775,18 @@ var MIGRATIONS = [
775
775
  CREATE INDEX IF NOT EXISTS idx_tool_events_created
776
776
  ON tool_events(created_at_epoch DESC, id DESC);
777
777
  `
778
+ },
779
+ {
780
+ version: 11,
781
+ description: "Add observation provenance from tool and prompt chronology",
782
+ sql: `
783
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
784
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
785
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
786
+ ON observations(source_tool, created_at_epoch DESC);
787
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
788
+ ON observations(session_id, source_prompt_number DESC);
789
+ `
778
790
  }
779
791
  ];
780
792
  function isVecExtensionLoaded(db) {
@@ -828,6 +840,8 @@ function inferLegacySchemaVersion(db) {
828
840
  version = Math.max(version, 9);
829
841
  if (tableExists(db, "tool_events"))
830
842
  version = Math.max(version, 10);
843
+ if (columnExists(db, "observations", "source_tool"))
844
+ version = Math.max(version, 11);
831
845
  return version;
832
846
  }
833
847
  function runMigrations(db) {
@@ -910,6 +924,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
910
924
 
911
925
  // src/storage/sqlite.ts
912
926
  import { createHash as createHash2 } from "node:crypto";
927
+
928
+ // src/intelligence/summary-sections.ts
929
+ function extractSummaryItems(section, limit) {
930
+ if (!section || !section.trim())
931
+ return [];
932
+ const rawLines = section.split(`
933
+ `).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
934
+ const items = [];
935
+ const seen = new Set;
936
+ let heading = null;
937
+ for (const rawLine of rawLines) {
938
+ const line = stripSectionPrefix(rawLine);
939
+ if (!line)
940
+ continue;
941
+ const headingOnly = parseHeading(line);
942
+ if (headingOnly) {
943
+ heading = headingOnly;
944
+ continue;
945
+ }
946
+ const isBullet = /^[-*•]\s+/.test(line);
947
+ const stripped = line.replace(/^[-*•]\s+/, "").trim();
948
+ if (!stripped)
949
+ continue;
950
+ const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
951
+ const normalized = normalizeItem(item);
952
+ if (!normalized || seen.has(normalized))
953
+ continue;
954
+ seen.add(normalized);
955
+ items.push(item);
956
+ if (limit && items.length >= limit)
957
+ break;
958
+ }
959
+ return items;
960
+ }
961
+ function formatSummaryItems(section, maxLen) {
962
+ const items = extractSummaryItems(section);
963
+ if (items.length === 0)
964
+ return null;
965
+ const cleaned = items.map((item) => `- ${item}`).join(`
966
+ `);
967
+ if (cleaned.length <= maxLen)
968
+ return cleaned;
969
+ const truncated = cleaned.slice(0, maxLen).trimEnd();
970
+ const lastBreak = Math.max(truncated.lastIndexOf(`
971
+ `), truncated.lastIndexOf(" "));
972
+ const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
973
+ return `${safe.trimEnd()}…`;
974
+ }
975
+ function normalizeSummarySection(section) {
976
+ const items = extractSummaryItems(section);
977
+ if (items.length === 0) {
978
+ const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
979
+ return cleaned || null;
980
+ }
981
+ return items.map((item) => `- ${item}`).join(`
982
+ `);
983
+ }
984
+ function normalizeSummaryRequest(value) {
985
+ const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
986
+ return cleaned || null;
987
+ }
988
+ function stripSectionPrefix(value) {
989
+ return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
990
+ }
991
+ function parseHeading(value) {
992
+ const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
993
+ if (boldMatch?.[1]) {
994
+ return boldMatch[1].trim().replace(/\s+/g, " ");
995
+ }
996
+ const plainMatch = value.match(/^(.+?):$/);
997
+ if (plainMatch?.[1]) {
998
+ return plainMatch[1].trim().replace(/\s+/g, " ");
999
+ }
1000
+ return null;
1001
+ }
1002
+ function normalizeItem(value) {
1003
+ return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
1004
+ }
1005
+
1006
+ // src/storage/sqlite.ts
913
1007
  var IS_BUN = typeof globalThis.Bun !== "undefined";
914
1008
  function openDatabase(dbPath) {
915
1009
  if (IS_BUN) {
@@ -1025,8 +1119,9 @@ class MemDatabase {
1025
1119
  const result = this.db.query(`INSERT INTO observations (
1026
1120
  session_id, project_id, type, title, narrative, facts, concepts,
1027
1121
  files_read, files_modified, quality, lifecycle, sensitivity,
1028
- user_id, device_id, agent, created_at, created_at_epoch
1029
- ) 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);
1122
+ user_id, device_id, agent, source_tool, source_prompt_number,
1123
+ created_at, created_at_epoch
1124
+ ) 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);
1030
1125
  const id = Number(result.lastInsertRowid);
1031
1126
  const row = this.getObservationById(id);
1032
1127
  this.ftsInsert(row);
@@ -1267,6 +1362,13 @@ class MemDatabase {
1267
1362
  ORDER BY prompt_number ASC
1268
1363
  LIMIT ?`).all(sessionId, limit);
1269
1364
  }
1365
+ getLatestSessionPromptNumber(sessionId) {
1366
+ const row = this.db.query(`SELECT prompt_number FROM user_prompts
1367
+ WHERE session_id = ?
1368
+ ORDER BY prompt_number DESC
1369
+ LIMIT 1`).get(sessionId);
1370
+ return row?.prompt_number ?? null;
1371
+ }
1270
1372
  insertToolEvent(input) {
1271
1373
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1272
1374
  const result = this.db.query(`INSERT INTO tool_events (
@@ -1376,8 +1478,15 @@ class MemDatabase {
1376
1478
  }
1377
1479
  insertSessionSummary(summary) {
1378
1480
  const now = Math.floor(Date.now() / 1000);
1481
+ const normalized = {
1482
+ request: normalizeSummaryRequest(summary.request),
1483
+ investigated: normalizeSummarySection(summary.investigated),
1484
+ learned: normalizeSummarySection(summary.learned),
1485
+ completed: normalizeSummarySection(summary.completed),
1486
+ next_steps: normalizeSummarySection(summary.next_steps)
1487
+ };
1379
1488
  const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
1380
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
1489
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1381
1490
  const id = Number(result.lastInsertRowid);
1382
1491
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1383
1492
  }
@@ -2320,6 +2429,7 @@ async function saveObservation(db, config, input) {
2320
2429
  reason: `Merged into existing observation #${duplicate.id}`
2321
2430
  };
2322
2431
  }
2432
+ const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
2323
2433
  const obs = db.insertObservation({
2324
2434
  session_id: input.session_id ?? null,
2325
2435
  project_id: project.id,
@@ -2335,7 +2445,9 @@ async function saveObservation(db, config, input) {
2335
2445
  sensitivity,
2336
2446
  user_id: config.user_id,
2337
2447
  device_id: config.device_id,
2338
- agent: input.agent ?? "claude-code"
2448
+ agent: input.agent ?? "claude-code",
2449
+ source_tool: input.source_tool ?? null,
2450
+ source_prompt_number: sourcePromptNumber
2339
2451
  });
2340
2452
  db.addToOutbox("observation", obs.id);
2341
2453
  if (db.vecAvailable) {
@@ -3148,7 +3260,8 @@ async function main() {
3148
3260
  files_read: extracted.files_read,
3149
3261
  files_modified: extracted.files_modified,
3150
3262
  session_id: event.session_id,
3151
- cwd: event.cwd
3263
+ cwd: event.cwd,
3264
+ source_tool: event.tool_name
3152
3265
  });
3153
3266
  incrementObserverSaveCount(event.session_id);
3154
3267
  }