claude-memory-layer 1.0.9 → 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 (73) hide show
  1. package/dist/cli/index.js +1373 -184
  2. package/dist/cli/index.js.map +4 -4
  3. package/dist/core/index.js +445 -7
  4. package/dist/core/index.js.map +2 -2
  5. package/dist/hooks/post-tool-use.js +705 -43
  6. package/dist/hooks/post-tool-use.js.map +4 -4
  7. package/dist/hooks/session-end.js +593 -52
  8. package/dist/hooks/session-end.js.map +3 -3
  9. package/dist/hooks/session-start.js +581 -25
  10. package/dist/hooks/session-start.js.map +3 -3
  11. package/dist/hooks/stop.js +693 -73
  12. package/dist/hooks/stop.js.map +4 -4
  13. package/dist/hooks/user-prompt-submit.js +674 -94
  14. package/dist/hooks/user-prompt-submit.js.map +4 -4
  15. package/dist/server/api/index.js +1045 -42
  16. package/dist/server/api/index.js.map +4 -4
  17. package/dist/server/index.js +1054 -51
  18. package/dist/server/index.js.map +4 -4
  19. package/dist/services/memory-service.js +599 -25
  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 +542 -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 +78 -65
  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 +208 -9
  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/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,31 @@ 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);
1079
+
1080
+ -- FTS5 Full-Text Search for fast keyword search
1081
+ CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
1082
+ content,
1083
+ event_id UNINDEXED,
1084
+ content='events',
1085
+ content_rowid='rowid'
1086
+ );
1087
+
1088
+ -- Triggers to keep FTS in sync with events table
1089
+ CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
1090
+ INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
1091
+ END;
1092
+
1093
+ CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
1094
+ INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
1095
+ END;
1096
+
1097
+ CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
1098
+ INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
1099
+ INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
1100
+ END;
1042
1101
  `);
1043
1102
  const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
1044
1103
  const columnNames = tableInfo.map((col) => col.name);
@@ -1060,6 +1119,15 @@ var SQLiteEventStore = class {
1060
1119
  console.error("Error adding last_accessed_at column:", err);
1061
1120
  }
1062
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
+ }
1063
1131
  try {
1064
1132
  sqliteExec(this.db, `
1065
1133
  CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
@@ -1072,6 +1140,12 @@ var SQLiteEventStore = class {
1072
1140
  `);
1073
1141
  } catch (err) {
1074
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
+ }
1075
1149
  this.initialized = true;
1076
1150
  }
1077
1151
  /**
@@ -1096,9 +1170,11 @@ var SQLiteEventStore = class {
1096
1170
  const id = randomUUID2();
1097
1171
  const timestamp = toSQLiteTimestamp(input.timestamp);
1098
1172
  try {
1173
+ const metadata = input.metadata || {};
1174
+ const turnId = metadata.turnId || null;
1099
1175
  const insertEvent = this.db.prepare(`
1100
- INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
1101
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1176
+ INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
1177
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1102
1178
  `);
1103
1179
  const insertDedup = this.db.prepare(`
1104
1180
  INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
@@ -1115,7 +1191,8 @@ var SQLiteEventStore = class {
1115
1191
  input.content,
1116
1192
  canonicalKey,
1117
1193
  dedupeKey,
1118
- JSON.stringify(input.metadata || {})
1194
+ JSON.stringify(metadata),
1195
+ turnId
1119
1196
  );
1120
1197
  insertDedup.run(dedupeKey, id);
1121
1198
  insertLevel.run(id);
@@ -1465,11 +1542,11 @@ var SQLiteEventStore = class {
1465
1542
  );
1466
1543
  }
1467
1544
  /**
1468
- * Get most accessed memories
1545
+ * Get most accessed memories (falls back to recent events if none accessed)
1469
1546
  */
1470
1547
  async getMostAccessed(limit = 10) {
1471
1548
  await this.initialize();
1472
- const rows = sqliteAll(
1549
+ let rows = sqliteAll(
1473
1550
  this.db,
1474
1551
  `SELECT * FROM events
1475
1552
  WHERE access_count > 0
@@ -1477,8 +1554,222 @@ var SQLiteEventStore = class {
1477
1554
  LIMIT ?`,
1478
1555
  [limit]
1479
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
+ }
1480
1566
  return rows.map((row) => this.rowToEvent(row));
1481
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
+ }
1717
+ /**
1718
+ * Fast keyword search using FTS5
1719
+ * Returns events matching the search query, ranked by relevance
1720
+ */
1721
+ async keywordSearch(query, limit = 10) {
1722
+ await this.initialize();
1723
+ const searchTerms = query.replace(/['"(){}[\]^~*?:\\/-]/g, " ").split(/\s+/).filter((term) => term.length > 1).map((term) => `"${term}"*`).join(" OR ");
1724
+ if (!searchTerms) {
1725
+ return [];
1726
+ }
1727
+ try {
1728
+ const rows = sqliteAll(
1729
+ this.db,
1730
+ `SELECT e.*, fts.rank
1731
+ FROM events_fts fts
1732
+ JOIN events e ON e.id = fts.event_id
1733
+ WHERE events_fts MATCH ?
1734
+ ORDER BY fts.rank
1735
+ LIMIT ?`,
1736
+ [searchTerms, limit]
1737
+ );
1738
+ return rows.map((row) => ({
1739
+ event: this.rowToEvent(row),
1740
+ rank: row.rank
1741
+ }));
1742
+ } catch (error) {
1743
+ const likePattern = `%${query}%`;
1744
+ const rows = sqliteAll(
1745
+ this.db,
1746
+ `SELECT *, 0 as rank FROM events
1747
+ WHERE content LIKE ?
1748
+ ORDER BY timestamp DESC
1749
+ LIMIT ?`,
1750
+ [likePattern, limit]
1751
+ );
1752
+ return rows.map((row) => ({
1753
+ event: this.rowToEvent(row),
1754
+ rank: 0
1755
+ }));
1756
+ }
1757
+ }
1758
+ /**
1759
+ * Rebuild FTS index from existing events
1760
+ * Call this once after upgrading to FTS5
1761
+ */
1762
+ async rebuildFtsIndex() {
1763
+ await this.initialize();
1764
+ const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
1765
+ const totalEvents = countRow?.count ?? 0;
1766
+ sqliteExec(this.db, `
1767
+ DELETE FROM events_fts;
1768
+ INSERT INTO events_fts(rowid, content, event_id)
1769
+ SELECT rowid, content, id FROM events;
1770
+ `);
1771
+ return totalEvents;
1772
+ }
1482
1773
  /**
1483
1774
  * Get database instance for direct access
1484
1775
  */
@@ -1491,6 +1782,143 @@ var SQLiteEventStore = class {
1491
1782
  async close() {
1492
1783
  sqliteClose(this.db);
1493
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
+ }
1494
1922
  /**
1495
1923
  * Convert database row to MemoryEvent
1496
1924
  */
@@ -1511,6 +1939,9 @@ var SQLiteEventStore = class {
1511
1939
  if (row.last_accessed_at !== void 0) {
1512
1940
  event.last_accessed_at = row.last_accessed_at;
1513
1941
  }
1942
+ if (row.turn_id !== void 0 && row.turn_id !== null) {
1943
+ event.turn_id = row.turn_id;
1944
+ }
1514
1945
  return event;
1515
1946
  }
1516
1947
  };
@@ -1722,7 +2153,16 @@ var VectorStore = class {
1722
2153
  metadata: JSON.stringify(record.metadata || {})
1723
2154
  };
1724
2155
  if (!this.table) {
1725
- 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
+ }
1726
2166
  } else {
1727
2167
  await this.table.add([data]);
1728
2168
  }
@@ -1748,7 +2188,16 @@ var VectorStore = class {
1748
2188
  metadata: JSON.stringify(record.metadata || {})
1749
2189
  }));
1750
2190
  if (!this.table) {
1751
- 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
+ }
1752
2201
  } else {
1753
2202
  await this.table.add(data);
1754
2203
  }
@@ -4494,7 +4943,7 @@ function createGraduationWorker(eventStore, graduation, config) {
4494
4943
  function normalizePath(projectPath) {
4495
4944
  const expanded = projectPath.startsWith("~") ? path.join(os.homedir(), projectPath.slice(1)) : projectPath;
4496
4945
  try {
4497
- return fs.realpathSync(expanded);
4946
+ return fs2.realpathSync(expanded);
4498
4947
  } catch {
4499
4948
  return path.resolve(expanded);
4500
4949
  }
@@ -4509,6 +4958,17 @@ function getProjectStoragePath(projectPath) {
4509
4958
  }
4510
4959
  var REGISTRY_PATH = path.join(os.homedir(), ".claude-code", "memory", "session-registry.json");
4511
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
+ }
4512
4972
  var MemoryService = class {
4513
4973
  // Primary store: SQLite (WAL mode) - for hooks, always available
4514
4974
  sqliteStore;
@@ -4537,11 +4997,13 @@ var MemoryService = class {
4537
4997
  sharedStoreConfig = null;
4538
4998
  projectHash = null;
4539
4999
  readOnly;
5000
+ lightweightMode;
4540
5001
  constructor(config) {
4541
5002
  const storagePath = this.expandPath(config.storagePath);
4542
5003
  this.readOnly = config.readOnly ?? false;
4543
- if (!this.readOnly && !fs.existsSync(storagePath)) {
4544
- fs.mkdirSync(storagePath, { recursive: true });
5004
+ this.lightweightMode = config.lightweightMode ?? false;
5005
+ if (!this.readOnly && !fs2.existsSync(storagePath)) {
5006
+ fs2.mkdirSync(storagePath, { recursive: true });
4545
5007
  }
4546
5008
  this.projectHash = config.projectHash || null;
4547
5009
  this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
@@ -4586,6 +5048,10 @@ var MemoryService = class {
4586
5048
  if (this.initialized)
4587
5049
  return;
4588
5050
  await this.sqliteStore.initialize();
5051
+ if (this.lightweightMode) {
5052
+ this.initialized = true;
5053
+ return;
5054
+ }
4589
5055
  if (this.analyticsStore) {
4590
5056
  try {
4591
5057
  await this.analyticsStore.initialize();
@@ -4632,8 +5098,8 @@ var MemoryService = class {
4632
5098
  */
4633
5099
  async initializeSharedStore() {
4634
5100
  const sharedPath = this.sharedStoreConfig?.sharedStoragePath ? this.expandPath(this.sharedStoreConfig.sharedStoragePath) : SHARED_STORAGE_PATH;
4635
- if (!fs.existsSync(sharedPath)) {
4636
- fs.mkdirSync(sharedPath, { recursive: true });
5101
+ if (!fs2.existsSync(sharedPath)) {
5102
+ fs2.mkdirSync(sharedPath, { recursive: true });
4637
5103
  }
4638
5104
  this.sharedEventStore = createSharedEventStore(
4639
5105
  path.join(sharedPath, "shared.duckdb")
@@ -4730,6 +5196,7 @@ var MemoryService = class {
4730
5196
  async storeToolObservation(sessionId, payload) {
4731
5197
  await this.initialize();
4732
5198
  const content = JSON.stringify(payload);
5199
+ const turnId = payload.metadata?.turnId;
4733
5200
  const result = await this.sqliteStore.append({
4734
5201
  eventType: "tool_observation",
4735
5202
  sessionId,
@@ -4737,7 +5204,8 @@ var MemoryService = class {
4737
5204
  content,
4738
5205
  metadata: {
4739
5206
  toolName: payload.toolName,
4740
- success: payload.success
5207
+ success: payload.success,
5208
+ ...turnId ? { turnId } : {}
4741
5209
  }
4742
5210
  });
4743
5211
  if (result.success && !result.isDuplicate) {
@@ -4755,9 +5223,6 @@ var MemoryService = class {
4755
5223
  */
4756
5224
  async retrieveMemories(query, options) {
4757
5225
  await this.initialize();
4758
- if (this.vectorWorker) {
4759
- await this.vectorWorker.processAll();
4760
- }
4761
5226
  if (options?.includeShared && this.sharedStore) {
4762
5227
  return this.retriever.retrieveUnified(query, {
4763
5228
  ...options,
@@ -4767,6 +5232,29 @@ var MemoryService = class {
4767
5232
  }
4768
5233
  return this.retriever.retrieve(query, options);
4769
5234
  }
5235
+ /**
5236
+ * Fast keyword search using SQLite FTS5
5237
+ * Much faster than vector search - no embedding model needed
5238
+ */
5239
+ async keywordSearch(query, options) {
5240
+ await this.initialize();
5241
+ const results = await this.sqliteStore.keywordSearch(query, options?.topK ?? 10);
5242
+ const maxRank = Math.min(...results.map((r) => r.rank), -1e-3);
5243
+ const minRank = Math.max(...results.map((r) => r.rank), -1e3);
5244
+ const rankRange = maxRank - minRank || 1;
5245
+ return results.map((r) => ({
5246
+ event: r.event,
5247
+ score: 1 - (r.rank - minRank) / rankRange
5248
+ // Normalize to 0-1
5249
+ })).filter((r) => !options?.minScore || r.score >= options.minScore);
5250
+ }
5251
+ /**
5252
+ * Rebuild FTS index (call after database upgrade)
5253
+ */
5254
+ async rebuildFtsIndex() {
5255
+ await this.initialize();
5256
+ return this.sqliteStore.rebuildFtsIndex();
5257
+ }
4770
5258
  /**
4771
5259
  * Get session history
4772
5260
  */
@@ -5000,6 +5488,31 @@ var MemoryService = class {
5000
5488
  return [];
5001
5489
  return this.consolidatedStore.getAll({ limit });
5002
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
+ }
5003
5516
  /**
5004
5517
  * Increment access count for memories that were used in prompts
5005
5518
  */
@@ -5023,8 +5536,7 @@ var MemoryService = class {
5023
5536
  return events.map((event) => ({
5024
5537
  memoryId: event.id,
5025
5538
  summary: event.content.substring(0, 200) + (event.content.length > 200 ? "..." : ""),
5026
- topics: [],
5027
- // Could extract topics from content if needed
5539
+ topics: this.extractTopicsFromContent(event.content),
5028
5540
  accessCount: event.access_count || 0,
5029
5541
  lastAccessed: event.last_accessed_at || null,
5030
5542
  confidence: 1,
@@ -5045,6 +5557,34 @@ var MemoryService = class {
5045
5557
  }
5046
5558
  return [];
5047
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
+ }
5048
5588
  /**
5049
5589
  * Mark a consolidated memory as accessed
5050
5590
  */
@@ -5108,6 +5648,44 @@ var MemoryService = class {
5108
5648
  lastConsolidation
5109
5649
  };
5110
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
+ }
5111
5689
  /**
5112
5690
  * Format Endless Mode context for Claude
5113
5691
  */
@@ -5216,12 +5794,36 @@ function getMemoryServiceForProject(projectPath, sharedStoreConfig) {
5216
5794
  return serviceCache.get(hash);
5217
5795
  }
5218
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
+
5219
5821
  // src/server/api/sessions.ts
5220
5822
  var sessionsRouter = new Hono();
5221
5823
  sessionsRouter.get("/", async (c) => {
5222
5824
  const page = parseInt(c.req.query("page") || "1", 10);
5223
5825
  const pageSize = parseInt(c.req.query("pageSize") || "20", 10);
5224
- const memoryService = getReadOnlyMemoryService();
5826
+ const memoryService = getServiceFromQuery(c);
5225
5827
  try {
5226
5828
  await memoryService.initialize();
5227
5829
  const recentEvents = await memoryService.getRecentEvents(1e3);
@@ -5265,7 +5867,7 @@ sessionsRouter.get("/", async (c) => {
5265
5867
  });
5266
5868
  sessionsRouter.get("/:id", async (c) => {
5267
5869
  const { id } = c.req.param();
5268
- const memoryService = getReadOnlyMemoryService();
5870
+ const memoryService = getServiceFromQuery(c);
5269
5871
  try {
5270
5872
  await memoryService.initialize();
5271
5873
  const events = await memoryService.getSessionHistory(id);
@@ -5306,18 +5908,36 @@ var eventsRouter = new Hono2();
5306
5908
  eventsRouter.get("/", async (c) => {
5307
5909
  const sessionId = c.req.query("sessionId");
5308
5910
  const eventType = c.req.query("type");
5911
+ const level = c.req.query("level");
5912
+ const sort = c.req.query("sort") || "recent";
5309
5913
  const limit = parseInt(c.req.query("limit") || "100", 10);
5310
5914
  const offset = parseInt(c.req.query("offset") || "0", 10);
5311
- const memoryService = getReadOnlyMemoryService();
5915
+ const memoryService = getServiceFromQuery(c);
5312
5916
  try {
5313
5917
  await memoryService.initialize();
5314
- 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
+ }
5315
5924
  if (sessionId) {
5316
5925
  events = events.filter((e) => e.sessionId === sessionId);
5317
5926
  }
5318
5927
  if (eventType) {
5319
5928
  events = events.filter((e) => e.eventType === eventType);
5320
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
+ }
5321
5941
  const total = events.length;
5322
5942
  events = events.slice(offset, offset + limit);
5323
5943
  return c.json({
@@ -5327,7 +5947,9 @@ eventsRouter.get("/", async (c) => {
5327
5947
  timestamp: e.timestamp,
5328
5948
  sessionId: e.sessionId,
5329
5949
  preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
5330
- contentLength: e.content.length
5950
+ contentLength: e.content.length,
5951
+ accessCount: e.access_count || 0,
5952
+ lastAccessedAt: e.last_accessed_at || null
5331
5953
  })),
5332
5954
  total,
5333
5955
  limit,
@@ -5342,7 +5964,7 @@ eventsRouter.get("/", async (c) => {
5342
5964
  });
5343
5965
  eventsRouter.get("/:id", async (c) => {
5344
5966
  const { id } = c.req.param();
5345
- const memoryService = getReadOnlyMemoryService();
5967
+ const memoryService = getServiceFromQuery(c);
5346
5968
  try {
5347
5969
  await memoryService.initialize();
5348
5970
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -5382,7 +6004,7 @@ eventsRouter.get("/:id", async (c) => {
5382
6004
  import { Hono as Hono3 } from "hono";
5383
6005
  var searchRouter = new Hono3();
5384
6006
  searchRouter.post("/", async (c) => {
5385
- const memoryService = getReadOnlyMemoryService();
6007
+ const memoryService = getServiceFromQuery(c);
5386
6008
  try {
5387
6009
  const body = await c.req.json();
5388
6010
  if (!body.query) {
@@ -5426,7 +6048,7 @@ searchRouter.get("/", async (c) => {
5426
6048
  return c.json({ error: 'Query parameter "q" is required' }, 400);
5427
6049
  }
5428
6050
  const topK = parseInt(c.req.query("topK") || "5", 10);
5429
- const memoryService = getReadOnlyMemoryService();
6051
+ const memoryService = getServiceFromQuery(c);
5430
6052
  try {
5431
6053
  await memoryService.initialize();
5432
6054
  const result = await memoryService.retrieveMemories(query, { topK });
@@ -5454,7 +6076,7 @@ searchRouter.get("/", async (c) => {
5454
6076
  import { Hono as Hono4 } from "hono";
5455
6077
  var statsRouter = new Hono4();
5456
6078
  statsRouter.get("/shared", async (c) => {
5457
- const memoryService = getReadOnlyMemoryService();
6079
+ const memoryService = getServiceFromQuery(c);
5458
6080
  try {
5459
6081
  await memoryService.initialize();
5460
6082
  const sharedStats = await memoryService.getSharedStoreStats();
@@ -5511,7 +6133,7 @@ statsRouter.get("/levels/:level", async (c) => {
5511
6133
  if (!validLevels.includes(level)) {
5512
6134
  return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(", ")}` }, 400);
5513
6135
  }
5514
- const memoryService = getReadOnlyMemoryService();
6136
+ const memoryService = getServiceFromQuery(c);
5515
6137
  try {
5516
6138
  await memoryService.initialize();
5517
6139
  let events = await memoryService.getEventsByLevel(level, { limit: limit * 2, offset });
@@ -5558,7 +6180,7 @@ statsRouter.get("/levels/:level", async (c) => {
5558
6180
  }
5559
6181
  });
5560
6182
  statsRouter.get("/", async (c) => {
5561
- const memoryService = getReadOnlyMemoryService();
6183
+ const memoryService = getServiceFromQuery(c);
5562
6184
  try {
5563
6185
  await memoryService.initialize();
5564
6186
  const stats = await memoryService.getStats();
@@ -5602,7 +6224,7 @@ statsRouter.get("/", async (c) => {
5602
6224
  });
5603
6225
  statsRouter.get("/most-accessed", async (c) => {
5604
6226
  const limit = parseInt(c.req.query("limit") || "10", 10);
5605
- const memoryService = getReadOnlyMemoryService();
6227
+ const memoryService = getServiceFromQuery(c);
5606
6228
  try {
5607
6229
  await memoryService.initialize();
5608
6230
  console.log("[most-accessed] Fetching most accessed memories, limit:", limit);
@@ -5633,7 +6255,7 @@ statsRouter.get("/most-accessed", async (c) => {
5633
6255
  });
5634
6256
  statsRouter.get("/timeline", async (c) => {
5635
6257
  const days = parseInt(c.req.query("days") || "7", 10);
5636
- const memoryService = getReadOnlyMemoryService();
6258
+ const memoryService = getServiceFromQuery(c);
5637
6259
  try {
5638
6260
  await memoryService.initialize();
5639
6261
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -5663,8 +6285,39 @@ statsRouter.get("/timeline", async (c) => {
5663
6285
  await memoryService.shutdown();
5664
6286
  }
5665
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
+ });
5666
6319
  statsRouter.post("/graduation/run", async (c) => {
5667
- const memoryService = getReadOnlyMemoryService();
6320
+ const memoryService = getServiceFromQuery(c);
5668
6321
  try {
5669
6322
  await memoryService.initialize();
5670
6323
  const result = await memoryService.forceGraduation();
@@ -5725,7 +6378,7 @@ var citationsRouter = new Hono5();
5725
6378
  citationsRouter.get("/:id", async (c) => {
5726
6379
  const { id } = c.req.param();
5727
6380
  const citationId = parseCitationId(id) || id;
5728
- const memoryService = getReadOnlyMemoryService();
6381
+ const memoryService = getServiceFromQuery(c);
5729
6382
  try {
5730
6383
  await memoryService.initialize();
5731
6384
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -5759,7 +6412,7 @@ citationsRouter.get("/:id", async (c) => {
5759
6412
  citationsRouter.get("/:id/related", async (c) => {
5760
6413
  const { id } = c.req.param();
5761
6414
  const citationId = parseCitationId(id) || id;
5762
- const memoryService = getReadOnlyMemoryService();
6415
+ const memoryService = getServiceFromQuery(c);
5763
6416
  try {
5764
6417
  await memoryService.initialize();
5765
6418
  const recentEvents = await memoryService.getRecentEvents(1e4);
@@ -5795,8 +6448,358 @@ citationsRouter.get("/:id/related", async (c) => {
5795
6448
  }
5796
6449
  });
5797
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
+
5798
6801
  // src/server/api/index.ts
5799
- 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);
5800
6803
  export {
5801
6804
  apiRouter
5802
6805
  };