engrm 0.4.18 → 0.4.21

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.
@@ -477,6 +477,16 @@ function normalizeItem(value) {
477
477
  function tokenizeProjectHint(text) {
478
478
  return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
479
479
  }
480
+ function parseSummaryJsonList(value) {
481
+ if (!value)
482
+ return [];
483
+ try {
484
+ const parsed = JSON.parse(value);
485
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
486
+ } catch {
487
+ return [];
488
+ }
489
+ }
480
490
  function isObservationRelatedToProject(obs, detected) {
481
491
  const hints = new Set([
482
492
  ...tokenizeProjectHint(detected.name),
@@ -608,7 +618,7 @@ function buildSessionContext(db, cwd, options = {}) {
608
618
  const recentToolEvents2 = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
609
619
  const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
610
620
  const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
611
- const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
621
+ const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions2);
612
622
  return {
613
623
  project_name: projectName,
614
624
  canonical_id: canonicalId,
@@ -646,7 +656,7 @@ function buildSessionContext(db, cwd, options = {}) {
646
656
  const recentToolEvents = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
647
657
  const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
648
658
  const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
649
- const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
659
+ const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId, recentSessions);
650
660
  let securityFindings = [];
651
661
  if (!isNewProject) {
652
662
  try {
@@ -989,7 +999,7 @@ function getProjectTypeCounts(db, projectId, userId) {
989
999
  }
990
1000
  return counts;
991
1001
  }
992
- function getRecentOutcomes(db, projectId, userId) {
1002
+ function getRecentOutcomes(db, projectId, userId, recentSessions) {
993
1003
  const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
994
1004
  const visibilityParams = userId ? [userId] : [];
995
1005
  const summaries = db.db.query(`SELECT * FROM session_summaries
@@ -999,6 +1009,15 @@ function getRecentOutcomes(db, projectId, userId) {
999
1009
  const picked = [];
1000
1010
  const seen = new Set;
1001
1011
  for (const summary of summaries) {
1012
+ for (const item of parseSummaryJsonList(summary.recent_outcomes)) {
1013
+ const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
1014
+ if (!normalized || seen.has(normalized))
1015
+ continue;
1016
+ seen.add(normalized);
1017
+ picked.push(item);
1018
+ if (picked.length >= 5)
1019
+ return picked;
1020
+ }
1002
1021
  for (const line of [
1003
1022
  ...extractMeaningfulLines(summary.completed, 2),
1004
1023
  ...extractMeaningfulLines(summary.learned, 1)
@@ -1012,6 +1031,17 @@ function getRecentOutcomes(db, projectId, userId) {
1012
1031
  return picked;
1013
1032
  }
1014
1033
  }
1034
+ for (const session of recentSessions ?? []) {
1035
+ for (const item of parseSummaryJsonList(session.recent_outcomes)) {
1036
+ const normalized = item.toLowerCase().replace(/\s+/g, " ").trim();
1037
+ if (!normalized || seen.has(normalized))
1038
+ continue;
1039
+ seen.add(normalized);
1040
+ picked.push(item);
1041
+ if (picked.length >= 5)
1042
+ return picked;
1043
+ }
1044
+ }
1015
1045
  const rows = db.db.query(`SELECT * FROM observations
1016
1046
  WHERE project_id = ?
1017
1047
  AND lifecycle IN ('active', 'aging', 'pinned')
@@ -1154,7 +1184,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync
1154
1184
  import { join as join3 } from "node:path";
1155
1185
  import { homedir } from "node:os";
1156
1186
  var STATE_PATH = join3(homedir(), ".engrm", "config-fingerprint.json");
1157
- var CLIENT_VERSION = "0.4.18";
1187
+ var CLIENT_VERSION = "0.4.21";
1158
1188
  function hashFile(filePath) {
1159
1189
  try {
1160
1190
  if (!existsSync3(filePath))
@@ -1696,10 +1726,20 @@ function mergeRemoteSummary(db, config, change, projectId) {
1696
1726
  investigated: typeof change.metadata?.investigated === "string" ? change.metadata.investigated : null,
1697
1727
  learned: typeof change.metadata?.learned === "string" ? change.metadata.learned : null,
1698
1728
  completed: typeof change.metadata?.completed === "string" ? change.metadata.completed : null,
1699
- next_steps: typeof change.metadata?.next_steps === "string" ? change.metadata.next_steps : null
1729
+ next_steps: typeof change.metadata?.next_steps === "string" ? change.metadata.next_steps : null,
1730
+ capture_state: typeof change.metadata?.capture_state === "string" ? change.metadata.capture_state : null,
1731
+ recent_tool_names: encodeStringArray(change.metadata?.recent_tool_names),
1732
+ hot_files: encodeStringArray(change.metadata?.hot_files),
1733
+ recent_outcomes: encodeStringArray(change.metadata?.recent_outcomes)
1700
1734
  });
1701
1735
  return Boolean(summary);
1702
1736
  }
1737
+ function encodeStringArray(value) {
1738
+ if (!Array.isArray(value))
1739
+ return null;
1740
+ const normalized = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
1741
+ return normalized.length > 0 ? JSON.stringify(normalized) : null;
1742
+ }
1703
1743
  function normalizeRemoteObservationType(rawType, sourceId) {
1704
1744
  const type = typeof rawType === "string" ? rawType.trim().toLowerCase() : "";
1705
1745
  if (type === "bugfix" || type === "discovery" || type === "decision" || type === "pattern" || type === "change" || type === "feature" || type === "refactor" || type === "digest" || type === "standard" || type === "message") {
@@ -2246,6 +2286,16 @@ var MIGRATIONS = [
2246
2286
  ON tool_events(created_at_epoch DESC, id DESC);
2247
2287
  `
2248
2288
  },
2289
+ {
2290
+ version: 11,
2291
+ description: "Add synced handoff metadata to session summaries",
2292
+ sql: `
2293
+ ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
2294
+ ALTER TABLE session_summaries ADD COLUMN recent_tool_names TEXT;
2295
+ ALTER TABLE session_summaries ADD COLUMN hot_files TEXT;
2296
+ ALTER TABLE session_summaries ADD COLUMN recent_outcomes TEXT;
2297
+ `
2298
+ },
2249
2299
  {
2250
2300
  version: 11,
2251
2301
  description: "Add observation provenance from tool and prompt chronology",
@@ -2689,6 +2739,10 @@ class MemDatabase {
2689
2739
  p.name AS project_name,
2690
2740
  ss.request AS request,
2691
2741
  ss.completed AS completed,
2742
+ ss.capture_state AS capture_state,
2743
+ ss.recent_tool_names AS recent_tool_names,
2744
+ ss.hot_files AS hot_files,
2745
+ ss.recent_outcomes AS recent_outcomes,
2692
2746
  (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
2693
2747
  (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
2694
2748
  FROM sessions s
@@ -2703,6 +2757,10 @@ class MemDatabase {
2703
2757
  p.name AS project_name,
2704
2758
  ss.request AS request,
2705
2759
  ss.completed AS completed,
2760
+ ss.capture_state AS capture_state,
2761
+ ss.recent_tool_names AS recent_tool_names,
2762
+ ss.hot_files AS hot_files,
2763
+ ss.recent_outcomes AS recent_outcomes,
2706
2764
  (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
2707
2765
  (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
2708
2766
  FROM sessions s
@@ -2875,8 +2933,11 @@ class MemDatabase {
2875
2933
  completed: normalizeSummarySection(summary.completed),
2876
2934
  next_steps: normalizeSummarySection(summary.next_steps)
2877
2935
  };
2878
- const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
2879
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
2936
+ const result = this.db.query(`INSERT INTO session_summaries (
2937
+ session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
2938
+ capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
2939
+ )
2940
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, summary.capture_state ?? null, summary.recent_tool_names ?? null, summary.hot_files ?? null, summary.recent_outcomes ?? null, now);
2880
2941
  const id = Number(result.lastInsertRowid);
2881
2942
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
2882
2943
  }
@@ -2891,7 +2952,11 @@ class MemDatabase {
2891
2952
  investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
2892
2953
  learned: normalizeSummarySection(summary.learned ?? existing.learned),
2893
2954
  completed: normalizeSummarySection(summary.completed ?? existing.completed),
2894
- next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
2955
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
2956
+ capture_state: summary.capture_state ?? existing.capture_state,
2957
+ recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
2958
+ hot_files: summary.hot_files ?? existing.hot_files,
2959
+ recent_outcomes: summary.recent_outcomes ?? existing.recent_outcomes
2895
2960
  };
2896
2961
  this.db.query(`UPDATE session_summaries
2897
2962
  SET project_id = ?,
@@ -2901,8 +2966,12 @@ class MemDatabase {
2901
2966
  learned = ?,
2902
2967
  completed = ?,
2903
2968
  next_steps = ?,
2969
+ capture_state = ?,
2970
+ recent_tool_names = ?,
2971
+ hot_files = ?,
2972
+ recent_outcomes = ?,
2904
2973
  created_at_epoch = ?
2905
- WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
2974
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, normalized.capture_state, normalized.recent_tool_names, normalized.hot_files, normalized.recent_outcomes, now, summary.session_id);
2906
2975
  return this.getSessionSummary(summary.session_id);
2907
2976
  }
2908
2977
  getSessionSummary(sessionId) {
@@ -3210,13 +3279,13 @@ function formatSplashScreen(data) {
3210
3279
  }
3211
3280
  }
3212
3281
  const contextIndex = formatContextIndex(data.context, handoffShownItems);
3213
- if (contextIndex.length > 0) {
3282
+ if (contextIndex.lines.length > 0) {
3214
3283
  lines.push("");
3215
- for (const line of contextIndex) {
3284
+ for (const line of contextIndex.lines) {
3216
3285
  lines.push(` ${line}`);
3217
3286
  }
3218
3287
  }
3219
- const inspectHints = formatInspectHints(data.context);
3288
+ const inspectHints = formatInspectHints(data.context, contextIndex.observationIds);
3220
3289
  if (inspectHints.length > 0) {
3221
3290
  lines.push("");
3222
3291
  for (const line of inspectHints) {
@@ -3353,19 +3422,23 @@ function formatLegend() {
3353
3422
  ];
3354
3423
  }
3355
3424
  function formatContextIndex(context, shownItems) {
3356
- const rows = pickContextIndexObservations(context, shownItems).map((obs) => {
3425
+ const selected = pickContextIndexObservations(context, shownItems);
3426
+ const rows = selected.map((obs) => {
3357
3427
  const icon = observationIcon(obs.type);
3358
3428
  const fileHint = extractPrimaryFileHint(obs);
3359
3429
  return `${icon} #${obs.id} ${truncateInline(obs.title, 110)}${fileHint ? ` ${c2.dim}(${fileHint})${c2.reset}` : ""}`;
3360
3430
  });
3361
3431
  if (rows.length === 0)
3362
- return [];
3363
- return [
3364
- `${c2.dim}Handoff index:${c2.reset} use IDs when you want the deeper thread`,
3365
- ...rows
3366
- ];
3432
+ return { lines: [], observationIds: [] };
3433
+ return {
3434
+ lines: [
3435
+ `${c2.dim}Handoff index:${c2.reset} use IDs when you want the deeper thread`,
3436
+ ...rows
3437
+ ],
3438
+ observationIds: selected.map((obs) => obs.id)
3439
+ };
3367
3440
  }
3368
- function formatInspectHints(context) {
3441
+ function formatInspectHints(context, visibleObservationIds = []) {
3369
3442
  const hints = [];
3370
3443
  if ((context.recentSessions?.length ?? 0) > 0) {
3371
3444
  hints.push("recent_sessions");
@@ -3380,7 +3453,7 @@ function formatInspectHints(context) {
3380
3453
  const unique = Array.from(new Set(hints)).slice(0, 4);
3381
3454
  if (unique.length === 0)
3382
3455
  return [];
3383
- const ids = context.observations.slice(0, 5).map((obs) => obs.id);
3456
+ const ids = visibleObservationIds.slice(0, 5);
3384
3457
  const fetchHint = ids.length > 0 ? `get_observations([${ids.join(", ")}])` : null;
3385
3458
  return [
3386
3459
  `${c2.dim}Next look:${c2.reset} ${unique.join(" \xB7 ")}`,
@@ -3446,10 +3519,13 @@ function duplicatesPromptLine(request, promptLine) {
3446
3519
  return normalizeStartupItem(request) === normalizeStartupItem(promptBody);
3447
3520
  }
3448
3521
  function buildToolFallbacks(context) {
3449
- return (context.recentToolEvents ?? []).slice(0, 3).map((tool) => {
3522
+ const fromEvents = (context.recentToolEvents ?? []).slice(0, 3).map((tool) => {
3450
3523
  const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
3451
3524
  return `${tool.tool_name}${detail ? `: ${detail}` : ""}`.trim();
3452
3525
  }).filter((item) => item.length > 0);
3526
+ if (fromEvents.length > 0)
3527
+ return fromEvents;
3528
+ return (context.recentSessions ?? []).flatMap((session) => parseSessionJsonList(session.recent_tool_names)).slice(0, 3).filter((item) => item.length > 0);
3453
3529
  }
3454
3530
  function sessionFallbacksFromContext(context) {
3455
3531
  return (context.recentSessions ?? []).slice(0, 2).map((session) => {
@@ -3475,6 +3551,19 @@ function buildRecentOutcomeLines(context, summary) {
3475
3551
  };
3476
3552
  push(summary?.completed);
3477
3553
  push(summary?.learned);
3554
+ if (picked.length < 2) {
3555
+ for (const session of context.recentSessions ?? []) {
3556
+ for (const item of parseSessionJsonList(session.recent_outcomes)) {
3557
+ const normalized = normalizeStartupItem(item);
3558
+ if (!normalized || seen.has(normalized))
3559
+ continue;
3560
+ seen.add(normalized);
3561
+ picked.push(item);
3562
+ if (picked.length >= 2)
3563
+ return picked;
3564
+ }
3565
+ }
3566
+ }
3478
3567
  if (picked.length < 2) {
3479
3568
  for (const obs of context.observations) {
3480
3569
  if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
@@ -3504,6 +3593,16 @@ function chooseMeaningfulSessionSummary(request, completed) {
3504
3593
  }
3505
3594
  return request ?? completed ?? null;
3506
3595
  }
3596
+ function parseSessionJsonList(value) {
3597
+ if (!value)
3598
+ return [];
3599
+ try {
3600
+ const parsed = JSON.parse(value);
3601
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
3602
+ } catch {
3603
+ return [];
3604
+ }
3605
+ }
3507
3606
  function buildProjectSignalLine(context) {
3508
3607
  if (!context.projectTypeCounts)
3509
3608
  return null;
@@ -237,6 +237,84 @@ function normalizeObservationKey(value) {
237
237
  return value.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\b(modified|updated|edited|touched|changed)\b/g, "").replace(/\s+/g, " ").trim();
238
238
  }
239
239
 
240
+ // src/intelligence/summary-sections.ts
241
+ function extractSummaryItems(section, limit) {
242
+ if (!section || !section.trim())
243
+ return [];
244
+ const rawLines = section.split(`
245
+ `).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
246
+ const items = [];
247
+ const seen = new Set;
248
+ let heading = null;
249
+ for (const rawLine of rawLines) {
250
+ const line = stripSectionPrefix(rawLine);
251
+ if (!line)
252
+ continue;
253
+ const headingOnly = parseHeading(line);
254
+ if (headingOnly) {
255
+ heading = headingOnly;
256
+ continue;
257
+ }
258
+ const isBullet = /^[-*•]\s+/.test(line);
259
+ const stripped = line.replace(/^[-*•]\s+/, "").trim();
260
+ if (!stripped)
261
+ continue;
262
+ const item = heading && isBullet ? `${heading}: ${stripped}` : stripped;
263
+ const normalized = normalizeItem(item);
264
+ if (!normalized || seen.has(normalized))
265
+ continue;
266
+ seen.add(normalized);
267
+ items.push(item);
268
+ if (limit && items.length >= limit)
269
+ break;
270
+ }
271
+ return items;
272
+ }
273
+ function formatSummaryItems(section, maxLen) {
274
+ const items = extractSummaryItems(section);
275
+ if (items.length === 0)
276
+ return null;
277
+ const cleaned = items.map((item) => `- ${item}`).join(`
278
+ `);
279
+ if (cleaned.length <= maxLen)
280
+ return cleaned;
281
+ const truncated = cleaned.slice(0, maxLen).trimEnd();
282
+ const lastBreak = Math.max(truncated.lastIndexOf(`
283
+ `), truncated.lastIndexOf(" "));
284
+ const safe = lastBreak > maxLen * 0.5 ? truncated.slice(0, lastBreak) : truncated;
285
+ return `${safe.trimEnd()}…`;
286
+ }
287
+ function normalizeSummarySection(section) {
288
+ const items = extractSummaryItems(section);
289
+ if (items.length === 0) {
290
+ const cleaned = section?.replace(/\s+/g, " ").trim() ?? "";
291
+ return cleaned || null;
292
+ }
293
+ return items.map((item) => `- ${item}`).join(`
294
+ `);
295
+ }
296
+ function normalizeSummaryRequest(value) {
297
+ const cleaned = value?.replace(/\s+/g, " ").trim() ?? "";
298
+ return cleaned || null;
299
+ }
300
+ function stripSectionPrefix(value) {
301
+ return value.replace(/^(request|investigated|learned|completed|next steps|summary):\s*/i, "").trim();
302
+ }
303
+ function parseHeading(value) {
304
+ const boldMatch = value.match(/^\*{1,2}\s*(.+?)\s*:\*{1,2}$/);
305
+ if (boldMatch?.[1]) {
306
+ return boldMatch[1].trim().replace(/\s+/g, " ");
307
+ }
308
+ const plainMatch = value.match(/^(.+?):$/);
309
+ if (plainMatch?.[1]) {
310
+ return plainMatch[1].trim().replace(/\s+/g, " ");
311
+ }
312
+ return null;
313
+ }
314
+ function normalizeItem(value) {
315
+ return value.toLowerCase().replace(/\*+/g, "").replace(/\s+/g, " ").trim();
316
+ }
317
+
240
318
  // src/config.ts
241
319
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
242
320
  import { homedir, hostname, networkInterfaces } from "node:os";
@@ -805,6 +883,16 @@ var MIGRATIONS = [
805
883
  ON tool_events(created_at_epoch DESC, id DESC);
806
884
  `
807
885
  },
886
+ {
887
+ version: 11,
888
+ description: "Add synced handoff metadata to session summaries",
889
+ sql: `
890
+ ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
891
+ ALTER TABLE session_summaries ADD COLUMN recent_tool_names TEXT;
892
+ ALTER TABLE session_summaries ADD COLUMN hot_files TEXT;
893
+ ALTER TABLE session_summaries ADD COLUMN recent_outcomes TEXT;
894
+ `
895
+ },
808
896
  {
809
897
  version: 11,
810
898
  description: "Add observation provenance from tool and prompt chronology",
@@ -953,86 +1041,6 @@ var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max,
953
1041
 
954
1042
  // src/storage/sqlite.ts
955
1043
  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
1036
1044
  var IS_BUN = typeof globalThis.Bun !== "undefined";
1037
1045
  function openDatabase(dbPath) {
1038
1046
  if (IS_BUN) {
@@ -1328,6 +1336,10 @@ class MemDatabase {
1328
1336
  p.name AS project_name,
1329
1337
  ss.request AS request,
1330
1338
  ss.completed AS completed,
1339
+ ss.capture_state AS capture_state,
1340
+ ss.recent_tool_names AS recent_tool_names,
1341
+ ss.hot_files AS hot_files,
1342
+ ss.recent_outcomes AS recent_outcomes,
1331
1343
  (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
1332
1344
  (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
1333
1345
  FROM sessions s
@@ -1342,6 +1354,10 @@ class MemDatabase {
1342
1354
  p.name AS project_name,
1343
1355
  ss.request AS request,
1344
1356
  ss.completed AS completed,
1357
+ ss.capture_state AS capture_state,
1358
+ ss.recent_tool_names AS recent_tool_names,
1359
+ ss.hot_files AS hot_files,
1360
+ ss.recent_outcomes AS recent_outcomes,
1345
1361
  (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
1346
1362
  (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
1347
1363
  FROM sessions s
@@ -1514,8 +1530,11 @@ class MemDatabase {
1514
1530
  completed: normalizeSummarySection(summary.completed),
1515
1531
  next_steps: normalizeSummarySection(summary.next_steps)
1516
1532
  };
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)
1518
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1533
+ const result = this.db.query(`INSERT INTO session_summaries (
1534
+ session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
1535
+ capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
1536
+ )
1537
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, summary.capture_state ?? null, summary.recent_tool_names ?? null, summary.hot_files ?? null, summary.recent_outcomes ?? null, now);
1519
1538
  const id = Number(result.lastInsertRowid);
1520
1539
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1521
1540
  }
@@ -1530,7 +1549,11 @@ class MemDatabase {
1530
1549
  investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
1531
1550
  learned: normalizeSummarySection(summary.learned ?? existing.learned),
1532
1551
  completed: normalizeSummarySection(summary.completed ?? existing.completed),
1533
- next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
1552
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
1553
+ capture_state: summary.capture_state ?? existing.capture_state,
1554
+ recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
1555
+ hot_files: summary.hot_files ?? existing.hot_files,
1556
+ recent_outcomes: summary.recent_outcomes ?? existing.recent_outcomes
1534
1557
  };
1535
1558
  this.db.query(`UPDATE session_summaries
1536
1559
  SET project_id = ?,
@@ -1540,8 +1563,12 @@ class MemDatabase {
1540
1563
  learned = ?,
1541
1564
  completed = ?,
1542
1565
  next_steps = ?,
1566
+ capture_state = ?,
1567
+ recent_tool_names = ?,
1568
+ hot_files = ?,
1569
+ recent_outcomes = ?,
1543
1570
  created_at_epoch = ?
1544
- WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now, summary.session_id);
1571
+ WHERE session_id = ?`).run(summary.project_id ?? existing.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, normalized.capture_state, normalized.recent_tool_names, normalized.hot_files, normalized.recent_outcomes, now, summary.session_id);
1545
1572
  return this.getSessionSummary(summary.session_id);
1546
1573
  }
1547
1574
  getSessionSummary(sessionId) {
@@ -2560,7 +2587,7 @@ function buildBeacon(db, config, sessionId, metrics) {
2560
2587
  sentinel_used: valueSignals.security_findings_count > 0,
2561
2588
  risk_score: riskScore,
2562
2589
  stacks_detected: stacks,
2563
- client_version: "0.4.18",
2590
+ client_version: "0.4.21",
2564
2591
  context_observations_injected: metrics?.contextObsInjected ?? 0,
2565
2592
  context_total_available: metrics?.contextTotalAvailable ?? 0,
2566
2593
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -3592,7 +3619,9 @@ async function main() {
3592
3619
  if (!existing) {
3593
3620
  const observations = db.getObservationsBySession(event.session_id);
3594
3621
  const session = db.getSessionMetrics(event.session_id);
3595
- const summary = extractRetrospective(observations, event.session_id, session?.project_id ?? null, config.user_id) ?? buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message);
3622
+ const retrospective = extractRetrospective(observations, event.session_id, session?.project_id ?? null, config.user_id);
3623
+ const assistantSections = extractAssistantSummarySections(event.last_assistant_message);
3624
+ const summary = mergeSessionSummary(retrospective, assistantSections, event.session_id, session?.project_id ?? null, config.user_id) ?? mergeSessionSummary(buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message), assistantSections, event.session_id, session?.project_id ?? null, config.user_id) ?? buildFallbackSessionSummary(db, event.session_id, session?.project_id ?? null, config.user_id, event.last_assistant_message);
3596
3625
  if (summary) {
3597
3626
  const row = db.insertSessionSummary(summary);
3598
3627
  db.addToOutbox("summary", row.id);
@@ -3701,6 +3730,85 @@ function buildCheckpointCompleted(checkpoint) {
3701
3730
  return lines.join(`
3702
3731
  `);
3703
3732
  }
3733
+ function mergeSessionSummary(base, extra, sessionId, projectId, userId) {
3734
+ if (!base && !extra)
3735
+ return null;
3736
+ return {
3737
+ session_id: sessionId,
3738
+ project_id: projectId,
3739
+ user_id: userId,
3740
+ request: chooseRicherSummaryValue(base?.request ?? null, extra?.request ?? null, true),
3741
+ investigated: chooseRicherSummaryValue(base?.investigated ?? null, extra?.investigated ?? null, false),
3742
+ learned: chooseRicherSummaryValue(base?.learned ?? null, extra?.learned ?? null, false),
3743
+ completed: chooseRicherSummaryValue(base?.completed ?? null, extra?.completed ?? null, false),
3744
+ next_steps: chooseRicherSummaryValue(base?.next_steps ?? null, extra?.next_steps ?? null, false)
3745
+ };
3746
+ }
3747
+ function chooseRicherSummaryValue(base, extra, isRequest) {
3748
+ const normalizedBase = isRequest ? normalizeSummaryRequest(base) : normalizeSummarySection(base);
3749
+ const normalizedExtra = isRequest ? normalizeSummaryRequest(extra) : normalizeSummarySection(extra);
3750
+ if (!normalizedBase)
3751
+ return normalizedExtra;
3752
+ if (!normalizedExtra)
3753
+ return normalizedBase;
3754
+ if (normalizedExtra.length > normalizedBase.length + 24)
3755
+ return normalizedExtra;
3756
+ if (isRequest && isGenericCheckpointLine(normalizedBase) && !isGenericCheckpointLine(normalizedExtra)) {
3757
+ return normalizedExtra;
3758
+ }
3759
+ return normalizedBase;
3760
+ }
3761
+ function extractAssistantSummarySections(message) {
3762
+ const compact = message?.replace(/\r/g, "").trim();
3763
+ if (!compact || compact.length < 80)
3764
+ return null;
3765
+ const sections = new Map;
3766
+ let current = null;
3767
+ for (const rawLine of compact.split(`
3768
+ `)) {
3769
+ const line = rawLine.trim();
3770
+ if (!line)
3771
+ continue;
3772
+ const heading = parseAssistantSectionHeading(line);
3773
+ if (heading) {
3774
+ current = heading;
3775
+ if (!sections.has(current))
3776
+ sections.set(current, []);
3777
+ continue;
3778
+ }
3779
+ if (!current)
3780
+ continue;
3781
+ if (current === "request" && isGenericCheckpointLine(line))
3782
+ continue;
3783
+ sections.get(current)?.push(line);
3784
+ }
3785
+ const request = normalizeSummaryRequest(sections.get("request")?.join(" ") ?? null);
3786
+ const investigated = normalizeSummarySection(sections.get("investigated")?.join(`
3787
+ `) ?? null);
3788
+ const learned = normalizeSummarySection(sections.get("learned")?.join(`
3789
+ `) ?? null);
3790
+ const completed = normalizeSummarySection(sections.get("completed")?.join(`
3791
+ `) ?? null);
3792
+ const next_steps = normalizeSummarySection(sections.get("next_steps")?.join(`
3793
+ `) ?? null);
3794
+ if (!request && !investigated && !learned && !completed && !next_steps)
3795
+ return null;
3796
+ return { request, investigated, learned, completed, next_steps };
3797
+ }
3798
+ function parseAssistantSectionHeading(value) {
3799
+ const normalized = value.toLowerCase().replace(/\*+/g, "").trim();
3800
+ if (/^request:/.test(normalized))
3801
+ return "request";
3802
+ if (/^investigated:/.test(normalized))
3803
+ return "investigated";
3804
+ if (/^learned:/.test(normalized))
3805
+ return "learned";
3806
+ if (/^completed:/.test(normalized))
3807
+ return "completed";
3808
+ if (/^next steps?:/.test(normalized))
3809
+ return "next_steps";
3810
+ return null;
3811
+ }
3704
3812
  function createSessionDigest(db, sessionId, cwd) {
3705
3813
  const observations = db.getObservationsBySession(sessionId);
3706
3814
  if (observations.length < 2)
@@ -3856,9 +3964,13 @@ function extractAssistantCheckpoint(message) {
3856
3964
  };
3857
3965
  }
3858
3966
  function pickAssistantCheckpointTitle(substantiveLines, bulletLines) {
3859
- 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));
3967
+ const candidates = [...bulletLines, ...substantiveLines].map((line) => line.replace(/^Completed:\s*/i, "").trim()).filter((line) => line.length > 20).filter((line) => !isGenericCheckpointLine(line)).filter((line) => !/^Next Steps?:/i.test(line)).filter((line) => !/^Investigated:/i.test(line)).filter((line) => !/^Learned:/i.test(line));
3860
3968
  return candidates[0] ?? null;
3861
3969
  }
3970
+ function isGenericCheckpointLine(value) {
3971
+ const normalized = value.toLowerCase().replace(/\s+/g, " ").trim();
3972
+ return normalized === "here's where things stand:" || normalized === "here's where things stand" || normalized === "where things stand:" || normalized === "where things stand" || normalized === "current status:" || normalized === "current status" || normalized === "status update:" || normalized === "status update";
3973
+ }
3862
3974
  function detectUnsavedPlans(message) {
3863
3975
  const hints = [];
3864
3976
  const lower = message.toLowerCase();