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.
@@ -421,7 +421,7 @@ var MIGRATIONS = [
421
421
  -- Sync outbox (offline-first queue)
422
422
  CREATE TABLE sync_outbox (
423
423
  id INTEGER PRIMARY KEY AUTOINCREMENT,
424
- record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary')),
424
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
425
425
  record_id INTEGER NOT NULL,
426
426
  status TEXT DEFAULT 'pending' CHECK (status IN (
427
427
  'pending', 'syncing', 'synced', 'failed'
@@ -716,6 +716,18 @@ var MIGRATIONS = [
716
716
  },
717
717
  {
718
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
+ `
728
+ },
729
+ {
730
+ version: 12,
719
731
  description: "Add synced handoff metadata to session summaries",
720
732
  sql: `
721
733
  ALTER TABLE session_summaries ADD COLUMN capture_state TEXT;
@@ -725,15 +737,79 @@ var MIGRATIONS = [
725
737
  `
726
738
  },
727
739
  {
728
- version: 11,
729
- description: "Add observation provenance from tool and prompt chronology",
740
+ version: 13,
741
+ description: "Add current_thread to session summaries",
730
742
  sql: `
731
- ALTER TABLE observations ADD COLUMN source_tool TEXT;
732
- ALTER TABLE observations ADD COLUMN source_prompt_number INTEGER;
733
- CREATE INDEX IF NOT EXISTS idx_observations_source_tool
734
- ON observations(source_tool, created_at_epoch DESC);
735
- CREATE INDEX IF NOT EXISTS idx_observations_source_prompt
736
- ON observations(session_id, source_prompt_number DESC);
743
+ ALTER TABLE session_summaries ADD COLUMN current_thread TEXT;
744
+ `
745
+ },
746
+ {
747
+ version: 14,
748
+ description: "Add chat_messages lane for raw conversation recall",
749
+ sql: `
750
+ CREATE TABLE IF NOT EXISTS chat_messages (
751
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
752
+ session_id TEXT NOT NULL,
753
+ project_id INTEGER REFERENCES projects(id),
754
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
755
+ content TEXT NOT NULL,
756
+ user_id TEXT NOT NULL,
757
+ device_id TEXT NOT NULL,
758
+ agent TEXT DEFAULT 'claude-code',
759
+ created_at_epoch INTEGER NOT NULL
760
+ );
761
+
762
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_session
763
+ ON chat_messages(session_id, created_at_epoch DESC, id DESC);
764
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_project
765
+ ON chat_messages(project_id, created_at_epoch DESC, id DESC);
766
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_created
767
+ ON chat_messages(created_at_epoch DESC, id DESC);
768
+ `
769
+ },
770
+ {
771
+ version: 15,
772
+ description: "Add remote_source_id for chat message sync deduplication",
773
+ sql: `
774
+ ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT;
775
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_source
776
+ ON chat_messages(remote_source_id)
777
+ WHERE remote_source_id IS NOT NULL;
778
+ `
779
+ },
780
+ {
781
+ version: 16,
782
+ description: "Allow chat_message records in sync_outbox",
783
+ sql: `
784
+ CREATE TABLE sync_outbox_new (
785
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
786
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
787
+ record_id INTEGER NOT NULL,
788
+ status TEXT DEFAULT 'pending' CHECK (status IN (
789
+ 'pending', 'syncing', 'synced', 'failed'
790
+ )),
791
+ retry_count INTEGER DEFAULT 0,
792
+ max_retries INTEGER DEFAULT 10,
793
+ last_error TEXT,
794
+ created_at_epoch INTEGER NOT NULL,
795
+ synced_at_epoch INTEGER,
796
+ next_retry_epoch INTEGER
797
+ );
798
+
799
+ INSERT INTO sync_outbox_new (
800
+ id, record_type, record_id, status, retry_count, max_retries,
801
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
802
+ )
803
+ SELECT
804
+ id, record_type, record_id, status, retry_count, max_retries,
805
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
806
+ FROM sync_outbox;
807
+
808
+ DROP TABLE sync_outbox;
809
+ ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
810
+
811
+ CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
812
+ CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
737
813
  `
738
814
  }
739
815
  ];
@@ -790,6 +866,21 @@ function inferLegacySchemaVersion(db) {
790
866
  version = Math.max(version, 10);
791
867
  if (columnExists(db, "observations", "source_tool"))
792
868
  version = Math.max(version, 11);
869
+ 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")) {
870
+ version = Math.max(version, 12);
871
+ }
872
+ if (columnExists(db, "session_summaries", "current_thread")) {
873
+ version = Math.max(version, 13);
874
+ }
875
+ if (tableExists(db, "chat_messages")) {
876
+ version = Math.max(version, 14);
877
+ }
878
+ if (columnExists(db, "chat_messages", "remote_source_id")) {
879
+ version = Math.max(version, 15);
880
+ }
881
+ if (syncOutboxSupportsChatMessages(db)) {
882
+ version = Math.max(version, 16);
883
+ }
793
884
  return version;
794
885
  }
795
886
  function runMigrations(db) {
@@ -868,6 +959,93 @@ function ensureObservationTypes(db) {
868
959
  }
869
960
  }
870
961
  }
962
+ function ensureSessionSummaryColumns(db) {
963
+ const required = [
964
+ "capture_state",
965
+ "recent_tool_names",
966
+ "hot_files",
967
+ "recent_outcomes",
968
+ "current_thread"
969
+ ];
970
+ for (const column of required) {
971
+ if (columnExists(db, "session_summaries", column))
972
+ continue;
973
+ db.exec(`ALTER TABLE session_summaries ADD COLUMN ${column} TEXT`);
974
+ }
975
+ const current = getSchemaVersion(db);
976
+ if (current < 13) {
977
+ db.exec("PRAGMA user_version = 13");
978
+ }
979
+ }
980
+ function ensureChatMessageColumns(db) {
981
+ if (!tableExists(db, "chat_messages"))
982
+ return;
983
+ if (!columnExists(db, "chat_messages", "remote_source_id")) {
984
+ db.exec("ALTER TABLE chat_messages ADD COLUMN remote_source_id TEXT");
985
+ }
986
+ 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");
987
+ const current = getSchemaVersion(db);
988
+ if (current < 15) {
989
+ db.exec("PRAGMA user_version = 15");
990
+ }
991
+ }
992
+ function ensureSyncOutboxSupportsChatMessages(db) {
993
+ if (syncOutboxSupportsChatMessages(db)) {
994
+ const current = getSchemaVersion(db);
995
+ if (current < 16) {
996
+ db.exec("PRAGMA user_version = 16");
997
+ }
998
+ return;
999
+ }
1000
+ db.exec("BEGIN TRANSACTION");
1001
+ try {
1002
+ db.exec(`
1003
+ CREATE TABLE sync_outbox_new (
1004
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1005
+ record_type TEXT NOT NULL CHECK (record_type IN ('observation', 'summary', 'chat_message')),
1006
+ record_id INTEGER NOT NULL,
1007
+ status TEXT DEFAULT 'pending' CHECK (status IN (
1008
+ 'pending', 'syncing', 'synced', 'failed'
1009
+ )),
1010
+ retry_count INTEGER DEFAULT 0,
1011
+ max_retries INTEGER DEFAULT 10,
1012
+ last_error TEXT,
1013
+ created_at_epoch INTEGER NOT NULL,
1014
+ synced_at_epoch INTEGER,
1015
+ next_retry_epoch INTEGER
1016
+ );
1017
+
1018
+ INSERT INTO sync_outbox_new (
1019
+ id, record_type, record_id, status, retry_count, max_retries,
1020
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
1021
+ )
1022
+ SELECT
1023
+ id, record_type, record_id, status, retry_count, max_retries,
1024
+ last_error, created_at_epoch, synced_at_epoch, next_retry_epoch
1025
+ FROM sync_outbox;
1026
+
1027
+ DROP TABLE sync_outbox;
1028
+ ALTER TABLE sync_outbox_new RENAME TO sync_outbox;
1029
+
1030
+ CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
1031
+ CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
1032
+ `);
1033
+ db.exec("PRAGMA user_version = 16");
1034
+ db.exec("COMMIT");
1035
+ } catch (error) {
1036
+ db.exec("ROLLBACK");
1037
+ throw new Error(`sync_outbox repair failed: ${error instanceof Error ? error.message : String(error)}`);
1038
+ }
1039
+ }
1040
+ function syncOutboxSupportsChatMessages(db) {
1041
+ const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get("sync_outbox");
1042
+ const sql = row?.sql ?? "";
1043
+ return sql.includes("'chat_message'");
1044
+ }
1045
+ function getSchemaVersion(db) {
1046
+ const result = db.query("PRAGMA user_version").get();
1047
+ return result.user_version;
1048
+ }
871
1049
  var LATEST_SCHEMA_VERSION = MIGRATIONS.filter((m) => !m.condition).reduce((max, m) => Math.max(max, m.version), 0);
872
1050
 
873
1051
  // src/storage/sqlite.ts
@@ -1022,6 +1200,9 @@ class MemDatabase {
1022
1200
  this.vecAvailable = this.loadVecExtension();
1023
1201
  runMigrations(this.db);
1024
1202
  ensureObservationTypes(this.db);
1203
+ ensureSessionSummaryColumns(this.db);
1204
+ ensureChatMessageColumns(this.db);
1205
+ ensureSyncOutboxSupportsChatMessages(this.db);
1025
1206
  }
1026
1207
  loadVecExtension() {
1027
1208
  try {
@@ -1247,6 +1428,7 @@ class MemDatabase {
1247
1428
  p.name AS project_name,
1248
1429
  ss.request AS request,
1249
1430
  ss.completed AS completed,
1431
+ ss.current_thread AS current_thread,
1250
1432
  ss.capture_state AS capture_state,
1251
1433
  ss.recent_tool_names AS recent_tool_names,
1252
1434
  ss.hot_files AS hot_files,
@@ -1265,6 +1447,7 @@ class MemDatabase {
1265
1447
  p.name AS project_name,
1266
1448
  ss.request AS request,
1267
1449
  ss.completed AS completed,
1450
+ ss.current_thread AS current_thread,
1268
1451
  ss.capture_state AS capture_state,
1269
1452
  ss.recent_tool_names AS recent_tool_names,
1270
1453
  ss.hot_files AS hot_files,
@@ -1355,6 +1538,54 @@ class MemDatabase {
1355
1538
  ORDER BY created_at_epoch DESC, id DESC
1356
1539
  LIMIT ?`).all(...userId ? [userId] : [], limit);
1357
1540
  }
1541
+ insertChatMessage(input) {
1542
+ const createdAt = input.created_at_epoch ?? Math.floor(Date.now() / 1000);
1543
+ const content = input.content.trim();
1544
+ const result = this.db.query(`INSERT INTO chat_messages (
1545
+ session_id, project_id, role, content, user_id, device_id, agent, created_at_epoch, remote_source_id
1546
+ ) 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);
1547
+ return this.getChatMessageById(Number(result.lastInsertRowid));
1548
+ }
1549
+ getChatMessageById(id) {
1550
+ return this.db.query("SELECT * FROM chat_messages WHERE id = ?").get(id) ?? null;
1551
+ }
1552
+ getChatMessageByRemoteSourceId(remoteSourceId) {
1553
+ return this.db.query("SELECT * FROM chat_messages WHERE remote_source_id = ?").get(remoteSourceId) ?? null;
1554
+ }
1555
+ getSessionChatMessages(sessionId, limit = 50) {
1556
+ return this.db.query(`SELECT * FROM chat_messages
1557
+ WHERE session_id = ?
1558
+ ORDER BY created_at_epoch ASC, id ASC
1559
+ LIMIT ?`).all(sessionId, limit);
1560
+ }
1561
+ getRecentChatMessages(projectId, limit = 20, userId) {
1562
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1563
+ if (projectId !== null) {
1564
+ return this.db.query(`SELECT * FROM chat_messages
1565
+ WHERE project_id = ?${visibilityClause}
1566
+ ORDER BY created_at_epoch DESC, id DESC
1567
+ LIMIT ?`).all(projectId, ...userId ? [userId] : [], limit);
1568
+ }
1569
+ return this.db.query(`SELECT * FROM chat_messages
1570
+ WHERE 1 = 1${visibilityClause}
1571
+ ORDER BY created_at_epoch DESC, id DESC
1572
+ LIMIT ?`).all(...userId ? [userId] : [], limit);
1573
+ }
1574
+ searchChatMessages(query, projectId, limit = 20, userId) {
1575
+ const needle = `%${query.toLowerCase()}%`;
1576
+ const visibilityClause = userId ? " AND user_id = ?" : "";
1577
+ if (projectId !== null) {
1578
+ return this.db.query(`SELECT * FROM chat_messages
1579
+ WHERE project_id = ?
1580
+ AND lower(content) LIKE ?${visibilityClause}
1581
+ ORDER BY created_at_epoch DESC, id DESC
1582
+ LIMIT ?`).all(projectId, needle, ...userId ? [userId] : [], limit);
1583
+ }
1584
+ return this.db.query(`SELECT * FROM chat_messages
1585
+ WHERE lower(content) LIKE ?${visibilityClause}
1586
+ ORDER BY created_at_epoch DESC, id DESC
1587
+ LIMIT ?`).all(needle, ...userId ? [userId] : [], limit);
1588
+ }
1358
1589
  addToOutbox(recordType, recordId) {
1359
1590
  const now = Math.floor(Date.now() / 1000);
1360
1591
  this.db.query(`INSERT INTO sync_outbox (record_type, record_id, created_at_epoch)
@@ -1443,9 +1674,9 @@ class MemDatabase {
1443
1674
  };
1444
1675
  const result = this.db.query(`INSERT INTO session_summaries (
1445
1676
  session_id, project_id, user_id, request, investigated, learned, completed, next_steps,
1446
- capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
1677
+ current_thread, capture_state, recent_tool_names, hot_files, recent_outcomes, created_at_epoch
1447
1678
  )
1448
- 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);
1679
+ 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);
1449
1680
  const id = Number(result.lastInsertRowid);
1450
1681
  return this.db.query("SELECT * FROM session_summaries WHERE id = ?").get(id);
1451
1682
  }
@@ -1461,6 +1692,7 @@ class MemDatabase {
1461
1692
  learned: normalizeSummarySection(summary.learned ?? existing.learned),
1462
1693
  completed: normalizeSummarySection(summary.completed ?? existing.completed),
1463
1694
  next_steps: normalizeSummarySection(summary.next_steps ?? existing.next_steps),
1695
+ current_thread: summary.current_thread ?? existing.current_thread,
1464
1696
  capture_state: summary.capture_state ?? existing.capture_state,
1465
1697
  recent_tool_names: summary.recent_tool_names ?? existing.recent_tool_names,
1466
1698
  hot_files: summary.hot_files ?? existing.hot_files,
@@ -1474,12 +1706,13 @@ class MemDatabase {
1474
1706
  learned = ?,
1475
1707
  completed = ?,
1476
1708
  next_steps = ?,
1709
+ current_thread = ?,
1477
1710
  capture_state = ?,
1478
1711
  recent_tool_names = ?,
1479
1712
  hot_files = ?,
1480
1713
  recent_outcomes = ?,
1481
1714
  created_at_epoch = ?
1482
- 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);
1715
+ 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);
1483
1716
  return this.getSessionSummary(summary.session_id);
1484
1717
  }
1485
1718
  getSessionSummary(sessionId) {
@@ -1626,6 +1859,83 @@ function runHook(hookName, fn) {
1626
1859
  });
1627
1860
  }
1628
1861
 
1862
+ // src/capture/session-handoff.ts
1863
+ function buildSessionHandoffMetadata(prompts, toolEvents, observations) {
1864
+ const latestRequest = prompts.length > 0 ? prompts[prompts.length - 1]?.prompt ?? null : null;
1865
+ const recentRequestPrompts = prompts.slice(-3).map((prompt) => prompt.prompt.trim()).filter(Boolean);
1866
+ const recentToolNames = [...new Set(toolEvents.slice(-8).map((tool) => tool.tool_name).filter(Boolean))];
1867
+ const recentToolCommands = [...new Set(toolEvents.slice(-5).map((tool) => (tool.command ?? tool.file_path ?? "").trim()).filter(Boolean))];
1868
+ const hotFiles = [...new Set(observations.flatMap((obs) => [
1869
+ ...parseJsonArray(obs.files_modified),
1870
+ ...parseJsonArray(obs.files_read)
1871
+ ]).filter(Boolean))].slice(0, 6);
1872
+ 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);
1873
+ const captureState = prompts.length > 0 && toolEvents.length > 0 ? "rich" : prompts.length > 0 || toolEvents.length > 0 ? "partial" : "summary-only";
1874
+ const observationSourceTools = Array.from(observations.reduce((acc, obs) => {
1875
+ if (!obs.source_tool)
1876
+ return acc;
1877
+ acc.set(obs.source_tool, (acc.get(obs.source_tool) ?? 0) + 1);
1878
+ return acc;
1879
+ }, new Map).entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 6);
1880
+ const latestObservationPromptNumber = observations.map((obs) => obs.source_prompt_number).filter((value) => typeof value === "number").sort((a, b) => b - a)[0] ?? null;
1881
+ const currentThread = buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames);
1882
+ return {
1883
+ prompt_count: prompts.length,
1884
+ tool_event_count: toolEvents.length,
1885
+ recent_request_prompts: recentRequestPrompts,
1886
+ latest_request: latestRequest,
1887
+ current_thread: currentThread,
1888
+ recent_tool_names: recentToolNames,
1889
+ recent_tool_commands: recentToolCommands,
1890
+ capture_state: captureState,
1891
+ hot_files: hotFiles,
1892
+ recent_outcomes: recentOutcomes,
1893
+ observation_source_tools: observationSourceTools,
1894
+ latest_observation_prompt_number: latestObservationPromptNumber
1895
+ };
1896
+ }
1897
+ function buildCurrentThread(latestRequest, recentOutcomes, hotFiles, recentToolNames) {
1898
+ const request = compactLine(latestRequest);
1899
+ const outcome = recentOutcomes.map((item) => compactLine(item)).find(Boolean);
1900
+ const file = hotFiles[0] ? compactFileHint(hotFiles[0]) : null;
1901
+ const tools = recentToolNames.slice(0, 2).join("/");
1902
+ if (outcome && file) {
1903
+ return `${outcome} · ${file}${tools ? ` · ${tools}` : ""}`;
1904
+ }
1905
+ if (request && file) {
1906
+ return `${request} · ${file}${tools ? ` · ${tools}` : ""}`;
1907
+ }
1908
+ if (outcome) {
1909
+ return `${outcome}${tools ? ` · ${tools}` : ""}`;
1910
+ }
1911
+ if (request) {
1912
+ return `${request}${tools ? ` · ${tools}` : ""}`;
1913
+ }
1914
+ return null;
1915
+ }
1916
+ function compactLine(value) {
1917
+ const trimmed = value?.replace(/\s+/g, " ").trim();
1918
+ if (!trimmed)
1919
+ return null;
1920
+ return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
1921
+ }
1922
+ function compactFileHint(value) {
1923
+ const parts = value.split("/");
1924
+ if (parts.length <= 2)
1925
+ return value;
1926
+ return parts.slice(-2).join("/");
1927
+ }
1928
+ function parseJsonArray(value) {
1929
+ if (!value)
1930
+ return [];
1931
+ try {
1932
+ const parsed = JSON.parse(value);
1933
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
1934
+ } catch {
1935
+ return [];
1936
+ }
1937
+ }
1938
+
1629
1939
  // hooks/user-prompt-submit.ts
1630
1940
  async function main() {
1631
1941
  const event = await parseStdinJson();
@@ -1653,8 +1963,22 @@ async function main() {
1653
1963
  device_id: config.device_id,
1654
1964
  agent: "claude-code"
1655
1965
  });
1966
+ const chatMessage = db.insertChatMessage({
1967
+ session_id: event.session_id,
1968
+ project_id: project.id,
1969
+ role: "user",
1970
+ content: event.prompt,
1971
+ user_id: config.user_id,
1972
+ device_id: config.device_id,
1973
+ agent: "claude-code"
1974
+ });
1975
+ db.addToOutbox("chat_message", chatMessage.id);
1656
1976
  const compactPrompt = event.prompt.replace(/\s+/g, " ").trim();
1657
1977
  if (compactPrompt.length >= 8) {
1978
+ const sessionPrompts = db.getSessionUserPrompts(event.session_id, 20);
1979
+ const sessionToolEvents = db.getSessionToolEvents(event.session_id, 20);
1980
+ const sessionObservations = db.getObservationsBySession(event.session_id);
1981
+ const handoff = buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, sessionObservations);
1658
1982
  const summary = db.upsertSessionSummary({
1659
1983
  session_id: event.session_id,
1660
1984
  project_id: project.id,
@@ -1663,7 +1987,12 @@ async function main() {
1663
1987
  investigated: null,
1664
1988
  learned: null,
1665
1989
  completed: null,
1666
- next_steps: null
1990
+ next_steps: null,
1991
+ current_thread: handoff.current_thread,
1992
+ capture_state: handoff.capture_state,
1993
+ recent_tool_names: JSON.stringify(handoff.recent_tool_names),
1994
+ hot_files: JSON.stringify(handoff.hot_files),
1995
+ recent_outcomes: JSON.stringify(handoff.recent_outcomes)
1667
1996
  });
1668
1997
  db.addToOutbox("summary", summary.id);
1669
1998
  }