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.
@@ -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) {
@@ -1054,8 +1148,9 @@ class MemDatabase {
1054
1148
  const result = this.db.query(`INSERT INTO observations (
1055
1149
  session_id, project_id, type, title, narrative, facts, concepts,
1056
1150
  files_read, files_modified, quality, lifecycle, sensitivity,
1057
- user_id, device_id, agent, created_at, created_at_epoch
1058
- ) 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);
1059
1154
  const id = Number(result.lastInsertRowid);
1060
1155
  const row = this.getObservationById(id);
1061
1156
  this.ftsInsert(row);
@@ -1296,6 +1391,13 @@ class MemDatabase {
1296
1391
  ORDER BY prompt_number ASC
1297
1392
  LIMIT ?`).all(sessionId, limit);
1298
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
+ }
1299
1401
  insertToolEvent(input) {
1300
1402
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1301
1403
  const result = this.db.query(`INSERT INTO tool_events (
@@ -1405,8 +1507,15 @@ class MemDatabase {
1405
1507
  }
1406
1508
  insertSessionSummary(summary) {
1407
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
+ };
1408
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)
1409
- 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);
1410
1519
  const id = Number(result.lastInsertRowid);
1411
1520
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1412
1521
  }
@@ -1880,6 +1989,8 @@ function buildVectorDocument(obs, config, project) {
1880
1989
  concepts: obs.concepts ? JSON.parse(obs.concepts) : [],
1881
1990
  files_read: obs.files_read ? JSON.parse(obs.files_read) : [],
1882
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,
1883
1994
  session_id: obs.session_id,
1884
1995
  created_at_epoch: obs.created_at_epoch,
1885
1996
  created_at: obs.created_at,
@@ -1933,6 +2044,8 @@ function buildSummaryVectorDocument(summary, config, project, observations = [],
1933
2044
  recent_tool_commands: captureContext?.recent_tool_commands ?? [],
1934
2045
  hot_files: captureContext?.hot_files ?? [],
1935
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,
1936
2049
  decisions_count: valueSignals.decisions_count,
1937
2050
  lessons_count: valueSignals.lessons_count,
1938
2051
  discoveries_count: valueSignals.discoveries_count,
@@ -2045,10 +2158,7 @@ function countPresentSections(summary) {
2045
2158
  ].filter((value) => Boolean(value && value.trim())).length;
2046
2159
  }
2047
2160
  function extractSectionItems(section) {
2048
- if (!section)
2049
- return [];
2050
- return section.split(`
2051
- `).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).slice(0, 4);
2161
+ return extractSummaryItems(section, 4);
2052
2162
  }
2053
2163
  function buildSummaryCaptureContext(prompts, toolEvents, observations) {
2054
2164
  const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
@@ -2061,6 +2171,13 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
2061
2171
  ]).filter(Boolean))].slice(0, 6);
2062
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);
2063
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;
2064
2181
  return {
2065
2182
  prompt_count: prompts.length,
2066
2183
  tool_event_count: toolEvents.length,
@@ -2070,7 +2187,9 @@ function buildSummaryCaptureContext(prompts, toolEvents, observations) {
2070
2187
  recent_tool_commands: recentToolCommands,
2071
2188
  capture_state: captureState,
2072
2189
  hot_files: hotFiles,
2073
- recent_outcomes: recentOutcomes
2190
+ recent_outcomes: recentOutcomes,
2191
+ observation_source_tools: observationSourceTools,
2192
+ latest_observation_prompt_number: latestObservationPromptNumber
2074
2193
  };
2075
2194
  }
2076
2195
  function parseJsonArray2(value) {
@@ -2298,10 +2417,7 @@ function countPresentSections2(summary) {
2298
2417
  ].filter(hasContent).length;
2299
2418
  }
2300
2419
  function extractSectionItems2(section) {
2301
- if (!hasContent(section))
2302
- return [];
2303
- return section.split(`
2304
- `).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
2420
+ return extractSummaryItems(section);
2305
2421
  }
2306
2422
  function extractObservationTitles(observations, types) {
2307
2423
  const typeSet = new Set(types);
@@ -2419,7 +2535,7 @@ function buildBeacon(db, config, sessionId, metrics) {
2419
2535
  sentinel_used: valueSignals.security_findings_count > 0,
2420
2536
  risk_score: riskScore,
2421
2537
  stacks_detected: stacks,
2422
- client_version: "0.4.0",
2538
+ client_version: "0.4.13",
2423
2539
  context_observations_injected: metrics?.contextObsInjected ?? 0,
2424
2540
  context_total_available: metrics?.contextTotalAvailable ?? 0,
2425
2541
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -3203,6 +3319,7 @@ async function saveObservation(db, config, input) {
3203
3319
  reason: `Merged into existing observation #${duplicate.id}`
3204
3320
  };
3205
3321
  }
3322
+ const sourcePromptNumber = input.source_prompt_number ?? (input.session_id ? db.getLatestSessionPromptNumber(input.session_id) : null);
3206
3323
  const obs = db.insertObservation({
3207
3324
  session_id: input.session_id ?? null,
3208
3325
  project_id: project.id,
@@ -3218,7 +3335,9 @@ async function saveObservation(db, config, input) {
3218
3335
  sensitivity,
3219
3336
  user_id: config.user_id,
3220
3337
  device_id: config.device_id,
3221
- agent: input.agent ?? "claude-code"
3338
+ agent: input.agent ?? "claude-code",
3339
+ source_tool: input.source_tool ?? null,
3340
+ source_prompt_number: sourcePromptNumber
3222
3341
  });
3223
3342
  db.addToOutbox("observation", obs.id);
3224
3343
  if (db.vecAvailable) {
@@ -3470,6 +3589,11 @@ async function main() {
3470
3589
  }
3471
3590
  }
3472
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
+ }
3473
3597
  const unsaved = detectUnsavedPlans(event.last_assistant_message);
3474
3598
  if (unsaved.length > 0) {
3475
3599
  console.error("");
@@ -3609,6 +3733,71 @@ ${sections.join(`
3609
3733
  });
3610
3734
  db.addToOutbox("observation", digestObs.id);
3611
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
+ }
3612
3801
  function detectUnsavedPlans(message) {
3613
3802
  const hints = [];
3614
3803
  const lower = message.toLowerCase();
@@ -3678,4 +3867,10 @@ function readSessionMetrics(sessionId) {
3678
3867
  } catch {}
3679
3868
  return result;
3680
3869
  }
3870
+ var __testables = {
3871
+ extractAssistantCheckpoint
3872
+ };
3681
3873
  runHook("stop", main);
3874
+ export {
3875
+ __testables
3876
+ };
@@ -713,6 +713,18 @@ var MIGRATIONS = [
713
713
  CREATE INDEX IF NOT EXISTS idx_tool_events_created
714
714
  ON tool_events(created_at_epoch DESC, id DESC);
715
715
  `
716
+ },
717
+ {
718
+ version: 11,
719
+ description: "Add observation provenance from tool and prompt chronology",
720
+ sql: `
721
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
722
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
723
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
724
+ ON observations(source_tool, created_at_epoch DESC);
725
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
726
+ ON observations(session_id, source_prompt_number DESC);
727
+ `
716
728
  }
717
729
  ];
718
730
  function isVecExtensionLoaded(db) {
@@ -766,6 +778,8 @@ function inferLegacySchemaVersion(db) {
766
778
  version = Math.max(version, 9);
767
779
  if (tableExists(db, "tool_events"))
768
780
  version = Math.max(version, 10);
781
+ if (columnExists(db, "observations", "source_tool"))
782
+ version = Math.max(version, 11);
769
783
  return version;
770
784
  }
771
785
  function runMigrations(db) {
@@ -848,6 +862,86 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
848
862
 
849
863
  // src/storage/sqlite.ts
850
864
  import { createHash as createHash2 } from "node:crypto";
865
+
866
+ // src/intelligence/summary-sections.ts
867
+ function extractSummaryItems(section, limit) {
868
+ if (!section || !section.trim())
869
+ return [];
870
+ const rawLines = section.split(`
871
+ `).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
872
+ const items = [];
873
+ const seen = new Set;
874
+ let heading = null;
875
+ for (const rawLine of rawLines) {
876
+ const line = stripSectionPrefix(rawLine);
877
+ if (!line)
878
+ continue;
879
+ const headingOnly = parseHeading(line);
880
+ if (headingOnly) {
881
+ heading = headingOnly;
882
+ continue;
883
+ }
884
+ const isBullet = /^[-*•]\s+/.test(line);
885
+ const stripped = line.replace(/^[-*•]\s+/, "").trim();
886
+ if (!stripped)
887
+ continue;
888
+ const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
889
+ const normalized = normalizeItem(item);
890
+ if (!normalized || seen.has(normalized))
891
+ continue;
892
+ seen.add(normalized);
893
+ items.push(item);
894
+ if (limit && items.length >= limit)
895
+ break;
896
+ }
897
+ return items;
898
+ }
899
+ function formatSummaryItems(section, maxLen) {
900
+ const items = extractSummaryItems(section);
901
+ if (items.length === 0)
902
+ return null;
903
+ const cleaned = items.map((item) => `- ${item}`).join(`
904
+ `);
905
+ if (cleaned.length <= maxLen)
906
+ return cleaned;
907
+ const truncated = cleaned.slice(0, maxLen).trimEnd();
908
+ const lastBreak = Math.max(truncated.lastIndexOf(`
909
+ `), truncated.lastIndexOf(" "));
910
+ const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
911
+ return `${safe.trimEnd()}…`;
912
+ }
913
+ function normalizeSummarySection(section) {
914
+ const items = extractSummaryItems(section);
915
+ if (items.length === 0) {
916
+ const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
917
+ return cleaned || null;
918
+ }
919
+ return items.map((item) => `- ${item}`).join(`
920
+ `);
921
+ }
922
+ function normalizeSummaryRequest(value) {
923
+ const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
924
+ return cleaned || null;
925
+ }
926
+ function stripSectionPrefix(value) {
927
+ return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
928
+ }
929
+ function parseHeading(value) {
930
+ const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
931
+ if (boldMatch?.[1]) {
932
+ return boldMatch[1].trim().replace(/\s+/g, " ");
933
+ }
934
+ const plainMatch = value.match(/^(.+?):$/);
935
+ if (plainMatch?.[1]) {
936
+ return plainMatch[1].trim().replace(/\s+/g, " ");
937
+ }
938
+ return null;
939
+ }
940
+ function normalizeItem(value) {
941
+ return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
942
+ }
943
+
944
+ // src/storage/sqlite.ts
851
945
  var IS_BUN = typeof globalThis.Bun !== "undefined";
852
946
  function openDatabase(dbPath) {
853
947
  if (IS_BUN) {
@@ -963,8 +1057,9 @@ class MemDatabase {
963
1057
  const result = this.db.query(`INSERT INTO observations (
964
1058
  session_id, project_id, type, title, narrative, facts, concepts,
965
1059
  files_read, files_modified, quality, lifecycle, sensitivity,
966
- user_id, device_id, agent, created_at, created_at_epoch
967
- ) 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);
1060
+ user_id, device_id, agent, source_tool, source_prompt_number,
1061
+ created_at, created_at_epoch
1062
+ ) 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);
968
1063
  const id = Number(result.lastInsertRowid);
969
1064
  const row = this.getObservationById(id);
970
1065
  this.ftsInsert(row);
@@ -1205,6 +1300,13 @@ class MemDatabase {
1205
1300
  ORDER BY prompt_number ASC
1206
1301
  LIMIT ?`).all(sessionId, limit);
1207
1302
  }
1303
+ getLatestSessionPromptNumber(sessionId) {
1304
+ const row = this.db.query(`SELECT prompt_number FROM user_prompts
1305
+ WHERE session_id = ?
1306
+ ORDER BY prompt_number DESC
1307
+ LIMIT 1`).get(sessionId);
1308
+ return row?.prompt_number ?? null;
1309
+ }
1208
1310
  insertToolEvent(input) {
1209
1311
  const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1210
1312
  const result = this.db.query(`INSERT INTO tool_events (
@@ -1314,8 +1416,15 @@ class MemDatabase {
1314
1416
  }
1315
1417
  insertSessionSummary(summary) {
1316
1418
  const now = Math.floor(Date.now() / 1000);
1419
+ const normalized = {
1420
+ request: normalizeSummaryRequest(summary.request),
1421
+ investigated: normalizeSummarySection(summary.investigated),
1422
+ learned: normalizeSummarySection(summary.learned),
1423
+ completed: normalizeSummarySection(summary.completed),
1424
+ next_steps: normalizeSummarySection(summary.next_steps)
1425
+ };
1317
1426
  const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
1318
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, summary.request, summary.investigated, summary.learned, summary.completed, summary.next_steps, now);
1427
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1319
1428
  const id = Number(result.lastInsertRowid);
1320
1429
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1321
1430
  }