claude-memory-layer 1.0.52 → 1.0.54
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/README.md +5 -5
- package/dist/cli/index.js +960 -534
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +437 -164
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +502 -159
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/semantic-daemon.js +432 -134
- package/dist/hooks/semantic-daemon.js.map +4 -4
- package/dist/hooks/session-end.js +496 -152
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +503 -152
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +494 -154
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +503 -159
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/index.js +484 -193
- package/dist/index.js.map +4 -4
- package/dist/mcp/index.js +580 -234
- package/dist/mcp/index.js.map +4 -4
- package/dist/server/api/index.js +693 -354
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +744 -368
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +419 -133
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/assets/js/overview.js +34 -2
- package/package.json +5 -2
package/dist/core/index.js
CHANGED
|
@@ -1412,6 +1412,30 @@ var MemoryQueryService = class {
|
|
|
1412
1412
|
score: 1 - (r.rank - minRank) / rankRange
|
|
1413
1413
|
})).filter((r) => !options?.minScore || r.score >= options.minScore);
|
|
1414
1414
|
}
|
|
1415
|
+
async getEvent(id) {
|
|
1416
|
+
await this.initialize();
|
|
1417
|
+
return this.queryStore.getEvent(id);
|
|
1418
|
+
}
|
|
1419
|
+
async getEventByCitationId(citationId) {
|
|
1420
|
+
await this.initialize();
|
|
1421
|
+
return this.getMaintenanceStore("getEventByCitationId").getEventByCitationId(citationId);
|
|
1422
|
+
}
|
|
1423
|
+
async getEventsAfter(sinceIso) {
|
|
1424
|
+
await this.initialize();
|
|
1425
|
+
return this.getMaintenanceStore("getEventsAfter").getEventsAfter(sinceIso);
|
|
1426
|
+
}
|
|
1427
|
+
async getEventTypeCounts() {
|
|
1428
|
+
await this.initialize();
|
|
1429
|
+
return this.getMaintenanceStore("getEventTypeCounts").getEventTypeCounts();
|
|
1430
|
+
}
|
|
1431
|
+
async getDistinctSessionCount() {
|
|
1432
|
+
await this.initialize();
|
|
1433
|
+
return this.getMaintenanceStore("getDistinctSessionCount").getDistinctSessionCount();
|
|
1434
|
+
}
|
|
1435
|
+
async getDailyEventCounts(sinceIso) {
|
|
1436
|
+
await this.initialize();
|
|
1437
|
+
return this.getMaintenanceStore("getDailyEventCounts").getDailyEventCounts(sinceIso);
|
|
1438
|
+
}
|
|
1415
1439
|
async getSessionHistory(sessionId) {
|
|
1416
1440
|
await this.initialize();
|
|
1417
1441
|
return this.queryStore.getSessionEvents(sessionId);
|
|
@@ -2226,6 +2250,19 @@ function parseEntityCanonicalKey(canonicalKey) {
|
|
|
2226
2250
|
return null;
|
|
2227
2251
|
}
|
|
2228
2252
|
|
|
2253
|
+
// src/core/citation-generator.ts
|
|
2254
|
+
import { createHash as createHash3 } from "crypto";
|
|
2255
|
+
var ID_LENGTH = 6;
|
|
2256
|
+
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
2257
|
+
function generateCitationId(eventId) {
|
|
2258
|
+
const hash = createHash3("sha256").update(eventId).digest();
|
|
2259
|
+
let id = "";
|
|
2260
|
+
for (let i = 0; i < ID_LENGTH; i++) {
|
|
2261
|
+
id += CHARSET[hash[i] % CHARSET.length];
|
|
2262
|
+
}
|
|
2263
|
+
return id;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2229
2266
|
// src/core/sqlite-event-store.ts
|
|
2230
2267
|
import * as nodePath2 from "path";
|
|
2231
2268
|
|
|
@@ -2248,17 +2285,28 @@ function createSQLiteDatabase(path8, options) {
|
|
|
2248
2285
|
}
|
|
2249
2286
|
return db;
|
|
2250
2287
|
}
|
|
2288
|
+
var statementCache = /* @__PURE__ */ new WeakMap();
|
|
2289
|
+
function prepareCached(db, sql) {
|
|
2290
|
+
let cache = statementCache.get(db);
|
|
2291
|
+
if (!cache) {
|
|
2292
|
+
cache = /* @__PURE__ */ new Map();
|
|
2293
|
+
statementCache.set(db, cache);
|
|
2294
|
+
}
|
|
2295
|
+
let stmt = cache.get(sql);
|
|
2296
|
+
if (!stmt) {
|
|
2297
|
+
stmt = db.prepare(sql);
|
|
2298
|
+
cache.set(sql, stmt);
|
|
2299
|
+
}
|
|
2300
|
+
return stmt;
|
|
2301
|
+
}
|
|
2251
2302
|
function sqliteRun(db, sql, params = []) {
|
|
2252
|
-
|
|
2253
|
-
return stmt.run(...params);
|
|
2303
|
+
return prepareCached(db, sql).run(...params);
|
|
2254
2304
|
}
|
|
2255
2305
|
function sqliteAll(db, sql, params = []) {
|
|
2256
|
-
|
|
2257
|
-
return stmt.all(...params);
|
|
2306
|
+
return prepareCached(db, sql).all(...params);
|
|
2258
2307
|
}
|
|
2259
2308
|
function sqliteGet(db, sql, params = []) {
|
|
2260
|
-
|
|
2261
|
-
return stmt.get(...params);
|
|
2309
|
+
return prepareCached(db, sql).get(...params);
|
|
2262
2310
|
}
|
|
2263
2311
|
function sqliteExec(db, sql) {
|
|
2264
2312
|
db.exec(sql);
|
|
@@ -2352,7 +2400,13 @@ function toDate(value) {
|
|
|
2352
2400
|
return new Date(String(value));
|
|
2353
2401
|
}
|
|
2354
2402
|
function createDatabase(dbPath, options) {
|
|
2355
|
-
|
|
2403
|
+
const db = new BetterSqlite3(dbPath, { readonly: options?.readOnly });
|
|
2404
|
+
db.pragma("busy_timeout = 5000");
|
|
2405
|
+
if (!options?.readOnly) {
|
|
2406
|
+
db.pragma("journal_mode = WAL");
|
|
2407
|
+
db.pragma("synchronous = NORMAL");
|
|
2408
|
+
}
|
|
2409
|
+
return db;
|
|
2356
2410
|
}
|
|
2357
2411
|
function dbRun(db, sql, params = []) {
|
|
2358
2412
|
db.prepare(sql).run(...params);
|
|
@@ -2706,6 +2760,7 @@ function normalizeQueryRewriteKind(value) {
|
|
|
2706
2760
|
var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
|
|
2707
2761
|
var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2708
2762
|
var DEFAULT_OUTBOX_MAX_RETRIES = 3;
|
|
2763
|
+
var SQLITE_SCHEMA_VERSION = 1;
|
|
2709
2764
|
function emptyOutboxRecoveryResult() {
|
|
2710
2765
|
return {
|
|
2711
2766
|
embedding: { recoveredProcessing: 0, retriedFailed: 0 },
|
|
@@ -3050,6 +3105,15 @@ var SQLiteEventStore = class {
|
|
|
3050
3105
|
access_count INTEGER DEFAULT 0
|
|
3051
3106
|
);
|
|
3052
3107
|
|
|
3108
|
+
-- Junction: consolidated memory -> source event id (indexed lookup that
|
|
3109
|
+
-- replaces per-event source_events LIKE scans during consolidation).
|
|
3110
|
+
CREATE TABLE IF NOT EXISTS consolidated_memory_events (
|
|
3111
|
+
memory_id TEXT NOT NULL,
|
|
3112
|
+
event_id TEXT NOT NULL,
|
|
3113
|
+
PRIMARY KEY (memory_id, event_id)
|
|
3114
|
+
);
|
|
3115
|
+
CREATE INDEX IF NOT EXISTS idx_cme_event ON consolidated_memory_events(event_id);
|
|
3116
|
+
|
|
3053
3117
|
-- Continuity Log table (tracks context transitions)
|
|
3054
3118
|
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
3055
3119
|
log_id TEXT PRIMARY KEY,
|
|
@@ -3409,11 +3473,17 @@ var SQLiteEventStore = class {
|
|
|
3409
3473
|
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
3410
3474
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
3411
3475
|
END;
|
|
3476
|
+
|
|
3477
|
+
-- Reverse index for citation ids (a non-reversible hash of event id), so
|
|
3478
|
+
-- citation lookups are O(1) instead of scanning recent events and hashing
|
|
3479
|
+
-- each one. Populated lazily/self-healing by getEventByCitationId.
|
|
3480
|
+
CREATE TABLE IF NOT EXISTS event_citations (
|
|
3481
|
+
event_id TEXT PRIMARY KEY,
|
|
3482
|
+
citation_id TEXT NOT NULL
|
|
3483
|
+
);
|
|
3484
|
+
CREATE INDEX IF NOT EXISTS idx_event_citations_citation ON event_citations(citation_id);
|
|
3412
3485
|
`);
|
|
3413
|
-
|
|
3414
|
-
sqliteExec(this.db, `ALTER TABLE memory_action_edges ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';`);
|
|
3415
|
-
} catch {
|
|
3416
|
-
}
|
|
3486
|
+
this.addColumnIfMissing("memory_action_edges", "source", `TEXT NOT NULL DEFAULT 'manual'`);
|
|
3417
3487
|
try {
|
|
3418
3488
|
const edgeIndexes = sqliteAll(this.db, `PRAGMA index_list(memory_action_edges)`, []);
|
|
3419
3489
|
const hasSourceAwareUnique = edgeIndexes.some((index) => {
|
|
@@ -3450,55 +3520,17 @@ var SQLiteEventStore = class {
|
|
|
3450
3520
|
}
|
|
3451
3521
|
} catch {
|
|
3452
3522
|
}
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
try {
|
|
3458
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
|
|
3459
|
-
} catch {
|
|
3460
|
-
}
|
|
3461
|
-
try {
|
|
3462
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN raw_query_text TEXT;`);
|
|
3463
|
-
} catch {
|
|
3464
|
-
}
|
|
3465
|
-
try {
|
|
3466
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN query_rewrite_kind TEXT;`);
|
|
3467
|
-
} catch {
|
|
3468
|
-
}
|
|
3523
|
+
this.addColumnIfMissing("retrieval_traces", "selected_details_json", "TEXT");
|
|
3524
|
+
this.addColumnIfMissing("retrieval_traces", "candidate_details_json", "TEXT");
|
|
3525
|
+
this.addColumnIfMissing("retrieval_traces", "raw_query_text", "TEXT");
|
|
3526
|
+
this.addColumnIfMissing("retrieval_traces", "query_rewrite_kind", "TEXT");
|
|
3469
3527
|
try {
|
|
3470
3528
|
sqliteExec(this.db, `CREATE INDEX IF NOT EXISTS idx_retrieval_traces_query_rewrite_kind ON retrieval_traces(query_rewrite_kind);`);
|
|
3471
3529
|
} catch {
|
|
3472
3530
|
}
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
try {
|
|
3477
|
-
sqliteExec(this.db, `
|
|
3478
|
-
ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
|
|
3479
|
-
`);
|
|
3480
|
-
} catch (err) {
|
|
3481
|
-
console.error("Error adding access_count column:", err);
|
|
3482
|
-
}
|
|
3483
|
-
}
|
|
3484
|
-
if (!columnNames.includes("last_accessed_at")) {
|
|
3485
|
-
try {
|
|
3486
|
-
sqliteExec(this.db, `
|
|
3487
|
-
ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
|
|
3488
|
-
`);
|
|
3489
|
-
} catch (err) {
|
|
3490
|
-
console.error("Error adding last_accessed_at column:", err);
|
|
3491
|
-
}
|
|
3492
|
-
}
|
|
3493
|
-
if (!columnNames.includes("turn_id")) {
|
|
3494
|
-
try {
|
|
3495
|
-
sqliteExec(this.db, `
|
|
3496
|
-
ALTER TABLE events ADD COLUMN turn_id TEXT;
|
|
3497
|
-
`);
|
|
3498
|
-
} catch (err) {
|
|
3499
|
-
console.error("Error adding turn_id column:", err);
|
|
3500
|
-
}
|
|
3501
|
-
}
|
|
3531
|
+
this.addColumnIfMissing("events", "access_count", "INTEGER DEFAULT 0");
|
|
3532
|
+
this.addColumnIfMissing("events", "last_accessed_at", "TEXT");
|
|
3533
|
+
this.addColumnIfMissing("events", "turn_id", "TEXT");
|
|
3502
3534
|
try {
|
|
3503
3535
|
sqliteExec(this.db, `
|
|
3504
3536
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -3515,8 +3547,18 @@ var SQLiteEventStore = class {
|
|
|
3515
3547
|
sqliteExec(this.db, `
|
|
3516
3548
|
CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
|
|
3517
3549
|
`);
|
|
3518
|
-
} catch
|
|
3550
|
+
} catch {
|
|
3519
3551
|
}
|
|
3552
|
+
const cmeCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM consolidated_memory_events`, []);
|
|
3553
|
+
if ((cmeCount?.c ?? 0) === 0) {
|
|
3554
|
+
sqliteExec(this.db, `
|
|
3555
|
+
INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id)
|
|
3556
|
+
SELECT cm.memory_id, je.value
|
|
3557
|
+
FROM consolidated_memories cm, json_each(cm.source_events) je
|
|
3558
|
+
WHERE json_valid(cm.source_events);
|
|
3559
|
+
`);
|
|
3560
|
+
}
|
|
3561
|
+
sqliteExec(this.db, `PRAGMA user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
3520
3562
|
this.initialized = true;
|
|
3521
3563
|
}
|
|
3522
3564
|
/**
|
|
@@ -3648,6 +3690,67 @@ var SQLiteEventStore = class {
|
|
|
3648
3690
|
);
|
|
3649
3691
|
return rows.map(this.rowToEvent);
|
|
3650
3692
|
}
|
|
3693
|
+
/**
|
|
3694
|
+
* Aggregate event counts by type via SQL GROUP BY, instead of loading rows
|
|
3695
|
+
* into memory and counting in JS. Counts all events (the old recent-window
|
|
3696
|
+
* scan also under-counted stores larger than its cap).
|
|
3697
|
+
*/
|
|
3698
|
+
async getEventTypeCounts(options) {
|
|
3699
|
+
await this.initialize();
|
|
3700
|
+
const rows = sqliteAll(
|
|
3701
|
+
this.db,
|
|
3702
|
+
`SELECT event_type, COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)} GROUP BY event_type`,
|
|
3703
|
+
[]
|
|
3704
|
+
);
|
|
3705
|
+
return rows.map((row) => ({ eventType: row.event_type, count: row.count }));
|
|
3706
|
+
}
|
|
3707
|
+
/**
|
|
3708
|
+
* Fetch every event at/after an ISO timestamp (ascending), with no row cap.
|
|
3709
|
+
* For window-scoped analytics (KPI/usefulness) this fetches exactly the events
|
|
3710
|
+
* the window needs instead of an arbitrary "most recent N" slice that both
|
|
3711
|
+
* over-fetches sparse windows and truncates very active ones.
|
|
3712
|
+
*/
|
|
3713
|
+
async getEventsAfter(sinceIso, options) {
|
|
3714
|
+
await this.initialize();
|
|
3715
|
+
const rows = sqliteAll(
|
|
3716
|
+
this.db,
|
|
3717
|
+
`SELECT * FROM events WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3718
|
+
[sinceIso]
|
|
3719
|
+
);
|
|
3720
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3721
|
+
}
|
|
3722
|
+
/** Count distinct sessions via SQL instead of materializing a Set in JS. */
|
|
3723
|
+
async getDistinctSessionCount(options) {
|
|
3724
|
+
await this.initialize();
|
|
3725
|
+
const row = sqliteGet(
|
|
3726
|
+
this.db,
|
|
3727
|
+
`SELECT COUNT(DISTINCT session_id) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`,
|
|
3728
|
+
[]
|
|
3729
|
+
);
|
|
3730
|
+
return row?.count ?? 0;
|
|
3731
|
+
}
|
|
3732
|
+
/**
|
|
3733
|
+
* Per-day event counts (with type breakdown) since an ISO timestamp, computed
|
|
3734
|
+
* by SQL GROUP BY on the date prefix. Timestamps are stored as ISO-8601
|
|
3735
|
+
* strings, so substr(...,1,10) is the UTC day and `timestamp >= ?` compares
|
|
3736
|
+
* lexicographically.
|
|
3737
|
+
*/
|
|
3738
|
+
async getDailyEventCounts(sinceIso, options) {
|
|
3739
|
+
await this.initialize();
|
|
3740
|
+
return sqliteAll(
|
|
3741
|
+
this.db,
|
|
3742
|
+
`SELECT substr(timestamp, 1, 10) as day,
|
|
3743
|
+
COUNT(*) as total,
|
|
3744
|
+
SUM(CASE WHEN event_type = 'user_prompt' THEN 1 ELSE 0 END) as prompts,
|
|
3745
|
+
SUM(CASE WHEN event_type = 'agent_response' THEN 1 ELSE 0 END) as responses,
|
|
3746
|
+
SUM(CASE WHEN event_type = 'tool_observation' THEN 1 ELSE 0 END) as tools
|
|
3747
|
+
FROM events
|
|
3748
|
+
WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)}
|
|
3749
|
+
GROUP BY day
|
|
3750
|
+
ORDER BY day ASC`,
|
|
3751
|
+
[sinceIso]
|
|
3752
|
+
);
|
|
3753
|
+
}
|
|
3651
3754
|
/**
|
|
3652
3755
|
* Get event by ID
|
|
3653
3756
|
*/
|
|
@@ -3661,6 +3764,71 @@ var SQLiteEventStore = class {
|
|
|
3661
3764
|
if (!row) return null;
|
|
3662
3765
|
return this.rowToEvent(row);
|
|
3663
3766
|
}
|
|
3767
|
+
/**
|
|
3768
|
+
* Batch-fetch events by id in a single query, applying the same quarantine
|
|
3769
|
+
* predicate as getEvent. Lets hot paths replace N sequential round-trips with
|
|
3770
|
+
* one. Missing ids are simply absent from the result (order not guaranteed).
|
|
3771
|
+
*/
|
|
3772
|
+
async getEvents(ids, options) {
|
|
3773
|
+
await this.initialize();
|
|
3774
|
+
if (ids.length === 0) return [];
|
|
3775
|
+
const placeholders2 = ids.map(() => "?").join(",");
|
|
3776
|
+
const rows = sqliteAll(
|
|
3777
|
+
this.db,
|
|
3778
|
+
`SELECT * FROM events WHERE id IN (${placeholders2}) AND ${maybeQuarantinePredicate(options)}`,
|
|
3779
|
+
ids
|
|
3780
|
+
);
|
|
3781
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3782
|
+
}
|
|
3783
|
+
/**
|
|
3784
|
+
* Resolve an event from its citation id via the reverse index, instead of
|
|
3785
|
+
* scanning recent events and hashing each one. The index is self-healing: a
|
|
3786
|
+
* miss triggers a one-time backfill of any events not yet indexed (covering
|
|
3787
|
+
* freshly appended events) before retrying.
|
|
3788
|
+
*/
|
|
3789
|
+
async getEventByCitationId(citationId, options) {
|
|
3790
|
+
await this.initialize();
|
|
3791
|
+
if (!citationId) return null;
|
|
3792
|
+
let row = sqliteGet(
|
|
3793
|
+
this.db,
|
|
3794
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3795
|
+
[citationId]
|
|
3796
|
+
);
|
|
3797
|
+
if (!row) {
|
|
3798
|
+
this.indexMissingCitations();
|
|
3799
|
+
row = sqliteGet(
|
|
3800
|
+
this.db,
|
|
3801
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3802
|
+
[citationId]
|
|
3803
|
+
);
|
|
3804
|
+
}
|
|
3805
|
+
if (!row) return null;
|
|
3806
|
+
return this.getEvent(row.event_id, options);
|
|
3807
|
+
}
|
|
3808
|
+
/**
|
|
3809
|
+
* Backfill citation ids for any events not yet present in event_citations.
|
|
3810
|
+
* Gated on a cheap count comparison so the common (fully-indexed) path does no
|
|
3811
|
+
* work, and the scan only runs when there are genuinely new events to index.
|
|
3812
|
+
*/
|
|
3813
|
+
indexMissingCitations() {
|
|
3814
|
+
const eventCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM events`, [])?.c ?? 0;
|
|
3815
|
+
const indexedCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM event_citations`, [])?.c ?? 0;
|
|
3816
|
+
if (indexedCount >= eventCount) return;
|
|
3817
|
+
const unindexed = sqliteAll(
|
|
3818
|
+
this.db,
|
|
3819
|
+
`SELECT e.id FROM events e
|
|
3820
|
+
LEFT JOIN event_citations ec ON ec.event_id = e.id
|
|
3821
|
+
WHERE ec.event_id IS NULL`,
|
|
3822
|
+
[]
|
|
3823
|
+
);
|
|
3824
|
+
if (unindexed.length === 0) return;
|
|
3825
|
+
const insert = this.db.prepare(`INSERT OR IGNORE INTO event_citations (event_id, citation_id) VALUES (?, ?)`);
|
|
3826
|
+
this.db.transaction(() => {
|
|
3827
|
+
for (const { id } of unindexed) {
|
|
3828
|
+
insert.run(id, generateCitationId(id));
|
|
3829
|
+
}
|
|
3830
|
+
})();
|
|
3831
|
+
}
|
|
3664
3832
|
/**
|
|
3665
3833
|
* Get events since a timestamp (for sync)
|
|
3666
3834
|
*/
|
|
@@ -3862,25 +4030,22 @@ var SQLiteEventStore = class {
|
|
|
3862
4030
|
*/
|
|
3863
4031
|
async getPendingOutboxItems(limit = 32) {
|
|
3864
4032
|
await this.initialize();
|
|
3865
|
-
const
|
|
3866
|
-
this.db,
|
|
3867
|
-
`SELECT * FROM embedding_outbox
|
|
3868
|
-
WHERE status = 'pending'
|
|
3869
|
-
ORDER BY created_at
|
|
3870
|
-
LIMIT ?`,
|
|
3871
|
-
[limit]
|
|
3872
|
-
);
|
|
3873
|
-
if (pending.length === 0) return [];
|
|
3874
|
-
const ids = pending.map((r) => r.id);
|
|
3875
|
-
const placeholders2 = ids.map(() => "?").join(",");
|
|
3876
|
-
sqliteRun(
|
|
4033
|
+
const claimed = sqliteAll(
|
|
3877
4034
|
this.db,
|
|
3878
4035
|
`UPDATE embedding_outbox
|
|
3879
4036
|
SET status = 'processing', processed_at = datetime('now'), error_message = NULL
|
|
3880
|
-
WHERE id IN (
|
|
3881
|
-
|
|
4037
|
+
WHERE id IN (
|
|
4038
|
+
SELECT id FROM embedding_outbox
|
|
4039
|
+
WHERE status = 'pending'
|
|
4040
|
+
ORDER BY created_at
|
|
4041
|
+
LIMIT ?
|
|
4042
|
+
)
|
|
4043
|
+
RETURNING *`,
|
|
4044
|
+
[limit]
|
|
3882
4045
|
);
|
|
3883
|
-
|
|
4046
|
+
if (claimed.length === 0) return [];
|
|
4047
|
+
claimed.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
|
|
4048
|
+
return claimed.map((row) => ({
|
|
3884
4049
|
id: row.id,
|
|
3885
4050
|
eventId: row.event_id,
|
|
3886
4051
|
content: row.content,
|
|
@@ -4484,7 +4649,7 @@ var SQLiteEventStore = class {
|
|
|
4484
4649
|
const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
|
|
4485
4650
|
const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
|
|
4486
4651
|
let toolSuccessCount = 0;
|
|
4487
|
-
|
|
4652
|
+
const toolTotalCount = toolEvents.length;
|
|
4488
4653
|
for (const t of toolEvents) {
|
|
4489
4654
|
try {
|
|
4490
4655
|
const content = JSON.parse(t.content);
|
|
@@ -4647,10 +4812,14 @@ var SQLiteEventStore = class {
|
|
|
4647
4812
|
* Rebuild FTS index from existing events
|
|
4648
4813
|
* Call this once after upgrading to FTS5
|
|
4649
4814
|
*/
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4815
|
+
/**
|
|
4816
|
+
* Drop and recreate the events_fts virtual table + sync triggers, repopulating
|
|
4817
|
+
* from the current events table. Recreating the table (instead of issuing
|
|
4818
|
+
* DELETEs against it) sidesteps a quirk where older migrated FTS5 tables fail
|
|
4819
|
+
* with `no such column: T.event_id` on synthetic deletes. Safe to call inside
|
|
4820
|
+
* a transaction (pure DDL + INSERT...SELECT, no transaction control).
|
|
4821
|
+
*/
|
|
4822
|
+
recreateEventsFtsTable() {
|
|
4654
4823
|
sqliteExec(this.db, `
|
|
4655
4824
|
DROP TRIGGER IF EXISTS events_fts_insert;
|
|
4656
4825
|
DROP TRIGGER IF EXISTS events_fts_delete;
|
|
@@ -4679,6 +4848,12 @@ var SQLiteEventStore = class {
|
|
|
4679
4848
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4680
4849
|
END;
|
|
4681
4850
|
`);
|
|
4851
|
+
}
|
|
4852
|
+
async rebuildFtsIndex() {
|
|
4853
|
+
await this.initialize();
|
|
4854
|
+
const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
|
|
4855
|
+
const totalEvents = countRow?.count ?? 0;
|
|
4856
|
+
this.recreateEventsFtsTable();
|
|
4682
4857
|
return totalEvents;
|
|
4683
4858
|
}
|
|
4684
4859
|
/**
|
|
@@ -4696,6 +4871,35 @@ var SQLiteEventStore = class {
|
|
|
4696
4871
|
return false;
|
|
4697
4872
|
}
|
|
4698
4873
|
}
|
|
4874
|
+
hasTable(tableName) {
|
|
4875
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) return false;
|
|
4876
|
+
try {
|
|
4877
|
+
const rows = sqliteAll(
|
|
4878
|
+
this.db,
|
|
4879
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`,
|
|
4880
|
+
[tableName]
|
|
4881
|
+
);
|
|
4882
|
+
return rows.length > 0;
|
|
4883
|
+
} catch {
|
|
4884
|
+
return false;
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
/**
|
|
4888
|
+
* Add a column only when its table exists and the column is missing.
|
|
4889
|
+
*
|
|
4890
|
+
* Unlike a blind `try { ALTER } catch {}`, this distinguishes "already
|
|
4891
|
+
* migrated" / "table not present yet" (both safely skipped) from a genuine
|
|
4892
|
+
* failure (disk full, corruption), which is allowed to surface instead of
|
|
4893
|
+
* being silently swallowed as if the migration had succeeded.
|
|
4894
|
+
*/
|
|
4895
|
+
addColumnIfMissing(table, column, definition) {
|
|
4896
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
|
|
4897
|
+
throw new Error(`Invalid identifier in migration: ${table}.${column}`);
|
|
4898
|
+
}
|
|
4899
|
+
if (!this.hasTable(table)) return;
|
|
4900
|
+
if (this.hasTableColumn(table, column)) return;
|
|
4901
|
+
sqliteExec(this.db, `ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`);
|
|
4902
|
+
}
|
|
4699
4903
|
async recordRetrievalTrace(input) {
|
|
4700
4904
|
await this.initialize();
|
|
4701
4905
|
const traceId = randomUUID2();
|
|
@@ -4781,6 +4985,45 @@ var SQLiteEventStore = class {
|
|
|
4781
4985
|
FROM retrieval_traces`,
|
|
4782
4986
|
[]
|
|
4783
4987
|
);
|
|
4988
|
+
const strategyColumnSql = this.hasTableColumn("retrieval_traces", "strategy") ? "COALESCE(NULLIF(TRIM(strategy), ''), 'unknown')" : "'unknown'";
|
|
4989
|
+
const strategyRows = sqliteAll(
|
|
4990
|
+
this.db,
|
|
4991
|
+
`SELECT
|
|
4992
|
+
${strategyColumnSql} as strategy,
|
|
4993
|
+
COUNT(*) as total_queries,
|
|
4994
|
+
SUM(CASE WHEN selected_count > 0 THEN 1 ELSE 0 END) as queries_with_selection,
|
|
4995
|
+
SUM(CASE WHEN ${rewrittenQueryRewriteKindSql} THEN 1 ELSE 0 END) as rewritten_queries,
|
|
4996
|
+
SUM(candidate_count) as total_candidate_count,
|
|
4997
|
+
SUM(selected_count) as total_selected_count,
|
|
4998
|
+
AVG(candidate_count) as avg_candidate_count,
|
|
4999
|
+
AVG(selected_count) as avg_selected_count,
|
|
5000
|
+
CASE
|
|
5001
|
+
WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
|
|
5002
|
+
ELSE 0
|
|
5003
|
+
END as selection_rate
|
|
5004
|
+
FROM retrieval_traces
|
|
5005
|
+
GROUP BY ${strategyColumnSql}
|
|
5006
|
+
ORDER BY total_queries DESC, strategy ASC`,
|
|
5007
|
+
[]
|
|
5008
|
+
);
|
|
5009
|
+
const strategyBreakdown = strategyRows.map((strategyRow) => {
|
|
5010
|
+
const strategyTotalQueries = Number(strategyRow.total_queries || 0);
|
|
5011
|
+
const strategyQueriesWithSelection = Number(strategyRow.queries_with_selection || 0);
|
|
5012
|
+
const strategyRewrittenQueries = Number(strategyRow.rewritten_queries || 0);
|
|
5013
|
+
return {
|
|
5014
|
+
strategy: String(strategyRow.strategy || "unknown"),
|
|
5015
|
+
totalQueries: strategyTotalQueries,
|
|
5016
|
+
queriesWithSelection: strategyQueriesWithSelection,
|
|
5017
|
+
rewrittenQueries: strategyRewrittenQueries,
|
|
5018
|
+
rewriteRate: strategyTotalQueries > 0 ? strategyRewrittenQueries / strategyTotalQueries : 0,
|
|
5019
|
+
totalCandidateCount: Number(strategyRow.total_candidate_count || 0),
|
|
5020
|
+
totalSelectedCount: Number(strategyRow.total_selected_count || 0),
|
|
5021
|
+
avgCandidateCount: Number(strategyRow.avg_candidate_count || 0),
|
|
5022
|
+
avgSelectedCount: Number(strategyRow.avg_selected_count || 0),
|
|
5023
|
+
selectionRate: Number(strategyRow.selection_rate || 0),
|
|
5024
|
+
queryYieldRate: strategyTotalQueries > 0 ? strategyQueriesWithSelection / strategyTotalQueries : 0
|
|
5025
|
+
};
|
|
5026
|
+
});
|
|
4784
5027
|
const totalQueries = Number(row?.total_queries || 0);
|
|
4785
5028
|
const rewrittenQueries = Number(row?.rewritten_queries || 0);
|
|
4786
5029
|
const rawQueries = Math.max(0, totalQueries - rewrittenQueries);
|
|
@@ -4798,7 +5041,8 @@ var SQLiteEventStore = class {
|
|
|
4798
5041
|
rewrittenSelectionRate: rewrittenQueries > 0 ? rewrittenQueriesWithSelection / rewrittenQueries : 0,
|
|
4799
5042
|
rawSelectionRate: rawQueries > 0 ? rawQueriesWithSelection / rawQueries : 0,
|
|
4800
5043
|
avgSelectedCountForRewrittenQueries: Number(row?.avg_selected_count_for_rewritten_queries || 0),
|
|
4801
|
-
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0)
|
|
5044
|
+
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0),
|
|
5045
|
+
strategyBreakdown
|
|
4802
5046
|
};
|
|
4803
5047
|
} catch (err) {
|
|
4804
5048
|
if (err?.message?.includes("no such table")) {
|
|
@@ -4814,7 +5058,8 @@ var SQLiteEventStore = class {
|
|
|
4814
5058
|
rewrittenSelectionRate: 0,
|
|
4815
5059
|
rawSelectionRate: 0,
|
|
4816
5060
|
avgSelectedCountForRewrittenQueries: 0,
|
|
4817
|
-
avgSelectedCountForRawQueries: 0
|
|
5061
|
+
avgSelectedCountForRawQueries: 0,
|
|
5062
|
+
strategyBreakdown: []
|
|
4818
5063
|
};
|
|
4819
5064
|
}
|
|
4820
5065
|
throw err;
|
|
@@ -4931,38 +5176,19 @@ var SQLiteEventStore = class {
|
|
|
4931
5176
|
if (events.length === 0) return 0;
|
|
4932
5177
|
const eventIds = events.map((e) => e.id);
|
|
4933
5178
|
const placeholders2 = eventIds.map(() => "?").join(",");
|
|
4934
|
-
const
|
|
4935
|
-
|
|
4936
|
-
|
|
5179
|
+
const relatedTables = ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox", "event_citations"].filter((table) => this.hasTable(table) && this.hasTableColumn(table, "event_id"));
|
|
5180
|
+
const runDelete = this.db.transaction(() => {
|
|
5181
|
+
for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
|
|
4937
5182
|
sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
4938
|
-
ftsTriggersDropped.push(triggerName);
|
|
4939
|
-
} catch {
|
|
4940
5183
|
}
|
|
4941
|
-
|
|
4942
|
-
for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
|
|
4943
|
-
try {
|
|
5184
|
+
for (const table of relatedTables) {
|
|
4944
5185
|
sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders2})`, eventIds);
|
|
4945
|
-
} catch {
|
|
4946
|
-
}
|
|
4947
|
-
}
|
|
4948
|
-
const result2 = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
4949
|
-
if (ftsTriggersDropped.length > 0) {
|
|
4950
|
-
try {
|
|
4951
|
-
sqliteRun(this.db, `INSERT INTO events_fts(events_fts) VALUES('rebuild')`);
|
|
4952
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
4953
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4954
|
-
END`);
|
|
4955
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
4956
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4957
|
-
END`);
|
|
4958
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
4959
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4960
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4961
|
-
END`);
|
|
4962
|
-
} catch {
|
|
4963
5186
|
}
|
|
4964
|
-
|
|
4965
|
-
|
|
5187
|
+
const result2 = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
5188
|
+
this.recreateEventsFtsTable();
|
|
5189
|
+
return result2.changes || 0;
|
|
5190
|
+
});
|
|
5191
|
+
return runDelete();
|
|
4966
5192
|
}
|
|
4967
5193
|
/**
|
|
4968
5194
|
* Convert database row to MemoryEvent
|
|
@@ -6179,7 +6405,7 @@ var FrontierService = class {
|
|
|
6179
6405
|
};
|
|
6180
6406
|
|
|
6181
6407
|
// src/core/operations/action-projector.ts
|
|
6182
|
-
import { createHash as
|
|
6408
|
+
import { createHash as createHash4 } from "crypto";
|
|
6183
6409
|
import { z as z10 } from "zod";
|
|
6184
6410
|
var NonEmptyStringSchema3 = z10.string().transform((value) => value.trim()).pipe(z10.string().min(1));
|
|
6185
6411
|
var ProjectTaskActionsInputSchema = z10.object({
|
|
@@ -6374,7 +6600,7 @@ var TaskActionProjector = class {
|
|
|
6374
6600
|
}
|
|
6375
6601
|
};
|
|
6376
6602
|
function actionIdForTaskEntity(entityId) {
|
|
6377
|
-
const bytes =
|
|
6603
|
+
const bytes = createHash4("sha256").update(`memory-action:task:${entityId}`).digest().subarray(0, 16);
|
|
6378
6604
|
bytes[6] = bytes[6] & 15 | 64;
|
|
6379
6605
|
bytes[8] = bytes[8] & 63 | 128;
|
|
6380
6606
|
const hex = bytes.toString("hex");
|
|
@@ -8254,7 +8480,7 @@ var LessonRepository = class {
|
|
|
8254
8480
|
};
|
|
8255
8481
|
|
|
8256
8482
|
// src/core/operations/lesson-candidate-service.ts
|
|
8257
|
-
import { createHash as
|
|
8483
|
+
import { createHash as createHash5 } from "crypto";
|
|
8258
8484
|
import { z as z14 } from "zod";
|
|
8259
8485
|
var NonEmptyStringSchema4 = z14.string().transform((value) => value.trim()).pipe(z14.string().min(1));
|
|
8260
8486
|
var LessonCandidateInputSchema = z14.object({
|
|
@@ -8446,7 +8672,7 @@ function confidenceForGroup(group, tools, fileCategories, taskPatterns) {
|
|
|
8446
8672
|
return Math.min(0.95, Math.round((0.58 + sessionBonus + toolBonus + evidenceBonus + taskBonus) * 100) / 100);
|
|
8447
8673
|
}
|
|
8448
8674
|
function candidateIdFor(projectHash, signature) {
|
|
8449
|
-
return `lesson-candidate:${
|
|
8675
|
+
return `lesson-candidate:${createHash5("sha256").update(`${projectHash}
|
|
8450
8676
|
${signature}`).digest("hex").slice(0, 16)}`;
|
|
8451
8677
|
}
|
|
8452
8678
|
function sanitizeArray(values) {
|
|
@@ -9236,7 +9462,7 @@ var ActorCardRepository = class {
|
|
|
9236
9462
|
};
|
|
9237
9463
|
|
|
9238
9464
|
// src/core/operations/perspective-observation-repository.ts
|
|
9239
|
-
import { createHash as
|
|
9465
|
+
import { createHash as createHash6, randomUUID as randomUUID12 } from "crypto";
|
|
9240
9466
|
function projectHashToStorage6(projectHash) {
|
|
9241
9467
|
return projectHash ?? "";
|
|
9242
9468
|
}
|
|
@@ -9273,7 +9499,7 @@ function sanitizeStoredRecord(value) {
|
|
|
9273
9499
|
return sanitizeGovernanceAuditValue(value);
|
|
9274
9500
|
}
|
|
9275
9501
|
function stableHash(value) {
|
|
9276
|
-
return
|
|
9502
|
+
return createHash6("sha256").update(JSON.stringify(value)).digest("hex");
|
|
9277
9503
|
}
|
|
9278
9504
|
function evidenceHash(sourceEventIds, sourceObservationIds) {
|
|
9279
9505
|
return stableHash({
|
|
@@ -9781,19 +10007,6 @@ function createPerspectiveDeriver(options) {
|
|
|
9781
10007
|
return new PerspectiveDeriver(options);
|
|
9782
10008
|
}
|
|
9783
10009
|
|
|
9784
|
-
// src/core/citation-generator.ts
|
|
9785
|
-
import { createHash as createHash6 } from "crypto";
|
|
9786
|
-
var ID_LENGTH = 6;
|
|
9787
|
-
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
9788
|
-
function generateCitationId(eventId) {
|
|
9789
|
-
const hash = createHash6("sha256").update(eventId).digest();
|
|
9790
|
-
let id = "";
|
|
9791
|
-
for (let i = 0; i < ID_LENGTH; i++) {
|
|
9792
|
-
id += CHARSET[hash[i] % CHARSET.length];
|
|
9793
|
-
}
|
|
9794
|
-
return id;
|
|
9795
|
-
}
|
|
9796
|
-
|
|
9797
10010
|
// src/core/operations/perspective-query-agent.ts
|
|
9798
10011
|
var DEFAULT_TOOL_CAPS = {
|
|
9799
10012
|
minimal: 2,
|
|
@@ -11596,11 +11809,12 @@ var Retriever = class {
|
|
|
11596
11809
|
if (!scope && projectScopeMode === "global" && facetFilters === null) return results;
|
|
11597
11810
|
const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
|
|
11598
11811
|
const filtered = [];
|
|
11812
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
11599
11813
|
for (const result2 of results) {
|
|
11600
11814
|
if (scope?.sessionId && result2.sessionId !== scope.sessionId) continue;
|
|
11601
11815
|
if (scope?.sessionIdPrefix && !result2.sessionId.startsWith(scope.sessionIdPrefix)) continue;
|
|
11602
11816
|
if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result2.eventType)) continue;
|
|
11603
|
-
const event =
|
|
11817
|
+
const event = eventMap.get(result2.eventId);
|
|
11604
11818
|
if (!event) continue;
|
|
11605
11819
|
if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix)) continue;
|
|
11606
11820
|
if (normalizedIncludes.length > 0) {
|
|
@@ -11709,10 +11923,30 @@ var Retriever = class {
|
|
|
11709
11923
|
async retrieveRecent(limit = 100) {
|
|
11710
11924
|
return this.eventStore.getRecentEvents(limit);
|
|
11711
11925
|
}
|
|
11926
|
+
/**
|
|
11927
|
+
* Batch-load events by id into a Map in a single query, de-duplicating ids.
|
|
11928
|
+
* Replaces per-result getEvent round-trips in the scope/enrich hot paths.
|
|
11929
|
+
*/
|
|
11930
|
+
async fetchEventsByIds(ids) {
|
|
11931
|
+
const map = /* @__PURE__ */ new Map();
|
|
11932
|
+
if (ids.length === 0) return map;
|
|
11933
|
+
const unique2 = [...new Set(ids)];
|
|
11934
|
+
if (typeof this.eventStore.getEvents === "function") {
|
|
11935
|
+
const events = await this.eventStore.getEvents(unique2);
|
|
11936
|
+
for (const event of events) map.set(event.id, event);
|
|
11937
|
+
} else {
|
|
11938
|
+
const events = await Promise.all(unique2.map((id) => this.eventStore.getEvent(id)));
|
|
11939
|
+
for (const event of events) {
|
|
11940
|
+
if (event) map.set(event.id, event);
|
|
11941
|
+
}
|
|
11942
|
+
}
|
|
11943
|
+
return map;
|
|
11944
|
+
}
|
|
11712
11945
|
async enrichResults(results, options, query) {
|
|
11713
11946
|
const memories = [];
|
|
11947
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
11714
11948
|
for (const result2 of results) {
|
|
11715
|
-
const event =
|
|
11949
|
+
const event = eventMap.get(result2.eventId);
|
|
11716
11950
|
if (!event) continue;
|
|
11717
11951
|
if (this.graduation) {
|
|
11718
11952
|
this.graduation.recordAccess(event.id, options.sessionId || "unknown", result2.score);
|
|
@@ -13626,19 +13860,29 @@ var ConsolidatedStore = class {
|
|
|
13626
13860
|
*/
|
|
13627
13861
|
async create(input) {
|
|
13628
13862
|
const memoryId = randomUUID16();
|
|
13629
|
-
|
|
13630
|
-
|
|
13863
|
+
const sourceEvents = Array.isArray(input.sourceEvents) ? input.sourceEvents : [];
|
|
13864
|
+
const insertMemory = this.db.prepare(
|
|
13631
13865
|
`INSERT INTO consolidated_memories
|
|
13632
13866
|
(memory_id, summary, topics, source_events, confidence, created_at)
|
|
13633
|
-
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
13634
|
-
|
|
13867
|
+
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`
|
|
13868
|
+
);
|
|
13869
|
+
const insertEvent = this.db.prepare(
|
|
13870
|
+
`INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id) VALUES (?, ?)`
|
|
13871
|
+
);
|
|
13872
|
+
this.db.transaction(() => {
|
|
13873
|
+
insertMemory.run(
|
|
13635
13874
|
memoryId,
|
|
13636
13875
|
input.summary,
|
|
13637
13876
|
JSON.stringify(input.topics),
|
|
13638
|
-
JSON.stringify(
|
|
13877
|
+
JSON.stringify(sourceEvents),
|
|
13639
13878
|
input.confidence
|
|
13640
|
-
|
|
13641
|
-
|
|
13879
|
+
);
|
|
13880
|
+
for (const eventId of sourceEvents) {
|
|
13881
|
+
if (typeof eventId === "string" && eventId.length > 0) {
|
|
13882
|
+
insertEvent.run(memoryId, eventId);
|
|
13883
|
+
}
|
|
13884
|
+
}
|
|
13885
|
+
})();
|
|
13642
13886
|
return memoryId;
|
|
13643
13887
|
}
|
|
13644
13888
|
/**
|
|
@@ -13744,11 +13988,12 @@ var ConsolidatedStore = class {
|
|
|
13744
13988
|
* Delete a consolidated memory
|
|
13745
13989
|
*/
|
|
13746
13990
|
async delete(memoryId) {
|
|
13747
|
-
|
|
13748
|
-
|
|
13749
|
-
|
|
13750
|
-
|
|
13751
|
-
|
|
13991
|
+
const deleteEvents = this.db.prepare(`DELETE FROM consolidated_memory_events WHERE memory_id = ?`);
|
|
13992
|
+
const deleteMemory = this.db.prepare(`DELETE FROM consolidated_memories WHERE memory_id = ?`);
|
|
13993
|
+
this.db.transaction(() => {
|
|
13994
|
+
deleteEvents.run(memoryId);
|
|
13995
|
+
deleteMemory.run(memoryId);
|
|
13996
|
+
})();
|
|
13752
13997
|
}
|
|
13753
13998
|
/**
|
|
13754
13999
|
* Create a long-term rule promoted from stable summaries
|
|
@@ -13861,16 +14106,16 @@ var ConsolidatedStore = class {
|
|
|
13861
14106
|
* Check if source events are already consolidated
|
|
13862
14107
|
*/
|
|
13863
14108
|
async isAlreadyConsolidated(eventIds) {
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
|
|
13871
|
-
|
|
13872
|
-
|
|
13873
|
-
return
|
|
14109
|
+
if (eventIds.length === 0) return false;
|
|
14110
|
+
const placeholders2 = eventIds.map(() => "?").join(",");
|
|
14111
|
+
const rows = await dbAll(
|
|
14112
|
+
this.db,
|
|
14113
|
+
`SELECT memory_id FROM consolidated_memory_events
|
|
14114
|
+
WHERE event_id IN (${placeholders2})
|
|
14115
|
+
LIMIT 1`,
|
|
14116
|
+
eventIds
|
|
14117
|
+
);
|
|
14118
|
+
return rows.length > 0;
|
|
13874
14119
|
}
|
|
13875
14120
|
/**
|
|
13876
14121
|
* Get the last consolidation time
|
|
@@ -15779,6 +16024,23 @@ var EventStore = class {
|
|
|
15779
16024
|
access_count INTEGER DEFAULT 0
|
|
15780
16025
|
)
|
|
15781
16026
|
`);
|
|
16027
|
+
await dbRun(this.db, `
|
|
16028
|
+
CREATE TABLE IF NOT EXISTS consolidated_memory_events (
|
|
16029
|
+
memory_id VARCHAR NOT NULL,
|
|
16030
|
+
event_id VARCHAR NOT NULL,
|
|
16031
|
+
PRIMARY KEY (memory_id, event_id)
|
|
16032
|
+
)
|
|
16033
|
+
`);
|
|
16034
|
+
await dbRun(this.db, `CREATE INDEX IF NOT EXISTS idx_cme_event ON consolidated_memory_events(event_id)`);
|
|
16035
|
+
const cmeCount = await dbAll(this.db, `SELECT COUNT(*) as c FROM consolidated_memory_events`);
|
|
16036
|
+
if ((cmeCount[0]?.c ?? 0) === 0) {
|
|
16037
|
+
await dbRun(this.db, `
|
|
16038
|
+
INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id)
|
|
16039
|
+
SELECT cm.memory_id, je.value
|
|
16040
|
+
FROM consolidated_memories cm, json_each(cm.source_events) je
|
|
16041
|
+
WHERE json_valid(cm.source_events)
|
|
16042
|
+
`);
|
|
16043
|
+
}
|
|
15782
16044
|
await dbRun(this.db, `
|
|
15783
16045
|
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
15784
16046
|
log_id VARCHAR PRIMARY KEY,
|
|
@@ -15917,6 +16179,18 @@ var EventStore = class {
|
|
|
15917
16179
|
if (rows.length === 0) return null;
|
|
15918
16180
|
return this.rowToEvent(rows[0]);
|
|
15919
16181
|
}
|
|
16182
|
+
/** Batch-fetch events by id in a single query (missing ids are omitted). */
|
|
16183
|
+
async getEvents(ids) {
|
|
16184
|
+
await this.initialize();
|
|
16185
|
+
if (ids.length === 0) return [];
|
|
16186
|
+
const placeholders2 = ids.map(() => "?").join(",");
|
|
16187
|
+
const rows = await dbAll(
|
|
16188
|
+
this.db,
|
|
16189
|
+
`SELECT * FROM events WHERE id IN (${placeholders2})`,
|
|
16190
|
+
ids
|
|
16191
|
+
);
|
|
16192
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
16193
|
+
}
|
|
15920
16194
|
/**
|
|
15921
16195
|
* Create or update session
|
|
15922
16196
|
*/
|
|
@@ -16004,23 +16278,22 @@ var EventStore = class {
|
|
|
16004
16278
|
*/
|
|
16005
16279
|
async getPendingOutboxItems(limit = 32) {
|
|
16006
16280
|
await this.initialize();
|
|
16007
|
-
const
|
|
16281
|
+
const claimed = await dbAll(
|
|
16008
16282
|
this.db,
|
|
16009
|
-
`
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16283
|
+
`UPDATE embedding_outbox
|
|
16284
|
+
SET status = 'processing'
|
|
16285
|
+
WHERE id IN (
|
|
16286
|
+
SELECT id FROM embedding_outbox
|
|
16287
|
+
WHERE status = 'pending'
|
|
16288
|
+
ORDER BY created_at
|
|
16289
|
+
LIMIT ?
|
|
16290
|
+
)
|
|
16291
|
+
RETURNING *`,
|
|
16013
16292
|
[limit]
|
|
16014
16293
|
);
|
|
16015
|
-
if (
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
await dbRun(
|
|
16019
|
-
this.db,
|
|
16020
|
-
`UPDATE embedding_outbox SET status = 'processing' WHERE id IN (${placeholders2})`,
|
|
16021
|
-
ids
|
|
16022
|
-
);
|
|
16023
|
-
return pending.map((row) => ({
|
|
16294
|
+
if (claimed.length === 0) return [];
|
|
16295
|
+
claimed.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
|
|
16296
|
+
return claimed.map((row) => ({
|
|
16024
16297
|
id: row.id,
|
|
16025
16298
|
eventId: row.event_id,
|
|
16026
16299
|
content: row.content,
|