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
|
@@ -158,7 +158,13 @@ function toDate(value) {
|
|
|
158
158
|
return new Date(String(value));
|
|
159
159
|
}
|
|
160
160
|
function createDatabase(dbPath, options) {
|
|
161
|
-
|
|
161
|
+
const db = new BetterSqlite3(dbPath, { readonly: options?.readOnly });
|
|
162
|
+
db.pragma("busy_timeout = 5000");
|
|
163
|
+
if (!options?.readOnly) {
|
|
164
|
+
db.pragma("journal_mode = WAL");
|
|
165
|
+
db.pragma("synchronous = NORMAL");
|
|
166
|
+
}
|
|
167
|
+
return db;
|
|
162
168
|
}
|
|
163
169
|
function dbRun(db, sql, params = []) {
|
|
164
170
|
db.prepare(sql).run(...params);
|
|
@@ -391,19 +397,29 @@ var ConsolidatedStore = class {
|
|
|
391
397
|
*/
|
|
392
398
|
async create(input) {
|
|
393
399
|
const memoryId = randomUUID2();
|
|
394
|
-
|
|
395
|
-
|
|
400
|
+
const sourceEvents = Array.isArray(input.sourceEvents) ? input.sourceEvents : [];
|
|
401
|
+
const insertMemory = this.db.prepare(
|
|
396
402
|
`INSERT INTO consolidated_memories
|
|
397
403
|
(memory_id, summary, topics, source_events, confidence, created_at)
|
|
398
|
-
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
399
|
-
|
|
404
|
+
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`
|
|
405
|
+
);
|
|
406
|
+
const insertEvent = this.db.prepare(
|
|
407
|
+
`INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id) VALUES (?, ?)`
|
|
408
|
+
);
|
|
409
|
+
this.db.transaction(() => {
|
|
410
|
+
insertMemory.run(
|
|
400
411
|
memoryId,
|
|
401
412
|
input.summary,
|
|
402
413
|
JSON.stringify(input.topics),
|
|
403
|
-
JSON.stringify(
|
|
414
|
+
JSON.stringify(sourceEvents),
|
|
404
415
|
input.confidence
|
|
405
|
-
|
|
406
|
-
|
|
416
|
+
);
|
|
417
|
+
for (const eventId of sourceEvents) {
|
|
418
|
+
if (typeof eventId === "string" && eventId.length > 0) {
|
|
419
|
+
insertEvent.run(memoryId, eventId);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
})();
|
|
407
423
|
return memoryId;
|
|
408
424
|
}
|
|
409
425
|
/**
|
|
@@ -509,11 +525,12 @@ var ConsolidatedStore = class {
|
|
|
509
525
|
* Delete a consolidated memory
|
|
510
526
|
*/
|
|
511
527
|
async delete(memoryId) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
528
|
+
const deleteEvents = this.db.prepare(`DELETE FROM consolidated_memory_events WHERE memory_id = ?`);
|
|
529
|
+
const deleteMemory = this.db.prepare(`DELETE FROM consolidated_memories WHERE memory_id = ?`);
|
|
530
|
+
this.db.transaction(() => {
|
|
531
|
+
deleteEvents.run(memoryId);
|
|
532
|
+
deleteMemory.run(memoryId);
|
|
533
|
+
})();
|
|
517
534
|
}
|
|
518
535
|
/**
|
|
519
536
|
* Create a long-term rule promoted from stable summaries
|
|
@@ -626,16 +643,16 @@ var ConsolidatedStore = class {
|
|
|
626
643
|
* Check if source events are already consolidated
|
|
627
644
|
*/
|
|
628
645
|
async isAlreadyConsolidated(eventIds) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
return
|
|
646
|
+
if (eventIds.length === 0) return false;
|
|
647
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
648
|
+
const rows = await dbAll(
|
|
649
|
+
this.db,
|
|
650
|
+
`SELECT memory_id FROM consolidated_memory_events
|
|
651
|
+
WHERE event_id IN (${placeholders})
|
|
652
|
+
LIMIT 1`,
|
|
653
|
+
eventIds
|
|
654
|
+
);
|
|
655
|
+
return rows.length > 0;
|
|
639
656
|
}
|
|
640
657
|
/**
|
|
641
658
|
* Get the last consolidation time
|
|
@@ -2171,6 +2188,19 @@ function makeDedupeKey(content, sessionId) {
|
|
|
2171
2188
|
return `${sessionId}:${contentHash}`;
|
|
2172
2189
|
}
|
|
2173
2190
|
|
|
2191
|
+
// src/core/citation-generator.ts
|
|
2192
|
+
import { createHash as createHash2 } from "crypto";
|
|
2193
|
+
var ID_LENGTH = 6;
|
|
2194
|
+
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
2195
|
+
function generateCitationId(eventId) {
|
|
2196
|
+
const hash = createHash2("sha256").update(eventId).digest();
|
|
2197
|
+
let id = "";
|
|
2198
|
+
for (let i = 0; i < ID_LENGTH; i++) {
|
|
2199
|
+
id += CHARSET[hash[i] % CHARSET.length];
|
|
2200
|
+
}
|
|
2201
|
+
return id;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2174
2204
|
// src/core/sqlite-event-store.ts
|
|
2175
2205
|
import * as nodePath2 from "path";
|
|
2176
2206
|
|
|
@@ -2215,17 +2245,28 @@ function createSQLiteDatabase(path12, options) {
|
|
|
2215
2245
|
}
|
|
2216
2246
|
return db;
|
|
2217
2247
|
}
|
|
2248
|
+
var statementCache = /* @__PURE__ */ new WeakMap();
|
|
2249
|
+
function prepareCached(db, sql) {
|
|
2250
|
+
let cache = statementCache.get(db);
|
|
2251
|
+
if (!cache) {
|
|
2252
|
+
cache = /* @__PURE__ */ new Map();
|
|
2253
|
+
statementCache.set(db, cache);
|
|
2254
|
+
}
|
|
2255
|
+
let stmt = cache.get(sql);
|
|
2256
|
+
if (!stmt) {
|
|
2257
|
+
stmt = db.prepare(sql);
|
|
2258
|
+
cache.set(sql, stmt);
|
|
2259
|
+
}
|
|
2260
|
+
return stmt;
|
|
2261
|
+
}
|
|
2218
2262
|
function sqliteRun(db, sql, params = []) {
|
|
2219
|
-
|
|
2220
|
-
return stmt.run(...params);
|
|
2263
|
+
return prepareCached(db, sql).run(...params);
|
|
2221
2264
|
}
|
|
2222
2265
|
function sqliteAll(db, sql, params = []) {
|
|
2223
|
-
|
|
2224
|
-
return stmt.all(...params);
|
|
2266
|
+
return prepareCached(db, sql).all(...params);
|
|
2225
2267
|
}
|
|
2226
2268
|
function sqliteGet(db, sql, params = []) {
|
|
2227
|
-
|
|
2228
|
-
return stmt.get(...params);
|
|
2269
|
+
return prepareCached(db, sql).get(...params);
|
|
2229
2270
|
}
|
|
2230
2271
|
function sqliteExec(db, sql) {
|
|
2231
2272
|
db.exec(sql);
|
|
@@ -2647,6 +2688,7 @@ function normalizeQueryRewriteKind(value) {
|
|
|
2647
2688
|
var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
|
|
2648
2689
|
var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2649
2690
|
var DEFAULT_OUTBOX_MAX_RETRIES = 3;
|
|
2691
|
+
var SQLITE_SCHEMA_VERSION = 1;
|
|
2650
2692
|
function emptyOutboxRecoveryResult() {
|
|
2651
2693
|
return {
|
|
2652
2694
|
embedding: { recoveredProcessing: 0, retriedFailed: 0 },
|
|
@@ -2991,6 +3033,15 @@ var SQLiteEventStore = class {
|
|
|
2991
3033
|
access_count INTEGER DEFAULT 0
|
|
2992
3034
|
);
|
|
2993
3035
|
|
|
3036
|
+
-- Junction: consolidated memory -> source event id (indexed lookup that
|
|
3037
|
+
-- replaces per-event source_events LIKE scans during consolidation).
|
|
3038
|
+
CREATE TABLE IF NOT EXISTS consolidated_memory_events (
|
|
3039
|
+
memory_id TEXT NOT NULL,
|
|
3040
|
+
event_id TEXT NOT NULL,
|
|
3041
|
+
PRIMARY KEY (memory_id, event_id)
|
|
3042
|
+
);
|
|
3043
|
+
CREATE INDEX IF NOT EXISTS idx_cme_event ON consolidated_memory_events(event_id);
|
|
3044
|
+
|
|
2994
3045
|
-- Continuity Log table (tracks context transitions)
|
|
2995
3046
|
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
2996
3047
|
log_id TEXT PRIMARY KEY,
|
|
@@ -3350,11 +3401,17 @@ var SQLiteEventStore = class {
|
|
|
3350
3401
|
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
3351
3402
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
3352
3403
|
END;
|
|
3404
|
+
|
|
3405
|
+
-- Reverse index for citation ids (a non-reversible hash of event id), so
|
|
3406
|
+
-- citation lookups are O(1) instead of scanning recent events and hashing
|
|
3407
|
+
-- each one. Populated lazily/self-healing by getEventByCitationId.
|
|
3408
|
+
CREATE TABLE IF NOT EXISTS event_citations (
|
|
3409
|
+
event_id TEXT PRIMARY KEY,
|
|
3410
|
+
citation_id TEXT NOT NULL
|
|
3411
|
+
);
|
|
3412
|
+
CREATE INDEX IF NOT EXISTS idx_event_citations_citation ON event_citations(citation_id);
|
|
3353
3413
|
`);
|
|
3354
|
-
|
|
3355
|
-
sqliteExec(this.db, `ALTER TABLE memory_action_edges ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';`);
|
|
3356
|
-
} catch {
|
|
3357
|
-
}
|
|
3414
|
+
this.addColumnIfMissing("memory_action_edges", "source", `TEXT NOT NULL DEFAULT 'manual'`);
|
|
3358
3415
|
try {
|
|
3359
3416
|
const edgeIndexes = sqliteAll(this.db, `PRAGMA index_list(memory_action_edges)`, []);
|
|
3360
3417
|
const hasSourceAwareUnique = edgeIndexes.some((index) => {
|
|
@@ -3391,55 +3448,17 @@ var SQLiteEventStore = class {
|
|
|
3391
3448
|
}
|
|
3392
3449
|
} catch {
|
|
3393
3450
|
}
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
try {
|
|
3399
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
|
|
3400
|
-
} catch {
|
|
3401
|
-
}
|
|
3402
|
-
try {
|
|
3403
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN raw_query_text TEXT;`);
|
|
3404
|
-
} catch {
|
|
3405
|
-
}
|
|
3406
|
-
try {
|
|
3407
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN query_rewrite_kind TEXT;`);
|
|
3408
|
-
} catch {
|
|
3409
|
-
}
|
|
3451
|
+
this.addColumnIfMissing("retrieval_traces", "selected_details_json", "TEXT");
|
|
3452
|
+
this.addColumnIfMissing("retrieval_traces", "candidate_details_json", "TEXT");
|
|
3453
|
+
this.addColumnIfMissing("retrieval_traces", "raw_query_text", "TEXT");
|
|
3454
|
+
this.addColumnIfMissing("retrieval_traces", "query_rewrite_kind", "TEXT");
|
|
3410
3455
|
try {
|
|
3411
3456
|
sqliteExec(this.db, `CREATE INDEX IF NOT EXISTS idx_retrieval_traces_query_rewrite_kind ON retrieval_traces(query_rewrite_kind);`);
|
|
3412
3457
|
} catch {
|
|
3413
3458
|
}
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
try {
|
|
3418
|
-
sqliteExec(this.db, `
|
|
3419
|
-
ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
|
|
3420
|
-
`);
|
|
3421
|
-
} catch (err) {
|
|
3422
|
-
console.error("Error adding access_count column:", err);
|
|
3423
|
-
}
|
|
3424
|
-
}
|
|
3425
|
-
if (!columnNames.includes("last_accessed_at")) {
|
|
3426
|
-
try {
|
|
3427
|
-
sqliteExec(this.db, `
|
|
3428
|
-
ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
|
|
3429
|
-
`);
|
|
3430
|
-
} catch (err) {
|
|
3431
|
-
console.error("Error adding last_accessed_at column:", err);
|
|
3432
|
-
}
|
|
3433
|
-
}
|
|
3434
|
-
if (!columnNames.includes("turn_id")) {
|
|
3435
|
-
try {
|
|
3436
|
-
sqliteExec(this.db, `
|
|
3437
|
-
ALTER TABLE events ADD COLUMN turn_id TEXT;
|
|
3438
|
-
`);
|
|
3439
|
-
} catch (err) {
|
|
3440
|
-
console.error("Error adding turn_id column:", err);
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3459
|
+
this.addColumnIfMissing("events", "access_count", "INTEGER DEFAULT 0");
|
|
3460
|
+
this.addColumnIfMissing("events", "last_accessed_at", "TEXT");
|
|
3461
|
+
this.addColumnIfMissing("events", "turn_id", "TEXT");
|
|
3443
3462
|
try {
|
|
3444
3463
|
sqliteExec(this.db, `
|
|
3445
3464
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -3456,8 +3475,18 @@ var SQLiteEventStore = class {
|
|
|
3456
3475
|
sqliteExec(this.db, `
|
|
3457
3476
|
CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
|
|
3458
3477
|
`);
|
|
3459
|
-
} catch
|
|
3478
|
+
} catch {
|
|
3460
3479
|
}
|
|
3480
|
+
const cmeCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM consolidated_memory_events`, []);
|
|
3481
|
+
if ((cmeCount?.c ?? 0) === 0) {
|
|
3482
|
+
sqliteExec(this.db, `
|
|
3483
|
+
INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id)
|
|
3484
|
+
SELECT cm.memory_id, je.value
|
|
3485
|
+
FROM consolidated_memories cm, json_each(cm.source_events) je
|
|
3486
|
+
WHERE json_valid(cm.source_events);
|
|
3487
|
+
`);
|
|
3488
|
+
}
|
|
3489
|
+
sqliteExec(this.db, `PRAGMA user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
3461
3490
|
this.initialized = true;
|
|
3462
3491
|
}
|
|
3463
3492
|
/**
|
|
@@ -3589,6 +3618,67 @@ var SQLiteEventStore = class {
|
|
|
3589
3618
|
);
|
|
3590
3619
|
return rows.map(this.rowToEvent);
|
|
3591
3620
|
}
|
|
3621
|
+
/**
|
|
3622
|
+
* Aggregate event counts by type via SQL GROUP BY, instead of loading rows
|
|
3623
|
+
* into memory and counting in JS. Counts all events (the old recent-window
|
|
3624
|
+
* scan also under-counted stores larger than its cap).
|
|
3625
|
+
*/
|
|
3626
|
+
async getEventTypeCounts(options) {
|
|
3627
|
+
await this.initialize();
|
|
3628
|
+
const rows = sqliteAll(
|
|
3629
|
+
this.db,
|
|
3630
|
+
`SELECT event_type, COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)} GROUP BY event_type`,
|
|
3631
|
+
[]
|
|
3632
|
+
);
|
|
3633
|
+
return rows.map((row) => ({ eventType: row.event_type, count: row.count }));
|
|
3634
|
+
}
|
|
3635
|
+
/**
|
|
3636
|
+
* Fetch every event at/after an ISO timestamp (ascending), with no row cap.
|
|
3637
|
+
* For window-scoped analytics (KPI/usefulness) this fetches exactly the events
|
|
3638
|
+
* the window needs instead of an arbitrary "most recent N" slice that both
|
|
3639
|
+
* over-fetches sparse windows and truncates very active ones.
|
|
3640
|
+
*/
|
|
3641
|
+
async getEventsAfter(sinceIso, options) {
|
|
3642
|
+
await this.initialize();
|
|
3643
|
+
const rows = sqliteAll(
|
|
3644
|
+
this.db,
|
|
3645
|
+
`SELECT * FROM events WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3646
|
+
[sinceIso]
|
|
3647
|
+
);
|
|
3648
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3649
|
+
}
|
|
3650
|
+
/** Count distinct sessions via SQL instead of materializing a Set in JS. */
|
|
3651
|
+
async getDistinctSessionCount(options) {
|
|
3652
|
+
await this.initialize();
|
|
3653
|
+
const row = sqliteGet(
|
|
3654
|
+
this.db,
|
|
3655
|
+
`SELECT COUNT(DISTINCT session_id) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`,
|
|
3656
|
+
[]
|
|
3657
|
+
);
|
|
3658
|
+
return row?.count ?? 0;
|
|
3659
|
+
}
|
|
3660
|
+
/**
|
|
3661
|
+
* Per-day event counts (with type breakdown) since an ISO timestamp, computed
|
|
3662
|
+
* by SQL GROUP BY on the date prefix. Timestamps are stored as ISO-8601
|
|
3663
|
+
* strings, so substr(...,1,10) is the UTC day and `timestamp >= ?` compares
|
|
3664
|
+
* lexicographically.
|
|
3665
|
+
*/
|
|
3666
|
+
async getDailyEventCounts(sinceIso, options) {
|
|
3667
|
+
await this.initialize();
|
|
3668
|
+
return sqliteAll(
|
|
3669
|
+
this.db,
|
|
3670
|
+
`SELECT substr(timestamp, 1, 10) as day,
|
|
3671
|
+
COUNT(*) as total,
|
|
3672
|
+
SUM(CASE WHEN event_type = 'user_prompt' THEN 1 ELSE 0 END) as prompts,
|
|
3673
|
+
SUM(CASE WHEN event_type = 'agent_response' THEN 1 ELSE 0 END) as responses,
|
|
3674
|
+
SUM(CASE WHEN event_type = 'tool_observation' THEN 1 ELSE 0 END) as tools
|
|
3675
|
+
FROM events
|
|
3676
|
+
WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)}
|
|
3677
|
+
GROUP BY day
|
|
3678
|
+
ORDER BY day ASC`,
|
|
3679
|
+
[sinceIso]
|
|
3680
|
+
);
|
|
3681
|
+
}
|
|
3592
3682
|
/**
|
|
3593
3683
|
* Get event by ID
|
|
3594
3684
|
*/
|
|
@@ -3602,6 +3692,71 @@ var SQLiteEventStore = class {
|
|
|
3602
3692
|
if (!row) return null;
|
|
3603
3693
|
return this.rowToEvent(row);
|
|
3604
3694
|
}
|
|
3695
|
+
/**
|
|
3696
|
+
* Batch-fetch events by id in a single query, applying the same quarantine
|
|
3697
|
+
* predicate as getEvent. Lets hot paths replace N sequential round-trips with
|
|
3698
|
+
* one. Missing ids are simply absent from the result (order not guaranteed).
|
|
3699
|
+
*/
|
|
3700
|
+
async getEvents(ids, options) {
|
|
3701
|
+
await this.initialize();
|
|
3702
|
+
if (ids.length === 0) return [];
|
|
3703
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
3704
|
+
const rows = sqliteAll(
|
|
3705
|
+
this.db,
|
|
3706
|
+
`SELECT * FROM events WHERE id IN (${placeholders}) AND ${maybeQuarantinePredicate(options)}`,
|
|
3707
|
+
ids
|
|
3708
|
+
);
|
|
3709
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3710
|
+
}
|
|
3711
|
+
/**
|
|
3712
|
+
* Resolve an event from its citation id via the reverse index, instead of
|
|
3713
|
+
* scanning recent events and hashing each one. The index is self-healing: a
|
|
3714
|
+
* miss triggers a one-time backfill of any events not yet indexed (covering
|
|
3715
|
+
* freshly appended events) before retrying.
|
|
3716
|
+
*/
|
|
3717
|
+
async getEventByCitationId(citationId, options) {
|
|
3718
|
+
await this.initialize();
|
|
3719
|
+
if (!citationId) return null;
|
|
3720
|
+
let row = sqliteGet(
|
|
3721
|
+
this.db,
|
|
3722
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3723
|
+
[citationId]
|
|
3724
|
+
);
|
|
3725
|
+
if (!row) {
|
|
3726
|
+
this.indexMissingCitations();
|
|
3727
|
+
row = sqliteGet(
|
|
3728
|
+
this.db,
|
|
3729
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3730
|
+
[citationId]
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
if (!row) return null;
|
|
3734
|
+
return this.getEvent(row.event_id, options);
|
|
3735
|
+
}
|
|
3736
|
+
/**
|
|
3737
|
+
* Backfill citation ids for any events not yet present in event_citations.
|
|
3738
|
+
* Gated on a cheap count comparison so the common (fully-indexed) path does no
|
|
3739
|
+
* work, and the scan only runs when there are genuinely new events to index.
|
|
3740
|
+
*/
|
|
3741
|
+
indexMissingCitations() {
|
|
3742
|
+
const eventCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM events`, [])?.c ?? 0;
|
|
3743
|
+
const indexedCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM event_citations`, [])?.c ?? 0;
|
|
3744
|
+
if (indexedCount >= eventCount) return;
|
|
3745
|
+
const unindexed = sqliteAll(
|
|
3746
|
+
this.db,
|
|
3747
|
+
`SELECT e.id FROM events e
|
|
3748
|
+
LEFT JOIN event_citations ec ON ec.event_id = e.id
|
|
3749
|
+
WHERE ec.event_id IS NULL`,
|
|
3750
|
+
[]
|
|
3751
|
+
);
|
|
3752
|
+
if (unindexed.length === 0) return;
|
|
3753
|
+
const insert = this.db.prepare(`INSERT OR IGNORE INTO event_citations (event_id, citation_id) VALUES (?, ?)`);
|
|
3754
|
+
this.db.transaction(() => {
|
|
3755
|
+
for (const { id } of unindexed) {
|
|
3756
|
+
insert.run(id, generateCitationId(id));
|
|
3757
|
+
}
|
|
3758
|
+
})();
|
|
3759
|
+
}
|
|
3605
3760
|
/**
|
|
3606
3761
|
* Get events since a timestamp (for sync)
|
|
3607
3762
|
*/
|
|
@@ -3803,25 +3958,22 @@ var SQLiteEventStore = class {
|
|
|
3803
3958
|
*/
|
|
3804
3959
|
async getPendingOutboxItems(limit = 32) {
|
|
3805
3960
|
await this.initialize();
|
|
3806
|
-
const
|
|
3807
|
-
this.db,
|
|
3808
|
-
`SELECT * FROM embedding_outbox
|
|
3809
|
-
WHERE status = 'pending'
|
|
3810
|
-
ORDER BY created_at
|
|
3811
|
-
LIMIT ?`,
|
|
3812
|
-
[limit]
|
|
3813
|
-
);
|
|
3814
|
-
if (pending.length === 0) return [];
|
|
3815
|
-
const ids = pending.map((r) => r.id);
|
|
3816
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
3817
|
-
sqliteRun(
|
|
3961
|
+
const claimed = sqliteAll(
|
|
3818
3962
|
this.db,
|
|
3819
3963
|
`UPDATE embedding_outbox
|
|
3820
3964
|
SET status = 'processing', processed_at = datetime('now'), error_message = NULL
|
|
3821
|
-
WHERE id IN (
|
|
3822
|
-
|
|
3965
|
+
WHERE id IN (
|
|
3966
|
+
SELECT id FROM embedding_outbox
|
|
3967
|
+
WHERE status = 'pending'
|
|
3968
|
+
ORDER BY created_at
|
|
3969
|
+
LIMIT ?
|
|
3970
|
+
)
|
|
3971
|
+
RETURNING *`,
|
|
3972
|
+
[limit]
|
|
3823
3973
|
);
|
|
3824
|
-
|
|
3974
|
+
if (claimed.length === 0) return [];
|
|
3975
|
+
claimed.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
|
|
3976
|
+
return claimed.map((row) => ({
|
|
3825
3977
|
id: row.id,
|
|
3826
3978
|
eventId: row.event_id,
|
|
3827
3979
|
content: row.content,
|
|
@@ -4425,7 +4577,7 @@ var SQLiteEventStore = class {
|
|
|
4425
4577
|
const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
|
|
4426
4578
|
const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
|
|
4427
4579
|
let toolSuccessCount = 0;
|
|
4428
|
-
|
|
4580
|
+
const toolTotalCount = toolEvents.length;
|
|
4429
4581
|
for (const t of toolEvents) {
|
|
4430
4582
|
try {
|
|
4431
4583
|
const content = JSON.parse(t.content);
|
|
@@ -4588,10 +4740,14 @@ var SQLiteEventStore = class {
|
|
|
4588
4740
|
* Rebuild FTS index from existing events
|
|
4589
4741
|
* Call this once after upgrading to FTS5
|
|
4590
4742
|
*/
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4743
|
+
/**
|
|
4744
|
+
* Drop and recreate the events_fts virtual table + sync triggers, repopulating
|
|
4745
|
+
* from the current events table. Recreating the table (instead of issuing
|
|
4746
|
+
* DELETEs against it) sidesteps a quirk where older migrated FTS5 tables fail
|
|
4747
|
+
* with `no such column: T.event_id` on synthetic deletes. Safe to call inside
|
|
4748
|
+
* a transaction (pure DDL + INSERT...SELECT, no transaction control).
|
|
4749
|
+
*/
|
|
4750
|
+
recreateEventsFtsTable() {
|
|
4595
4751
|
sqliteExec(this.db, `
|
|
4596
4752
|
DROP TRIGGER IF EXISTS events_fts_insert;
|
|
4597
4753
|
DROP TRIGGER IF EXISTS events_fts_delete;
|
|
@@ -4620,6 +4776,12 @@ var SQLiteEventStore = class {
|
|
|
4620
4776
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4621
4777
|
END;
|
|
4622
4778
|
`);
|
|
4779
|
+
}
|
|
4780
|
+
async rebuildFtsIndex() {
|
|
4781
|
+
await this.initialize();
|
|
4782
|
+
const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
|
|
4783
|
+
const totalEvents = countRow?.count ?? 0;
|
|
4784
|
+
this.recreateEventsFtsTable();
|
|
4623
4785
|
return totalEvents;
|
|
4624
4786
|
}
|
|
4625
4787
|
/**
|
|
@@ -4637,6 +4799,35 @@ var SQLiteEventStore = class {
|
|
|
4637
4799
|
return false;
|
|
4638
4800
|
}
|
|
4639
4801
|
}
|
|
4802
|
+
hasTable(tableName) {
|
|
4803
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) return false;
|
|
4804
|
+
try {
|
|
4805
|
+
const rows = sqliteAll(
|
|
4806
|
+
this.db,
|
|
4807
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`,
|
|
4808
|
+
[tableName]
|
|
4809
|
+
);
|
|
4810
|
+
return rows.length > 0;
|
|
4811
|
+
} catch {
|
|
4812
|
+
return false;
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
/**
|
|
4816
|
+
* Add a column only when its table exists and the column is missing.
|
|
4817
|
+
*
|
|
4818
|
+
* Unlike a blind `try { ALTER } catch {}`, this distinguishes "already
|
|
4819
|
+
* migrated" / "table not present yet" (both safely skipped) from a genuine
|
|
4820
|
+
* failure (disk full, corruption), which is allowed to surface instead of
|
|
4821
|
+
* being silently swallowed as if the migration had succeeded.
|
|
4822
|
+
*/
|
|
4823
|
+
addColumnIfMissing(table, column, definition) {
|
|
4824
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
|
|
4825
|
+
throw new Error(`Invalid identifier in migration: ${table}.${column}`);
|
|
4826
|
+
}
|
|
4827
|
+
if (!this.hasTable(table)) return;
|
|
4828
|
+
if (this.hasTableColumn(table, column)) return;
|
|
4829
|
+
sqliteExec(this.db, `ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`);
|
|
4830
|
+
}
|
|
4640
4831
|
async recordRetrievalTrace(input) {
|
|
4641
4832
|
await this.initialize();
|
|
4642
4833
|
const traceId = randomUUID6();
|
|
@@ -4722,6 +4913,45 @@ var SQLiteEventStore = class {
|
|
|
4722
4913
|
FROM retrieval_traces`,
|
|
4723
4914
|
[]
|
|
4724
4915
|
);
|
|
4916
|
+
const strategyColumnSql = this.hasTableColumn("retrieval_traces", "strategy") ? "COALESCE(NULLIF(TRIM(strategy), ''), 'unknown')" : "'unknown'";
|
|
4917
|
+
const strategyRows = sqliteAll(
|
|
4918
|
+
this.db,
|
|
4919
|
+
`SELECT
|
|
4920
|
+
${strategyColumnSql} as strategy,
|
|
4921
|
+
COUNT(*) as total_queries,
|
|
4922
|
+
SUM(CASE WHEN selected_count > 0 THEN 1 ELSE 0 END) as queries_with_selection,
|
|
4923
|
+
SUM(CASE WHEN ${rewrittenQueryRewriteKindSql} THEN 1 ELSE 0 END) as rewritten_queries,
|
|
4924
|
+
SUM(candidate_count) as total_candidate_count,
|
|
4925
|
+
SUM(selected_count) as total_selected_count,
|
|
4926
|
+
AVG(candidate_count) as avg_candidate_count,
|
|
4927
|
+
AVG(selected_count) as avg_selected_count,
|
|
4928
|
+
CASE
|
|
4929
|
+
WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
|
|
4930
|
+
ELSE 0
|
|
4931
|
+
END as selection_rate
|
|
4932
|
+
FROM retrieval_traces
|
|
4933
|
+
GROUP BY ${strategyColumnSql}
|
|
4934
|
+
ORDER BY total_queries DESC, strategy ASC`,
|
|
4935
|
+
[]
|
|
4936
|
+
);
|
|
4937
|
+
const strategyBreakdown = strategyRows.map((strategyRow) => {
|
|
4938
|
+
const strategyTotalQueries = Number(strategyRow.total_queries || 0);
|
|
4939
|
+
const strategyQueriesWithSelection = Number(strategyRow.queries_with_selection || 0);
|
|
4940
|
+
const strategyRewrittenQueries = Number(strategyRow.rewritten_queries || 0);
|
|
4941
|
+
return {
|
|
4942
|
+
strategy: String(strategyRow.strategy || "unknown"),
|
|
4943
|
+
totalQueries: strategyTotalQueries,
|
|
4944
|
+
queriesWithSelection: strategyQueriesWithSelection,
|
|
4945
|
+
rewrittenQueries: strategyRewrittenQueries,
|
|
4946
|
+
rewriteRate: strategyTotalQueries > 0 ? strategyRewrittenQueries / strategyTotalQueries : 0,
|
|
4947
|
+
totalCandidateCount: Number(strategyRow.total_candidate_count || 0),
|
|
4948
|
+
totalSelectedCount: Number(strategyRow.total_selected_count || 0),
|
|
4949
|
+
avgCandidateCount: Number(strategyRow.avg_candidate_count || 0),
|
|
4950
|
+
avgSelectedCount: Number(strategyRow.avg_selected_count || 0),
|
|
4951
|
+
selectionRate: Number(strategyRow.selection_rate || 0),
|
|
4952
|
+
queryYieldRate: strategyTotalQueries > 0 ? strategyQueriesWithSelection / strategyTotalQueries : 0
|
|
4953
|
+
};
|
|
4954
|
+
});
|
|
4725
4955
|
const totalQueries = Number(row?.total_queries || 0);
|
|
4726
4956
|
const rewrittenQueries = Number(row?.rewritten_queries || 0);
|
|
4727
4957
|
const rawQueries = Math.max(0, totalQueries - rewrittenQueries);
|
|
@@ -4739,7 +4969,8 @@ var SQLiteEventStore = class {
|
|
|
4739
4969
|
rewrittenSelectionRate: rewrittenQueries > 0 ? rewrittenQueriesWithSelection / rewrittenQueries : 0,
|
|
4740
4970
|
rawSelectionRate: rawQueries > 0 ? rawQueriesWithSelection / rawQueries : 0,
|
|
4741
4971
|
avgSelectedCountForRewrittenQueries: Number(row?.avg_selected_count_for_rewritten_queries || 0),
|
|
4742
|
-
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0)
|
|
4972
|
+
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0),
|
|
4973
|
+
strategyBreakdown
|
|
4743
4974
|
};
|
|
4744
4975
|
} catch (err) {
|
|
4745
4976
|
if (err?.message?.includes("no such table")) {
|
|
@@ -4755,7 +4986,8 @@ var SQLiteEventStore = class {
|
|
|
4755
4986
|
rewrittenSelectionRate: 0,
|
|
4756
4987
|
rawSelectionRate: 0,
|
|
4757
4988
|
avgSelectedCountForRewrittenQueries: 0,
|
|
4758
|
-
avgSelectedCountForRawQueries: 0
|
|
4989
|
+
avgSelectedCountForRawQueries: 0,
|
|
4990
|
+
strategyBreakdown: []
|
|
4759
4991
|
};
|
|
4760
4992
|
}
|
|
4761
4993
|
throw err;
|
|
@@ -4872,38 +5104,19 @@ var SQLiteEventStore = class {
|
|
|
4872
5104
|
if (events.length === 0) return 0;
|
|
4873
5105
|
const eventIds = events.map((e) => e.id);
|
|
4874
5106
|
const placeholders = eventIds.map(() => "?").join(",");
|
|
4875
|
-
const
|
|
4876
|
-
|
|
4877
|
-
|
|
5107
|
+
const relatedTables = ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox", "event_citations"].filter((table) => this.hasTable(table) && this.hasTableColumn(table, "event_id"));
|
|
5108
|
+
const runDelete = this.db.transaction(() => {
|
|
5109
|
+
for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
|
|
4878
5110
|
sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
4879
|
-
ftsTriggersDropped.push(triggerName);
|
|
4880
|
-
} catch {
|
|
4881
5111
|
}
|
|
4882
|
-
|
|
4883
|
-
for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
|
|
4884
|
-
try {
|
|
5112
|
+
for (const table of relatedTables) {
|
|
4885
5113
|
sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders})`, eventIds);
|
|
4886
|
-
} catch {
|
|
4887
|
-
}
|
|
4888
|
-
}
|
|
4889
|
-
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
4890
|
-
if (ftsTriggersDropped.length > 0) {
|
|
4891
|
-
try {
|
|
4892
|
-
sqliteRun(this.db, `INSERT INTO events_fts(events_fts) VALUES('rebuild')`);
|
|
4893
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
4894
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4895
|
-
END`);
|
|
4896
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
4897
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4898
|
-
END`);
|
|
4899
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
4900
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4901
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4902
|
-
END`);
|
|
4903
|
-
} catch {
|
|
4904
5114
|
}
|
|
4905
|
-
|
|
4906
|
-
|
|
5115
|
+
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
5116
|
+
this.recreateEventsFtsTable();
|
|
5117
|
+
return result.changes || 0;
|
|
5118
|
+
});
|
|
5119
|
+
return runDelete();
|
|
4907
5120
|
}
|
|
4908
5121
|
/**
|
|
4909
5122
|
* Convert database row to MemoryEvent
|
|
@@ -7236,7 +7449,7 @@ var SessionActorRepository = class {
|
|
|
7236
7449
|
};
|
|
7237
7450
|
|
|
7238
7451
|
// src/core/operations/perspective-observation-repository.ts
|
|
7239
|
-
import { createHash as
|
|
7452
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
7240
7453
|
function projectHashToStorage4(projectHash) {
|
|
7241
7454
|
return projectHash ?? "";
|
|
7242
7455
|
}
|
|
@@ -7273,7 +7486,7 @@ function sanitizeStoredRecord(value) {
|
|
|
7273
7486
|
return sanitizeGovernanceAuditValue(value);
|
|
7274
7487
|
}
|
|
7275
7488
|
function stableHash(value) {
|
|
7276
|
-
return
|
|
7489
|
+
return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
7277
7490
|
}
|
|
7278
7491
|
function evidenceHash(sourceEventIds, sourceObservationIds) {
|
|
7279
7492
|
return stableHash({
|
|
@@ -8388,6 +8601,30 @@ var MemoryQueryService = class {
|
|
|
8388
8601
|
score: 1 - (r.rank - minRank) / rankRange
|
|
8389
8602
|
})).filter((r) => !options?.minScore || r.score >= options.minScore);
|
|
8390
8603
|
}
|
|
8604
|
+
async getEvent(id) {
|
|
8605
|
+
await this.initialize();
|
|
8606
|
+
return this.queryStore.getEvent(id);
|
|
8607
|
+
}
|
|
8608
|
+
async getEventByCitationId(citationId) {
|
|
8609
|
+
await this.initialize();
|
|
8610
|
+
return this.getMaintenanceStore("getEventByCitationId").getEventByCitationId(citationId);
|
|
8611
|
+
}
|
|
8612
|
+
async getEventsAfter(sinceIso) {
|
|
8613
|
+
await this.initialize();
|
|
8614
|
+
return this.getMaintenanceStore("getEventsAfter").getEventsAfter(sinceIso);
|
|
8615
|
+
}
|
|
8616
|
+
async getEventTypeCounts() {
|
|
8617
|
+
await this.initialize();
|
|
8618
|
+
return this.getMaintenanceStore("getEventTypeCounts").getEventTypeCounts();
|
|
8619
|
+
}
|
|
8620
|
+
async getDistinctSessionCount() {
|
|
8621
|
+
await this.initialize();
|
|
8622
|
+
return this.getMaintenanceStore("getDistinctSessionCount").getDistinctSessionCount();
|
|
8623
|
+
}
|
|
8624
|
+
async getDailyEventCounts(sinceIso) {
|
|
8625
|
+
await this.initialize();
|
|
8626
|
+
return this.getMaintenanceStore("getDailyEventCounts").getDailyEventCounts(sinceIso);
|
|
8627
|
+
}
|
|
8391
8628
|
async getSessionHistory(sessionId) {
|
|
8392
8629
|
await this.initialize();
|
|
8393
8630
|
return this.queryStore.getSessionEvents(sessionId);
|
|
@@ -9356,11 +9593,12 @@ var Retriever = class {
|
|
|
9356
9593
|
if (!scope && projectScopeMode === "global" && facetFilters === null) return results;
|
|
9357
9594
|
const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
|
|
9358
9595
|
const filtered = [];
|
|
9596
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9359
9597
|
for (const result of results) {
|
|
9360
9598
|
if (scope?.sessionId && result.sessionId !== scope.sessionId) continue;
|
|
9361
9599
|
if (scope?.sessionIdPrefix && !result.sessionId.startsWith(scope.sessionIdPrefix)) continue;
|
|
9362
9600
|
if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result.eventType)) continue;
|
|
9363
|
-
const event =
|
|
9601
|
+
const event = eventMap.get(result.eventId);
|
|
9364
9602
|
if (!event) continue;
|
|
9365
9603
|
if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix)) continue;
|
|
9366
9604
|
if (normalizedIncludes.length > 0) {
|
|
@@ -9469,10 +9707,30 @@ var Retriever = class {
|
|
|
9469
9707
|
async retrieveRecent(limit = 100) {
|
|
9470
9708
|
return this.eventStore.getRecentEvents(limit);
|
|
9471
9709
|
}
|
|
9710
|
+
/**
|
|
9711
|
+
* Batch-load events by id into a Map in a single query, de-duplicating ids.
|
|
9712
|
+
* Replaces per-result getEvent round-trips in the scope/enrich hot paths.
|
|
9713
|
+
*/
|
|
9714
|
+
async fetchEventsByIds(ids) {
|
|
9715
|
+
const map = /* @__PURE__ */ new Map();
|
|
9716
|
+
if (ids.length === 0) return map;
|
|
9717
|
+
const unique = [...new Set(ids)];
|
|
9718
|
+
if (typeof this.eventStore.getEvents === "function") {
|
|
9719
|
+
const events = await this.eventStore.getEvents(unique);
|
|
9720
|
+
for (const event of events) map.set(event.id, event);
|
|
9721
|
+
} else {
|
|
9722
|
+
const events = await Promise.all(unique.map((id) => this.eventStore.getEvent(id)));
|
|
9723
|
+
for (const event of events) {
|
|
9724
|
+
if (event) map.set(event.id, event);
|
|
9725
|
+
}
|
|
9726
|
+
}
|
|
9727
|
+
return map;
|
|
9728
|
+
}
|
|
9472
9729
|
async enrichResults(results, options, query) {
|
|
9473
9730
|
const memories = [];
|
|
9731
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9474
9732
|
for (const result of results) {
|
|
9475
|
-
const event =
|
|
9733
|
+
const event = eventMap.get(result.eventId);
|
|
9476
9734
|
if (!event) continue;
|
|
9477
9735
|
if (this.graduation) {
|
|
9478
9736
|
this.graduation.recordAccess(event.id, options.sessionId || "unknown", result.score);
|
|
@@ -12225,6 +12483,34 @@ var MemoryService = class {
|
|
|
12225
12483
|
async rebuildFtsIndex() {
|
|
12226
12484
|
return this.queryService.rebuildFtsIndex();
|
|
12227
12485
|
}
|
|
12486
|
+
/**
|
|
12487
|
+
* Get a single event by id (indexed lookup).
|
|
12488
|
+
*/
|
|
12489
|
+
async getEvent(id) {
|
|
12490
|
+
return this.queryService.getEvent(id);
|
|
12491
|
+
}
|
|
12492
|
+
/**
|
|
12493
|
+
* Resolve an event from its citation id via the reverse index.
|
|
12494
|
+
*/
|
|
12495
|
+
async getEventByCitationId(citationId) {
|
|
12496
|
+
return this.queryService.getEventByCitationId(citationId);
|
|
12497
|
+
}
|
|
12498
|
+
/** All events at/after an ISO timestamp (ascending, uncapped). */
|
|
12499
|
+
async getEventsAfter(sinceIso) {
|
|
12500
|
+
return this.queryService.getEventsAfter(sinceIso);
|
|
12501
|
+
}
|
|
12502
|
+
/** Aggregate event counts by type (SQL GROUP BY). */
|
|
12503
|
+
async getEventTypeCounts() {
|
|
12504
|
+
return this.queryService.getEventTypeCounts();
|
|
12505
|
+
}
|
|
12506
|
+
/** Count distinct sessions (SQL). */
|
|
12507
|
+
async getDistinctSessionCount() {
|
|
12508
|
+
return this.queryService.getDistinctSessionCount();
|
|
12509
|
+
}
|
|
12510
|
+
/** Per-day event counts with type breakdown since an ISO timestamp (SQL). */
|
|
12511
|
+
async getDailyEventCounts(sinceIso) {
|
|
12512
|
+
return this.queryService.getDailyEventCounts(sinceIso);
|
|
12513
|
+
}
|
|
12228
12514
|
/**
|
|
12229
12515
|
* Get session history
|
|
12230
12516
|
*/
|
|
@@ -12554,6 +12840,77 @@ import * as fs9 from "fs";
|
|
|
12554
12840
|
import * as net from "net";
|
|
12555
12841
|
import * as os6 from "os";
|
|
12556
12842
|
import * as path11 from "path";
|
|
12843
|
+
|
|
12844
|
+
// src/adapters/claude/hooks/hook-runtime.ts
|
|
12845
|
+
var DEFAULT_STDIN_TIMEOUT_MS = 1e4;
|
|
12846
|
+
var DEFAULT_OVERALL_TIMEOUT_MS = 3e4;
|
|
12847
|
+
function readNumberEnv(name, fallback, options = {}) {
|
|
12848
|
+
const raw = process.env[name];
|
|
12849
|
+
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
12850
|
+
const parsed = options.integer ? Number.parseInt(raw, 10) : Number.parseFloat(raw);
|
|
12851
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
12852
|
+
let value = parsed;
|
|
12853
|
+
if (options.min !== void 0) value = Math.max(options.min, value);
|
|
12854
|
+
if (options.max !== void 0) value = Math.min(options.max, value);
|
|
12855
|
+
return value;
|
|
12856
|
+
}
|
|
12857
|
+
function readPositiveIntEnv(name, fallback) {
|
|
12858
|
+
return readNumberEnv(name, fallback, { integer: true, min: 1 });
|
|
12859
|
+
}
|
|
12860
|
+
function readStdin(options = {}) {
|
|
12861
|
+
const timeoutMs = options.timeoutMs ?? readPositiveIntEnv("CLAUDE_MEMORY_HOOK_STDIN_TIMEOUT_MS", DEFAULT_STDIN_TIMEOUT_MS);
|
|
12862
|
+
return new Promise((resolve2) => {
|
|
12863
|
+
const stdin = process.stdin;
|
|
12864
|
+
let data = "";
|
|
12865
|
+
let settled = false;
|
|
12866
|
+
const onData = (chunk) => {
|
|
12867
|
+
data += chunk;
|
|
12868
|
+
};
|
|
12869
|
+
const finish = () => {
|
|
12870
|
+
if (settled) return;
|
|
12871
|
+
settled = true;
|
|
12872
|
+
clearTimeout(timer);
|
|
12873
|
+
stdin.off("data", onData);
|
|
12874
|
+
stdin.off("end", finish);
|
|
12875
|
+
stdin.off("error", finish);
|
|
12876
|
+
resolve2(data);
|
|
12877
|
+
};
|
|
12878
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
12879
|
+
timer.unref?.();
|
|
12880
|
+
stdin.setEncoding("utf8");
|
|
12881
|
+
stdin.on("data", onData);
|
|
12882
|
+
stdin.on("end", finish);
|
|
12883
|
+
stdin.on("error", finish);
|
|
12884
|
+
});
|
|
12885
|
+
}
|
|
12886
|
+
function emitHookOutput(json) {
|
|
12887
|
+
process.stdout.write(json.endsWith("\n") ? json : `${json}
|
|
12888
|
+
`);
|
|
12889
|
+
}
|
|
12890
|
+
async function runHook(options, run) {
|
|
12891
|
+
const debug = Boolean(process.env.CLAUDE_MEMORY_DEBUG);
|
|
12892
|
+
const timeoutMs = options.timeoutMs ?? readPositiveIntEnv("CLAUDE_MEMORY_HOOK_TIMEOUT_MS", DEFAULT_OVERALL_TIMEOUT_MS);
|
|
12893
|
+
let emitted = false;
|
|
12894
|
+
const emit = (json) => {
|
|
12895
|
+
if (emitted) return;
|
|
12896
|
+
emitted = true;
|
|
12897
|
+
emitHookOutput(json);
|
|
12898
|
+
};
|
|
12899
|
+
const watchdog = setTimeout(() => {
|
|
12900
|
+
if (debug) console.error(`[${options.name}] hook timed out after ${timeoutMs}ms; forcing exit`);
|
|
12901
|
+
emit(options.fallbackOutput);
|
|
12902
|
+
process.exit(0);
|
|
12903
|
+
}, timeoutMs);
|
|
12904
|
+
watchdog.unref?.();
|
|
12905
|
+
try {
|
|
12906
|
+
emit(await run());
|
|
12907
|
+
} catch (error) {
|
|
12908
|
+
if (debug) console.error(`[${options.name}] hook error:`, error);
|
|
12909
|
+
emit(options.fallbackOutput);
|
|
12910
|
+
}
|
|
12911
|
+
}
|
|
12912
|
+
|
|
12913
|
+
// src/adapters/claude/hooks/semantic-daemon-client.ts
|
|
12557
12914
|
var DEFAULT_SOCKET_PATH = path11.join(
|
|
12558
12915
|
os6.homedir(),
|
|
12559
12916
|
".claude-code",
|
|
@@ -12561,7 +12918,7 @@ var DEFAULT_SOCKET_PATH = path11.join(
|
|
|
12561
12918
|
"semantic-daemon.sock"
|
|
12562
12919
|
);
|
|
12563
12920
|
var DAEMON_SOCKET_PATH = process.env.CLAUDE_MEMORY_SEMANTIC_SOCKET || DEFAULT_SOCKET_PATH;
|
|
12564
|
-
var DAEMON_START_TIMEOUT_MS =
|
|
12921
|
+
var DAEMON_START_TIMEOUT_MS = readNumberEnv("CLAUDE_MEMORY_SEMANTIC_DAEMON_START_MS", 1500, { integer: true, min: 0 });
|
|
12565
12922
|
var daemonStartPromise = null;
|
|
12566
12923
|
async function ensureDaemonRunning() {
|
|
12567
12924
|
if (daemonStartPromise) {
|
|
@@ -12624,8 +12981,12 @@ function sleep(ms) {
|
|
|
12624
12981
|
|
|
12625
12982
|
// src/adapters/claude/hooks/session-start.ts
|
|
12626
12983
|
async function main() {
|
|
12627
|
-
|
|
12628
|
-
|
|
12984
|
+
let input;
|
|
12985
|
+
try {
|
|
12986
|
+
input = JSON.parse(await readStdin());
|
|
12987
|
+
} catch {
|
|
12988
|
+
return JSON.stringify({ context: "" });
|
|
12989
|
+
}
|
|
12629
12990
|
registerSession(input.session_id, input.cwd);
|
|
12630
12991
|
ensureDaemonRunning().catch(() => {
|
|
12631
12992
|
});
|
|
@@ -12649,10 +13010,12 @@ You have worked on this project before. Here are some relevant memories:
|
|
|
12649
13010
|
}
|
|
12650
13011
|
}
|
|
12651
13012
|
const output = { context };
|
|
12652
|
-
|
|
13013
|
+
return JSON.stringify(output);
|
|
12653
13014
|
} catch (error) {
|
|
12654
|
-
|
|
12655
|
-
|
|
13015
|
+
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
13016
|
+
console.error("Memory hook error:", error);
|
|
13017
|
+
}
|
|
13018
|
+
return JSON.stringify({ context: "" });
|
|
12656
13019
|
} finally {
|
|
12657
13020
|
try {
|
|
12658
13021
|
await memoryService.close();
|
|
@@ -12660,19 +13023,7 @@ You have worked on this project before. Here are some relevant memories:
|
|
|
12660
13023
|
}
|
|
12661
13024
|
}
|
|
12662
13025
|
}
|
|
12663
|
-
function readStdin() {
|
|
12664
|
-
return new Promise((resolve2) => {
|
|
12665
|
-
let data = "";
|
|
12666
|
-
process.stdin.setEncoding("utf8");
|
|
12667
|
-
process.stdin.on("data", (chunk) => {
|
|
12668
|
-
data += chunk;
|
|
12669
|
-
});
|
|
12670
|
-
process.stdin.on("end", () => {
|
|
12671
|
-
resolve2(data);
|
|
12672
|
-
});
|
|
12673
|
-
});
|
|
12674
|
-
}
|
|
12675
13026
|
|
|
12676
13027
|
// src/hooks/session-start.ts
|
|
12677
|
-
main
|
|
13028
|
+
void runHook({ name: "session-start", fallbackOutput: '{"context":""}' }, main);
|
|
12678
13029
|
//# sourceMappingURL=session-start.js.map
|