claude-memory-layer 1.0.10 → 1.0.11

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.
Files changed (74) hide show
  1. package/dist/cli/index.js +1266 -181
  2. package/dist/cli/index.js.map +4 -4
  3. package/dist/core/index.js +367 -7
  4. package/dist/core/index.js.map +2 -2
  5. package/dist/hooks/post-tool-use.js +598 -40
  6. package/dist/hooks/post-tool-use.js.map +4 -4
  7. package/dist/hooks/session-end.js +486 -49
  8. package/dist/hooks/session-end.js.map +3 -3
  9. package/dist/hooks/session-start.js +474 -22
  10. package/dist/hooks/session-start.js.map +3 -3
  11. package/dist/hooks/stop.js +586 -70
  12. package/dist/hooks/stop.js.map +4 -4
  13. package/dist/hooks/user-prompt-submit.js +537 -27
  14. package/dist/hooks/user-prompt-submit.js.map +4 -4
  15. package/dist/server/api/index.js +938 -39
  16. package/dist/server/api/index.js.map +4 -4
  17. package/dist/server/index.js +947 -48
  18. package/dist/server/index.js.map +4 -4
  19. package/dist/services/memory-service.js +475 -22
  20. package/dist/services/memory-service.js.map +3 -3
  21. package/dist/ui/app.js +1380 -55
  22. package/dist/ui/index.html +311 -148
  23. package/dist/ui/style.css +892 -0
  24. package/docs/OPERATIONS.md +18 -0
  25. package/package.json +8 -2
  26. package/scripts/fix-sync-gap.js +32 -0
  27. package/scripts/heartbeat-memory-orchestrator.sh +28 -0
  28. package/scripts/report-sync-gap.js +26 -0
  29. package/scripts/review-queue-auto-resolve.js +21 -0
  30. package/scripts/sync-gap-auto-heal.sh +17 -0
  31. package/specs/20260207-dashboard-upgrade/context.md +38 -0
  32. package/specs/20260207-dashboard-upgrade/spec.md +96 -0
  33. package/src/cli/index.ts +110 -58
  34. package/src/core/sqlite-event-store.ts +444 -6
  35. package/src/core/sqlite-wrapper.ts +8 -0
  36. package/src/core/turn-state.ts +159 -0
  37. package/src/core/types.ts +23 -8
  38. package/src/core/vector-store.ts +21 -3
  39. package/src/hooks/post-tool-use.ts +68 -23
  40. package/src/hooks/session-end.ts +8 -3
  41. package/src/hooks/stop.ts +96 -25
  42. package/src/hooks/user-prompt-submit.ts +44 -5
  43. package/src/server/api/chat.ts +244 -0
  44. package/src/server/api/citations.ts +3 -3
  45. package/src/server/api/events.ts +30 -5
  46. package/src/server/api/index.ts +7 -1
  47. package/src/server/api/projects.ts +74 -0
  48. package/src/server/api/search.ts +3 -3
  49. package/src/server/api/sessions.ts +3 -3
  50. package/src/server/api/stats.ts +43 -7
  51. package/src/server/api/turns.ts +143 -0
  52. package/src/server/api/utils.ts +46 -0
  53. package/src/services/memory-service.ts +137 -5
  54. package/src/services/session-history-importer.ts +215 -51
  55. package/src/ui/app.js +1380 -55
  56. package/src/ui/index.html +311 -148
  57. package/src/ui/style.css +892 -0
  58. package/.claude/settings.local.json +0 -27
  59. package/.claude-memory/test.sqlite +0 -0
  60. package/.history/package_20260201112328.json +0 -45
  61. package/.history/package_20260201113602.json +0 -45
  62. package/.history/package_20260201113713.json +0 -45
  63. package/.history/package_20260201114110.json +0 -45
  64. package/.history/package_20260201114632.json +0 -46
  65. package/.history/package_20260201133143.json +0 -45
  66. package/.history/package_20260201134319.json +0 -45
  67. package/.history/package_20260201134326.json +0 -45
  68. package/.history/package_20260201134334.json +0 -45
  69. package/.history/package_20260201134912.json +0 -45
  70. package/.history/package_20260201142928.json +0 -46
  71. package/.history/package_20260201192048.json +0 -47
  72. package/.history/package_20260202114053.json +0 -49
  73. package/.history/package_20260202121115.json +0 -49
  74. package/test_access.js +0 -49
@@ -4,17 +4,28 @@ import { dirname } from 'path';
4
4
  const require = createRequire(import.meta.url);
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = dirname(__filename);
7
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
+ }) : x)(function(x) {
10
+ if (typeof require !== "undefined")
11
+ return require.apply(this, arguments);
12
+ throw Error('Dynamic require of "' + x + '" is not supported');
13
+ });
7
14
 
8
15
  // src/server/api/index.ts
9
- import { Hono as Hono6 } from "hono";
16
+ import { Hono as Hono9 } from "hono";
10
17
 
11
18
  // src/server/api/sessions.ts
12
19
  import { Hono } from "hono";
13
20
 
21
+ // src/server/api/utils.ts
22
+ import * as path2 from "path";
23
+ import * as os2 from "os";
24
+
14
25
  // src/services/memory-service.ts
15
26
  import * as path from "path";
16
27
  import * as os from "os";
17
- import * as fs from "fs";
28
+ import * as fs2 from "fs";
18
29
  import * as crypto2 from "crypto";
19
30
 
20
31
  // src/core/event-store.ts
@@ -72,11 +83,11 @@ function toDate(value) {
72
83
  return new Date(value);
73
84
  return new Date(String(value));
74
85
  }
75
- function createDatabase(path2, options) {
86
+ function createDatabase(path4, options) {
76
87
  if (options?.readOnly) {
77
- return new duckdb.Database(path2, { access_mode: "READ_ONLY" });
88
+ return new duckdb.Database(path4, { access_mode: "READ_ONLY" });
78
89
  }
79
- return new duckdb.Database(path2);
90
+ return new duckdb.Database(path4);
80
91
  }
81
92
  function dbRun(db, sql, params = []) {
82
93
  return new Promise((resolve2, reject) => {
@@ -746,8 +757,14 @@ import { randomUUID as randomUUID2 } from "crypto";
746
757
 
747
758
  // src/core/sqlite-wrapper.ts
748
759
  import Database from "better-sqlite3";
749
- function createSQLiteDatabase(path2, options) {
750
- const db = new Database(path2, {
760
+ import * as fs from "fs";
761
+ import * as nodePath from "path";
762
+ function createSQLiteDatabase(path4, options) {
763
+ const dir = nodePath.dirname(path4);
764
+ if (!fs.existsSync(dir)) {
765
+ fs.mkdirSync(dir, { recursive: true });
766
+ }
767
+ const db = new Database(path4, {
751
768
  readonly: options?.readonly ?? false
752
769
  });
753
770
  if (!options?.readonly && (options?.walMode ?? true)) {
@@ -1014,6 +1031,23 @@ var SQLiteEventStore = class {
1014
1031
  updated_at TEXT DEFAULT (datetime('now'))
1015
1032
  );
1016
1033
 
1034
+ -- Memory Helpfulness tracking
1035
+ CREATE TABLE IF NOT EXISTS memory_helpfulness (
1036
+ id TEXT PRIMARY KEY,
1037
+ event_id TEXT NOT NULL,
1038
+ session_id TEXT NOT NULL,
1039
+ retrieval_score REAL DEFAULT 0,
1040
+ query_preview TEXT,
1041
+ session_continued INTEGER DEFAULT 0,
1042
+ prompt_count_after INTEGER DEFAULT 0,
1043
+ tool_success_count INTEGER DEFAULT 0,
1044
+ tool_total_count INTEGER DEFAULT 0,
1045
+ was_reasked INTEGER DEFAULT 0,
1046
+ helpfulness_score REAL DEFAULT 0.5,
1047
+ created_at TEXT DEFAULT (datetime('now')),
1048
+ measured_at TEXT
1049
+ );
1050
+
1017
1051
  -- Sync position tracking (for SQLite -> DuckDB sync)
1018
1052
  CREATE TABLE IF NOT EXISTS sync_positions (
1019
1053
  target_name TEXT PRIMARY KEY,
@@ -1039,6 +1073,9 @@ var SQLiteEventStore = class {
1039
1073
  CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
1040
1074
  CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
1041
1075
  CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
1076
+ CREATE INDEX IF NOT EXISTS idx_helpfulness_event ON memory_helpfulness(event_id);
1077
+ CREATE INDEX IF NOT EXISTS idx_helpfulness_session ON memory_helpfulness(session_id);
1078
+ CREATE INDEX IF NOT EXISTS idx_helpfulness_score ON memory_helpfulness(helpfulness_score DESC);
1042
1079
 
1043
1080
  -- FTS5 Full-Text Search for fast keyword search
1044
1081
  CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
@@ -1082,6 +1119,15 @@ var SQLiteEventStore = class {
1082
1119
  console.error("Error adding last_accessed_at column:", err);
1083
1120
  }
1084
1121
  }
1122
+ if (!columnNames.includes("turn_id")) {
1123
+ try {
1124
+ sqliteExec(this.db, `
1125
+ ALTER TABLE events ADD COLUMN turn_id TEXT;
1126
+ `);
1127
+ } catch (err) {
1128
+ console.error("Error adding turn_id column:", err);
1129
+ }
1130
+ }
1085
1131
  try {
1086
1132
  sqliteExec(this.db, `
1087
1133
  CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
@@ -1094,6 +1140,12 @@ var SQLiteEventStore = class {
1094
1140
  `);
1095
1141
  } catch (err) {
1096
1142
  }
1143
+ try {
1144
+ sqliteExec(this.db, `
1145
+ CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
1146
+ `);
1147
+ } catch (err) {
1148
+ }
1097
1149
  this.initialized = true;
1098
1150
  }
1099
1151
  /**
@@ -1118,9 +1170,11 @@ var SQLiteEventStore = class {
1118
1170
  const id = randomUUID2();
1119
1171
  const timestamp = toSQLiteTimestamp(input.timestamp);
1120
1172
  try {
1173
+ const metadata = input.metadata || {};
1174
+ const turnId = metadata.turnId || null;
1121
1175
  const insertEvent = this.db.prepare(`
1122
- INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
1123
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1176
+ INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
1177
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1124
1178
  `);
1125
1179
  const insertDedup = this.db.prepare(`
1126
1180
  INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
@@ -1137,7 +1191,8 @@ var SQLiteEventStore = class {
1137
1191
  input.content,
1138
1192
  canonicalKey,
1139
1193
  dedupeKey,
1140
- JSON.stringify(input.metadata || {})
1194
+ JSON.stringify(metadata),
1195
+ turnId
1141
1196
  );
1142
1197
  insertDedup.run(dedupeKey, id);
1143
1198
  insertLevel.run(id);
@@ -1487,11 +1542,11 @@ var SQLiteEventStore = class {
1487
1542
  );
1488
1543
  }
1489
1544
  /**
1490
- * Get most accessed memories
1545
+ * Get most accessed memories (falls back to recent events if none accessed)
1491
1546
  */
1492
1547
  async getMostAccessed(limit = 10) {
1493
1548
  await this.initialize();
1494
- const rows = sqliteAll(
1549
+ let rows = sqliteAll(
1495
1550
  this.db,
1496
1551
  `SELECT * FROM events
1497
1552
  WHERE access_count > 0
@@ -1499,8 +1554,166 @@ var SQLiteEventStore = class {
1499
1554
  LIMIT ?`,
1500
1555
  [limit]
1501
1556
  );
1557
+ if (rows.length === 0) {
1558
+ rows = sqliteAll(
1559
+ this.db,
1560
+ `SELECT * FROM events
1561
+ ORDER BY timestamp DESC
1562
+ LIMIT ?`,
1563
+ [limit]
1564
+ );
1565
+ }
1502
1566
  return rows.map((row) => this.rowToEvent(row));
1503
1567
  }
1568
+ /**
1569
+ * Record a memory retrieval for helpfulness tracking
1570
+ */
1571
+ async recordRetrieval(eventId, sessionId, score, query) {
1572
+ if (this.readOnly)
1573
+ return;
1574
+ await this.initialize();
1575
+ const id = randomUUID2();
1576
+ sqliteRun(
1577
+ this.db,
1578
+ `INSERT INTO memory_helpfulness (id, event_id, session_id, retrieval_score, query_preview, created_at)
1579
+ VALUES (?, ?, ?, ?, ?, datetime('now'))`,
1580
+ [id, eventId, sessionId, score, query.slice(0, 100)]
1581
+ );
1582
+ }
1583
+ /**
1584
+ * Evaluate helpfulness for all retrievals in a session
1585
+ * Called at session end - uses behavioral signals to compute score
1586
+ */
1587
+ async evaluateSessionHelpfulness(sessionId) {
1588
+ if (this.readOnly)
1589
+ return;
1590
+ await this.initialize();
1591
+ const retrievals = sqliteAll(
1592
+ this.db,
1593
+ `SELECT * FROM memory_helpfulness WHERE session_id = ? AND measured_at IS NULL`,
1594
+ [sessionId]
1595
+ );
1596
+ if (retrievals.length === 0)
1597
+ return;
1598
+ const sessionEvents = sqliteAll(
1599
+ this.db,
1600
+ `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
1601
+ [sessionId]
1602
+ );
1603
+ const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
1604
+ const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
1605
+ let toolSuccessCount = 0;
1606
+ let toolTotalCount = toolEvents.length;
1607
+ for (const t of toolEvents) {
1608
+ try {
1609
+ const content = JSON.parse(t.content);
1610
+ if (content.success !== false)
1611
+ toolSuccessCount++;
1612
+ } catch {
1613
+ toolSuccessCount++;
1614
+ }
1615
+ }
1616
+ const toolSuccessRatio = toolTotalCount > 0 ? toolSuccessCount / toolTotalCount : 0.5;
1617
+ for (const retrieval of retrievals) {
1618
+ const retrievalTime = retrieval.created_at;
1619
+ const eventsAfter = sessionEvents.filter((e) => e.timestamp > retrievalTime);
1620
+ const sessionContinued = eventsAfter.length > 0 ? 1 : 0;
1621
+ const promptsAfter = promptEvents.filter((e) => e.timestamp > retrievalTime);
1622
+ const promptCountAfter = promptsAfter.length;
1623
+ const queryWords = new Set((retrieval.query_preview || "").toLowerCase().split(/\s+/).filter((w) => w.length > 2));
1624
+ let wasReasked = 0;
1625
+ for (const p of promptsAfter) {
1626
+ const pWords = new Set(p.content.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
1627
+ let overlap = 0;
1628
+ for (const w of queryWords) {
1629
+ if (pWords.has(w))
1630
+ overlap++;
1631
+ }
1632
+ if (queryWords.size > 0 && overlap / queryWords.size > 0.5) {
1633
+ wasReasked = 1;
1634
+ break;
1635
+ }
1636
+ }
1637
+ const retrievalScore = retrieval.retrieval_score || 0;
1638
+ const helpfulnessScore = 0.3 * Math.min(retrievalScore, 1) + 0.25 * (sessionContinued ? 1 : 0) + 0.25 * toolSuccessRatio + 0.2 * (wasReasked ? 0 : 1);
1639
+ sqliteRun(
1640
+ this.db,
1641
+ `UPDATE memory_helpfulness
1642
+ SET session_continued = ?, prompt_count_after = ?,
1643
+ tool_success_count = ?, tool_total_count = ?,
1644
+ was_reasked = ?, helpfulness_score = ?,
1645
+ measured_at = datetime('now')
1646
+ WHERE id = ?`,
1647
+ [
1648
+ sessionContinued,
1649
+ promptCountAfter,
1650
+ toolSuccessCount,
1651
+ toolTotalCount,
1652
+ wasReasked,
1653
+ helpfulnessScore,
1654
+ retrieval.id
1655
+ ]
1656
+ );
1657
+ }
1658
+ }
1659
+ /**
1660
+ * Get most helpful memories ranked by helpfulness score
1661
+ */
1662
+ async getHelpfulMemories(limit = 10) {
1663
+ await this.initialize();
1664
+ const rows = sqliteAll(
1665
+ this.db,
1666
+ `SELECT
1667
+ mh.event_id,
1668
+ AVG(mh.helpfulness_score) as avg_score,
1669
+ COUNT(*) as eval_count,
1670
+ e.content,
1671
+ e.access_count
1672
+ FROM memory_helpfulness mh
1673
+ JOIN events e ON e.id = mh.event_id
1674
+ WHERE mh.measured_at IS NOT NULL
1675
+ GROUP BY mh.event_id
1676
+ ORDER BY avg_score DESC
1677
+ LIMIT ?`,
1678
+ [limit]
1679
+ );
1680
+ return rows.map((r) => ({
1681
+ eventId: r.event_id,
1682
+ summary: r.content.substring(0, 200) + (r.content.length > 200 ? "..." : ""),
1683
+ helpfulnessScore: Math.round(r.avg_score * 100) / 100,
1684
+ accessCount: r.access_count || 0,
1685
+ evaluationCount: r.eval_count
1686
+ }));
1687
+ }
1688
+ /**
1689
+ * Get helpfulness statistics for dashboard
1690
+ */
1691
+ async getHelpfulnessStats() {
1692
+ await this.initialize();
1693
+ const stats = sqliteGet(
1694
+ this.db,
1695
+ `SELECT
1696
+ AVG(helpfulness_score) as avg_score,
1697
+ COUNT(*) as total_evaluated,
1698
+ SUM(CASE WHEN helpfulness_score >= 0.7 THEN 1 ELSE 0 END) as helpful,
1699
+ SUM(CASE WHEN helpfulness_score >= 0.4 AND helpfulness_score < 0.7 THEN 1 ELSE 0 END) as neutral,
1700
+ SUM(CASE WHEN helpfulness_score < 0.4 THEN 1 ELSE 0 END) as unhelpful
1701
+ FROM memory_helpfulness
1702
+ WHERE measured_at IS NOT NULL`
1703
+ );
1704
+ const totalRow = sqliteGet(
1705
+ this.db,
1706
+ `SELECT COUNT(*) as total FROM memory_helpfulness`
1707
+ );
1708
+ return {
1709
+ avgScore: Math.round((stats?.avg_score || 0) * 100) / 100,
1710
+ totalEvaluated: stats?.total_evaluated || 0,
1711
+ totalRetrievals: totalRow?.total || 0,
1712
+ helpful: stats?.helpful || 0,
1713
+ neutral: stats?.neutral || 0,
1714
+ unhelpful: stats?.unhelpful || 0
1715
+ };
1716
+ }
1504
1717
  /**
1505
1718
  * Fast keyword search using FTS5
1506
1719
  * Returns events matching the search query, ranked by relevance
@@ -1569,6 +1782,143 @@ var SQLiteEventStore = class {
1569
1782
  async close() {
1570
1783
  sqliteClose(this.db);
1571
1784
  }
1785
+ /**
1786
+ * Get events grouped by turn_id for a session
1787
+ * Returns turns ordered by first event timestamp (newest first)
1788
+ */
1789
+ async getSessionTurns(sessionId, options) {
1790
+ await this.initialize();
1791
+ const limit = options?.limit || 20;
1792
+ const offset = options?.offset || 0;
1793
+ const turnRows = sqliteAll(
1794
+ this.db,
1795
+ `SELECT turn_id, MIN(timestamp) as min_ts
1796
+ FROM events
1797
+ WHERE session_id = ? AND turn_id IS NOT NULL
1798
+ GROUP BY turn_id
1799
+ ORDER BY min_ts DESC
1800
+ LIMIT ? OFFSET ?`,
1801
+ [sessionId, limit, offset]
1802
+ );
1803
+ const turns = [];
1804
+ for (const turnRow of turnRows) {
1805
+ const events = await this.getEventsByTurn(turnRow.turn_id);
1806
+ const promptEvent = events.find((e) => e.eventType === "user_prompt");
1807
+ const toolEvents = events.filter((e) => e.eventType === "tool_observation");
1808
+ const hasResponse = events.some((e) => e.eventType === "agent_response");
1809
+ turns.push({
1810
+ turnId: turnRow.turn_id,
1811
+ events,
1812
+ startedAt: toDateFromSQLite(turnRow.min_ts),
1813
+ promptPreview: promptEvent ? promptEvent.content.slice(0, 200) + (promptEvent.content.length > 200 ? "..." : "") : "(no prompt)",
1814
+ eventCount: events.length,
1815
+ toolCount: toolEvents.length,
1816
+ hasResponse
1817
+ });
1818
+ }
1819
+ return turns;
1820
+ }
1821
+ /**
1822
+ * Get all events for a specific turn_id
1823
+ */
1824
+ async getEventsByTurn(turnId) {
1825
+ await this.initialize();
1826
+ const rows = sqliteAll(
1827
+ this.db,
1828
+ `SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
1829
+ [turnId]
1830
+ );
1831
+ return rows.map(this.rowToEvent);
1832
+ }
1833
+ /**
1834
+ * Count total turns for a session
1835
+ */
1836
+ async countSessionTurns(sessionId) {
1837
+ await this.initialize();
1838
+ const row = sqliteGet(
1839
+ this.db,
1840
+ `SELECT COUNT(DISTINCT turn_id) as count
1841
+ FROM events
1842
+ WHERE session_id = ? AND turn_id IS NOT NULL`,
1843
+ [sessionId]
1844
+ );
1845
+ return row?.count || 0;
1846
+ }
1847
+ /**
1848
+ * Migrate existing events: backfill turn_id for events that have turnId in metadata
1849
+ * but no turn_id column value (for events stored before this migration)
1850
+ */
1851
+ async backfillTurnIds() {
1852
+ await this.initialize();
1853
+ const rows = sqliteAll(
1854
+ this.db,
1855
+ `SELECT id, metadata FROM events
1856
+ WHERE turn_id IS NULL AND metadata IS NOT NULL AND metadata LIKE '%turnId%'`
1857
+ );
1858
+ let updated = 0;
1859
+ for (const row of rows) {
1860
+ try {
1861
+ const metadata = JSON.parse(row.metadata);
1862
+ if (metadata.turnId) {
1863
+ sqliteRun(
1864
+ this.db,
1865
+ `UPDATE events SET turn_id = ? WHERE id = ?`,
1866
+ [metadata.turnId, row.id]
1867
+ );
1868
+ updated++;
1869
+ }
1870
+ } catch {
1871
+ }
1872
+ }
1873
+ return updated;
1874
+ }
1875
+ /**
1876
+ * Delete all events for a session (for force reimport)
1877
+ */
1878
+ async deleteSessionEvents(sessionId) {
1879
+ await this.initialize();
1880
+ const events = sqliteAll(
1881
+ this.db,
1882
+ `SELECT id FROM events WHERE session_id = ?`,
1883
+ [sessionId]
1884
+ );
1885
+ if (events.length === 0)
1886
+ return 0;
1887
+ const eventIds = events.map((e) => e.id);
1888
+ const placeholders = eventIds.map(() => "?").join(",");
1889
+ const ftsTriggersDropped = [];
1890
+ for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
1891
+ try {
1892
+ sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
1893
+ ftsTriggersDropped.push(triggerName);
1894
+ } catch {
1895
+ }
1896
+ }
1897
+ for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
1898
+ try {
1899
+ sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders})`, eventIds);
1900
+ } catch {
1901
+ }
1902
+ }
1903
+ const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
1904
+ if (ftsTriggersDropped.length > 0) {
1905
+ try {
1906
+ sqliteRun(this.db, `INSERT INTO events_fts(events_fts) VALUES('rebuild')`);
1907
+ sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
1908
+ INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
1909
+ END`);
1910
+ sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
1911
+ INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
1912
+ END`);
1913
+ sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
1914
+ INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
1915
+ INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
1916
+ END`);
1917
+ } catch {
1918
+ }
1919
+ }
1920
+ return result.changes || 0;
1921
+ }
1572
1922
  /**
1573
1923
  * Convert database row to MemoryEvent
1574
1924
  */
@@ -1589,6 +1939,9 @@ var SQLiteEventStore = class {
1589
1939
  if (row.last_accessed_at !== void 0) {
1590
1940
  event.last_accessed_at = row.last_accessed_at;
1591
1941
  }
1942
+ if (row.turn_id !== void 0 && row.turn_id !== null) {
1943
+ event.turn_id = row.turn_id;
1944
+ }
1592
1945
  return event;
1593
1946
  }
1594
1947
  };
@@ -1800,7 +2153,16 @@ var VectorStore = class {
1800
2153
  metadata: JSON.stringify(record.metadata || {})
1801
2154
  };
1802
2155
  if (!this.table) {
1803
- this.table = await this.db.createTable(this.tableName, [data]);
2156
+ try {
2157
+ this.table = await this.db.createTable(this.tableName, [data]);
2158
+ } catch (e) {
2159
+ if (e?.message?.includes("already exists")) {
2160
+ this.table = await this.db.openTable(this.tableName);
2161
+ await this.table.add([data]);
2162
+ } else {
2163
+ throw e;
2164
+ }
2165
+ }
1804
2166
  } else {
1805
2167
  await this.table.add([data]);
1806
2168
  }
@@ -1826,7 +2188,16 @@ var VectorStore = class {
1826
2188
  metadata: JSON.stringify(record.metadata || {})
1827
2189
  }));
1828
2190
  if (!this.table) {
1829
- this.table = await this.db.createTable(this.tableName, data);
2191
+ try {
2192
+ this.table = await this.db.createTable(this.tableName, data);
2193
+ } catch (e) {
2194
+ if (e?.message?.includes("already exists")) {
2195
+ this.table = await this.db.openTable(this.tableName);
2196
+ await this.table.add(data);
2197
+ } else {
2198
+ throw e;
2199
+ }
2200
+ }
1830
2201
  } else {
1831
2202
  await this.table.add(data);
1832
2203
  }
@@ -4572,7 +4943,7 @@ function createGraduationWorker(eventStore, graduation, config) {
4572
4943
  function normalizePath(projectPath) {
4573
4944
  const expanded = projectPath.startsWith("~") ? path.join(os.homedir(), projectPath.slice(1)) : projectPath;
4574
4945
  try {
4575
- return fs.realpathSync(expanded);
4946
+ return fs2.realpathSync(expanded);
4576
4947
  } catch {
4577
4948
  return path.resolve(expanded);
4578
4949
  }
@@ -4587,6 +4958,17 @@ function getProjectStoragePath(projectPath) {
4587
4958
  }
4588
4959
  var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
4589
4960
  var SHARED_STORAGE_PATH = path.join(os.homedir(), ".claude-code", "memory", "shared");
4961
+ function loadSessionRegistry() {
4962
+ try {
4963
+ if (fs2.existsSync(REGISTRY_PATH)) {
4964
+ const data = fs2.readFileSync(REGISTRY_PATH, "utf-8");
4965
+ return JSON.parse(data);
4966
+ }
4967
+ } catch (error) {
4968
+ console.error("Failed to load session registry:", error);
4969
+ }
4970
+ return { version: 1, sessions: {} };
4971
+ }
4590
4972
  var MemoryService = class {
4591
4973
  // Primary store: SQLite (WAL mode) - for hooks, always available
4592
4974
  sqliteStore;
@@ -4620,8 +5002,8 @@ var MemoryService = class {
4620
5002
  const storagePath = this.expandPath(config.storagePath);
4621
5003
  this.readOnly = config.readOnly ?? false;
4622
5004
  this.lightweightMode = config.lightweightMode ?? false;
4623
- if (!this.readOnly && !fs.existsSync(storagePath)) {
4624
- fs.mkdirSync(storagePath, { recursive: true });
5005
+ if (!this.readOnly && !fs2.existsSync(storagePath)) {
5006
+ fs2.mkdirSync(storagePath, { recursive: true });
4625
5007
  }
4626
5008
  this.projectHash = config.projectHash || null;
4627
5009
  this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
@@ -4716,8 +5098,8 @@ var MemoryService = class {
4716
5098
  */
4717
5099
  async initializeSharedStore() {
4718
5100
  const sharedPath = this.sharedStoreConfig?.sharedStoragePath ? this.expandPath(this.sharedStoreConfig.sharedStoragePath) : SHARED_STORAGE_PATH;
4719
- if (!fs.existsSync(sharedPath)) {
4720
- fs.mkdirSync(sharedPath, { recursive: true });
5101
+ if (!fs2.existsSync(sharedPath)) {
5102
+ fs2.mkdirSync(sharedPath, { recursive: true });
4721
5103
  }
4722
5104
  this.sharedEventStore = createSharedEventStore(
4723
5105
  path.join(sharedPath, "shared.duckdb")
@@ -4814,6 +5196,7 @@ var MemoryService = class {
4814
5196
  async storeToolObservation(sessionId, payload) {
4815
5197
  await this.initialize();
4816
5198
  const content = JSON.stringify(payload);
5199
+ const turnId = payload.metadata?.turnId;
4817
5200
  const result = await this.sqliteStore.append({
4818
5201
  eventType: "tool_observation",
4819
5202
  sessionId,
@@ -4821,7 +5204,8 @@ var MemoryService = class {
4821
5204
  content,
4822
5205
  metadata: {
4823
5206
  toolName: payload.toolName,
4824
- success: payload.success
5207
+ success: payload.success,
5208
+ ...turnId ? { turnId } : {}
4825
5209
  }
4826
5210
  });
4827
5211
  if (result.success && !result.isDuplicate) {
@@ -5104,6 +5488,31 @@ var MemoryService = class {
5104
5488
  return [];
5105
5489
  return this.consolidatedStore.getAll({ limit });
5106
5490
  }
5491
+ /**
5492
+ * Extract topic keywords from event content (markdown headings and key terms)
5493
+ */
5494
+ extractTopicsFromContent(content) {
5495
+ const topics = /* @__PURE__ */ new Set();
5496
+ const headings = content.match(/^#{1,3}\s+(.+)$/gm);
5497
+ if (headings) {
5498
+ for (const h of headings.slice(0, 5)) {
5499
+ const text = h.replace(/^#+\s+/, "").replace(/[*_`#]/g, "").trim();
5500
+ if (text.length > 2 && text.length < 50) {
5501
+ topics.add(text);
5502
+ }
5503
+ }
5504
+ }
5505
+ const boldTerms = content.match(/\*\*([^*]+)\*\*/g);
5506
+ if (boldTerms) {
5507
+ for (const b of boldTerms.slice(0, 5)) {
5508
+ const text = b.replace(/\*\*/g, "").trim();
5509
+ if (text.length > 2 && text.length < 30) {
5510
+ topics.add(text);
5511
+ }
5512
+ }
5513
+ }
5514
+ return Array.from(topics).slice(0, 5);
5515
+ }
5107
5516
  /**
5108
5517
  * Increment access count for memories that were used in prompts
5109
5518
  */
@@ -5127,8 +5536,7 @@ var MemoryService = class {
5127
5536
  return events.map((event) => ({
5128
5537
  memoryId: event.id,
5129
5538
  summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
5130
- topics: [],
5131
- // Could extract topics from content if needed
5539
+ topics: this.extractTopicsFromContent(event.content),
5132
5540
  accessCount: event.access_count || 0,
5133
5541
  lastAccessed: event.last_accessed_at || null,
5134
5542
  confidence: 1,
@@ -5149,6 +5557,34 @@ var MemoryService = class {
5149
5557
  }
5150
5558
  return [];
5151
5559
  }
5560
+ /**
5561
+ * Record a memory retrieval for helpfulness tracking
5562
+ */
5563
+ async recordRetrieval(eventId, sessionId, score, query) {
5564
+ await this.initialize();
5565
+ await this.sqliteStore.recordRetrieval(eventId, sessionId, score, query);
5566
+ }
5567
+ /**
5568
+ * Evaluate helpfulness of retrievals in a session (called at session end)
5569
+ */
5570
+ async evaluateSessionHelpfulness(sessionId) {
5571
+ await this.initialize();
5572
+ await this.sqliteStore.evaluateSessionHelpfulness(sessionId);
5573
+ }
5574
+ /**
5575
+ * Get most helpful memories ranked by helpfulness score
5576
+ */
5577
+ async getHelpfulMemories(limit = 10) {
5578
+ await this.initialize();
5579
+ return this.sqliteStore.getHelpfulMemories(limit);
5580
+ }
5581
+ /**
5582
+ * Get helpfulness statistics for dashboard
5583
+ */
5584
+ async getHelpfulnessStats() {
5585
+ await this.initialize();
5586
+ return this.sqliteStore.getHelpfulnessStats();
5587
+ }
5152
5588
  /**
5153
5589
  * Mark a consolidated memory as accessed
5154
5590
  */
@@ -5212,6 +5648,44 @@ var MemoryService = class {
5212
5648
  lastConsolidation
5213
5649
  };
5214
5650
  }
5651
+ // ============================================================
5652
+ // Turn Grouping Methods
5653
+ // ============================================================
5654
+ /**
5655
+ * Get events grouped by turn for a session
5656
+ */
5657
+ async getSessionTurns(sessionId, options) {
5658
+ await this.initialize();
5659
+ return this.sqliteStore.getSessionTurns(sessionId, options);
5660
+ }
5661
+ /**
5662
+ * Get all events for a specific turn
5663
+ */
5664
+ async getEventsByTurn(turnId) {
5665
+ await this.initialize();
5666
+ return this.sqliteStore.getEventsByTurn(turnId);
5667
+ }
5668
+ /**
5669
+ * Count total turns for a session
5670
+ */
5671
+ async countSessionTurns(sessionId) {
5672
+ await this.initialize();
5673
+ return this.sqliteStore.countSessionTurns(sessionId);
5674
+ }
5675
+ /**
5676
+ * Backfill turn_ids from metadata for events stored before the migration
5677
+ */
5678
+ async backfillTurnIds() {
5679
+ await this.initialize();
5680
+ return this.sqliteStore.backfillTurnIds();
5681
+ }
5682
+ /**
5683
+ * Delete all events for a session (for force reimport)
5684
+ */
5685
+ async deleteSessionEvents(sessionId) {
5686
+ await this.initialize();
5687
+ return this.sqliteStore.deleteSessionEvents(sessionId);
5688
+ }
5215
5689
  /**
5216
5690
  * Format Endless Mode context for Claude
5217
5691
  */
@@ -5320,12 +5794,36 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
5320
5794
  return serviceCache.get(hash);
5321
5795
  }
5322
5796
 
5797
+ // src/server/api/utils.ts
5798
+ function getServiceFromQuery(c) {
5799
+ const project = c.req.query("project");
5800
+ if (project) {
5801
+ const isHash = /^[a-f0-9]{8}$/.test(project);
5802
+ let storagePath;
5803
+ if (isHash) {
5804
+ storagePath = path2.join(os2.homedir(), ".claude-code", "memory", "projects", project);
5805
+ } else {
5806
+ const crypto3 = __require("crypto");
5807
+ const normalized = project.replace(/\/+$/, "") || "/";
5808
+ const hash = crypto3.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
5809
+ storagePath = path2.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
5810
+ }
5811
+ return new MemoryService({
5812
+ storagePath,
5813
+ readOnly: true,
5814
+ analyticsEnabled: false,
5815
+ sharedStoreConfig: { enabled: false }
5816
+ });
5817
+ }
5818
+ return getReadOnlyMemoryService();
5819
+ }
5820
+
5323
5821
  // src/server/api/sessions.ts
5324
5822
  var sessionsRouter = new Hono();
5325
5823
  sessionsRouter.get("/", async (c) => {
5326
5824
  const page = parseInt(c.req.query("page") || "1", 10);
5327
5825
  const pageSize = parseInt(c.req.query("pageSize") || "20", 10);
5328
- const memoryService = getReadOnlyMemoryService();
5826
+ const memoryService = getServiceFromQuery(c);
5329
5827
  try {
5330
5828
  await memoryService.initialize();
5331
5829
  const recentEvents = await memoryService.getRecentEvents(1e3);
@@ -5369,7 +5867,7 @@ sessionsRouter.get("/", async (c) => {
5369
5867
  });
5370
5868
  sessionsRouter.get("/:id", async (c) => {
5371
5869
  const { id } = c.req.param();
5372
- const memoryService = getReadOnlyMemoryService();
5870
+ const memoryService = getServiceFromQuery(c);
5373
5871
  try {
5374
5872
  await memoryService.initialize();
5375
5873
  const events = await memoryService.getSessionHistory(id);
@@ -5410,18 +5908,36 @@ var eventsRouter = new Hono2();
5410
5908
  eventsRouter.get("/", async (c) => {
5411
5909
  const sessionId = c.req.query("sessionId");
5412
5910
  const eventType = c.req.query("type");
5911
+ const level = c.req.query("level");
5912
+ const sort = c.req.query("sort") || "recent";
5413
5913
  const limit = parseInt(c.req.query("limit") || "100", 10);
5414
5914
  const offset = parseInt(c.req.query("offset") || "0", 10);
5415
- const memoryService = getReadOnlyMemoryService();
5915
+ const memoryService = getServiceFromQuery(c);
5416
5916
  try {
5417
5917
  await memoryService.initialize();
5418
- let events = await memoryService.getRecentEvents(limit + offset + 1e3);
5918
+ let events;
5919
+ if (level) {
5920
+ events = await memoryService.getEventsByLevel(level, { limit: limit + offset + 1e3, offset: 0 });
5921
+ } else {
5922
+ events = await memoryService.getRecentEvents(limit + offset + 1e3);
5923
+ }
5419
5924
  if (sessionId) {
5420
5925
  events = events.filter((e) => e.sessionId === sessionId);
5421
5926
  }
5422
5927
  if (eventType) {
5423
5928
  events = events.filter((e) => e.eventType === eventType);
5424
5929
  }
5930
+ if (sort === "accessed") {
5931
+ events.sort((a, b) => {
5932
+ const aTime = a.last_accessed_at || "";
5933
+ const bTime = b.last_accessed_at || "";
5934
+ return bTime.localeCompare(aTime);
5935
+ });
5936
+ } else if (sort === "most-accessed") {
5937
+ events.sort((a, b) => (b.access_count || 0) - (a.access_count || 0));
5938
+ } else if (sort === "oldest") {
5939
+ events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
5940
+ }
5425
5941
  const total = events.length;
5426
5942
  events = events.slice(offset, offset + limit);
5427
5943
  return c.json({
@@ -5431,7 +5947,9 @@ eventsRouter.get("/", async (c) => {
5431
5947
  timestamp: e.timestamp,
5432
5948
  sessionId: e.sessionId,
5433
5949
  preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
5434
- contentLength: e.content.length
5950
+ contentLength: e.content.length,
5951
+ accessCount: e.access_count || 0,
5952
+ lastAccessedAt: e.last_accessed_at || null
5435
5953
  })),
5436
5954
  total,
5437
5955
  limit,
@@ -5446,7 +5964,7 @@ eventsRouter.get("/", async (c) => {
5446
5964
  });
5447
5965
  eventsRouter.get("/:id", async (c) => {
5448
5966
  const { id } = c.req.param();
5449
- const memoryService = getReadOnlyMemoryService();
5967
+ const memoryService = getServiceFromQuery(c);
5450
5968
  try {
5451
5969
  await memoryService.initialize();
5452
5970
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -5486,7 +6004,7 @@ eventsRouter.get("/:id", async (c) => {
5486
6004
  import { Hono as Hono3 } from "hono";
5487
6005
  var searchRouter = new Hono3();
5488
6006
  searchRouter.post("/", async (c) => {
5489
- const memoryService = getReadOnlyMemoryService();
6007
+ const memoryService = getServiceFromQuery(c);
5490
6008
  try {
5491
6009
  const body = await c.req.json();
5492
6010
  if (!body.query) {
@@ -5530,7 +6048,7 @@ searchRouter.get("/", async (c) => {
5530
6048
  return c.json({ error: 'Query parameter "q" is required' }, 400);
5531
6049
  }
5532
6050
  const topK = parseInt(c.req.query("topK") || "5", 10);
5533
- const memoryService = getReadOnlyMemoryService();
6051
+ const memoryService = getServiceFromQuery(c);
5534
6052
  try {
5535
6053
  await memoryService.initialize();
5536
6054
  const result = await memoryService.retrieveMemories(query, { topK });
@@ -5558,7 +6076,7 @@ searchRouter.get("/", async (c) => {
5558
6076
  import { Hono as Hono4 } from "hono";
5559
6077
  var statsRouter = new Hono4();
5560
6078
  statsRouter.get("/shared", async (c) => {
5561
- const memoryService = getReadOnlyMemoryService();
6079
+ const memoryService = getServiceFromQuery(c);
5562
6080
  try {
5563
6081
  await memoryService.initialize();
5564
6082
  const sharedStats = await memoryService.getSharedStoreStats();
@@ -5615,7 +6133,7 @@ statsRouter.get("/levels/:level", async (c) => {
5615
6133
  if (!validLevels.includes(level)) {
5616
6134
  return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
5617
6135
  }
5618
- const memoryService = getReadOnlyMemoryService();
6136
+ const memoryService = getServiceFromQuery(c);
5619
6137
  try {
5620
6138
  await memoryService.initialize();
5621
6139
  let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
@@ -5662,7 +6180,7 @@ statsRouter.get("/levels/:level", async (c) => {
5662
6180
  }
5663
6181
  });
5664
6182
  statsRouter.get("/", async (c) => {
5665
- const memoryService = getReadOnlyMemoryService();
6183
+ const memoryService = getServiceFromQuery(c);
5666
6184
  try {
5667
6185
  await memoryService.initialize();
5668
6186
  const stats = await memoryService.getStats();
@@ -5706,7 +6224,7 @@ statsRouter.get("/", async (c) => {
5706
6224
  });
5707
6225
  statsRouter.get("/most-accessed", async (c) => {
5708
6226
  const limit = parseInt(c.req.query("limit") || "10", 10);
5709
- const memoryService = getReadOnlyMemoryService();
6227
+ const memoryService = getServiceFromQuery(c);
5710
6228
  try {
5711
6229
  await memoryService.initialize();
5712
6230
  console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
@@ -5737,7 +6255,7 @@ statsRouter.get("/most-accessed", async (c) => {
5737
6255
  });
5738
6256
  statsRouter.get("/timeline", async (c) => {
5739
6257
  const days = parseInt(c.req.query("days") || "7", 10);
5740
- const memoryService = getReadOnlyMemoryService();
6258
+ const memoryService = getServiceFromQuery(c);
5741
6259
  try {
5742
6260
  await memoryService.initialize();
5743
6261
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -5767,8 +6285,39 @@ statsRouter.get("/timeline", async (c) => {
5767
6285
  await memoryService.shutdown();
5768
6286
  }
5769
6287
  });
6288
+ statsRouter.get("/helpfulness", async (c) => {
6289
+ const limit = parseInt(c.req.query("limit") || "10", 10);
6290
+ const memoryService = getServiceFromQuery(c);
6291
+ try {
6292
+ await memoryService.initialize();
6293
+ const stats = await memoryService.getHelpfulnessStats();
6294
+ const topMemories = await memoryService.getHelpfulMemories(limit);
6295
+ return c.json({
6296
+ ...stats,
6297
+ topMemories: topMemories.map((m) => ({
6298
+ eventId: m.eventId,
6299
+ summary: m.summary,
6300
+ helpfulnessScore: m.helpfulnessScore,
6301
+ accessCount: m.accessCount,
6302
+ evaluationCount: m.evaluationCount
6303
+ }))
6304
+ });
6305
+ } catch (error) {
6306
+ return c.json({
6307
+ avgScore: 0,
6308
+ totalEvaluated: 0,
6309
+ totalRetrievals: 0,
6310
+ helpful: 0,
6311
+ neutral: 0,
6312
+ unhelpful: 0,
6313
+ topMemories: []
6314
+ });
6315
+ } finally {
6316
+ await memoryService.shutdown();
6317
+ }
6318
+ });
5770
6319
  statsRouter.post("/graduation/run", async (c) => {
5771
- const memoryService = getReadOnlyMemoryService();
6320
+ const memoryService = getServiceFromQuery(c);
5772
6321
  try {
5773
6322
  await memoryService.initialize();
5774
6323
  const result = await memoryService.forceGraduation();
@@ -5829,7 +6378,7 @@ var citationsRouter = new Hono5();
5829
6378
  citationsRouter.get("/:id", async (c) => {
5830
6379
  const { id } = c.req.param();
5831
6380
  const citationId = parseCitationId(id) || id;
5832
- const memoryService = getReadOnlyMemoryService();
6381
+ const memoryService = getServiceFromQuery(c);
5833
6382
  try {
5834
6383
  await memoryService.initialize();
5835
6384
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -5863,7 +6412,7 @@ citationsRouter.get("/:id", async (c) => {
5863
6412
  citationsRouter.get("/:id/related", async (c) => {
5864
6413
  const { id } = c.req.param();
5865
6414
  const citationId = parseCitationId(id) || id;
5866
- const memoryService = getReadOnlyMemoryService();
6415
+ const memoryService = getServiceFromQuery(c);
5867
6416
  try {
5868
6417
  await memoryService.initialize();
5869
6418
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -5899,8 +6448,358 @@ citationsRouter.get("/:id/related", async (c) => {
5899
6448
  }
5900
6449
  });
5901
6450
 
6451
+ // src/server/api/turns.ts
6452
+ import { Hono as Hono6 } from "hono";
6453
+ var turnsRouter = new Hono6();
6454
+ turnsRouter.get("/", async (c) => {
6455
+ const sessionId = c.req.query("sessionId");
6456
+ const limit = parseInt(c.req.query("limit") || "20", 10);
6457
+ const offset = parseInt(c.req.query("offset") || "0", 10);
6458
+ if (!sessionId) {
6459
+ return c.json({ error: "sessionId is required" }, 400);
6460
+ }
6461
+ const memoryService = getServiceFromQuery(c);
6462
+ try {
6463
+ await memoryService.initialize();
6464
+ const turns = await memoryService.getSessionTurns(sessionId, { limit, offset });
6465
+ const totalTurns = await memoryService.countSessionTurns(sessionId);
6466
+ return c.json({
6467
+ turns: turns.map((t) => ({
6468
+ turnId: t.turnId,
6469
+ startedAt: t.startedAt.toISOString(),
6470
+ promptPreview: t.promptPreview,
6471
+ eventCount: t.eventCount,
6472
+ toolCount: t.toolCount,
6473
+ hasResponse: t.hasResponse,
6474
+ events: t.events.map((e) => ({
6475
+ id: e.id,
6476
+ eventType: e.eventType,
6477
+ timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
6478
+ preview: e.content.slice(0, 300) + (e.content.length > 300 ? "..." : ""),
6479
+ contentLength: e.content.length
6480
+ }))
6481
+ })),
6482
+ total: totalTurns,
6483
+ limit,
6484
+ offset,
6485
+ hasMore: offset + limit < totalTurns
6486
+ });
6487
+ } catch (error) {
6488
+ return c.json({ error: error.message }, 500);
6489
+ } finally {
6490
+ await memoryService.shutdown();
6491
+ }
6492
+ });
6493
+ turnsRouter.get("/:turnId", async (c) => {
6494
+ const { turnId } = c.req.param();
6495
+ const memoryService = getServiceFromQuery(c);
6496
+ try {
6497
+ await memoryService.initialize();
6498
+ const events = await memoryService.getEventsByTurn(turnId);
6499
+ if (events.length === 0) {
6500
+ return c.json({ error: "Turn not found" }, 404);
6501
+ }
6502
+ const promptEvent = events.find((e) => e.eventType === "user_prompt");
6503
+ const toolEvents = events.filter((e) => e.eventType === "tool_observation");
6504
+ const responseEvents = events.filter((e) => e.eventType === "agent_response");
6505
+ return c.json({
6506
+ turnId,
6507
+ sessionId: events[0].sessionId,
6508
+ startedAt: events[0].timestamp instanceof Date ? events[0].timestamp.toISOString() : events[0].timestamp,
6509
+ prompt: promptEvent ? {
6510
+ id: promptEvent.id,
6511
+ content: promptEvent.content,
6512
+ timestamp: promptEvent.timestamp instanceof Date ? promptEvent.timestamp.toISOString() : promptEvent.timestamp
6513
+ } : null,
6514
+ tools: toolEvents.map((e) => {
6515
+ let toolName = "";
6516
+ let success = true;
6517
+ try {
6518
+ const parsed = JSON.parse(e.content);
6519
+ toolName = parsed.toolName || "";
6520
+ success = parsed.success !== false;
6521
+ } catch {
6522
+ }
6523
+ return {
6524
+ id: e.id,
6525
+ toolName,
6526
+ success,
6527
+ timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp,
6528
+ preview: e.content.slice(0, 500) + (e.content.length > 500 ? "..." : "")
6529
+ };
6530
+ }),
6531
+ responses: responseEvents.map((e) => ({
6532
+ id: e.id,
6533
+ content: e.content,
6534
+ timestamp: e.timestamp instanceof Date ? e.timestamp.toISOString() : e.timestamp
6535
+ })),
6536
+ totalEvents: events.length
6537
+ });
6538
+ } catch (error) {
6539
+ return c.json({ error: error.message }, 500);
6540
+ } finally {
6541
+ await memoryService.shutdown();
6542
+ }
6543
+ });
6544
+ turnsRouter.post("/backfill", async (c) => {
6545
+ const memoryService = getServiceFromQuery(c);
6546
+ try {
6547
+ await memoryService.initialize();
6548
+ const updated = await memoryService.backfillTurnIds();
6549
+ return c.json({
6550
+ success: true,
6551
+ updated,
6552
+ message: `Backfilled turn_id for ${updated} events`
6553
+ });
6554
+ } catch (error) {
6555
+ return c.json({
6556
+ success: false,
6557
+ error: error.message
6558
+ }, 500);
6559
+ } finally {
6560
+ await memoryService.shutdown();
6561
+ }
6562
+ });
6563
+
6564
+ // src/server/api/projects.ts
6565
+ import { Hono as Hono7 } from "hono";
6566
+ import * as fs3 from "fs";
6567
+ import * as path3 from "path";
6568
+ import * as os3 from "os";
6569
+ var projectsRouter = new Hono7();
6570
+ projectsRouter.get("/", async (c) => {
6571
+ try {
6572
+ const projectsDir = path3.join(os3.homedir(), ".claude-code", "memory", "projects");
6573
+ if (!fs3.existsSync(projectsDir)) {
6574
+ return c.json({ projects: [] });
6575
+ }
6576
+ const projectHashes = fs3.readdirSync(projectsDir).filter((name) => {
6577
+ const fullPath = path3.join(projectsDir, name);
6578
+ return fs3.statSync(fullPath).isDirectory();
6579
+ });
6580
+ const registry = loadSessionRegistry();
6581
+ const hashToPath = /* @__PURE__ */ new Map();
6582
+ for (const entry of Object.values(registry.sessions)) {
6583
+ if (!hashToPath.has(entry.projectHash)) {
6584
+ hashToPath.set(entry.projectHash, entry.projectPath);
6585
+ }
6586
+ }
6587
+ const projects = projectHashes.map((hash) => {
6588
+ const dirPath = path3.join(projectsDir, hash);
6589
+ const dbPath = path3.join(dirPath, "events.sqlite");
6590
+ let dbSize = 0;
6591
+ if (fs3.existsSync(dbPath)) {
6592
+ dbSize = fs3.statSync(dbPath).size;
6593
+ }
6594
+ const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
6595
+ return {
6596
+ hash,
6597
+ projectPath,
6598
+ projectName: path3.basename(projectPath),
6599
+ dbSize,
6600
+ dbSizeHuman: formatBytes(dbSize)
6601
+ };
6602
+ });
6603
+ projects.sort((a, b) => a.projectName.localeCompare(b.projectName));
6604
+ return c.json({ projects });
6605
+ } catch (error) {
6606
+ return c.json({ projects: [], error: error.message }, 500);
6607
+ }
6608
+ });
6609
+ function formatBytes(bytes) {
6610
+ if (bytes === 0)
6611
+ return "0 B";
6612
+ const k = 1024;
6613
+ const sizes = ["B", "KB", "MB", "GB"];
6614
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
6615
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
6616
+ }
6617
+
6618
+ // src/server/api/chat.ts
6619
+ import { Hono as Hono8 } from "hono";
6620
+ import { streamSSE } from "hono/streaming";
6621
+ import { spawn } from "child_process";
6622
+ var chatRouter = new Hono8();
6623
+ var CLAUDE_TIMEOUT_MS = 12e4;
6624
+ chatRouter.post("/", async (c) => {
6625
+ let body;
6626
+ try {
6627
+ body = await c.req.json();
6628
+ } catch {
6629
+ return c.json({ error: "Invalid JSON body" }, 400);
6630
+ }
6631
+ if (!body.message?.trim()) {
6632
+ return c.json({ error: "Message is required" }, 400);
6633
+ }
6634
+ const memoryService = getServiceFromQuery(c);
6635
+ try {
6636
+ await memoryService.initialize();
6637
+ let memoryContext = "";
6638
+ let statsContext = "";
6639
+ try {
6640
+ const result = await memoryService.retrieveMemories(body.message, {
6641
+ topK: 8,
6642
+ minScore: 0.5
6643
+ });
6644
+ if (result.memories.length > 0) {
6645
+ const parts = ["## Relevant Memories\n"];
6646
+ for (const m of result.memories) {
6647
+ const date = new Date(m.event.timestamp).toISOString().split("T")[0];
6648
+ const content = m.event.content.slice(0, 500);
6649
+ parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
6650
+ parts.push(content);
6651
+ if (m.sessionContext) {
6652
+ parts.push(`_Context: ${m.sessionContext}_`);
6653
+ }
6654
+ parts.push("");
6655
+ }
6656
+ memoryContext = parts.join("\n");
6657
+ }
6658
+ } catch {
6659
+ }
6660
+ try {
6661
+ const stats = await memoryService.getStats();
6662
+ const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
6663
+ statsContext = [
6664
+ "## Memory Stats",
6665
+ `- Total events: ${stats.totalEvents}`,
6666
+ `- Vector nodes: ${stats.vectorCount}`,
6667
+ `- By level: ${levels}`
6668
+ ].join("\n");
6669
+ } catch {
6670
+ }
6671
+ const fullPrompt = buildPrompt(
6672
+ statsContext,
6673
+ memoryContext,
6674
+ body.history || [],
6675
+ body.message
6676
+ );
6677
+ return streamSSE(c, async (stream) => {
6678
+ try {
6679
+ await streamClaudeResponse(fullPrompt, stream);
6680
+ } catch (err) {
6681
+ await stream.writeSSE({
6682
+ event: "error",
6683
+ data: JSON.stringify({ error: err.message })
6684
+ });
6685
+ }
6686
+ });
6687
+ } catch (error) {
6688
+ return c.json({ error: error.message }, 500);
6689
+ } finally {
6690
+ await memoryService.shutdown();
6691
+ }
6692
+ });
6693
+ function buildPrompt(statsContext, memoryContext, history, currentMessage) {
6694
+ const parts = [];
6695
+ parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
6696
+ parts.push("The memory system tracks coding sessions, tool usage, prompts, and responses.");
6697
+ parts.push("Answer concisely based on the memory context below. If you don't have enough data, say so.");
6698
+ parts.push("Use markdown formatting in your responses.\n");
6699
+ if (statsContext) {
6700
+ parts.push(statsContext);
6701
+ parts.push("");
6702
+ }
6703
+ if (memoryContext) {
6704
+ parts.push(memoryContext);
6705
+ } else {
6706
+ parts.push("No directly relevant memories found for this query.");
6707
+ parts.push("Answer based on general knowledge or suggest the user rephrase.\n");
6708
+ }
6709
+ parts.push("---\n");
6710
+ const recentHistory = history.slice(-10);
6711
+ if (recentHistory.length > 0) {
6712
+ parts.push("## Conversation History\n");
6713
+ for (const msg of recentHistory) {
6714
+ const prefix = msg.role === "user" ? "User" : "Assistant";
6715
+ parts.push(`**${prefix}:** ${msg.content}
6716
+ `);
6717
+ }
6718
+ }
6719
+ parts.push(`**User:** ${currentMessage}`);
6720
+ return parts.join("\n");
6721
+ }
6722
+ function streamClaudeResponse(prompt, stream) {
6723
+ return new Promise((resolve2, reject) => {
6724
+ const proc = spawn("claude", [
6725
+ "-p",
6726
+ "--output-format",
6727
+ "stream-json",
6728
+ "--verbose"
6729
+ ], {
6730
+ stdio: ["pipe", "pipe", "pipe"],
6731
+ env: { ...process.env }
6732
+ });
6733
+ const timeout = setTimeout(() => {
6734
+ proc.kill("SIGTERM");
6735
+ reject(new Error("Chat response timed out after 2 minutes"));
6736
+ }, CLAUDE_TIMEOUT_MS);
6737
+ proc.stdin.write(prompt);
6738
+ proc.stdin.end();
6739
+ let buffer = "";
6740
+ let lastSentText = "";
6741
+ proc.stdout.on("data", async (chunk) => {
6742
+ buffer += chunk.toString();
6743
+ const lines = buffer.split("\n");
6744
+ buffer = lines.pop() || "";
6745
+ for (const line of lines) {
6746
+ if (!line.trim())
6747
+ continue;
6748
+ try {
6749
+ const parsed = JSON.parse(line);
6750
+ if (parsed.type === "assistant" && parsed.message?.content) {
6751
+ const textBlocks = parsed.message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
6752
+ if (textBlocks.length > lastSentText.length) {
6753
+ const delta = textBlocks.slice(lastSentText.length);
6754
+ lastSentText = textBlocks;
6755
+ await stream.writeSSE({
6756
+ event: "message",
6757
+ data: JSON.stringify({ content: delta })
6758
+ });
6759
+ }
6760
+ }
6761
+ if (parsed.type === "result") {
6762
+ await stream.writeSSE({ event: "done", data: "{}" });
6763
+ }
6764
+ } catch {
6765
+ }
6766
+ }
6767
+ });
6768
+ proc.stderr.on("data", (chunk) => {
6769
+ if (process.env.CLAUDE_MEMORY_DEBUG) {
6770
+ console.error("[chat] claude stderr:", chunk.toString());
6771
+ }
6772
+ });
6773
+ proc.on("error", (err) => {
6774
+ clearTimeout(timeout);
6775
+ if (err.code === "ENOENT") {
6776
+ reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
6777
+ } else {
6778
+ reject(err);
6779
+ }
6780
+ });
6781
+ proc.on("close", async (code) => {
6782
+ clearTimeout(timeout);
6783
+ if (buffer.trim()) {
6784
+ try {
6785
+ const parsed = JSON.parse(buffer);
6786
+ if (parsed.type === "result") {
6787
+ await stream.writeSSE({ event: "done", data: "{}" });
6788
+ }
6789
+ } catch {
6790
+ }
6791
+ }
6792
+ if (code !== 0 && code !== null) {
6793
+ reject(new Error(`Claude CLI exited with code ${code}`));
6794
+ } else {
6795
+ resolve2();
6796
+ }
6797
+ });
6798
+ });
6799
+ }
6800
+
5902
6801
  // src/server/api/index.ts
5903
- var apiRouter = new Hono6().route("/sessions", sessionsRouter).route("/events", eventsRouter).route("/search", searchRouter).route("/stats", statsRouter).route("/citations", citationsRouter);
6802
+ var apiRouter = new Hono9().route("/sessions", sessionsRouter).route("/events", eventsRouter).route("/search", searchRouter).route("/stats", statsRouter).route("/citations", citationsRouter).route("/turns", turnsRouter).route("/projects", projectsRouter).route("/chat", chatRouter);
5904
6803
  export {
5905
6804
  apiRouter
5906
6805
  };