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.
- package/dist/cli/index.js +1373 -184
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +445 -7
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +705 -43
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +593 -52
- package/dist/hooks/session-end.js.map +3 -3
- package/dist/hooks/session-start.js +581 -25
- package/dist/hooks/session-start.js.map +3 -3
- package/dist/hooks/stop.js +693 -73
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +674 -94
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1045 -42
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1054 -51
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +599 -25
- package/dist/services/memory-service.js.map +3 -3
- package/dist/ui/app.js +1380 -55
- package/dist/ui/index.html +311 -148
- package/dist/ui/style.css +892 -0
- package/docs/OPERATIONS.md +18 -0
- package/package.json +8 -2
- package/scripts/fix-sync-gap.js +32 -0
- package/scripts/heartbeat-memory-orchestrator.sh +28 -0
- package/scripts/report-sync-gap.js +26 -0
- package/scripts/review-queue-auto-resolve.js +21 -0
- package/scripts/sync-gap-auto-heal.sh +17 -0
- package/specs/20260207-dashboard-upgrade/context.md +38 -0
- package/specs/20260207-dashboard-upgrade/spec.md +96 -0
- package/src/cli/index.ts +110 -58
- package/src/core/sqlite-event-store.ts +542 -6
- package/src/core/sqlite-wrapper.ts +8 -0
- package/src/core/turn-state.ts +159 -0
- package/src/core/types.ts +23 -8
- package/src/core/vector-store.ts +21 -3
- package/src/hooks/post-tool-use.ts +68 -23
- package/src/hooks/session-end.ts +8 -3
- package/src/hooks/stop.ts +96 -25
- package/src/hooks/user-prompt-submit.ts +78 -65
- package/src/server/api/chat.ts +244 -0
- package/src/server/api/citations.ts +3 -3
- package/src/server/api/events.ts +30 -5
- package/src/server/api/index.ts +7 -1
- package/src/server/api/projects.ts +74 -0
- package/src/server/api/search.ts +3 -3
- package/src/server/api/sessions.ts +3 -3
- package/src/server/api/stats.ts +43 -7
- package/src/server/api/turns.ts +143 -0
- package/src/server/api/utils.ts +46 -0
- package/src/services/memory-service.ts +208 -9
- package/src/services/session-history-importer.ts +215 -51
- package/src/ui/app.js +1380 -55
- package/src/ui/index.html +311 -148
- package/src/ui/style.css +892 -0
- package/.claude/settings.local.json +0 -27
- package/.claude-memory/test.sqlite +0 -0
- package/.history/package_20260201112328.json +0 -45
- package/.history/package_20260201113602.json +0 -45
- package/.history/package_20260201113713.json +0 -45
- package/.history/package_20260201114110.json +0 -45
- package/.history/package_20260201114632.json +0 -46
- package/.history/package_20260201133143.json +0 -45
- package/.history/package_20260201134319.json +0 -45
- package/.history/package_20260201134326.json +0 -45
- package/.history/package_20260201134334.json +0 -45
- package/.history/package_20260201134912.json +0 -45
- package/.history/package_20260201142928.json +0 -46
- package/.history/package_20260201192048.json +0 -47
- package/.history/package_20260202114053.json +0 -49
- package/test_access.js +0 -49
package/dist/server/api/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
86
|
+
function createDatabase(path4, options) {
|
|
76
87
|
if (options?.readOnly) {
|
|
77
|
-
return new duckdb.Database(
|
|
88
|
+
return new duckdb.Database(path4, { access_mode: "READ_ONLY" });
|
|
78
89
|
}
|
|
79
|
-
return new duckdb.Database(
|
|
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
|
-
|
|
750
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4544
|
-
|
|
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 (!
|
|
4636
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5915
|
+
const memoryService = getServiceFromQuery(c);
|
|
5312
5916
|
try {
|
|
5313
5917
|
await memoryService.initialize();
|
|
5314
|
-
let events
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
};
|