engrm 0.4.19 → 0.4.22

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.
@@ -883,6 +883,16 @@ var MIGRATIONS = [
883
883
  ON tool_events(created_at_epoch DESC, id DESC);
884
884
  `
885
885
  },
886
+ {
887
+ version: 12,
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
+ },
886
896
  {
887
897
  version: 11,
888
898
  description: "Add observation provenance from tool and prompt chronology",
@@ -949,6 +959,9 @@ function inferLegacySchemaVersion(db) {
949
959
  version = Math.max(version, 10);
950
960
  if (columnExists(db, "observations", "source_tool"))
951
961
  version = Math.max(version, 11);
962
+ if (columnExists(db, "session_summaries", "capture_state") && columnExists(db, "session_summaries", "recent_tool_names") && columnExists(db, "session_summaries", "hot_files") && columnExists(db, "session_summaries", "recent_outcomes")) {
963
+ version = Math.max(version, 12);
964
+ }
952
965
  return version;
953
966
  }
954
967
  function runMigrations(db) {
@@ -1027,6 +1040,27 @@ function ensureObservationTypes(db) {
1027
1040
  }
1028
1041
  }
1029
1042
  }
1043
+ function ensureSessionSummaryColumns(db) {
1044
+ const required = [
1045
+ "capture_state",
1046
+ "recent_tool_names",
1047
+ "hot_files",
1048
+ "recent_outcomes"
1049
+ ];
1050
+ for (const column of required) {
1051
+ if (columnExists(db, "session_summaries", column))
1052
+ continue;
1053
+ db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
1054
+ }
1055
+ const current = getSchemaVersion(db);
1056
+ if (current < 12) {
1057
+ db.exec("PRAGMA user_version = 12");
1058
+ }
1059
+ }
1060
+ function getSchemaVersion(db) {
1061
+ const result = db.query("PRAGMA user_version").get();
1062
+ return result.user_version;
1063
+ }
1030
1064
  var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
1031
1065
 
1032
1066
  // src/storage/sqlite.ts
@@ -1101,6 +1135,7 @@ class MemDatabase {
1101
1135
  this.vecAvailable = this.loadVecExtension();
1102
1136
  runMigrations(this.db);
1103
1137
  ensureObservationTypes(this.db);
1138
+ ensureSessionSummaryColumns(this.db);
1104
1139
  }
1105
1140
  loadVecExtension() {
1106
1141
  try {
@@ -1326,6 +1361,10 @@ class MemDatabase {
1326
1361
  p.name AS project_name,
1327
1362
  ss.request AS request,
1328
1363
  ss.completed AS completed,
1364
+ ss.capture_state AS capture_state,
1365
+ ss.recent_tool_names AS recent_tool_names,
1366
+ ss.hot_files AS hot_files,
1367
+ ss.recent_outcomes AS recent_outcomes,
1329
1368
  (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
1330
1369
  (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
1331
1370
  FROM sessions s
@@ -1340,6 +1379,10 @@ class MemDatabase {
1340
1379
  p.name AS project_name,
1341
1380
  ss.request AS request,
1342
1381
  ss.completed AS completed,
1382
+ ss.capture_state AS capture_state,
1383
+ ss.recent_tool_names AS recent_tool_names,
1384
+ ss.hot_files AS hot_files,
1385
+ ss.recent_outcomes AS recent_outcomes,
1343
1386
  (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
1344
1387
  (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
1345
1388
  FROM sessions s
@@ -1512,8 +1555,11 @@ class MemDatabase {
1512
1555
  completed: normalizeSummarySection(summary.completed),
1513
1556
  next_steps: normalizeSummarySection(summary.next_steps)
1514
1557
  };
1515
- const result = this.db.query(`INSERT INTO session_summaries (session_id, project_id, user_id, request, investigated, learned, completed, next_steps, created_at_epoch)
1516
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1558
+ const result = this.db.query(`INSERT INTO session_summaries (
1559
+ session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
1560
+ capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
1561
+ )
1562
+ 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);
1517
1563
  const id = Number(result.lastInsertRowid);
1518
1564
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1519
1565
  }
@@ -1528,7 +1574,11 @@ class MemDatabase {
1528
1574
  investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
1529
1575
  learned: normalizeSummarySection(summary.learned ?? existing.learned),
1530
1576
  completed: normalizeSummarySection(summary.completed ?? existing.completed),
1531
- next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
1577
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
1578
+ capture_state: summary.capture_state ?? existing.capture_state,
1579
+ recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
1580
+ hot_files: summary.hot_files ?? existing.hot_files,
1581
+ recent_outcomes: summary.recent_outcomes ?? existing.recent_outcomes
1532
1582
  };
1533
1583
  this.db.query(`UPDATE session_summaries
1534
1584
  SET project_id = ?,
@@ -1538,8 +1588,12 @@ class MemDatabase {
1538
1588
  learned = ?,
1539
1589
  completed = ?,
1540
1590
  next_steps = ?,
1591
+ capture_state = ?,
1592
+ recent_tool_names = ?,
1593
+ hot_files = ?,
1594
+ recent_outcomes = ?,
1541
1595
  created_at_epoch = ?
1542
- 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);
1596
+ 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);
1543
1597
  return this.getSessionSummary(summary.session_id);
1544
1598
  }
1545
1599
  getSessionSummary(sessionId) {
@@ -1972,6 +2026,50 @@ function computeSessionValueSignals(observations, securityFindings = []) {
1972
2026
  };
1973
2027
  }
1974
2028
 
2029
+ // src/capture/session-handoff.ts
2030
+ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
2031
+ const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
2032
+ const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
2033
+ const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
2034
+ const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
2035
+ const hotFiles = [...new Set(observations.flatMap((obs) => [
2036
+ ...parseJsonArray2(obs.files_modified),
2037
+ ...parseJsonArray2(obs.files_read)
2038
+ ]).filter(Boolean))].slice(0, 6);
2039
+ 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);
2040
+ const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
2041
+ const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
2042
+ if (!obs.source_tool)
2043
+ return acc;
2044
+ acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
2045
+ return acc;
2046
+ }, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
2047
+ const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
2048
+ return {
2049
+ prompt_count: prompts.length,
2050
+ tool_event_count: toolEvents.length,
2051
+ recent_request_prompts: recentRequestPrompts,
2052
+ latest_request: latestRequest,
2053
+ recent_tool_names: recentToolNames,
2054
+ recent_tool_commands: recentToolCommands,
2055
+ capture_state: captureState,
2056
+ hot_files: hotFiles,
2057
+ recent_outcomes: recentOutcomes,
2058
+ observation_source_tools: observationSourceTools,
2059
+ latest_observation_prompt_number: latestObservationPromptNumber
2060
+ };
2061
+ }
2062
+ function parseJsonArray2(value) {
2063
+ if (!value)
2064
+ return [];
2065
+ try {
2066
+ const parsed = JSON.parse(value);
2067
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
2068
+ } catch {
2069
+ return [];
2070
+ }
2071
+ }
2072
+
1975
2073
  // src/sync/push.ts
1976
2074
  function buildVectorDocument(obs, config, project) {
1977
2075
  const parts = [obs.title];
@@ -2112,7 +2210,7 @@ async function pushOutbox(db, client, config, batchSize = 50) {
2112
2210
  const doc2 = buildSummaryVectorDocument(summary, config, {
2113
2211
  canonical_id: project2.canonical_id,
2114
2212
  name: project2.name
2115
- }, summaryObservations, buildSummaryCaptureContext(sessionPrompts, sessionToolEvents, summaryObservations));
2213
+ }, summaryObservations, buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, summaryObservations));
2116
2214
  batch.push({ entryId: entry.id, doc: doc2 });
2117
2215
  continue;
2118
2216
  }
@@ -2183,48 +2281,6 @@ function countPresentSections(summary) {
2183
2281
  function extractSectionItems(section) {
2184
2282
  return extractSummaryItems(section, 4);
2185
2283
  }
2186
- function buildSummaryCaptureContext(prompts, toolEvents, observations) {
2187
- const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
2188
- const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
2189
- const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
2190
- const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
2191
- const hotFiles = [...new Set(observations.flatMap((obs) => [
2192
- ...parseJsonArray2(obs.files_modified),
2193
- ...parseJsonArray2(obs.files_read)
2194
- ]).filter(Boolean))].slice(0, 6);
2195
- 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);
2196
- const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
2197
- const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
2198
- if (!obs.source_tool)
2199
- return acc;
2200
- acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
2201
- return acc;
2202
- }, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
2203
- const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
2204
- return {
2205
- prompt_count: prompts.length,
2206
- tool_event_count: toolEvents.length,
2207
- recent_request_prompts: recentRequestPrompts,
2208
- latest_request: latestRequest,
2209
- recent_tool_names: recentToolNames,
2210
- recent_tool_commands: recentToolCommands,
2211
- capture_state: captureState,
2212
- hot_files: hotFiles,
2213
- recent_outcomes: recentOutcomes,
2214
- observation_source_tools: observationSourceTools,
2215
- latest_observation_prompt_number: latestObservationPromptNumber
2216
- };
2217
- }
2218
- function parseJsonArray2(value) {
2219
- if (!value)
2220
- return [];
2221
- try {
2222
- const parsed = JSON.parse(value);
2223
- return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
2224
- } catch {
2225
- return [];
2226
- }
2227
- }
2228
2284
 
2229
2285
  // src/embeddings/embedder.ts
2230
2286
  var _available = null;
@@ -2558,7 +2614,7 @@ function buildBeacon(db, config, sessionId, metrics) {
2558
2614
  sentinel_used: valueSignals.security_findings_count > 0,
2559
2615
  risk_score: riskScore,
2560
2616
  stacks_detected: stacks,
2561
- client_version: "0.4.19",
2617
+ client_version: "0.4.22",
2562
2618
  context_observations_injected: metrics?.contextObsInjected ?? 0,
2563
2619
  context_total_available: metrics?.contextTotalAvailable ?? 0,
2564
2620
  recall_attempts: metrics?.recallAttempts ?? 0,
@@ -3594,7 +3650,7 @@ async function main() {
3594
3650
  const assistantSections = extractAssistantSummarySections(event.last_assistant_message);
3595
3651
  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
3652
  if (summary) {
3597
- const row = db.insertSessionSummary(summary);
3653
+ const row = db.upsertSessionSummary(summary);
3598
3654
  db.addToOutbox("summary", row.id);
3599
3655
  let securityFindings = [];
3600
3656
  try {
@@ -3940,7 +3996,7 @@ function pickAssistantCheckpointTitle(substantiveLines, bulletLines) {
3940
3996
  }
3941
3997
  function isGenericCheckpointLine(value) {
3942
3998
  const normalized = value.toLowerCase().replace(/\s+/g, " ").trim();
3943
- 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";
3999
+ 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" || normalized.startsWith("all clean. here's a summary of what was fixed") || normalized.startsWith("all clean, here's a summary of what was fixed") || normalized.startsWith("now i have enough to give a clear, accurate assessment") || normalized.startsWith("here's the real picture") || normalized === "tl;dr:" || normalized.startsWith("tl;dr:");
3944
4000
  }
3945
4001
  function detectUnsavedPlans(message) {
3946
4002
  const hints = [];
@@ -714,6 +714,16 @@ var MIGRATIONS = [
714
714
  ON tool_events(created_at_epoch DESC, id DESC);
715
715
  `
716
716
  },
717
+ {
718
+ version: 12,
719
+ description: "Add synced handoff metadata to session summaries",
720
+ sql: `
721
+ ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
722
+ ALTER TABLE session_summaries ADD COLUMN recent_tool_names TEXT;
723
+ ALTER TABLE session_summaries ADD COLUMN hot_files TEXT;
724
+ ALTER TABLE session_summaries ADD COLUMN recent_outcomes TEXT;
725
+ `
726
+ },
717
727
  {
718
728
  version: 11,
719
729
  description: "Add observation provenance from tool and prompt chronology",
@@ -780,6 +790,9 @@ function inferLegacySchemaVersion(db) {
780
790
  version = Math.max(version, 10);
781
791
  if (columnExists(db, "observations", "source_tool"))
782
792
  version = Math.max(version, 11);
793
+ if (columnExists(db, "session_summaries", "capture_state") && columnExists(db, "session_summaries", "recent_tool_names") && columnExists(db, "session_summaries", "hot_files") && columnExists(db, "session_summaries", "recent_outcomes")) {
794
+ version = Math.max(version, 12);
795
+ }
783
796
  return version;
784
797
  }
785
798
  function runMigrations(db) {
@@ -858,6 +871,27 @@ function ensureObservationTypes(db) {
858
871
  }
859
872
  }
860
873
  }
874
+ function ensureSessionSummaryColumns(db) {
875
+ const required = [
876
+ "capture_state",
877
+ "recent_tool_names",
878
+ "hot_files",
879
+ "recent_outcomes"
880
+ ];
881
+ for (const column of required) {
882
+ if (columnExists(db, "session_summaries", column))
883
+ continue;
884
+ db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
885
+ }
886
+ const current = getSchemaVersion(db);
887
+ if (current < 12) {
888
+ db.exec("PRAGMA user_version = 12");
889
+ }
890
+ }
891
+ function getSchemaVersion(db) {
892
+ const result = db.query("PRAGMA user_version").get();
893
+ return result.user_version;
894
+ }
861
895
  var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
862
896
 
863
897
  // src/storage/sqlite.ts
@@ -1012,6 +1046,7 @@ class MemDatabase {
1012
1046
  this.vecAvailable = this.loadVecExtension();
1013
1047
  runMigrations(this.db);
1014
1048
  ensureObservationTypes(this.db);
1049
+ ensureSessionSummaryColumns(this.db);
1015
1050
  }
1016
1051
  loadVecExtension() {
1017
1052
  try {
@@ -1237,6 +1272,10 @@ class MemDatabase {
1237
1272
  p.name AS project_name,
1238
1273
  ss.request AS request,
1239
1274
  ss.completed AS completed,
1275
+ ss.capture_state AS capture_state,
1276
+ ss.recent_tool_names AS recent_tool_names,
1277
+ ss.hot_files AS hot_files,
1278
+ ss.recent_outcomes AS recent_outcomes,
1240
1279
  (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
1241
1280
  (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
1242
1281
  FROM sessions s
@@ -1251,6 +1290,10 @@ class MemDatabase {
1251
1290
  p.name AS project_name,
1252
1291
  ss.request AS request,
1253
1292
  ss.completed AS completed,
1293
+ ss.capture_state AS capture_state,
1294
+ ss.recent_tool_names AS recent_tool_names,
1295
+ ss.hot_files AS hot_files,
1296
+ ss.recent_outcomes AS recent_outcomes,
1254
1297
  (SELECT COUNT(*) FROM user_prompts up WHERE up.session_id = s.session_id) AS prompt_count,
1255
1298
  (SELECT COUNT(*) FROM tool_events te WHERE te.session_id = s.session_id) AS tool_event_count
1256
1299
  FROM sessions s
@@ -1423,8 +1466,11 @@ class MemDatabase {
1423
1466
  completed: normalizeSummarySection(summary.completed),
1424
1467
  next_steps: normalizeSummarySection(summary.next_steps)
1425
1468
  };
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)
1427
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, now);
1469
+ const result = this.db.query(`INSERT INTO session_summaries (
1470
+ session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
1471
+ capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
1472
+ )
1473
+ 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);
1428
1474
  const id = Number(result.lastInsertRowid);
1429
1475
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1430
1476
  }
@@ -1439,7 +1485,11 @@ class MemDatabase {
1439
1485
  investigated: normalizeSummarySection(summary.investigated ?? existing.investigated),
1440
1486
  learned: normalizeSummarySection(summary.learned ?? existing.learned),
1441
1487
  completed: normalizeSummarySection(summary.completed ?? existing.completed),
1442
- next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps)
1488
+ next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
1489
+ capture_state: summary.capture_state ?? existing.capture_state,
1490
+ recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
1491
+ hot_files: summary.hot_files ?? existing.hot_files,
1492
+ recent_outcomes: summary.recent_outcomes ?? existing.recent_outcomes
1443
1493
  };
1444
1494
  this.db.query(`UPDATE session_summaries
1445
1495
  SET project_id = ?,
@@ -1449,8 +1499,12 @@ class MemDatabase {
1449
1499
  learned = ?,
1450
1500
  completed = ?,
1451
1501
  next_steps = ?,
1502
+ capture_state = ?,
1503
+ recent_tool_names = ?,
1504
+ hot_files = ?,
1505
+ recent_outcomes = ?,
1452
1506
  created_at_epoch = ?
1453
- 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);
1507
+ 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);
1454
1508
  return this.getSessionSummary(summary.session_id);
1455
1509
  }
1456
1510
  getSessionSummary(sessionId) {
@@ -1597,6 +1651,50 @@ function runHook(hookName, fn) {
1597
1651
  });
1598
1652
  }
1599
1653
 
1654
+ // src/capture/session-handoff.ts
1655
+ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
1656
+ const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
1657
+ const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
1658
+ const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
1659
+ const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
1660
+ const hotFiles = [...new Set(observations.flatMap((obs) => [
1661
+ ...parseJsonArray(obs.files_modified),
1662
+ ...parseJsonArray(obs.files_read)
1663
+ ]).filter(Boolean))].slice(0, 6);
1664
+ 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);
1665
+ const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
1666
+ const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
1667
+ if (!obs.source_tool)
1668
+ return acc;
1669
+ acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
1670
+ return acc;
1671
+ }, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
1672
+ const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
1673
+ return {
1674
+ prompt_count: prompts.length,
1675
+ tool_event_count: toolEvents.length,
1676
+ recent_request_prompts: recentRequestPrompts,
1677
+ latest_request: latestRequest,
1678
+ recent_tool_names: recentToolNames,
1679
+ recent_tool_commands: recentToolCommands,
1680
+ capture_state: captureState,
1681
+ hot_files: hotFiles,
1682
+ recent_outcomes: recentOutcomes,
1683
+ observation_source_tools: observationSourceTools,
1684
+ latest_observation_prompt_number: latestObservationPromptNumber
1685
+ };
1686
+ }
1687
+ function parseJsonArray(value) {
1688
+ if (!value)
1689
+ return [];
1690
+ try {
1691
+ const parsed = JSON.parse(value);
1692
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
1693
+ } catch {
1694
+ return [];
1695
+ }
1696
+ }
1697
+
1600
1698
  // hooks/user-prompt-submit.ts
1601
1699
  async function main() {
1602
1700
  const event = await parseStdinJson();
@@ -1626,6 +1724,10 @@ async function main() {
1626
1724
  });
1627
1725
  const compactPrompt = event.prompt.replace(/\s+/g, " ").trim();
1628
1726
  if (compactPrompt.length >= 8) {
1727
+ const sessionPrompts = db.getSessionUserPrompts(event.session_id, 20);
1728
+ const sessionToolEvents = db.getSessionToolEvents(event.session_id, 20);
1729
+ const sessionObservations = db.getObservationsBySession(event.session_id);
1730
+ const handoff = buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, sessionObservations);
1629
1731
  const summary = db.upsertSessionSummary({
1630
1732
  session_id: event.session_id,
1631
1733
  project_id: project.id,
@@ -1634,7 +1736,11 @@ async function main() {
1634
1736
  investigated: null,
1635
1737
  learned: null,
1636
1738
  completed: null,
1637
- next_steps: null
1739
+ next_steps: null,
1740
+ capture_state: handoff.capture_state,
1741
+ recent_tool_names: JSON.stringify(handoff.recent_tool_names),
1742
+ hot_files: JSON.stringify(handoff.hot_files),
1743
+ recent_outcomes: JSON.stringify(handoff.recent_outcomes)
1638
1744
  });
1639
1745
  db.addToOutbox("summary", summary.id);
1640
1746
  }