engrm 0.4.21 → 0.4.23

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.
@@ -483,7 +483,7 @@ var MIGRATIONS = [
483
483
  -- Sync outbox (offline-first queue)
484
484
  CREATE TABLE sync_outbox (
485
485
  id INTEGER PRIMARY KEY AUTOINCREMENT,
486
- record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
486
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
487
487
  record_id INTEGER NOT NULL,
488
488
  status TEXT DEFAULT 'pending' CHECK (status IN (
489
489
  'pending', 'syncing', 'synced', 'failed'
@@ -778,6 +778,18 @@ var MIGRATIONS = [
778
778
  },
779
779
  {
780
780
  version: 11,
781
+ description: "Add observation provenance from tool and prompt chronology",
782
+ sql: `
783
+ ALTER TABLE observations ADD COLUMN source_tool TEXT;
784
+ ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
785
+ CREATE INDEX IF NOT EXISTS idx_observations_source_tool
786
+ ON observations(source_tool, created_at_epoch DESC);
787
+ CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
788
+ ON observations(session_id, source_prompt_number DESC);
789
+ `
790
+ },
791
+ {
792
+ version: 12,
781
793
  description: "Add synced handoff metadata to session summaries",
782
794
  sql: `
783
795
  ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
@@ -787,15 +799,79 @@ var MIGRATIONS = [
787
799
  `
788
800
  },
789
801
  {
790
- version: 11,
791
- description: "Add observation provenance from tool and prompt chronology",
802
+ version: 13,
803
+ description: "Add current_thread to session summaries",
792
804
  sql: `
793
- ALTER TABLE observations ADD COLUMN source_tool TEXT;
794
- ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
795
- CREATE INDEX IF NOT EXISTS idx_observations_source_tool
796
- ON observations(source_tool, created_at_epoch DESC);
797
- CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
798
- ON observations(session_id, source_prompt_number DESC);
805
+ ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
806
+ `
807
+ },
808
+ {
809
+ version: 14,
810
+ description: "Add chat_messages lane for raw conversation recall",
811
+ sql: `
812
+ CREATE TABLE IF NOT EXISTS chat_messages (
813
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
814
+ session_id TEXT NOT NULL,
815
+ project_id INTEGER REFERENCES projects(id),
816
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
817
+ content TEXT NOT NULL,
818
+ user_id TEXT NOT NULL,
819
+ device_id TEXT NOT NULL,
820
+ agent TEXT DEFAULT 'claude-code',
821
+ created_at_epoch INTEGER NOT NULL
822
+ );
823
+
824
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_session
825
+ ON chat_messages(session_id, created_at_epoch DESC, id DESC);
826
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_project
827
+ ON chat_messages(project_id, created_at_epoch DESC, id DESC);
828
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_created
829
+ ON chat_messages(created_at_epoch DESC, id DESC);
830
+ `
831
+ },
832
+ {
833
+ version: 15,
834
+ description: "Add remote_source_id for chat message sync deduplication",
835
+ sql: `
836
+ ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
837
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
838
+ ON chat_messages(remote_source_id)
839
+ WHERE remote_source_id IS NOT NULL;
840
+ `
841
+ },
842
+ {
843
+ version: 16,
844
+ description: "Allow chat_message records in sync_outbox",
845
+ sql: `
846
+ CREATE TABLE sync_outbox_new (
847
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
848
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
849
+ record_id INTEGER NOT NULL,
850
+ status TEXT DEFAULT 'pending' CHECK (status IN (
851
+ 'pending', 'syncing', 'synced', 'failed'
852
+ )),
853
+ retry_count INTEGER DEFAULT 0,
854
+ max_retries INTEGER DEFAULT 10,
855
+ last_error TEXT,
856
+ created_at_epoch INTEGER NOT NULL,
857
+ synced_at_epoch INTEGER,
858
+ next_retry_epoch INTEGER
859
+ );
860
+
861
+ INSERT INTO sync_outbox_new (
862
+ id, record_type, record_id, status, retry_count, max_retries,
863
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
864
+ )
865
+ SELECT
866
+ id, record_type, record_id, status, retry_count, max_retries,
867
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
868
+ FROM sync_outbox;
869
+
870
+ DROP TABLE sync_outbox;
871
+ ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
872
+
873
+ CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
874
+ CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
799
875
  `
800
876
  }
801
877
  ];
@@ -852,6 +928,21 @@ function inferLegacySchemaVersion(db) {
852
928
  version = Math.max(version, 10);
853
929
  if (columnExists(db, "observations", "source_tool"))
854
930
  version = Math.max(version, 11);
931
+ 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")) {
932
+ version = Math.max(version, 12);
933
+ }
934
+ if (columnExists(db, "session_summaries", "current_thread")) {
935
+ version = Math.max(version, 13);
936
+ }
937
+ if (tableExists(db, "chat_messages")) {
938
+ version = Math.max(version, 14);
939
+ }
940
+ if (columnExists(db, "chat_messages", "remote_source_id")) {
941
+ version = Math.max(version, 15);
942
+ }
943
+ if (syncOutboxSupportsChatMessages(db)) {
944
+ version = Math.max(version, 16);
945
+ }
855
946
  return version;
856
947
  }
857
948
  function runMigrations(db) {
@@ -930,6 +1021,93 @@ function ensureObservationTypes(db) {
930
1021
  }
931
1022
  }
932
1023
  }
1024
+ function ensureSessionSummaryColumns(db) {
1025
+ const required = [
1026
+ "capture_state",
1027
+ "recent_tool_names",
1028
+ "hot_files",
1029
+ "recent_outcomes",
1030
+ "current_thread"
1031
+ ];
1032
+ for (const column of required) {
1033
+ if (columnExists(db, "session_summaries", column))
1034
+ continue;
1035
+ db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
1036
+ }
1037
+ const current = getSchemaVersion(db);
1038
+ if (current < 13) {
1039
+ db.exec("PRAGMA user_version = 13");
1040
+ }
1041
+ }
1042
+ function ensureChatMessageColumns(db) {
1043
+ if (!tableExists(db, "chat_messages"))
1044
+ return;
1045
+ if (!columnExists(db, "chat_messages", "remote_source_id")) {
1046
+ db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
1047
+ }
1048
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source ON chat_messages(remote_source_id) WHERE remote_source_id IS NOT NULL");
1049
+ const current = getSchemaVersion(db);
1050
+ if (current < 15) {
1051
+ db.exec("PRAGMA user_version = 15");
1052
+ }
1053
+ }
1054
+ function ensureSyncOutboxSupportsChatMessages(db) {
1055
+ if (syncOutboxSupportsChatMessages(db)) {
1056
+ const current = getSchemaVersion(db);
1057
+ if (current < 16) {
1058
+ db.exec("PRAGMA user_version = 16");
1059
+ }
1060
+ return;
1061
+ }
1062
+ db.exec("BEGIN TRANSACTION");
1063
+ try {
1064
+ db.exec(`
1065
+ CREATE TABLE sync_outbox_new (
1066
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1067
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
1068
+ record_id INTEGER NOT NULL,
1069
+ status TEXT DEFAULT 'pending' CHECK (status IN (
1070
+ 'pending', 'syncing', 'synced', 'failed'
1071
+ )),
1072
+ retry_count INTEGER DEFAULT 0,
1073
+ max_retries INTEGER DEFAULT 10,
1074
+ last_error TEXT,
1075
+ created_at_epoch INTEGER NOT NULL,
1076
+ synced_at_epoch INTEGER,
1077
+ next_retry_epoch INTEGER
1078
+ );
1079
+
1080
+ INSERT INTO sync_outbox_new (
1081
+ id, record_type, record_id, status, retry_count, max_retries,
1082
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
1083
+ )
1084
+ SELECT
1085
+ id, record_type, record_id, status, retry_count, max_retries,
1086
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
1087
+ FROM sync_outbox;
1088
+
1089
+ DROP TABLE sync_outbox;
1090
+ ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
1091
+
1092
+ CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
1093
+ CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
1094
+ `);
1095
+ db.exec("PRAGMA user_version = 16");
1096
+ db.exec("COMMIT");
1097
+ } catch (error) {
1098
+ db.exec("ROLLBACK");
1099
+ throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
1100
+ }
1101
+ }
1102
+ function syncOutboxSupportsChatMessages(db) {
1103
+ const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
1104
+ const sql = row?.sql ?? "";
1105
+ return sql.includes("'chat_message'");
1106
+ }
1107
+ function getSchemaVersion(db) {
1108
+ const result = db.query("PRAGMA user_version").get();
1109
+ return result.user_version;
1110
+ }
933
1111
  var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
934
1112
 
935
1113
  // src/storage/sqlite.ts
@@ -1084,6 +1262,9 @@ class MemDatabase {
1084
1262
  this.vecAvailable = this.loadVecExtension();
1085
1263
  runMigrations(this.db);
1086
1264
  ensureObservationTypes(this.db);
1265
+ ensureSessionSummaryColumns(this.db);
1266
+ ensureChatMessageColumns(this.db);
1267
+ ensureSyncOutboxSupportsChatMessages(this.db);
1087
1268
  }
1088
1269
  loadVecExtension() {
1089
1270
  try {
@@ -1309,6 +1490,7 @@ class MemDatabase {
1309
1490
  p.name AS project_name,
1310
1491
  ss.request AS request,
1311
1492
  ss.completed AS completed,
1493
+ ss.current_thread AS current_thread,
1312
1494
  ss.capture_state AS capture_state,
1313
1495
  ss.recent_tool_names AS recent_tool_names,
1314
1496
  ss.hot_files AS hot_files,
@@ -1327,6 +1509,7 @@ class MemDatabase {
1327
1509
  p.name AS project_name,
1328
1510
  ss.request AS request,
1329
1511
  ss.completed AS completed,
1512
+ ss.current_thread AS current_thread,
1330
1513
  ss.capture_state AS capture_state,
1331
1514
  ss.recent_tool_names AS recent_tool_names,
1332
1515
  ss.hot_files AS hot_files,
@@ -1417,6 +1600,54 @@ class MemDatabase {
1417
1600
  ORDER BY created_at_epoch DESC, id DESC
1418
1601
  LIMIT ?`).all(...userId ? [userId] : [], limit);
1419
1602
  }
1603
+ insertChatMessage(input) {
1604
+ const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1605
+ const content = input.content.trim();
1606
+ const result = this.db.query(`INSERT INTO chat_messages (
1607
+ session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
1608
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.session_id, input.project_id, input.role, content, input.user_id, input.device_id, input.agent ?? "claude-code", createdAt, input.remote_source_id ?? null);
1609
+ return this.getChatMessageById(Number(result.lastInsertRowid));
1610
+ }
1611
+ getChatMessageById(id) {
1612
+ return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
1613
+ }
1614
+ getChatMessageByRemoteSourceId(remoteSourceId) {
1615
+ return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
1616
+ }
1617
+ getSessionChatMessages(sessionId, limit = 50) {
1618
+ return this.db.query(`SELECT * FROM chat_messages
1619
+ WHERE session_id = ?
1620
+ ORDER BY created_at_epoch ASC, id ASC
1621
+ LIMIT ?`).all(sessionId, limit);
1622
+ }
1623
+ getRecentChatMessages(projectId, limit = 20, userId) {
1624
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1625
+ if (projectId !== null) {
1626
+ return this.db.query(`SELECT * FROM chat_messages
1627
+ WHERE project_id = ?${visibilityClause}
1628
+ ORDER BY created_at_epoch DESC, id DESC
1629
+ LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
1630
+ }
1631
+ return this.db.query(`SELECT * FROM chat_messages
1632
+ WHERE 1 = 1${visibilityClause}
1633
+ ORDER BY created_at_epoch DESC, id DESC
1634
+ LIMIT ?`).all(...userId ? [userId] : [], limit);
1635
+ }
1636
+ searchChatMessages(query, projectId, limit = 20, userId) {
1637
+ const needle = `%${query.toLowerCase()}%`;
1638
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1639
+ if (projectId !== null) {
1640
+ return this.db.query(`SELECT * FROM chat_messages
1641
+ WHERE project_id = ?
1642
+ AND lower(content) LIKE ?${visibilityClause}
1643
+ ORDER BY created_at_epoch DESC, id DESC
1644
+ LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
1645
+ }
1646
+ return this.db.query(`SELECT * FROM chat_messages
1647
+ WHERE lower(content) LIKE ?${visibilityClause}
1648
+ ORDER BY created_at_epoch DESC, id DESC
1649
+ LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
1650
+ }
1420
1651
  addToOutbox(recordType, recordId) {
1421
1652
  const now = Math.floor(Date.now() / 1000);
1422
1653
  this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
@@ -1505,9 +1736,9 @@ class MemDatabase {
1505
1736
  };
1506
1737
  const result = this.db.query(`INSERT INTO session_summaries (
1507
1738
  session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
1508
- capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
1739
+ current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
1509
1740
  )
1510
- 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);
1741
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(summary.session_id, summary.project_id, summary.user_id, normalized.request, normalized.investigated, normalized.learned, normalized.completed, normalized.next_steps, summary.current_thread ?? null, summary.capture_state ?? null, summary.recent_tool_names ?? null, summary.hot_files ?? null, summary.recent_outcomes ?? null, now);
1511
1742
  const id = Number(result.lastInsertRowid);
1512
1743
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1513
1744
  }
@@ -1523,6 +1754,7 @@ class MemDatabase {
1523
1754
  learned: normalizeSummarySection(summary.learned ?? existing.learned),
1524
1755
  completed: normalizeSummarySection(summary.completed ?? existing.completed),
1525
1756
  next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
1757
+ current_thread: summary.current_thread ?? existing.current_thread,
1526
1758
  capture_state: summary.capture_state ?? existing.capture_state,
1527
1759
  recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
1528
1760
  hot_files: summary.hot_files ?? existing.hot_files,
@@ -1536,12 +1768,13 @@ class MemDatabase {
1536
1768
  learned = ?,
1537
1769
  completed = ?,
1538
1770
  next_steps = ?,
1771
+ current_thread = ?,
1539
1772
  capture_state = ?,
1540
1773
  recent_tool_names = ?,
1541
1774
  hot_files = ?,
1542
1775
  recent_outcomes = ?,
1543
1776
  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, normalized.capture_state, normalized.recent_tool_names, normalized.hot_files, normalized.recent_outcomes, now, summary.session_id);
1777
+ 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.current_thread, normalized.capture_state, normalized.recent_tool_names, normalized.hot_files, normalized.recent_outcomes, now, summary.session_id);
1545
1778
  return this.getSessionSummary(summary.session_id);
1546
1779
  }
1547
1780
  getSessionSummary(sessionId) {
@@ -3252,6 +3485,83 @@ function mergeSectionItem(existing, item) {
3252
3485
  - ${normalizedItem}`;
3253
3486
  }
3254
3487
 
3488
+ // src/capture/session-handoff.ts
3489
+ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
3490
+ const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
3491
+ const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
3492
+ const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
3493
+ const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
3494
+ const hotFiles = [...new Set(observations.flatMap((obs) => [
3495
+ ...parseJsonArray(obs.files_modified),
3496
+ ...parseJsonArray(obs.files_read)
3497
+ ]).filter(Boolean))].slice(0, 6);
3498
+ 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);
3499
+ const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
3500
+ const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
3501
+ if (!obs.source_tool)
3502
+ return acc;
3503
+ acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
3504
+ return acc;
3505
+ }, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
3506
+ const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
3507
+ const currentThread = buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames);
3508
+ return {
3509
+ prompt_count: prompts.length,
3510
+ tool_event_count: toolEvents.length,
3511
+ recent_request_prompts: recentRequestPrompts,
3512
+ latest_request: latestRequest,
3513
+ current_thread: currentThread,
3514
+ recent_tool_names: recentToolNames,
3515
+ recent_tool_commands: recentToolCommands,
3516
+ capture_state: captureState,
3517
+ hot_files: hotFiles,
3518
+ recent_outcomes: recentOutcomes,
3519
+ observation_source_tools: observationSourceTools,
3520
+ latest_observation_prompt_number: latestObservationPromptNumber
3521
+ };
3522
+ }
3523
+ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames) {
3524
+ const request = compactLine(latestRequest);
3525
+ const outcome = recentOutcomes.map((item) => compactLine(item)).find(Boolean);
3526
+ const file = hotFiles[0] ? compactFileHint(hotFiles[0]) : null;
3527
+ const tools = recentToolNames.slice(0, 2).join("/");
3528
+ if (outcome && file) {
3529
+ return `${outcome} · ${file}${tools ? ` · ${tools}` : ""}`;
3530
+ }
3531
+ if (request && file) {
3532
+ return `${request} · ${file}${tools ? ` · ${tools}` : ""}`;
3533
+ }
3534
+ if (outcome) {
3535
+ return `${outcome}${tools ? ` · ${tools}` : ""}`;
3536
+ }
3537
+ if (request) {
3538
+ return `${request}${tools ? ` · ${tools}` : ""}`;
3539
+ }
3540
+ return null;
3541
+ }
3542
+ function compactLine(value) {
3543
+ const trimmed = value?.replace(/\s+/g, " ").trim();
3544
+ if (!trimmed)
3545
+ return null;
3546
+ return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
3547
+ }
3548
+ function compactFileHint(value) {
3549
+ const parts = value.split("/");
3550
+ if (parts.length <= 2)
3551
+ return value;
3552
+ return parts.slice(-2).join("/");
3553
+ }
3554
+ function parseJsonArray(value) {
3555
+ if (!value)
3556
+ return [];
3557
+ try {
3558
+ const parsed = JSON.parse(value);
3559
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
3560
+ } catch {
3561
+ return [];
3562
+ }
3563
+ }
3564
+
3255
3565
  // hooks/post-tool-use.ts
3256
3566
  async function main() {
3257
3567
  const raw = await readStdin();
@@ -3450,8 +3760,12 @@ function updateRollingSummaryFromObservation(db, observationId, event, userId) {
3450
3760
  if (!update)
3451
3761
  return;
3452
3762
  const existing = db.getSessionSummary(event.session_id);
3763
+ const sessionPrompts = db.getSessionUserPrompts(event.session_id, 20);
3764
+ const sessionToolEvents = db.getSessionToolEvents(event.session_id, 20);
3765
+ const sessionObservations = db.getObservationsBySession(event.session_id);
3453
3766
  const merged = mergeLiveSummarySections(existing, update);
3454
- const currentRequest = existing?.request ?? db.getSessionUserPrompts(event.session_id, 1).at(-1)?.prompt ?? null;
3767
+ const handoff = buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, sessionObservations);
3768
+ const currentRequest = existing?.request ?? handoff.latest_request ?? null;
3455
3769
  const summary = db.upsertSessionSummary({
3456
3770
  session_id: event.session_id,
3457
3771
  project_id: observation.project_id,
@@ -3460,7 +3774,12 @@ function updateRollingSummaryFromObservation(db, observationId, event, userId) {
3460
3774
  investigated: merged.investigated,
3461
3775
  learned: merged.learned,
3462
3776
  completed: merged.completed,
3463
- next_steps: existing?.next_steps ?? null
3777
+ next_steps: existing?.next_steps ?? null,
3778
+ current_thread: handoff.current_thread,
3779
+ capture_state: handoff.capture_state,
3780
+ recent_tool_names: JSON.stringify(handoff.recent_tool_names),
3781
+ hot_files: JSON.stringify(handoff.hot_files),
3782
+ recent_outcomes: JSON.stringify(handoff.recent_outcomes)
3464
3783
  });
3465
3784
  db.addToOutbox("summary", summary.id);
3466
3785
  }