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/core/index.js
CHANGED
|
@@ -1310,7 +1310,13 @@ var EventStore = class {
|
|
|
1310
1310
|
|
|
1311
1311
|
// src/core/sqlite-wrapper.ts
|
|
1312
1312
|
import Database from "better-sqlite3";
|
|
1313
|
+
import * as fs from "fs";
|
|
1314
|
+
import * as nodePath from "path";
|
|
1313
1315
|
function createSQLiteDatabase(path, options) {
|
|
1316
|
+
const dir = nodePath.dirname(path);
|
|
1317
|
+
if (!fs.existsSync(dir)) {
|
|
1318
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1319
|
+
}
|
|
1314
1320
|
const db = new Database(path, {
|
|
1315
1321
|
readonly: options?.readonly ?? false
|
|
1316
1322
|
});
|
|
@@ -1582,6 +1588,23 @@ var SQLiteEventStore = class {
|
|
|
1582
1588
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
1583
1589
|
);
|
|
1584
1590
|
|
|
1591
|
+
-- Memory Helpfulness tracking
|
|
1592
|
+
CREATE TABLE IF NOT EXISTS memory_helpfulness (
|
|
1593
|
+
id TEXT PRIMARY KEY,
|
|
1594
|
+
event_id TEXT NOT NULL,
|
|
1595
|
+
session_id TEXT NOT NULL,
|
|
1596
|
+
retrieval_score REAL DEFAULT 0,
|
|
1597
|
+
query_preview TEXT,
|
|
1598
|
+
session_continued INTEGER DEFAULT 0,
|
|
1599
|
+
prompt_count_after INTEGER DEFAULT 0,
|
|
1600
|
+
tool_success_count INTEGER DEFAULT 0,
|
|
1601
|
+
tool_total_count INTEGER DEFAULT 0,
|
|
1602
|
+
was_reasked INTEGER DEFAULT 0,
|
|
1603
|
+
helpfulness_score REAL DEFAULT 0.5,
|
|
1604
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
1605
|
+
measured_at TEXT
|
|
1606
|
+
);
|
|
1607
|
+
|
|
1585
1608
|
-- Sync position tracking (for SQLite -> DuckDB sync)
|
|
1586
1609
|
CREATE TABLE IF NOT EXISTS sync_positions (
|
|
1587
1610
|
target_name TEXT PRIMARY KEY,
|
|
@@ -1607,6 +1630,31 @@ var SQLiteEventStore = class {
|
|
|
1607
1630
|
CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence);
|
|
1608
1631
|
CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at);
|
|
1609
1632
|
CREATE INDEX IF NOT EXISTS idx_embedding_outbox_status ON embedding_outbox(status);
|
|
1633
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_event ON memory_helpfulness(event_id);
|
|
1634
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_session ON memory_helpfulness(session_id);
|
|
1635
|
+
CREATE INDEX IF NOT EXISTS idx_helpfulness_score ON memory_helpfulness(helpfulness_score DESC);
|
|
1636
|
+
|
|
1637
|
+
-- FTS5 Full-Text Search for fast keyword search
|
|
1638
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
|
1639
|
+
content,
|
|
1640
|
+
event_id UNINDEXED,
|
|
1641
|
+
content='events',
|
|
1642
|
+
content_rowid='rowid'
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
-- Triggers to keep FTS in sync with events table
|
|
1646
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
1647
|
+
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
1648
|
+
END;
|
|
1649
|
+
|
|
1650
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
1651
|
+
INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
|
|
1652
|
+
END;
|
|
1653
|
+
|
|
1654
|
+
CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
1655
|
+
INSERT INTO events_fts(events_fts, rowid, content, event_id) VALUES('delete', OLD.rowid, OLD.content, OLD.id);
|
|
1656
|
+
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
1657
|
+
END;
|
|
1610
1658
|
`);
|
|
1611
1659
|
const tableInfo = sqliteAll(this.db, "PRAGMA table_info(events)", []);
|
|
1612
1660
|
const columnNames = tableInfo.map((col) => col.name);
|
|
@@ -1628,6 +1676,15 @@ var SQLiteEventStore = class {
|
|
|
1628
1676
|
console.error("Error adding last_accessed_at column:", err);
|
|
1629
1677
|
}
|
|
1630
1678
|
}
|
|
1679
|
+
if (!columnNames.includes("turn_id")) {
|
|
1680
|
+
try {
|
|
1681
|
+
sqliteExec(this.db, `
|
|
1682
|
+
ALTER TABLE events ADD COLUMN turn_id TEXT;
|
|
1683
|
+
`);
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
console.error("Error adding turn_id column:", err);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1631
1688
|
try {
|
|
1632
1689
|
sqliteExec(this.db, `
|
|
1633
1690
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -1640,6 +1697,12 @@ var SQLiteEventStore = class {
|
|
|
1640
1697
|
`);
|
|
1641
1698
|
} catch (err) {
|
|
1642
1699
|
}
|
|
1700
|
+
try {
|
|
1701
|
+
sqliteExec(this.db, `
|
|
1702
|
+
CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
|
|
1703
|
+
`);
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
}
|
|
1643
1706
|
this.initialized = true;
|
|
1644
1707
|
}
|
|
1645
1708
|
/**
|
|
@@ -1664,9 +1727,11 @@ var SQLiteEventStore = class {
|
|
|
1664
1727
|
const id = randomUUID2();
|
|
1665
1728
|
const timestamp = toSQLiteTimestamp(input.timestamp);
|
|
1666
1729
|
try {
|
|
1730
|
+
const metadata = input.metadata || {};
|
|
1731
|
+
const turnId = metadata.turnId || null;
|
|
1667
1732
|
const insertEvent = this.db.prepare(`
|
|
1668
|
-
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
1669
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1733
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
|
|
1734
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1670
1735
|
`);
|
|
1671
1736
|
const insertDedup = this.db.prepare(`
|
|
1672
1737
|
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
@@ -1683,7 +1748,8 @@ var SQLiteEventStore = class {
|
|
|
1683
1748
|
input.content,
|
|
1684
1749
|
canonicalKey,
|
|
1685
1750
|
dedupeKey,
|
|
1686
|
-
JSON.stringify(
|
|
1751
|
+
JSON.stringify(metadata),
|
|
1752
|
+
turnId
|
|
1687
1753
|
);
|
|
1688
1754
|
insertDedup.run(dedupeKey, id);
|
|
1689
1755
|
insertLevel.run(id);
|
|
@@ -2033,11 +2099,11 @@ var SQLiteEventStore = class {
|
|
|
2033
2099
|
);
|
|
2034
2100
|
}
|
|
2035
2101
|
/**
|
|
2036
|
-
* Get most accessed memories
|
|
2102
|
+
* Get most accessed memories (falls back to recent events if none accessed)
|
|
2037
2103
|
*/
|
|
2038
2104
|
async getMostAccessed(limit = 10) {
|
|
2039
2105
|
await this.initialize();
|
|
2040
|
-
|
|
2106
|
+
let rows = sqliteAll(
|
|
2041
2107
|
this.db,
|
|
2042
2108
|
`SELECT * FROM events
|
|
2043
2109
|
WHERE access_count > 0
|
|
@@ -2045,8 +2111,222 @@ var SQLiteEventStore = class {
|
|
|
2045
2111
|
LIMIT ?`,
|
|
2046
2112
|
[limit]
|
|
2047
2113
|
);
|
|
2114
|
+
if (rows.length === 0) {
|
|
2115
|
+
rows = sqliteAll(
|
|
2116
|
+
this.db,
|
|
2117
|
+
`SELECT * FROM events
|
|
2118
|
+
ORDER BY timestamp DESC
|
|
2119
|
+
LIMIT ?`,
|
|
2120
|
+
[limit]
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2048
2123
|
return rows.map((row) => this.rowToEvent(row));
|
|
2049
2124
|
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Record a memory retrieval for helpfulness tracking
|
|
2127
|
+
*/
|
|
2128
|
+
async recordRetrieval(eventId, sessionId, score, query) {
|
|
2129
|
+
if (this.readOnly)
|
|
2130
|
+
return;
|
|
2131
|
+
await this.initialize();
|
|
2132
|
+
const id = randomUUID2();
|
|
2133
|
+
sqliteRun(
|
|
2134
|
+
this.db,
|
|
2135
|
+
`INSERT INTO memory_helpfulness (id, event_id, session_id, retrieval_score, query_preview, created_at)
|
|
2136
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
|
2137
|
+
[id, eventId, sessionId, score, query.slice(0, 100)]
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Evaluate helpfulness for all retrievals in a session
|
|
2142
|
+
* Called at session end - uses behavioral signals to compute score
|
|
2143
|
+
*/
|
|
2144
|
+
async evaluateSessionHelpfulness(sessionId) {
|
|
2145
|
+
if (this.readOnly)
|
|
2146
|
+
return;
|
|
2147
|
+
await this.initialize();
|
|
2148
|
+
const retrievals = sqliteAll(
|
|
2149
|
+
this.db,
|
|
2150
|
+
`SELECT * FROM memory_helpfulness WHERE session_id = ? AND measured_at IS NULL`,
|
|
2151
|
+
[sessionId]
|
|
2152
|
+
);
|
|
2153
|
+
if (retrievals.length === 0)
|
|
2154
|
+
return;
|
|
2155
|
+
const sessionEvents = sqliteAll(
|
|
2156
|
+
this.db,
|
|
2157
|
+
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
2158
|
+
[sessionId]
|
|
2159
|
+
);
|
|
2160
|
+
const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
|
|
2161
|
+
const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
|
|
2162
|
+
let toolSuccessCount = 0;
|
|
2163
|
+
let toolTotalCount = toolEvents.length;
|
|
2164
|
+
for (const t of toolEvents) {
|
|
2165
|
+
try {
|
|
2166
|
+
const content = JSON.parse(t.content);
|
|
2167
|
+
if (content.success !== false)
|
|
2168
|
+
toolSuccessCount++;
|
|
2169
|
+
} catch {
|
|
2170
|
+
toolSuccessCount++;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
const toolSuccessRatio = toolTotalCount > 0 ? toolSuccessCount / toolTotalCount : 0.5;
|
|
2174
|
+
for (const retrieval of retrievals) {
|
|
2175
|
+
const retrievalTime = retrieval.created_at;
|
|
2176
|
+
const eventsAfter = sessionEvents.filter((e) => e.timestamp > retrievalTime);
|
|
2177
|
+
const sessionContinued = eventsAfter.length > 0 ? 1 : 0;
|
|
2178
|
+
const promptsAfter = promptEvents.filter((e) => e.timestamp > retrievalTime);
|
|
2179
|
+
const promptCountAfter = promptsAfter.length;
|
|
2180
|
+
const queryWords = new Set((retrieval.query_preview || "").toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
2181
|
+
let wasReasked = 0;
|
|
2182
|
+
for (const p of promptsAfter) {
|
|
2183
|
+
const pWords = new Set(p.content.toLowerCase().split(/\s+/).filter((w) => w.length > 2));
|
|
2184
|
+
let overlap = 0;
|
|
2185
|
+
for (const w of queryWords) {
|
|
2186
|
+
if (pWords.has(w))
|
|
2187
|
+
overlap++;
|
|
2188
|
+
}
|
|
2189
|
+
if (queryWords.size > 0 && overlap / queryWords.size > 0.5) {
|
|
2190
|
+
wasReasked = 1;
|
|
2191
|
+
break;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
const retrievalScore = retrieval.retrieval_score || 0;
|
|
2195
|
+
const helpfulnessScore = 0.3 * Math.min(retrievalScore, 1) + 0.25 * (sessionContinued ? 1 : 0) + 0.25 * toolSuccessRatio + 0.2 * (wasReasked ? 0 : 1);
|
|
2196
|
+
sqliteRun(
|
|
2197
|
+
this.db,
|
|
2198
|
+
`UPDATE memory_helpfulness
|
|
2199
|
+
SET session_continued = ?, prompt_count_after = ?,
|
|
2200
|
+
tool_success_count = ?, tool_total_count = ?,
|
|
2201
|
+
was_reasked = ?, helpfulness_score = ?,
|
|
2202
|
+
measured_at = datetime('now')
|
|
2203
|
+
WHERE id = ?`,
|
|
2204
|
+
[
|
|
2205
|
+
sessionContinued,
|
|
2206
|
+
promptCountAfter,
|
|
2207
|
+
toolSuccessCount,
|
|
2208
|
+
toolTotalCount,
|
|
2209
|
+
wasReasked,
|
|
2210
|
+
helpfulnessScore,
|
|
2211
|
+
retrieval.id
|
|
2212
|
+
]
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
/**
|
|
2217
|
+
* Get most helpful memories ranked by helpfulness score
|
|
2218
|
+
*/
|
|
2219
|
+
async getHelpfulMemories(limit = 10) {
|
|
2220
|
+
await this.initialize();
|
|
2221
|
+
const rows = sqliteAll(
|
|
2222
|
+
this.db,
|
|
2223
|
+
`SELECT
|
|
2224
|
+
mh.event_id,
|
|
2225
|
+
AVG(mh.helpfulness_score) as avg_score,
|
|
2226
|
+
COUNT(*) as eval_count,
|
|
2227
|
+
e.content,
|
|
2228
|
+
e.access_count
|
|
2229
|
+
FROM memory_helpfulness mh
|
|
2230
|
+
JOIN events e ON e.id = mh.event_id
|
|
2231
|
+
WHERE mh.measured_at IS NOT NULL
|
|
2232
|
+
GROUP BY mh.event_id
|
|
2233
|
+
ORDER BY avg_score DESC
|
|
2234
|
+
LIMIT ?`,
|
|
2235
|
+
[limit]
|
|
2236
|
+
);
|
|
2237
|
+
return rows.map((r) => ({
|
|
2238
|
+
eventId: r.event_id,
|
|
2239
|
+
summary: r.content.substring(0, 200) + (r.content.length > 200 ? "..." : ""),
|
|
2240
|
+
helpfulnessScore: Math.round(r.avg_score * 100) / 100,
|
|
2241
|
+
accessCount: r.access_count || 0,
|
|
2242
|
+
evaluationCount: r.eval_count
|
|
2243
|
+
}));
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Get helpfulness statistics for dashboard
|
|
2247
|
+
*/
|
|
2248
|
+
async getHelpfulnessStats() {
|
|
2249
|
+
await this.initialize();
|
|
2250
|
+
const stats = sqliteGet(
|
|
2251
|
+
this.db,
|
|
2252
|
+
`SELECT
|
|
2253
|
+
AVG(helpfulness_score) as avg_score,
|
|
2254
|
+
COUNT(*) as total_evaluated,
|
|
2255
|
+
SUM(CASE WHEN helpfulness_score >= 0.7 THEN 1 ELSE 0 END) as helpful,
|
|
2256
|
+
SUM(CASE WHEN helpfulness_score >= 0.4 AND helpfulness_score < 0.7 THEN 1 ELSE 0 END) as neutral,
|
|
2257
|
+
SUM(CASE WHEN helpfulness_score < 0.4 THEN 1 ELSE 0 END) as unhelpful
|
|
2258
|
+
FROM memory_helpfulness
|
|
2259
|
+
WHERE measured_at IS NOT NULL`
|
|
2260
|
+
);
|
|
2261
|
+
const totalRow = sqliteGet(
|
|
2262
|
+
this.db,
|
|
2263
|
+
`SELECT COUNT(*) as total FROM memory_helpfulness`
|
|
2264
|
+
);
|
|
2265
|
+
return {
|
|
2266
|
+
avgScore: Math.round((stats?.avg_score || 0) * 100) / 100,
|
|
2267
|
+
totalEvaluated: stats?.total_evaluated || 0,
|
|
2268
|
+
totalRetrievals: totalRow?.total || 0,
|
|
2269
|
+
helpful: stats?.helpful || 0,
|
|
2270
|
+
neutral: stats?.neutral || 0,
|
|
2271
|
+
unhelpful: stats?.unhelpful || 0
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Fast keyword search using FTS5
|
|
2276
|
+
* Returns events matching the search query, ranked by relevance
|
|
2277
|
+
*/
|
|
2278
|
+
async keywordSearch(query, limit = 10) {
|
|
2279
|
+
await this.initialize();
|
|
2280
|
+
const searchTerms = query.replace(/['"(){}[\]^~*?:\\/-]/g, " ").split(/\s+/).filter((term) => term.length > 1).map((term) => `"${term}"*`).join(" OR ");
|
|
2281
|
+
if (!searchTerms) {
|
|
2282
|
+
return [];
|
|
2283
|
+
}
|
|
2284
|
+
try {
|
|
2285
|
+
const rows = sqliteAll(
|
|
2286
|
+
this.db,
|
|
2287
|
+
`SELECT e.*, fts.rank
|
|
2288
|
+
FROM events_fts fts
|
|
2289
|
+
JOIN events e ON e.id = fts.event_id
|
|
2290
|
+
WHERE events_fts MATCH ?
|
|
2291
|
+
ORDER BY fts.rank
|
|
2292
|
+
LIMIT ?`,
|
|
2293
|
+
[searchTerms, limit]
|
|
2294
|
+
);
|
|
2295
|
+
return rows.map((row) => ({
|
|
2296
|
+
event: this.rowToEvent(row),
|
|
2297
|
+
rank: row.rank
|
|
2298
|
+
}));
|
|
2299
|
+
} catch (error) {
|
|
2300
|
+
const likePattern = `%${query}%`;
|
|
2301
|
+
const rows = sqliteAll(
|
|
2302
|
+
this.db,
|
|
2303
|
+
`SELECT *, 0 as rank FROM events
|
|
2304
|
+
WHERE content LIKE ?
|
|
2305
|
+
ORDER BY timestamp DESC
|
|
2306
|
+
LIMIT ?`,
|
|
2307
|
+
[likePattern, limit]
|
|
2308
|
+
);
|
|
2309
|
+
return rows.map((row) => ({
|
|
2310
|
+
event: this.rowToEvent(row),
|
|
2311
|
+
rank: 0
|
|
2312
|
+
}));
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* Rebuild FTS index from existing events
|
|
2317
|
+
* Call this once after upgrading to FTS5
|
|
2318
|
+
*/
|
|
2319
|
+
async rebuildFtsIndex() {
|
|
2320
|
+
await this.initialize();
|
|
2321
|
+
const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
|
|
2322
|
+
const totalEvents = countRow?.count ?? 0;
|
|
2323
|
+
sqliteExec(this.db, `
|
|
2324
|
+
DELETE FROM events_fts;
|
|
2325
|
+
INSERT INTO events_fts(rowid, content, event_id)
|
|
2326
|
+
SELECT rowid, content, id FROM events;
|
|
2327
|
+
`);
|
|
2328
|
+
return totalEvents;
|
|
2329
|
+
}
|
|
2050
2330
|
/**
|
|
2051
2331
|
* Get database instance for direct access
|
|
2052
2332
|
*/
|
|
@@ -2059,6 +2339,143 @@ var SQLiteEventStore = class {
|
|
|
2059
2339
|
async close() {
|
|
2060
2340
|
sqliteClose(this.db);
|
|
2061
2341
|
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Get events grouped by turn_id for a session
|
|
2344
|
+
* Returns turns ordered by first event timestamp (newest first)
|
|
2345
|
+
*/
|
|
2346
|
+
async getSessionTurns(sessionId, options) {
|
|
2347
|
+
await this.initialize();
|
|
2348
|
+
const limit = options?.limit || 20;
|
|
2349
|
+
const offset = options?.offset || 0;
|
|
2350
|
+
const turnRows = sqliteAll(
|
|
2351
|
+
this.db,
|
|
2352
|
+
`SELECT turn_id, MIN(timestamp) as min_ts
|
|
2353
|
+
FROM events
|
|
2354
|
+
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
2355
|
+
GROUP BY turn_id
|
|
2356
|
+
ORDER BY min_ts DESC
|
|
2357
|
+
LIMIT ? OFFSET ?`,
|
|
2358
|
+
[sessionId, limit, offset]
|
|
2359
|
+
);
|
|
2360
|
+
const turns = [];
|
|
2361
|
+
for (const turnRow of turnRows) {
|
|
2362
|
+
const events = await this.getEventsByTurn(turnRow.turn_id);
|
|
2363
|
+
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
2364
|
+
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
2365
|
+
const hasResponse = events.some((e) => e.eventType === "agent_response");
|
|
2366
|
+
turns.push({
|
|
2367
|
+
turnId: turnRow.turn_id,
|
|
2368
|
+
events,
|
|
2369
|
+
startedAt: toDateFromSQLite(turnRow.min_ts),
|
|
2370
|
+
promptPreview: promptEvent ? promptEvent.content.slice(0, 200) + (promptEvent.content.length > 200 ? "..." : "") : "(no prompt)",
|
|
2371
|
+
eventCount: events.length,
|
|
2372
|
+
toolCount: toolEvents.length,
|
|
2373
|
+
hasResponse
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
return turns;
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Get all events for a specific turn_id
|
|
2380
|
+
*/
|
|
2381
|
+
async getEventsByTurn(turnId) {
|
|
2382
|
+
await this.initialize();
|
|
2383
|
+
const rows = sqliteAll(
|
|
2384
|
+
this.db,
|
|
2385
|
+
`SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
|
|
2386
|
+
[turnId]
|
|
2387
|
+
);
|
|
2388
|
+
return rows.map(this.rowToEvent);
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Count total turns for a session
|
|
2392
|
+
*/
|
|
2393
|
+
async countSessionTurns(sessionId) {
|
|
2394
|
+
await this.initialize();
|
|
2395
|
+
const row = sqliteGet(
|
|
2396
|
+
this.db,
|
|
2397
|
+
`SELECT COUNT(DISTINCT turn_id) as count
|
|
2398
|
+
FROM events
|
|
2399
|
+
WHERE session_id = ? AND turn_id IS NOT NULL`,
|
|
2400
|
+
[sessionId]
|
|
2401
|
+
);
|
|
2402
|
+
return row?.count || 0;
|
|
2403
|
+
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Migrate existing events: backfill turn_id for events that have turnId in metadata
|
|
2406
|
+
* but no turn_id column value (for events stored before this migration)
|
|
2407
|
+
*/
|
|
2408
|
+
async backfillTurnIds() {
|
|
2409
|
+
await this.initialize();
|
|
2410
|
+
const rows = sqliteAll(
|
|
2411
|
+
this.db,
|
|
2412
|
+
`SELECT id, metadata FROM events
|
|
2413
|
+
WHERE turn_id IS NULL AND metadata IS NOT NULL AND metadata LIKE '%turnId%'`
|
|
2414
|
+
);
|
|
2415
|
+
let updated = 0;
|
|
2416
|
+
for (const row of rows) {
|
|
2417
|
+
try {
|
|
2418
|
+
const metadata = JSON.parse(row.metadata);
|
|
2419
|
+
if (metadata.turnId) {
|
|
2420
|
+
sqliteRun(
|
|
2421
|
+
this.db,
|
|
2422
|
+
`UPDATE events SET turn_id = ? WHERE id = ?`,
|
|
2423
|
+
[metadata.turnId, row.id]
|
|
2424
|
+
);
|
|
2425
|
+
updated++;
|
|
2426
|
+
}
|
|
2427
|
+
} catch {
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
return updated;
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Delete all events for a session (for force reimport)
|
|
2434
|
+
*/
|
|
2435
|
+
async deleteSessionEvents(sessionId) {
|
|
2436
|
+
await this.initialize();
|
|
2437
|
+
const events = sqliteAll(
|
|
2438
|
+
this.db,
|
|
2439
|
+
`SELECT id FROM events WHERE session_id = ?`,
|
|
2440
|
+
[sessionId]
|
|
2441
|
+
);
|
|
2442
|
+
if (events.length === 0)
|
|
2443
|
+
return 0;
|
|
2444
|
+
const eventIds = events.map((e) => e.id);
|
|
2445
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
2446
|
+
const ftsTriggersDropped = [];
|
|
2447
|
+
for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
|
|
2448
|
+
try {
|
|
2449
|
+
sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
2450
|
+
ftsTriggersDropped.push(triggerName);
|
|
2451
|
+
} catch {
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
|
|
2455
|
+
try {
|
|
2456
|
+
sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders})`, eventIds);
|
|
2457
|
+
} catch {
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
2461
|
+
if (ftsTriggersDropped.length > 0) {
|
|
2462
|
+
try {
|
|
2463
|
+
sqliteRun(this.db, `INSERT INTO events_fts(events_fts) VALUES('rebuild')`);
|
|
2464
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
2465
|
+
INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
2466
|
+
END`);
|
|
2467
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
2468
|
+
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
|
|
2469
|
+
END`);
|
|
2470
|
+
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
2471
|
+
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
|
|
2472
|
+
INSERT INTO events_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
|
|
2473
|
+
END`);
|
|
2474
|
+
} catch {
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
return result.changes || 0;
|
|
2478
|
+
}
|
|
2062
2479
|
/**
|
|
2063
2480
|
* Convert database row to MemoryEvent
|
|
2064
2481
|
*/
|
|
@@ -2079,6 +2496,9 @@ var SQLiteEventStore = class {
|
|
|
2079
2496
|
if (row.last_accessed_at !== void 0) {
|
|
2080
2497
|
event.last_accessed_at = row.last_accessed_at;
|
|
2081
2498
|
}
|
|
2499
|
+
if (row.turn_id !== void 0 && row.turn_id !== null) {
|
|
2500
|
+
event.turn_id = row.turn_id;
|
|
2501
|
+
}
|
|
2082
2502
|
return event;
|
|
2083
2503
|
}
|
|
2084
2504
|
};
|
|
@@ -2808,7 +3228,16 @@ var VectorStore = class {
|
|
|
2808
3228
|
metadata: JSON.stringify(record.metadata || {})
|
|
2809
3229
|
};
|
|
2810
3230
|
if (!this.table) {
|
|
2811
|
-
|
|
3231
|
+
try {
|
|
3232
|
+
this.table = await this.db.createTable(this.tableName, [data]);
|
|
3233
|
+
} catch (e) {
|
|
3234
|
+
if (e?.message?.includes("already exists")) {
|
|
3235
|
+
this.table = await this.db.openTable(this.tableName);
|
|
3236
|
+
await this.table.add([data]);
|
|
3237
|
+
} else {
|
|
3238
|
+
throw e;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
2812
3241
|
} else {
|
|
2813
3242
|
await this.table.add([data]);
|
|
2814
3243
|
}
|
|
@@ -2834,7 +3263,16 @@ var VectorStore = class {
|
|
|
2834
3263
|
metadata: JSON.stringify(record.metadata || {})
|
|
2835
3264
|
}));
|
|
2836
3265
|
if (!this.table) {
|
|
2837
|
-
|
|
3266
|
+
try {
|
|
3267
|
+
this.table = await this.db.createTable(this.tableName, data);
|
|
3268
|
+
} catch (e) {
|
|
3269
|
+
if (e?.message?.includes("already exists")) {
|
|
3270
|
+
this.table = await this.db.openTable(this.tableName);
|
|
3271
|
+
await this.table.add(data);
|
|
3272
|
+
} else {
|
|
3273
|
+
throw e;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
2838
3276
|
} else {
|
|
2839
3277
|
await this.table.add(data);
|
|
2840
3278
|
}
|