claude-memory-layer 1.0.53 → 1.0.55
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 +1224 -548
- 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 +577 -231
- package/dist/mcp/index.js.map +4 -4
- package/dist/server/api/index.js +896 -354
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +954 -375
- 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 +8 -3
package/dist/hooks/stop.js
CHANGED
|
@@ -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 {
|
|
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
|
+
`);
|
|
3460
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
|
|
@@ -7416,7 +7629,7 @@ var SessionActorRepository = class {
|
|
|
7416
7629
|
};
|
|
7417
7630
|
|
|
7418
7631
|
// src/core/operations/perspective-observation-repository.ts
|
|
7419
|
-
import { createHash as
|
|
7632
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
7420
7633
|
function projectHashToStorage4(projectHash) {
|
|
7421
7634
|
return projectHash ?? "";
|
|
7422
7635
|
}
|
|
@@ -7453,7 +7666,7 @@ function sanitizeStoredRecord(value) {
|
|
|
7453
7666
|
return sanitizeGovernanceAuditValue(value);
|
|
7454
7667
|
}
|
|
7455
7668
|
function stableHash(value) {
|
|
7456
|
-
return
|
|
7669
|
+
return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
7457
7670
|
}
|
|
7458
7671
|
function evidenceHash(sourceEventIds, sourceObservationIds) {
|
|
7459
7672
|
return stableHash({
|
|
@@ -8568,6 +8781,30 @@ var MemoryQueryService = class {
|
|
|
8568
8781
|
score: 1 - (r.rank - minRank) / rankRange
|
|
8569
8782
|
})).filter((r) => !options?.minScore || r.score >= options.minScore);
|
|
8570
8783
|
}
|
|
8784
|
+
async getEvent(id) {
|
|
8785
|
+
await this.initialize();
|
|
8786
|
+
return this.queryStore.getEvent(id);
|
|
8787
|
+
}
|
|
8788
|
+
async getEventByCitationId(citationId) {
|
|
8789
|
+
await this.initialize();
|
|
8790
|
+
return this.getMaintenanceStore("getEventByCitationId").getEventByCitationId(citationId);
|
|
8791
|
+
}
|
|
8792
|
+
async getEventsAfter(sinceIso) {
|
|
8793
|
+
await this.initialize();
|
|
8794
|
+
return this.getMaintenanceStore("getEventsAfter").getEventsAfter(sinceIso);
|
|
8795
|
+
}
|
|
8796
|
+
async getEventTypeCounts() {
|
|
8797
|
+
await this.initialize();
|
|
8798
|
+
return this.getMaintenanceStore("getEventTypeCounts").getEventTypeCounts();
|
|
8799
|
+
}
|
|
8800
|
+
async getDistinctSessionCount() {
|
|
8801
|
+
await this.initialize();
|
|
8802
|
+
return this.getMaintenanceStore("getDistinctSessionCount").getDistinctSessionCount();
|
|
8803
|
+
}
|
|
8804
|
+
async getDailyEventCounts(sinceIso) {
|
|
8805
|
+
await this.initialize();
|
|
8806
|
+
return this.getMaintenanceStore("getDailyEventCounts").getDailyEventCounts(sinceIso);
|
|
8807
|
+
}
|
|
8571
8808
|
async getSessionHistory(sessionId) {
|
|
8572
8809
|
await this.initialize();
|
|
8573
8810
|
return this.queryStore.getSessionEvents(sessionId);
|
|
@@ -9536,11 +9773,12 @@ var Retriever = class {
|
|
|
9536
9773
|
if (!scope && projectScopeMode === "global" && facetFilters === null) return results;
|
|
9537
9774
|
const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
|
|
9538
9775
|
const filtered = [];
|
|
9776
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9539
9777
|
for (const result of results) {
|
|
9540
9778
|
if (scope?.sessionId && result.sessionId !== scope.sessionId) continue;
|
|
9541
9779
|
if (scope?.sessionIdPrefix && !result.sessionId.startsWith(scope.sessionIdPrefix)) continue;
|
|
9542
9780
|
if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result.eventType)) continue;
|
|
9543
|
-
const event =
|
|
9781
|
+
const event = eventMap.get(result.eventId);
|
|
9544
9782
|
if (!event) continue;
|
|
9545
9783
|
if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix)) continue;
|
|
9546
9784
|
if (normalizedIncludes.length > 0) {
|
|
@@ -9649,10 +9887,30 @@ var Retriever = class {
|
|
|
9649
9887
|
async retrieveRecent(limit = 100) {
|
|
9650
9888
|
return this.eventStore.getRecentEvents(limit);
|
|
9651
9889
|
}
|
|
9890
|
+
/**
|
|
9891
|
+
* Batch-load events by id into a Map in a single query, de-duplicating ids.
|
|
9892
|
+
* Replaces per-result getEvent round-trips in the scope/enrich hot paths.
|
|
9893
|
+
*/
|
|
9894
|
+
async fetchEventsByIds(ids) {
|
|
9895
|
+
const map = /* @__PURE__ */ new Map();
|
|
9896
|
+
if (ids.length === 0) return map;
|
|
9897
|
+
const unique = [...new Set(ids)];
|
|
9898
|
+
if (typeof this.eventStore.getEvents === "function") {
|
|
9899
|
+
const events = await this.eventStore.getEvents(unique);
|
|
9900
|
+
for (const event of events) map.set(event.id, event);
|
|
9901
|
+
} else {
|
|
9902
|
+
const events = await Promise.all(unique.map((id) => this.eventStore.getEvent(id)));
|
|
9903
|
+
for (const event of events) {
|
|
9904
|
+
if (event) map.set(event.id, event);
|
|
9905
|
+
}
|
|
9906
|
+
}
|
|
9907
|
+
return map;
|
|
9908
|
+
}
|
|
9652
9909
|
async enrichResults(results, options, query) {
|
|
9653
9910
|
const memories = [];
|
|
9911
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9654
9912
|
for (const result of results) {
|
|
9655
|
-
const event =
|
|
9913
|
+
const event = eventMap.get(result.eventId);
|
|
9656
9914
|
if (!event) continue;
|
|
9657
9915
|
if (this.graduation) {
|
|
9658
9916
|
this.graduation.recordAccess(event.id, options.sessionId || "unknown", result.score);
|
|
@@ -12380,6 +12638,34 @@ var MemoryService = class {
|
|
|
12380
12638
|
async rebuildFtsIndex() {
|
|
12381
12639
|
return this.queryService.rebuildFtsIndex();
|
|
12382
12640
|
}
|
|
12641
|
+
/**
|
|
12642
|
+
* Get a single event by id (indexed lookup).
|
|
12643
|
+
*/
|
|
12644
|
+
async getEvent(id) {
|
|
12645
|
+
return this.queryService.getEvent(id);
|
|
12646
|
+
}
|
|
12647
|
+
/**
|
|
12648
|
+
* Resolve an event from its citation id via the reverse index.
|
|
12649
|
+
*/
|
|
12650
|
+
async getEventByCitationId(citationId) {
|
|
12651
|
+
return this.queryService.getEventByCitationId(citationId);
|
|
12652
|
+
}
|
|
12653
|
+
/** All events at/after an ISO timestamp (ascending, uncapped). */
|
|
12654
|
+
async getEventsAfter(sinceIso) {
|
|
12655
|
+
return this.queryService.getEventsAfter(sinceIso);
|
|
12656
|
+
}
|
|
12657
|
+
/** Aggregate event counts by type (SQL GROUP BY). */
|
|
12658
|
+
async getEventTypeCounts() {
|
|
12659
|
+
return this.queryService.getEventTypeCounts();
|
|
12660
|
+
}
|
|
12661
|
+
/** Count distinct sessions (SQL). */
|
|
12662
|
+
async getDistinctSessionCount() {
|
|
12663
|
+
return this.queryService.getDistinctSessionCount();
|
|
12664
|
+
}
|
|
12665
|
+
/** Per-day event counts with type breakdown since an ISO timestamp (SQL). */
|
|
12666
|
+
async getDailyEventCounts(sinceIso) {
|
|
12667
|
+
return this.queryService.getDailyEventCounts(sinceIso);
|
|
12668
|
+
}
|
|
12383
12669
|
/**
|
|
12384
12670
|
* Get session history
|
|
12385
12671
|
*/
|
|
@@ -12816,6 +13102,75 @@ async function extractAssistantMessages(transcriptPath, options) {
|
|
|
12816
13102
|
return extractAssistantTextMessages(entries);
|
|
12817
13103
|
}
|
|
12818
13104
|
|
|
13105
|
+
// src/adapters/claude/hooks/hook-runtime.ts
|
|
13106
|
+
var DEFAULT_STDIN_TIMEOUT_MS = 1e4;
|
|
13107
|
+
var DEFAULT_OVERALL_TIMEOUT_MS = 3e4;
|
|
13108
|
+
function readNumberEnv(name, fallback, options = {}) {
|
|
13109
|
+
const raw = process.env[name];
|
|
13110
|
+
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
13111
|
+
const parsed = options.integer ? Number.parseInt(raw, 10) : Number.parseFloat(raw);
|
|
13112
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
13113
|
+
let value = parsed;
|
|
13114
|
+
if (options.min !== void 0) value = Math.max(options.min, value);
|
|
13115
|
+
if (options.max !== void 0) value = Math.min(options.max, value);
|
|
13116
|
+
return value;
|
|
13117
|
+
}
|
|
13118
|
+
function readPositiveIntEnv(name, fallback) {
|
|
13119
|
+
return readNumberEnv(name, fallback, { integer: true, min: 1 });
|
|
13120
|
+
}
|
|
13121
|
+
function readStdin(options = {}) {
|
|
13122
|
+
const timeoutMs = options.timeoutMs ?? readPositiveIntEnv("CLAUDE_MEMORY_HOOK_STDIN_TIMEOUT_MS", DEFAULT_STDIN_TIMEOUT_MS);
|
|
13123
|
+
return new Promise((resolve2) => {
|
|
13124
|
+
const stdin = process.stdin;
|
|
13125
|
+
let data = "";
|
|
13126
|
+
let settled = false;
|
|
13127
|
+
const onData = (chunk) => {
|
|
13128
|
+
data += chunk;
|
|
13129
|
+
};
|
|
13130
|
+
const finish = () => {
|
|
13131
|
+
if (settled) return;
|
|
13132
|
+
settled = true;
|
|
13133
|
+
clearTimeout(timer);
|
|
13134
|
+
stdin.off("data", onData);
|
|
13135
|
+
stdin.off("end", finish);
|
|
13136
|
+
stdin.off("error", finish);
|
|
13137
|
+
resolve2(data);
|
|
13138
|
+
};
|
|
13139
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
13140
|
+
timer.unref?.();
|
|
13141
|
+
stdin.setEncoding("utf8");
|
|
13142
|
+
stdin.on("data", onData);
|
|
13143
|
+
stdin.on("end", finish);
|
|
13144
|
+
stdin.on("error", finish);
|
|
13145
|
+
});
|
|
13146
|
+
}
|
|
13147
|
+
function emitHookOutput(json) {
|
|
13148
|
+
process.stdout.write(json.endsWith("\n") ? json : `${json}
|
|
13149
|
+
`);
|
|
13150
|
+
}
|
|
13151
|
+
async function runHook(options, run) {
|
|
13152
|
+
const debug = Boolean(process.env.CLAUDE_MEMORY_DEBUG);
|
|
13153
|
+
const timeoutMs = options.timeoutMs ?? readPositiveIntEnv("CLAUDE_MEMORY_HOOK_TIMEOUT_MS", DEFAULT_OVERALL_TIMEOUT_MS);
|
|
13154
|
+
let emitted = false;
|
|
13155
|
+
const emit = (json) => {
|
|
13156
|
+
if (emitted) return;
|
|
13157
|
+
emitted = true;
|
|
13158
|
+
emitHookOutput(json);
|
|
13159
|
+
};
|
|
13160
|
+
const watchdog = setTimeout(() => {
|
|
13161
|
+
if (debug) console.error(`[${options.name}] hook timed out after ${timeoutMs}ms; forcing exit`);
|
|
13162
|
+
emit(options.fallbackOutput);
|
|
13163
|
+
process.exit(0);
|
|
13164
|
+
}, timeoutMs);
|
|
13165
|
+
watchdog.unref?.();
|
|
13166
|
+
try {
|
|
13167
|
+
emit(await run());
|
|
13168
|
+
} catch (error) {
|
|
13169
|
+
if (debug) console.error(`[${options.name}] hook error:`, error);
|
|
13170
|
+
emit(options.fallbackOutput);
|
|
13171
|
+
}
|
|
13172
|
+
}
|
|
13173
|
+
|
|
12819
13174
|
// src/adapters/claude/hooks/stop.ts
|
|
12820
13175
|
var DEFAULT_PRIVACY_CONFIG = {
|
|
12821
13176
|
excludePatterns: ["password", "secret", "api_key", "token", "bearer"],
|
|
@@ -12828,15 +13183,12 @@ var DEFAULT_PRIVACY_CONFIG = {
|
|
|
12828
13183
|
}
|
|
12829
13184
|
};
|
|
12830
13185
|
async function main() {
|
|
12831
|
-
const inputData = await readStdin();
|
|
12832
|
-
const input = JSON.parse(inputData);
|
|
12833
|
-
const memoryService = getLightweightMemoryService(input.session_id);
|
|
12834
13186
|
try {
|
|
13187
|
+
const input = JSON.parse(await readStdin());
|
|
13188
|
+
const memoryService = getLightweightMemoryService(input.session_id);
|
|
12835
13189
|
const turnId = readTurnState(input.session_id);
|
|
12836
13190
|
const assistantMessages = await extractAssistantMessages(input.transcript_path);
|
|
12837
|
-
const MIN_AGENT_RESPONSE_LEN =
|
|
12838
|
-
process.env.CLAUDE_MEMORY_AGENT_RESPONSE_MIN_LEN || "150"
|
|
12839
|
-
);
|
|
13191
|
+
const MIN_AGENT_RESPONSE_LEN = readNumberEnv("CLAUDE_MEMORY_AGENT_RESPONSE_MIN_LEN", 150, { integer: true, min: 0 });
|
|
12840
13192
|
const lastIdx = assistantMessages.length - 1;
|
|
12841
13193
|
for (let i = 0; i < assistantMessages.length; i++) {
|
|
12842
13194
|
const text = assistantMessages[i];
|
|
@@ -12870,27 +13222,15 @@ async function main() {
|
|
|
12870
13222
|
} catch {
|
|
12871
13223
|
}
|
|
12872
13224
|
await memoryService.processPendingEmbeddings();
|
|
12873
|
-
|
|
13225
|
+
return JSON.stringify({});
|
|
12874
13226
|
} catch (error) {
|
|
12875
13227
|
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
12876
13228
|
console.error("Stop hook error:", error);
|
|
12877
13229
|
}
|
|
12878
|
-
|
|
13230
|
+
return JSON.stringify({});
|
|
12879
13231
|
}
|
|
12880
13232
|
}
|
|
12881
|
-
function readStdin() {
|
|
12882
|
-
return new Promise((resolve2) => {
|
|
12883
|
-
let data = "";
|
|
12884
|
-
process.stdin.setEncoding("utf8");
|
|
12885
|
-
process.stdin.on("data", (chunk) => {
|
|
12886
|
-
data += chunk;
|
|
12887
|
-
});
|
|
12888
|
-
process.stdin.on("end", () => {
|
|
12889
|
-
resolve2(data);
|
|
12890
|
-
});
|
|
12891
|
-
});
|
|
12892
|
-
}
|
|
12893
13233
|
|
|
12894
13234
|
// src/hooks/stop.ts
|
|
12895
|
-
main
|
|
13235
|
+
void runHook({ name: "stop", fallbackOutput: "{}" }, main);
|
|
12896
13236
|
//# sourceMappingURL=stop.js.map
|