claude-memory-layer 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/cli/index.js +1373 -184
  2. package/dist/cli/index.js.map +4 -4
  3. package/dist/core/index.js +445 -7
  4. package/dist/core/index.js.map +2 -2
  5. package/dist/hooks/post-tool-use.js +705 -43
  6. package/dist/hooks/post-tool-use.js.map +4 -4
  7. package/dist/hooks/session-end.js +593 -52
  8. package/dist/hooks/session-end.js.map +3 -3
  9. package/dist/hooks/session-start.js +581 -25
  10. package/dist/hooks/session-start.js.map +3 -3
  11. package/dist/hooks/stop.js +693 -73
  12. package/dist/hooks/stop.js.map +4 -4
  13. package/dist/hooks/user-prompt-submit.js +674 -94
  14. package/dist/hooks/user-prompt-submit.js.map +4 -4
  15. package/dist/server/api/index.js +1045 -42
  16. package/dist/server/api/index.js.map +4 -4
  17. package/dist/server/index.js +1054 -51
  18. package/dist/server/index.js.map +4 -4
  19. package/dist/services/memory-service.js +599 -25
  20. package/dist/services/memory-service.js.map +3 -3
  21. package/dist/ui/app.js +1380 -55
  22. package/dist/ui/index.html +311 -148
  23. package/dist/ui/style.css +892 -0
  24. package/docs/OPERATIONS.md +18 -0
  25. package/package.json +8 -2
  26. package/scripts/fix-sync-gap.js +32 -0
  27. package/scripts/heartbeat-memory-orchestrator.sh +28 -0
  28. package/scripts/report-sync-gap.js +26 -0
  29. package/scripts/review-queue-auto-resolve.js +21 -0
  30. package/scripts/sync-gap-auto-heal.sh +17 -0
  31. package/specs/20260207-dashboard-upgrade/context.md +38 -0
  32. package/specs/20260207-dashboard-upgrade/spec.md +96 -0
  33. package/src/cli/index.ts +110 -58
  34. package/src/core/sqlite-event-store.ts +542 -6
  35. package/src/core/sqlite-wrapper.ts +8 -0
  36. package/src/core/turn-state.ts +159 -0
  37. package/src/core/types.ts +23 -8
  38. package/src/core/vector-store.ts +21 -3
  39. package/src/hooks/post-tool-use.ts +68 -23
  40. package/src/hooks/session-end.ts +8 -3
  41. package/src/hooks/stop.ts +96 -25
  42. package/src/hooks/user-prompt-submit.ts +78 -65
  43. package/src/server/api/chat.ts +244 -0
  44. package/src/server/api/citations.ts +3 -3
  45. package/src/server/api/events.ts +30 -5
  46. package/src/server/api/index.ts +7 -1
  47. package/src/server/api/projects.ts +74 -0
  48. package/src/server/api/search.ts +3 -3
  49. package/src/server/api/sessions.ts +3 -3
  50. package/src/server/api/stats.ts +43 -7
  51. package/src/server/api/turns.ts +143 -0
  52. package/src/server/api/utils.ts +46 -0
  53. package/src/services/memory-service.ts +208 -9
  54. package/src/services/session-history-importer.ts +215 -51
  55. package/src/ui/app.js +1380 -55
  56. package/src/ui/index.html +311 -148
  57. package/src/ui/style.css +892 -0
  58. package/.claude/settings.local.json +0 -27
  59. package/.claude-memory/test.sqlite +0 -0
  60. package/.history/package_20260201112328.json +0 -45
  61. package/.history/package_20260201113602.json +0 -45
  62. package/.history/package_20260201113713.json +0 -45
  63. package/.history/package_20260201114110.json +0 -45
  64. package/.history/package_20260201114632.json +0 -46
  65. package/.history/package_20260201133143.json +0 -45
  66. package/.history/package_20260201134319.json +0 -45
  67. package/.history/package_20260201134326.json +0 -45
  68. package/.history/package_20260201134334.json +0 -45
  69. package/.history/package_20260201134912.json +0 -45
  70. package/.history/package_20260201142928.json +0 -46
  71. package/.history/package_20260201192048.json +0 -47
  72. package/.history/package_20260202114053.json +0 -49
  73. package/test_access.js +0 -49
@@ -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(input.metadata || {})
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
- const rows = sqliteAll(
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
- this.table = await this.db.createTable(this.tableName, [data]);
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
- this.table = await this.db.createTable(this.tableName, data);
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
  }