claude-memory-layer 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1266 -181
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +367 -7
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +598 -40
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +486 -49
- package/dist/hooks/session-end.js.map +3 -3
- package/dist/hooks/session-start.js +474 -22
- package/dist/hooks/session-start.js.map +3 -3
- package/dist/hooks/stop.js +586 -70
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +537 -27
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +938 -39
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +947 -48
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +475 -22
- 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 +444 -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 +44 -5
- 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 +137 -5
- 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/.history/package_20260202121115.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,9 @@ 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);
|
|
1610
1636
|
|
|
1611
1637
|
-- FTS5 Full-Text Search for fast keyword search
|
|
1612
1638
|
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
|
@@ -1650,6 +1676,15 @@ var SQLiteEventStore = class {
|
|
|
1650
1676
|
console.error("Error adding last_accessed_at column:", err);
|
|
1651
1677
|
}
|
|
1652
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
|
+
}
|
|
1653
1688
|
try {
|
|
1654
1689
|
sqliteExec(this.db, `
|
|
1655
1690
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -1662,6 +1697,12 @@ var SQLiteEventStore = class {
|
|
|
1662
1697
|
`);
|
|
1663
1698
|
} catch (err) {
|
|
1664
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
|
+
}
|
|
1665
1706
|
this.initialized = true;
|
|
1666
1707
|
}
|
|
1667
1708
|
/**
|
|
@@ -1686,9 +1727,11 @@ var SQLiteEventStore = class {
|
|
|
1686
1727
|
const id = randomUUID2();
|
|
1687
1728
|
const timestamp = toSQLiteTimestamp(input.timestamp);
|
|
1688
1729
|
try {
|
|
1730
|
+
const metadata = input.metadata || {};
|
|
1731
|
+
const turnId = metadata.turnId || null;
|
|
1689
1732
|
const insertEvent = this.db.prepare(`
|
|
1690
|
-
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
1691
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1733
|
+
INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata, turn_id)
|
|
1734
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1692
1735
|
`);
|
|
1693
1736
|
const insertDedup = this.db.prepare(`
|
|
1694
1737
|
INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)
|
|
@@ -1705,7 +1748,8 @@ var SQLiteEventStore = class {
|
|
|
1705
1748
|
input.content,
|
|
1706
1749
|
canonicalKey,
|
|
1707
1750
|
dedupeKey,
|
|
1708
|
-
JSON.stringify(
|
|
1751
|
+
JSON.stringify(metadata),
|
|
1752
|
+
turnId
|
|
1709
1753
|
);
|
|
1710
1754
|
insertDedup.run(dedupeKey, id);
|
|
1711
1755
|
insertLevel.run(id);
|
|
@@ -2055,11 +2099,11 @@ var SQLiteEventStore = class {
|
|
|
2055
2099
|
);
|
|
2056
2100
|
}
|
|
2057
2101
|
/**
|
|
2058
|
-
* Get most accessed memories
|
|
2102
|
+
* Get most accessed memories (falls back to recent events if none accessed)
|
|
2059
2103
|
*/
|
|
2060
2104
|
async getMostAccessed(limit = 10) {
|
|
2061
2105
|
await this.initialize();
|
|
2062
|
-
|
|
2106
|
+
let rows = sqliteAll(
|
|
2063
2107
|
this.db,
|
|
2064
2108
|
`SELECT * FROM events
|
|
2065
2109
|
WHERE access_count > 0
|
|
@@ -2067,8 +2111,166 @@ var SQLiteEventStore = class {
|
|
|
2067
2111
|
LIMIT ?`,
|
|
2068
2112
|
[limit]
|
|
2069
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
|
+
}
|
|
2070
2123
|
return rows.map((row) => this.rowToEvent(row));
|
|
2071
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
|
+
}
|
|
2072
2274
|
/**
|
|
2073
2275
|
* Fast keyword search using FTS5
|
|
2074
2276
|
* Returns events matching the search query, ranked by relevance
|
|
@@ -2137,6 +2339,143 @@ var SQLiteEventStore = class {
|
|
|
2137
2339
|
async close() {
|
|
2138
2340
|
sqliteClose(this.db);
|
|
2139
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
|
+
}
|
|
2140
2479
|
/**
|
|
2141
2480
|
* Convert database row to MemoryEvent
|
|
2142
2481
|
*/
|
|
@@ -2157,6 +2496,9 @@ var SQLiteEventStore = class {
|
|
|
2157
2496
|
if (row.last_accessed_at !== void 0) {
|
|
2158
2497
|
event.last_accessed_at = row.last_accessed_at;
|
|
2159
2498
|
}
|
|
2499
|
+
if (row.turn_id !== void 0 && row.turn_id !== null) {
|
|
2500
|
+
event.turn_id = row.turn_id;
|
|
2501
|
+
}
|
|
2160
2502
|
return event;
|
|
2161
2503
|
}
|
|
2162
2504
|
};
|
|
@@ -2886,7 +3228,16 @@ var VectorStore = class {
|
|
|
2886
3228
|
metadata: JSON.stringify(record.metadata || {})
|
|
2887
3229
|
};
|
|
2888
3230
|
if (!this.table) {
|
|
2889
|
-
|
|
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
|
+
}
|
|
2890
3241
|
} else {
|
|
2891
3242
|
await this.table.add([data]);
|
|
2892
3243
|
}
|
|
@@ -2912,7 +3263,16 @@ var VectorStore = class {
|
|
|
2912
3263
|
metadata: JSON.stringify(record.metadata || {})
|
|
2913
3264
|
}));
|
|
2914
3265
|
if (!this.table) {
|
|
2915
|
-
|
|
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
|
+
}
|
|
2916
3276
|
} else {
|
|
2917
3277
|
await this.table.add(data);
|
|
2918
3278
|
}
|