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.
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) {
@@ -772,6 +866,7 @@ function openNodeDatabase(dbPath) {
772
866
  const BetterSqlite3 = __require("better-sqlite3");
773
867
  const raw = new BetterSqlite3(dbPath);
774
868
  return {
869
+ __raw: raw,
775
870
  query(sql) {
776
871
  const stmt = raw.prepare(sql);
777
872
  return {
@@ -809,7 +904,7 @@ class MemDatabase {
809
904
  loadVecExtension() {
810
905
  try {
811
906
  const sqliteVec = __require("sqlite-vec");
812
- sqliteVec.load(this.db);
907
+ sqliteVec.load(this.db.__raw ?? this.db);
813
908
  return true;
814
909
  } catch {
815
910
  return false;
@@ -850,8 +945,9 @@ class MemDatabase {
850
945
  const result = this.db.query(`INSERT INTO observations (
851
946
  session_id, project_id, type, title, narrative, facts, concepts,
852
947
  files_read, files_modified, quality, lifecycle, sensitivity,
853
- user_id, device_id, agent, created_at, created_at_epoch
854
- ) 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);
855
951
  const id = Number(result.lastInsertRowid);
856
952
  const row = this.getObservationById(id);
857
953
  this.ftsInsert(row);
@@ -1092,6 +1188,13 @@ class MemDatabase {
1092
1188
  ORDER BY prompt_number ASC
1093
1189
  LIMIT ?`).all(sessionId, limit);
1094
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
+ }
1095
1198
  insertToolEvent(input) {
1096
1199
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1097
1200
  const result = this.db.query(`INSERT INTO tool_events (
@@ -1201,8 +1304,15 @@ class MemDatabase {
1201
1304
  }
1202
1305
  insertSessionSummary(summary) {
1203
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
+ };
1204
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)
1205
- 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);
1206
1316
  const id = Number(result.lastInsertRowid);
1207
1317
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1208
1318
  }
@@ -1707,6 +1817,18 @@ var MIGRATIONS2 = [
1707
1817
  CREATE INDEX IF NOT EXISTS idx_tool_events_created
1708
1818
  ON tool_events(created_at_epoch DESC, id DESC);
1709
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
+ `
1710
1832
  }
1711
1833
  ];
1712
1834
  function isVecExtensionLoaded2(db) {
@@ -2986,6 +3108,7 @@ async function saveObservation(db, config, input) {
2986
3108
  reason: `Merged into existing observation #${duplicate.id}`
2987
3109
  };
2988
3110
  }
3111
+ const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
2989
3112
  const obs = db.insertObservation({
2990
3113
  session_id: input.session_id ?? null,
2991
3114
  project_id: project.id,
@@ -3001,7 +3124,9 @@ async function saveObservation(db, config, input) {
3001
3124
  sensitivity,
3002
3125
  user_id: config.user_id,
3003
3126
  device_id: config.device_id,
3004
- agent: input.agent ?? "claude-code"
3127
+ agent: input.agent ?? "claude-code",
3128
+ source_tool: input.source_tool ?? null,
3129
+ source_prompt_number: sourcePromptNumber
3005
3130
  });
3006
3131
  db.addToOutbox("observation", obs.id);
3007
3132
  if (db.vecAvailable) {
@@ -4000,7 +4125,12 @@ function handleUpdate() {
4000
4125
  console.log(`Updating Engrm to latest version...
4001
4126
  `);
4002
4127
  try {
4003
- 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" });
4004
4134
  console.log(`
4005
4135
  Update complete. Re-registering integrations...`);
4006
4136
  const result = registerAll();
@@ -4011,7 +4141,8 @@ Update complete. Re-registering integrations...`);
4011
4141
  console.log(`
4012
4142
  Restart Claude Code or Codex to use the new version.`);
4013
4143
  } catch (error) {
4014
- 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>");
4015
4146
  }
4016
4147
  }
4017
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) {
@@ -1598,6 +1695,7 @@ function openNodeDatabase(dbPath) {
1598
1695
  const BetterSqlite3 = __require("better-sqlite3");
1599
1696
  const raw = new BetterSqlite3(dbPath);
1600
1697
  return {
1698
+ __raw: raw,
1601
1699
  query(sql) {
1602
1700
  const stmt = raw.prepare(sql);
1603
1701
  return {
@@ -1635,7 +1733,7 @@ class MemDatabase {
1635
1733
  loadVecExtension() {
1636
1734
  try {
1637
1735
  const sqliteVec = __require("sqlite-vec");
1638
- sqliteVec.load(this.db);
1736
+ sqliteVec.load(this.db.__raw ?? this.db);
1639
1737
  return true;
1640
1738
  } catch {
1641
1739
  return false;
@@ -1676,8 +1774,9 @@ class MemDatabase {
1676
1774
  const result = this.db.query(`INSERT INTO observations (
1677
1775
  session_id, project_id, type, title, narrative, facts, concepts,
1678
1776
  files_read, files_modified, quality, lifecycle, sensitivity,
1679
- user_id, device_id, agent, created_at, created_at_epoch
1680
- ) 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);
1681
1780
  const id = Number(result.lastInsertRowid);
1682
1781
  const row = this.getObservationById(id);
1683
1782
  this.ftsInsert(row);
@@ -1918,6 +2017,13 @@ class MemDatabase {
1918
2017
  ORDER BY prompt_number ASC
1919
2018
  LIMIT ?`).all(sessionId, limit);
1920
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
+ }
1921
2027
  insertToolEvent(input) {
1922
2028
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1923
2029
  const result = this.db.query(`INSERT INTO tool_events (
@@ -2027,8 +2133,15 @@ class MemDatabase {
2027
2133
  }
2028
2134
  insertSessionSummary(summary) {
2029
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
+ };
2030
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)
2031
- 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);
2032
2145
  const id = Number(result.lastInsertRowid);
2033
2146
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
2034
2147
  }