claude-memory-layer 1.0.53 → 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 +922 -524
- 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 +691 -354
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +742 -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
|
@@ -276,7 +276,13 @@ function toDate(value) {
|
|
|
276
276
|
return new Date(String(value));
|
|
277
277
|
}
|
|
278
278
|
function createDatabase(dbPath, options) {
|
|
279
|
-
|
|
279
|
+
const db = new BetterSqlite3(dbPath, { readonly: options?.readOnly });
|
|
280
|
+
db.pragma("busy_timeout = 5000");
|
|
281
|
+
if (!options?.readOnly) {
|
|
282
|
+
db.pragma("journal_mode = WAL");
|
|
283
|
+
db.pragma("synchronous = NORMAL");
|
|
284
|
+
}
|
|
285
|
+
return db;
|
|
280
286
|
}
|
|
281
287
|
function dbRun(db, sql, params = []) {
|
|
282
288
|
db.prepare(sql).run(...params);
|
|
@@ -509,19 +515,29 @@ var ConsolidatedStore = class {
|
|
|
509
515
|
*/
|
|
510
516
|
async create(input) {
|
|
511
517
|
const memoryId = randomUUID2();
|
|
512
|
-
|
|
513
|
-
|
|
518
|
+
const sourceEvents = Array.isArray(input.sourceEvents) ? input.sourceEvents : [];
|
|
519
|
+
const insertMemory = this.db.prepare(
|
|
514
520
|
`INSERT INTO consolidated_memories
|
|
515
521
|
(memory_id, summary, topics, source_events, confidence, created_at)
|
|
516
|
-
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
517
|
-
|
|
522
|
+
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`
|
|
523
|
+
);
|
|
524
|
+
const insertEvent = this.db.prepare(
|
|
525
|
+
`INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id) VALUES (?, ?)`
|
|
526
|
+
);
|
|
527
|
+
this.db.transaction(() => {
|
|
528
|
+
insertMemory.run(
|
|
518
529
|
memoryId,
|
|
519
530
|
input.summary,
|
|
520
531
|
JSON.stringify(input.topics),
|
|
521
|
-
JSON.stringify(
|
|
532
|
+
JSON.stringify(sourceEvents),
|
|
522
533
|
input.confidence
|
|
523
|
-
|
|
524
|
-
|
|
534
|
+
);
|
|
535
|
+
for (const eventId of sourceEvents) {
|
|
536
|
+
if (typeof eventId === "string" && eventId.length > 0) {
|
|
537
|
+
insertEvent.run(memoryId, eventId);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
})();
|
|
525
541
|
return memoryId;
|
|
526
542
|
}
|
|
527
543
|
/**
|
|
@@ -627,11 +643,12 @@ var ConsolidatedStore = class {
|
|
|
627
643
|
* Delete a consolidated memory
|
|
628
644
|
*/
|
|
629
645
|
async delete(memoryId) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
646
|
+
const deleteEvents = this.db.prepare(`DELETE FROM consolidated_memory_events WHERE memory_id = ?`);
|
|
647
|
+
const deleteMemory = this.db.prepare(`DELETE FROM consolidated_memories WHERE memory_id = ?`);
|
|
648
|
+
this.db.transaction(() => {
|
|
649
|
+
deleteEvents.run(memoryId);
|
|
650
|
+
deleteMemory.run(memoryId);
|
|
651
|
+
})();
|
|
635
652
|
}
|
|
636
653
|
/**
|
|
637
654
|
* Create a long-term rule promoted from stable summaries
|
|
@@ -744,16 +761,16 @@ var ConsolidatedStore = class {
|
|
|
744
761
|
* Check if source events are already consolidated
|
|
745
762
|
*/
|
|
746
763
|
async isAlreadyConsolidated(eventIds) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
return
|
|
764
|
+
if (eventIds.length === 0) return false;
|
|
765
|
+
const placeholders = eventIds.map(() => "?").join(",");
|
|
766
|
+
const rows = await dbAll(
|
|
767
|
+
this.db,
|
|
768
|
+
`SELECT memory_id FROM consolidated_memory_events
|
|
769
|
+
WHERE event_id IN (${placeholders})
|
|
770
|
+
LIMIT 1`,
|
|
771
|
+
eventIds
|
|
772
|
+
);
|
|
773
|
+
return rows.length > 0;
|
|
757
774
|
}
|
|
758
775
|
/**
|
|
759
776
|
* Get the last consolidation time
|
|
@@ -2289,6 +2306,19 @@ function makeDedupeKey(content, sessionId) {
|
|
|
2289
2306
|
return `${sessionId}:${contentHash}`;
|
|
2290
2307
|
}
|
|
2291
2308
|
|
|
2309
|
+
// src/core/citation-generator.ts
|
|
2310
|
+
import { createHash as createHash2 } from "crypto";
|
|
2311
|
+
var ID_LENGTH = 6;
|
|
2312
|
+
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
2313
|
+
function generateCitationId(eventId) {
|
|
2314
|
+
const hash = createHash2("sha256").update(eventId).digest();
|
|
2315
|
+
let id = "";
|
|
2316
|
+
for (let i = 0; i < ID_LENGTH; i++) {
|
|
2317
|
+
id += CHARSET[hash[i] % CHARSET.length];
|
|
2318
|
+
}
|
|
2319
|
+
return id;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2292
2322
|
// src/core/sqlite-event-store.ts
|
|
2293
2323
|
import * as nodePath2 from "path";
|
|
2294
2324
|
|
|
@@ -2333,17 +2363,28 @@ function createSQLiteDatabase(path12, options) {
|
|
|
2333
2363
|
}
|
|
2334
2364
|
return db;
|
|
2335
2365
|
}
|
|
2366
|
+
var statementCache = /* @__PURE__ */ new WeakMap();
|
|
2367
|
+
function prepareCached(db, sql) {
|
|
2368
|
+
let cache = statementCache.get(db);
|
|
2369
|
+
if (!cache) {
|
|
2370
|
+
cache = /* @__PURE__ */ new Map();
|
|
2371
|
+
statementCache.set(db, cache);
|
|
2372
|
+
}
|
|
2373
|
+
let stmt = cache.get(sql);
|
|
2374
|
+
if (!stmt) {
|
|
2375
|
+
stmt = db.prepare(sql);
|
|
2376
|
+
cache.set(sql, stmt);
|
|
2377
|
+
}
|
|
2378
|
+
return stmt;
|
|
2379
|
+
}
|
|
2336
2380
|
function sqliteRun(db, sql, params = []) {
|
|
2337
|
-
|
|
2338
|
-
return stmt.run(...params);
|
|
2381
|
+
return prepareCached(db, sql).run(...params);
|
|
2339
2382
|
}
|
|
2340
2383
|
function sqliteAll(db, sql, params = []) {
|
|
2341
|
-
|
|
2342
|
-
return stmt.all(...params);
|
|
2384
|
+
return prepareCached(db, sql).all(...params);
|
|
2343
2385
|
}
|
|
2344
2386
|
function sqliteGet(db, sql, params = []) {
|
|
2345
|
-
|
|
2346
|
-
return stmt.get(...params);
|
|
2387
|
+
return prepareCached(db, sql).get(...params);
|
|
2347
2388
|
}
|
|
2348
2389
|
function sqliteExec(db, sql) {
|
|
2349
2390
|
db.exec(sql);
|
|
@@ -2765,6 +2806,7 @@ function normalizeQueryRewriteKind(value) {
|
|
|
2765
2806
|
var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
|
|
2766
2807
|
var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2767
2808
|
var DEFAULT_OUTBOX_MAX_RETRIES = 3;
|
|
2809
|
+
var SQLITE_SCHEMA_VERSION = 1;
|
|
2768
2810
|
function emptyOutboxRecoveryResult() {
|
|
2769
2811
|
return {
|
|
2770
2812
|
embedding: { recoveredProcessing: 0, retriedFailed: 0 },
|
|
@@ -3109,6 +3151,15 @@ var SQLiteEventStore = class {
|
|
|
3109
3151
|
access_count INTEGER DEFAULT 0
|
|
3110
3152
|
);
|
|
3111
3153
|
|
|
3154
|
+
-- Junction: consolidated memory -> source event id (indexed lookup that
|
|
3155
|
+
-- replaces per-event source_events LIKE scans during consolidation).
|
|
3156
|
+
CREATE TABLE IF NOT EXISTS consolidated_memory_events (
|
|
3157
|
+
memory_id TEXT NOT NULL,
|
|
3158
|
+
event_id TEXT NOT NULL,
|
|
3159
|
+
PRIMARY KEY (memory_id, event_id)
|
|
3160
|
+
);
|
|
3161
|
+
CREATE INDEX IF NOT EXISTS idx_cme_event ON consolidated_memory_events(event_id);
|
|
3162
|
+
|
|
3112
3163
|
-- Continuity Log table (tracks context transitions)
|
|
3113
3164
|
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
3114
3165
|
log_id TEXT PRIMARY KEY,
|
|
@@ -3468,11 +3519,17 @@ var SQLiteEventStore = class {
|
|
|
3468
3519
|
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
3469
3520
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
3470
3521
|
END;
|
|
3522
|
+
|
|
3523
|
+
-- Reverse index for citation ids (a non-reversible hash of event id), so
|
|
3524
|
+
-- citation lookups are O(1) instead of scanning recent events and hashing
|
|
3525
|
+
-- each one. Populated lazily/self-healing by getEventByCitationId.
|
|
3526
|
+
CREATE TABLE IF NOT EXISTS event_citations (
|
|
3527
|
+
event_id TEXT PRIMARY KEY,
|
|
3528
|
+
citation_id TEXT NOT NULL
|
|
3529
|
+
);
|
|
3530
|
+
CREATE INDEX IF NOT EXISTS idx_event_citations_citation ON event_citations(citation_id);
|
|
3471
3531
|
`);
|
|
3472
|
-
|
|
3473
|
-
sqliteExec(this.db, `ALTER TABLE memory_action_edges ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';`);
|
|
3474
|
-
} catch {
|
|
3475
|
-
}
|
|
3532
|
+
this.addColumnIfMissing("memory_action_edges", "source", `TEXT NOT NULL DEFAULT 'manual'`);
|
|
3476
3533
|
try {
|
|
3477
3534
|
const edgeIndexes = sqliteAll(this.db, `PRAGMA index_list(memory_action_edges)`, []);
|
|
3478
3535
|
const hasSourceAwareUnique = edgeIndexes.some((index) => {
|
|
@@ -3509,55 +3566,17 @@ var SQLiteEventStore = class {
|
|
|
3509
3566
|
}
|
|
3510
3567
|
} catch {
|
|
3511
3568
|
}
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
try {
|
|
3517
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
|
|
3518
|
-
} catch {
|
|
3519
|
-
}
|
|
3520
|
-
try {
|
|
3521
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN raw_query_text TEXT;`);
|
|
3522
|
-
} catch {
|
|
3523
|
-
}
|
|
3524
|
-
try {
|
|
3525
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN query_rewrite_kind TEXT;`);
|
|
3526
|
-
} catch {
|
|
3527
|
-
}
|
|
3569
|
+
this.addColumnIfMissing("retrieval_traces", "selected_details_json", "TEXT");
|
|
3570
|
+
this.addColumnIfMissing("retrieval_traces", "candidate_details_json", "TEXT");
|
|
3571
|
+
this.addColumnIfMissing("retrieval_traces", "raw_query_text", "TEXT");
|
|
3572
|
+
this.addColumnIfMissing("retrieval_traces", "query_rewrite_kind", "TEXT");
|
|
3528
3573
|
try {
|
|
3529
3574
|
sqliteExec(this.db, `CREATE INDEX IF NOT EXISTS idx_retrieval_traces_query_rewrite_kind ON retrieval_traces(query_rewrite_kind);`);
|
|
3530
3575
|
} catch {
|
|
3531
3576
|
}
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
try {
|
|
3536
|
-
sqliteExec(this.db, `
|
|
3537
|
-
ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
|
|
3538
|
-
`);
|
|
3539
|
-
} catch (err) {
|
|
3540
|
-
console.error("Error adding access_count column:", err);
|
|
3541
|
-
}
|
|
3542
|
-
}
|
|
3543
|
-
if (!columnNames.includes("last_accessed_at")) {
|
|
3544
|
-
try {
|
|
3545
|
-
sqliteExec(this.db, `
|
|
3546
|
-
ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
|
|
3547
|
-
`);
|
|
3548
|
-
} catch (err) {
|
|
3549
|
-
console.error("Error adding last_accessed_at column:", err);
|
|
3550
|
-
}
|
|
3551
|
-
}
|
|
3552
|
-
if (!columnNames.includes("turn_id")) {
|
|
3553
|
-
try {
|
|
3554
|
-
sqliteExec(this.db, `
|
|
3555
|
-
ALTER TABLE events ADD COLUMN turn_id TEXT;
|
|
3556
|
-
`);
|
|
3557
|
-
} catch (err) {
|
|
3558
|
-
console.error("Error adding turn_id column:", err);
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3577
|
+
this.addColumnIfMissing("events", "access_count", "INTEGER DEFAULT 0");
|
|
3578
|
+
this.addColumnIfMissing("events", "last_accessed_at", "TEXT");
|
|
3579
|
+
this.addColumnIfMissing("events", "turn_id", "TEXT");
|
|
3561
3580
|
try {
|
|
3562
3581
|
sqliteExec(this.db, `
|
|
3563
3582
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -3574,8 +3593,18 @@ var SQLiteEventStore = class {
|
|
|
3574
3593
|
sqliteExec(this.db, `
|
|
3575
3594
|
CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
|
|
3576
3595
|
`);
|
|
3577
|
-
} catch
|
|
3596
|
+
} catch {
|
|
3597
|
+
}
|
|
3598
|
+
const cmeCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM consolidated_memory_events`, []);
|
|
3599
|
+
if ((cmeCount?.c ?? 0) === 0) {
|
|
3600
|
+
sqliteExec(this.db, `
|
|
3601
|
+
INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id)
|
|
3602
|
+
SELECT cm.memory_id, je.value
|
|
3603
|
+
FROM consolidated_memories cm, json_each(cm.source_events) je
|
|
3604
|
+
WHERE json_valid(cm.source_events);
|
|
3605
|
+
`);
|
|
3578
3606
|
}
|
|
3607
|
+
sqliteExec(this.db, `PRAGMA user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
3579
3608
|
this.initialized = true;
|
|
3580
3609
|
}
|
|
3581
3610
|
/**
|
|
@@ -3707,6 +3736,67 @@ var SQLiteEventStore = class {
|
|
|
3707
3736
|
);
|
|
3708
3737
|
return rows.map(this.rowToEvent);
|
|
3709
3738
|
}
|
|
3739
|
+
/**
|
|
3740
|
+
* Aggregate event counts by type via SQL GROUP BY, instead of loading rows
|
|
3741
|
+
* into memory and counting in JS. Counts all events (the old recent-window
|
|
3742
|
+
* scan also under-counted stores larger than its cap).
|
|
3743
|
+
*/
|
|
3744
|
+
async getEventTypeCounts(options) {
|
|
3745
|
+
await this.initialize();
|
|
3746
|
+
const rows = sqliteAll(
|
|
3747
|
+
this.db,
|
|
3748
|
+
`SELECT event_type, COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)} GROUP BY event_type`,
|
|
3749
|
+
[]
|
|
3750
|
+
);
|
|
3751
|
+
return rows.map((row) => ({ eventType: row.event_type, count: row.count }));
|
|
3752
|
+
}
|
|
3753
|
+
/**
|
|
3754
|
+
* Fetch every event at/after an ISO timestamp (ascending), with no row cap.
|
|
3755
|
+
* For window-scoped analytics (KPI/usefulness) this fetches exactly the events
|
|
3756
|
+
* the window needs instead of an arbitrary "most recent N" slice that both
|
|
3757
|
+
* over-fetches sparse windows and truncates very active ones.
|
|
3758
|
+
*/
|
|
3759
|
+
async getEventsAfter(sinceIso, options) {
|
|
3760
|
+
await this.initialize();
|
|
3761
|
+
const rows = sqliteAll(
|
|
3762
|
+
this.db,
|
|
3763
|
+
`SELECT * FROM events WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3764
|
+
[sinceIso]
|
|
3765
|
+
);
|
|
3766
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3767
|
+
}
|
|
3768
|
+
/** Count distinct sessions via SQL instead of materializing a Set in JS. */
|
|
3769
|
+
async getDistinctSessionCount(options) {
|
|
3770
|
+
await this.initialize();
|
|
3771
|
+
const row = sqliteGet(
|
|
3772
|
+
this.db,
|
|
3773
|
+
`SELECT COUNT(DISTINCT session_id) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`,
|
|
3774
|
+
[]
|
|
3775
|
+
);
|
|
3776
|
+
return row?.count ?? 0;
|
|
3777
|
+
}
|
|
3778
|
+
/**
|
|
3779
|
+
* Per-day event counts (with type breakdown) since an ISO timestamp, computed
|
|
3780
|
+
* by SQL GROUP BY on the date prefix. Timestamps are stored as ISO-8601
|
|
3781
|
+
* strings, so substr(...,1,10) is the UTC day and `timestamp >= ?` compares
|
|
3782
|
+
* lexicographically.
|
|
3783
|
+
*/
|
|
3784
|
+
async getDailyEventCounts(sinceIso, options) {
|
|
3785
|
+
await this.initialize();
|
|
3786
|
+
return sqliteAll(
|
|
3787
|
+
this.db,
|
|
3788
|
+
`SELECT substr(timestamp, 1, 10) as day,
|
|
3789
|
+
COUNT(*) as total,
|
|
3790
|
+
SUM(CASE WHEN event_type = 'user_prompt' THEN 1 ELSE 0 END) as prompts,
|
|
3791
|
+
SUM(CASE WHEN event_type = 'agent_response' THEN 1 ELSE 0 END) as responses,
|
|
3792
|
+
SUM(CASE WHEN event_type = 'tool_observation' THEN 1 ELSE 0 END) as tools
|
|
3793
|
+
FROM events
|
|
3794
|
+
WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)}
|
|
3795
|
+
GROUP BY day
|
|
3796
|
+
ORDER BY day ASC`,
|
|
3797
|
+
[sinceIso]
|
|
3798
|
+
);
|
|
3799
|
+
}
|
|
3710
3800
|
/**
|
|
3711
3801
|
* Get event by ID
|
|
3712
3802
|
*/
|
|
@@ -3720,6 +3810,71 @@ var SQLiteEventStore = class {
|
|
|
3720
3810
|
if (!row) return null;
|
|
3721
3811
|
return this.rowToEvent(row);
|
|
3722
3812
|
}
|
|
3813
|
+
/**
|
|
3814
|
+
* Batch-fetch events by id in a single query, applying the same quarantine
|
|
3815
|
+
* predicate as getEvent. Lets hot paths replace N sequential round-trips with
|
|
3816
|
+
* one. Missing ids are simply absent from the result (order not guaranteed).
|
|
3817
|
+
*/
|
|
3818
|
+
async getEvents(ids, options) {
|
|
3819
|
+
await this.initialize();
|
|
3820
|
+
if (ids.length === 0) return [];
|
|
3821
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
3822
|
+
const rows = sqliteAll(
|
|
3823
|
+
this.db,
|
|
3824
|
+
`SELECT * FROM events WHERE id IN (${placeholders}) AND ${maybeQuarantinePredicate(options)}`,
|
|
3825
|
+
ids
|
|
3826
|
+
);
|
|
3827
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3828
|
+
}
|
|
3829
|
+
/**
|
|
3830
|
+
* Resolve an event from its citation id via the reverse index, instead of
|
|
3831
|
+
* scanning recent events and hashing each one. The index is self-healing: a
|
|
3832
|
+
* miss triggers a one-time backfill of any events not yet indexed (covering
|
|
3833
|
+
* freshly appended events) before retrying.
|
|
3834
|
+
*/
|
|
3835
|
+
async getEventByCitationId(citationId, options) {
|
|
3836
|
+
await this.initialize();
|
|
3837
|
+
if (!citationId) return null;
|
|
3838
|
+
let row = sqliteGet(
|
|
3839
|
+
this.db,
|
|
3840
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3841
|
+
[citationId]
|
|
3842
|
+
);
|
|
3843
|
+
if (!row) {
|
|
3844
|
+
this.indexMissingCitations();
|
|
3845
|
+
row = sqliteGet(
|
|
3846
|
+
this.db,
|
|
3847
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3848
|
+
[citationId]
|
|
3849
|
+
);
|
|
3850
|
+
}
|
|
3851
|
+
if (!row) return null;
|
|
3852
|
+
return this.getEvent(row.event_id, options);
|
|
3853
|
+
}
|
|
3854
|
+
/**
|
|
3855
|
+
* Backfill citation ids for any events not yet present in event_citations.
|
|
3856
|
+
* Gated on a cheap count comparison so the common (fully-indexed) path does no
|
|
3857
|
+
* work, and the scan only runs when there are genuinely new events to index.
|
|
3858
|
+
*/
|
|
3859
|
+
indexMissingCitations() {
|
|
3860
|
+
const eventCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM events`, [])?.c ?? 0;
|
|
3861
|
+
const indexedCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM event_citations`, [])?.c ?? 0;
|
|
3862
|
+
if (indexedCount >= eventCount) return;
|
|
3863
|
+
const unindexed = sqliteAll(
|
|
3864
|
+
this.db,
|
|
3865
|
+
`SELECT e.id FROM events e
|
|
3866
|
+
LEFT JOIN event_citations ec ON ec.event_id = e.id
|
|
3867
|
+
WHERE ec.event_id IS NULL`,
|
|
3868
|
+
[]
|
|
3869
|
+
);
|
|
3870
|
+
if (unindexed.length === 0) return;
|
|
3871
|
+
const insert = this.db.prepare(`INSERT OR IGNORE INTO event_citations (event_id, citation_id) VALUES (?, ?)`);
|
|
3872
|
+
this.db.transaction(() => {
|
|
3873
|
+
for (const { id } of unindexed) {
|
|
3874
|
+
insert.run(id, generateCitationId(id));
|
|
3875
|
+
}
|
|
3876
|
+
})();
|
|
3877
|
+
}
|
|
3723
3878
|
/**
|
|
3724
3879
|
* Get events since a timestamp (for sync)
|
|
3725
3880
|
*/
|
|
@@ -3921,25 +4076,22 @@ var SQLiteEventStore = class {
|
|
|
3921
4076
|
*/
|
|
3922
4077
|
async getPendingOutboxItems(limit = 32) {
|
|
3923
4078
|
await this.initialize();
|
|
3924
|
-
const
|
|
3925
|
-
this.db,
|
|
3926
|
-
`SELECT * FROM embedding_outbox
|
|
3927
|
-
WHERE status = 'pending'
|
|
3928
|
-
ORDER BY created_at
|
|
3929
|
-
LIMIT ?`,
|
|
3930
|
-
[limit]
|
|
3931
|
-
);
|
|
3932
|
-
if (pending.length === 0) return [];
|
|
3933
|
-
const ids = pending.map((r) => r.id);
|
|
3934
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
3935
|
-
sqliteRun(
|
|
4079
|
+
const claimed = sqliteAll(
|
|
3936
4080
|
this.db,
|
|
3937
4081
|
`UPDATE embedding_outbox
|
|
3938
4082
|
SET status = 'processing', processed_at = datetime('now'), error_message = NULL
|
|
3939
|
-
WHERE id IN (
|
|
3940
|
-
|
|
4083
|
+
WHERE id IN (
|
|
4084
|
+
SELECT id FROM embedding_outbox
|
|
4085
|
+
WHERE status = 'pending'
|
|
4086
|
+
ORDER BY created_at
|
|
4087
|
+
LIMIT ?
|
|
4088
|
+
)
|
|
4089
|
+
RETURNING *`,
|
|
4090
|
+
[limit]
|
|
3941
4091
|
);
|
|
3942
|
-
|
|
4092
|
+
if (claimed.length === 0) return [];
|
|
4093
|
+
claimed.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
|
|
4094
|
+
return claimed.map((row) => ({
|
|
3943
4095
|
id: row.id,
|
|
3944
4096
|
eventId: row.event_id,
|
|
3945
4097
|
content: row.content,
|
|
@@ -4543,7 +4695,7 @@ var SQLiteEventStore = class {
|
|
|
4543
4695
|
const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
|
|
4544
4696
|
const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
|
|
4545
4697
|
let toolSuccessCount = 0;
|
|
4546
|
-
|
|
4698
|
+
const toolTotalCount = toolEvents.length;
|
|
4547
4699
|
for (const t of toolEvents) {
|
|
4548
4700
|
try {
|
|
4549
4701
|
const content = JSON.parse(t.content);
|
|
@@ -4706,10 +4858,14 @@ var SQLiteEventStore = class {
|
|
|
4706
4858
|
* Rebuild FTS index from existing events
|
|
4707
4859
|
* Call this once after upgrading to FTS5
|
|
4708
4860
|
*/
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4861
|
+
/**
|
|
4862
|
+
* Drop and recreate the events_fts virtual table + sync triggers, repopulating
|
|
4863
|
+
* from the current events table. Recreating the table (instead of issuing
|
|
4864
|
+
* DELETEs against it) sidesteps a quirk where older migrated FTS5 tables fail
|
|
4865
|
+
* with `no such column: T.event_id` on synthetic deletes. Safe to call inside
|
|
4866
|
+
* a transaction (pure DDL + INSERT...SELECT, no transaction control).
|
|
4867
|
+
*/
|
|
4868
|
+
recreateEventsFtsTable() {
|
|
4713
4869
|
sqliteExec(this.db, `
|
|
4714
4870
|
DROP TRIGGER IF EXISTS events_fts_insert;
|
|
4715
4871
|
DROP TRIGGER IF EXISTS events_fts_delete;
|
|
@@ -4738,6 +4894,12 @@ var SQLiteEventStore = class {
|
|
|
4738
4894
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4739
4895
|
END;
|
|
4740
4896
|
`);
|
|
4897
|
+
}
|
|
4898
|
+
async rebuildFtsIndex() {
|
|
4899
|
+
await this.initialize();
|
|
4900
|
+
const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
|
|
4901
|
+
const totalEvents = countRow?.count ?? 0;
|
|
4902
|
+
this.recreateEventsFtsTable();
|
|
4741
4903
|
return totalEvents;
|
|
4742
4904
|
}
|
|
4743
4905
|
/**
|
|
@@ -4755,6 +4917,35 @@ var SQLiteEventStore = class {
|
|
|
4755
4917
|
return false;
|
|
4756
4918
|
}
|
|
4757
4919
|
}
|
|
4920
|
+
hasTable(tableName) {
|
|
4921
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) return false;
|
|
4922
|
+
try {
|
|
4923
|
+
const rows = sqliteAll(
|
|
4924
|
+
this.db,
|
|
4925
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`,
|
|
4926
|
+
[tableName]
|
|
4927
|
+
);
|
|
4928
|
+
return rows.length > 0;
|
|
4929
|
+
} catch {
|
|
4930
|
+
return false;
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
/**
|
|
4934
|
+
* Add a column only when its table exists and the column is missing.
|
|
4935
|
+
*
|
|
4936
|
+
* Unlike a blind `try { ALTER } catch {}`, this distinguishes "already
|
|
4937
|
+
* migrated" / "table not present yet" (both safely skipped) from a genuine
|
|
4938
|
+
* failure (disk full, corruption), which is allowed to surface instead of
|
|
4939
|
+
* being silently swallowed as if the migration had succeeded.
|
|
4940
|
+
*/
|
|
4941
|
+
addColumnIfMissing(table, column, definition) {
|
|
4942
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
|
|
4943
|
+
throw new Error(`Invalid identifier in migration: ${table}.${column}`);
|
|
4944
|
+
}
|
|
4945
|
+
if (!this.hasTable(table)) return;
|
|
4946
|
+
if (this.hasTableColumn(table, column)) return;
|
|
4947
|
+
sqliteExec(this.db, `ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`);
|
|
4948
|
+
}
|
|
4758
4949
|
async recordRetrievalTrace(input) {
|
|
4759
4950
|
await this.initialize();
|
|
4760
4951
|
const traceId = randomUUID6();
|
|
@@ -4840,6 +5031,45 @@ var SQLiteEventStore = class {
|
|
|
4840
5031
|
FROM retrieval_traces`,
|
|
4841
5032
|
[]
|
|
4842
5033
|
);
|
|
5034
|
+
const strategyColumnSql = this.hasTableColumn("retrieval_traces", "strategy") ? "COALESCE(NULLIF(TRIM(strategy), ''), 'unknown')" : "'unknown'";
|
|
5035
|
+
const strategyRows = sqliteAll(
|
|
5036
|
+
this.db,
|
|
5037
|
+
`SELECT
|
|
5038
|
+
${strategyColumnSql} as strategy,
|
|
5039
|
+
COUNT(*) as total_queries,
|
|
5040
|
+
SUM(CASE WHEN selected_count > 0 THEN 1 ELSE 0 END) as queries_with_selection,
|
|
5041
|
+
SUM(CASE WHEN ${rewrittenQueryRewriteKindSql} THEN 1 ELSE 0 END) as rewritten_queries,
|
|
5042
|
+
SUM(candidate_count) as total_candidate_count,
|
|
5043
|
+
SUM(selected_count) as total_selected_count,
|
|
5044
|
+
AVG(candidate_count) as avg_candidate_count,
|
|
5045
|
+
AVG(selected_count) as avg_selected_count,
|
|
5046
|
+
CASE
|
|
5047
|
+
WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
|
|
5048
|
+
ELSE 0
|
|
5049
|
+
END as selection_rate
|
|
5050
|
+
FROM retrieval_traces
|
|
5051
|
+
GROUP BY ${strategyColumnSql}
|
|
5052
|
+
ORDER BY total_queries DESC, strategy ASC`,
|
|
5053
|
+
[]
|
|
5054
|
+
);
|
|
5055
|
+
const strategyBreakdown = strategyRows.map((strategyRow) => {
|
|
5056
|
+
const strategyTotalQueries = Number(strategyRow.total_queries || 0);
|
|
5057
|
+
const strategyQueriesWithSelection = Number(strategyRow.queries_with_selection || 0);
|
|
5058
|
+
const strategyRewrittenQueries = Number(strategyRow.rewritten_queries || 0);
|
|
5059
|
+
return {
|
|
5060
|
+
strategy: String(strategyRow.strategy || "unknown"),
|
|
5061
|
+
totalQueries: strategyTotalQueries,
|
|
5062
|
+
queriesWithSelection: strategyQueriesWithSelection,
|
|
5063
|
+
rewrittenQueries: strategyRewrittenQueries,
|
|
5064
|
+
rewriteRate: strategyTotalQueries > 0 ? strategyRewrittenQueries / strategyTotalQueries : 0,
|
|
5065
|
+
totalCandidateCount: Number(strategyRow.total_candidate_count || 0),
|
|
5066
|
+
totalSelectedCount: Number(strategyRow.total_selected_count || 0),
|
|
5067
|
+
avgCandidateCount: Number(strategyRow.avg_candidate_count || 0),
|
|
5068
|
+
avgSelectedCount: Number(strategyRow.avg_selected_count || 0),
|
|
5069
|
+
selectionRate: Number(strategyRow.selection_rate || 0),
|
|
5070
|
+
queryYieldRate: strategyTotalQueries > 0 ? strategyQueriesWithSelection / strategyTotalQueries : 0
|
|
5071
|
+
};
|
|
5072
|
+
});
|
|
4843
5073
|
const totalQueries = Number(row?.total_queries || 0);
|
|
4844
5074
|
const rewrittenQueries = Number(row?.rewritten_queries || 0);
|
|
4845
5075
|
const rawQueries = Math.max(0, totalQueries - rewrittenQueries);
|
|
@@ -4857,7 +5087,8 @@ var SQLiteEventStore = class {
|
|
|
4857
5087
|
rewrittenSelectionRate: rewrittenQueries > 0 ? rewrittenQueriesWithSelection / rewrittenQueries : 0,
|
|
4858
5088
|
rawSelectionRate: rawQueries > 0 ? rawQueriesWithSelection / rawQueries : 0,
|
|
4859
5089
|
avgSelectedCountForRewrittenQueries: Number(row?.avg_selected_count_for_rewritten_queries || 0),
|
|
4860
|
-
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0)
|
|
5090
|
+
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0),
|
|
5091
|
+
strategyBreakdown
|
|
4861
5092
|
};
|
|
4862
5093
|
} catch (err) {
|
|
4863
5094
|
if (err?.message?.includes("no such table")) {
|
|
@@ -4873,7 +5104,8 @@ var SQLiteEventStore = class {
|
|
|
4873
5104
|
rewrittenSelectionRate: 0,
|
|
4874
5105
|
rawSelectionRate: 0,
|
|
4875
5106
|
avgSelectedCountForRewrittenQueries: 0,
|
|
4876
|
-
avgSelectedCountForRawQueries: 0
|
|
5107
|
+
avgSelectedCountForRawQueries: 0,
|
|
5108
|
+
strategyBreakdown: []
|
|
4877
5109
|
};
|
|
4878
5110
|
}
|
|
4879
5111
|
throw err;
|
|
@@ -4990,38 +5222,19 @@ var SQLiteEventStore = class {
|
|
|
4990
5222
|
if (events.length === 0) return 0;
|
|
4991
5223
|
const eventIds = events.map((e) => e.id);
|
|
4992
5224
|
const placeholders = eventIds.map(() => "?").join(",");
|
|
4993
|
-
const
|
|
4994
|
-
|
|
4995
|
-
|
|
5225
|
+
const relatedTables = ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox", "event_citations"].filter((table) => this.hasTable(table) && this.hasTableColumn(table, "event_id"));
|
|
5226
|
+
const runDelete = this.db.transaction(() => {
|
|
5227
|
+
for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
|
|
4996
5228
|
sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
4997
|
-
ftsTriggersDropped.push(triggerName);
|
|
4998
|
-
} catch {
|
|
4999
5229
|
}
|
|
5000
|
-
|
|
5001
|
-
for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
|
|
5002
|
-
try {
|
|
5230
|
+
for (const table of relatedTables) {
|
|
5003
5231
|
sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders})`, eventIds);
|
|
5004
|
-
} catch {
|
|
5005
5232
|
}
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
5012
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
5013
|
-
END`);
|
|
5014
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
5015
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
5016
|
-
END`);
|
|
5017
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
5018
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
5019
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
5020
|
-
END`);
|
|
5021
|
-
} catch {
|
|
5022
|
-
}
|
|
5023
|
-
}
|
|
5024
|
-
return result.changes || 0;
|
|
5233
|
+
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
5234
|
+
this.recreateEventsFtsTable();
|
|
5235
|
+
return result.changes || 0;
|
|
5236
|
+
});
|
|
5237
|
+
return runDelete();
|
|
5025
5238
|
}
|
|
5026
5239
|
/**
|
|
5027
5240
|
* Convert database row to MemoryEvent
|
|
@@ -7594,7 +7807,7 @@ var SessionActorRepository = class {
|
|
|
7594
7807
|
};
|
|
7595
7808
|
|
|
7596
7809
|
// src/core/operations/perspective-observation-repository.ts
|
|
7597
|
-
import { createHash as
|
|
7810
|
+
import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
|
|
7598
7811
|
function projectHashToStorage4(projectHash) {
|
|
7599
7812
|
return projectHash ?? "";
|
|
7600
7813
|
}
|
|
@@ -7631,7 +7844,7 @@ function sanitizeStoredRecord(value) {
|
|
|
7631
7844
|
return sanitizeGovernanceAuditValue(value);
|
|
7632
7845
|
}
|
|
7633
7846
|
function stableHash(value) {
|
|
7634
|
-
return
|
|
7847
|
+
return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
7635
7848
|
}
|
|
7636
7849
|
function evidenceHash(sourceEventIds, sourceObservationIds) {
|
|
7637
7850
|
return stableHash({
|
|
@@ -8746,6 +8959,30 @@ var MemoryQueryService = class {
|
|
|
8746
8959
|
score: 1 - (r.rank - minRank) / rankRange
|
|
8747
8960
|
})).filter((r) => !options?.minScore || r.score >= options.minScore);
|
|
8748
8961
|
}
|
|
8962
|
+
async getEvent(id) {
|
|
8963
|
+
await this.initialize();
|
|
8964
|
+
return this.queryStore.getEvent(id);
|
|
8965
|
+
}
|
|
8966
|
+
async getEventByCitationId(citationId) {
|
|
8967
|
+
await this.initialize();
|
|
8968
|
+
return this.getMaintenanceStore("getEventByCitationId").getEventByCitationId(citationId);
|
|
8969
|
+
}
|
|
8970
|
+
async getEventsAfter(sinceIso) {
|
|
8971
|
+
await this.initialize();
|
|
8972
|
+
return this.getMaintenanceStore("getEventsAfter").getEventsAfter(sinceIso);
|
|
8973
|
+
}
|
|
8974
|
+
async getEventTypeCounts() {
|
|
8975
|
+
await this.initialize();
|
|
8976
|
+
return this.getMaintenanceStore("getEventTypeCounts").getEventTypeCounts();
|
|
8977
|
+
}
|
|
8978
|
+
async getDistinctSessionCount() {
|
|
8979
|
+
await this.initialize();
|
|
8980
|
+
return this.getMaintenanceStore("getDistinctSessionCount").getDistinctSessionCount();
|
|
8981
|
+
}
|
|
8982
|
+
async getDailyEventCounts(sinceIso) {
|
|
8983
|
+
await this.initialize();
|
|
8984
|
+
return this.getMaintenanceStore("getDailyEventCounts").getDailyEventCounts(sinceIso);
|
|
8985
|
+
}
|
|
8749
8986
|
async getSessionHistory(sessionId) {
|
|
8750
8987
|
await this.initialize();
|
|
8751
8988
|
return this.queryStore.getSessionEvents(sessionId);
|
|
@@ -9714,11 +9951,12 @@ var Retriever = class {
|
|
|
9714
9951
|
if (!scope && projectScopeMode === "global" && facetFilters === null) return results;
|
|
9715
9952
|
const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
|
|
9716
9953
|
const filtered = [];
|
|
9954
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9717
9955
|
for (const result of results) {
|
|
9718
9956
|
if (scope?.sessionId && result.sessionId !== scope.sessionId) continue;
|
|
9719
9957
|
if (scope?.sessionIdPrefix && !result.sessionId.startsWith(scope.sessionIdPrefix)) continue;
|
|
9720
9958
|
if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result.eventType)) continue;
|
|
9721
|
-
const event =
|
|
9959
|
+
const event = eventMap.get(result.eventId);
|
|
9722
9960
|
if (!event) continue;
|
|
9723
9961
|
if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix)) continue;
|
|
9724
9962
|
if (normalizedIncludes.length > 0) {
|
|
@@ -9827,10 +10065,30 @@ var Retriever = class {
|
|
|
9827
10065
|
async retrieveRecent(limit = 100) {
|
|
9828
10066
|
return this.eventStore.getRecentEvents(limit);
|
|
9829
10067
|
}
|
|
10068
|
+
/**
|
|
10069
|
+
* Batch-load events by id into a Map in a single query, de-duplicating ids.
|
|
10070
|
+
* Replaces per-result getEvent round-trips in the scope/enrich hot paths.
|
|
10071
|
+
*/
|
|
10072
|
+
async fetchEventsByIds(ids) {
|
|
10073
|
+
const map = /* @__PURE__ */ new Map();
|
|
10074
|
+
if (ids.length === 0) return map;
|
|
10075
|
+
const unique = [...new Set(ids)];
|
|
10076
|
+
if (typeof this.eventStore.getEvents === "function") {
|
|
10077
|
+
const events = await this.eventStore.getEvents(unique);
|
|
10078
|
+
for (const event of events) map.set(event.id, event);
|
|
10079
|
+
} else {
|
|
10080
|
+
const events = await Promise.all(unique.map((id) => this.eventStore.getEvent(id)));
|
|
10081
|
+
for (const event of events) {
|
|
10082
|
+
if (event) map.set(event.id, event);
|
|
10083
|
+
}
|
|
10084
|
+
}
|
|
10085
|
+
return map;
|
|
10086
|
+
}
|
|
9830
10087
|
async enrichResults(results, options, query) {
|
|
9831
10088
|
const memories = [];
|
|
10089
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
9832
10090
|
for (const result of results) {
|
|
9833
|
-
const event =
|
|
10091
|
+
const event = eventMap.get(result.eventId);
|
|
9834
10092
|
if (!event) continue;
|
|
9835
10093
|
if (this.graduation) {
|
|
9836
10094
|
this.graduation.recordAccess(event.id, options.sessionId || "unknown", result.score);
|
|
@@ -12558,6 +12816,34 @@ var MemoryService = class {
|
|
|
12558
12816
|
async rebuildFtsIndex() {
|
|
12559
12817
|
return this.queryService.rebuildFtsIndex();
|
|
12560
12818
|
}
|
|
12819
|
+
/**
|
|
12820
|
+
* Get a single event by id (indexed lookup).
|
|
12821
|
+
*/
|
|
12822
|
+
async getEvent(id) {
|
|
12823
|
+
return this.queryService.getEvent(id);
|
|
12824
|
+
}
|
|
12825
|
+
/**
|
|
12826
|
+
* Resolve an event from its citation id via the reverse index.
|
|
12827
|
+
*/
|
|
12828
|
+
async getEventByCitationId(citationId) {
|
|
12829
|
+
return this.queryService.getEventByCitationId(citationId);
|
|
12830
|
+
}
|
|
12831
|
+
/** All events at/after an ISO timestamp (ascending, uncapped). */
|
|
12832
|
+
async getEventsAfter(sinceIso) {
|
|
12833
|
+
return this.queryService.getEventsAfter(sinceIso);
|
|
12834
|
+
}
|
|
12835
|
+
/** Aggregate event counts by type (SQL GROUP BY). */
|
|
12836
|
+
async getEventTypeCounts() {
|
|
12837
|
+
return this.queryService.getEventTypeCounts();
|
|
12838
|
+
}
|
|
12839
|
+
/** Count distinct sessions (SQL). */
|
|
12840
|
+
async getDistinctSessionCount() {
|
|
12841
|
+
return this.queryService.getDistinctSessionCount();
|
|
12842
|
+
}
|
|
12843
|
+
/** Per-day event counts with type breakdown since an ISO timestamp (SQL). */
|
|
12844
|
+
async getDailyEventCounts(sinceIso) {
|
|
12845
|
+
return this.queryService.getDailyEventCounts(sinceIso);
|
|
12846
|
+
}
|
|
12561
12847
|
/**
|
|
12562
12848
|
* Get session history
|
|
12563
12849
|
*/
|
|
@@ -12927,6 +13213,75 @@ function clearTurnState(sessionId) {
|
|
|
12927
13213
|
}
|
|
12928
13214
|
}
|
|
12929
13215
|
|
|
13216
|
+
// src/adapters/claude/hooks/hook-runtime.ts
|
|
13217
|
+
var DEFAULT_STDIN_TIMEOUT_MS = 1e4;
|
|
13218
|
+
var DEFAULT_OVERALL_TIMEOUT_MS = 3e4;
|
|
13219
|
+
function readNumberEnv(name, fallback, options = {}) {
|
|
13220
|
+
const raw = process.env[name];
|
|
13221
|
+
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
13222
|
+
const parsed = options.integer ? Number.parseInt(raw, 10) : Number.parseFloat(raw);
|
|
13223
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
13224
|
+
let value = parsed;
|
|
13225
|
+
if (options.min !== void 0) value = Math.max(options.min, value);
|
|
13226
|
+
if (options.max !== void 0) value = Math.min(options.max, value);
|
|
13227
|
+
return value;
|
|
13228
|
+
}
|
|
13229
|
+
function readPositiveIntEnv(name, fallback) {
|
|
13230
|
+
return readNumberEnv(name, fallback, { integer: true, min: 1 });
|
|
13231
|
+
}
|
|
13232
|
+
function readStdin(options = {}) {
|
|
13233
|
+
const timeoutMs = options.timeoutMs ?? readPositiveIntEnv("CLAUDE_MEMORY_HOOK_STDIN_TIMEOUT_MS", DEFAULT_STDIN_TIMEOUT_MS);
|
|
13234
|
+
return new Promise((resolve2) => {
|
|
13235
|
+
const stdin = process.stdin;
|
|
13236
|
+
let data = "";
|
|
13237
|
+
let settled = false;
|
|
13238
|
+
const onData = (chunk) => {
|
|
13239
|
+
data += chunk;
|
|
13240
|
+
};
|
|
13241
|
+
const finish = () => {
|
|
13242
|
+
if (settled) return;
|
|
13243
|
+
settled = true;
|
|
13244
|
+
clearTimeout(timer);
|
|
13245
|
+
stdin.off("data", onData);
|
|
13246
|
+
stdin.off("end", finish);
|
|
13247
|
+
stdin.off("error", finish);
|
|
13248
|
+
resolve2(data);
|
|
13249
|
+
};
|
|
13250
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
13251
|
+
timer.unref?.();
|
|
13252
|
+
stdin.setEncoding("utf8");
|
|
13253
|
+
stdin.on("data", onData);
|
|
13254
|
+
stdin.on("end", finish);
|
|
13255
|
+
stdin.on("error", finish);
|
|
13256
|
+
});
|
|
13257
|
+
}
|
|
13258
|
+
function emitHookOutput(json) {
|
|
13259
|
+
process.stdout.write(json.endsWith("\n") ? json : `${json}
|
|
13260
|
+
`);
|
|
13261
|
+
}
|
|
13262
|
+
async function runHook(options, run) {
|
|
13263
|
+
const debug = Boolean(process.env.CLAUDE_MEMORY_DEBUG);
|
|
13264
|
+
const timeoutMs = options.timeoutMs ?? readPositiveIntEnv("CLAUDE_MEMORY_HOOK_TIMEOUT_MS", DEFAULT_OVERALL_TIMEOUT_MS);
|
|
13265
|
+
let emitted = false;
|
|
13266
|
+
const emit = (json) => {
|
|
13267
|
+
if (emitted) return;
|
|
13268
|
+
emitted = true;
|
|
13269
|
+
emitHookOutput(json);
|
|
13270
|
+
};
|
|
13271
|
+
const watchdog = setTimeout(() => {
|
|
13272
|
+
if (debug) console.error(`[${options.name}] hook timed out after ${timeoutMs}ms; forcing exit`);
|
|
13273
|
+
emit(options.fallbackOutput);
|
|
13274
|
+
process.exit(0);
|
|
13275
|
+
}, timeoutMs);
|
|
13276
|
+
watchdog.unref?.();
|
|
13277
|
+
try {
|
|
13278
|
+
emit(await run());
|
|
13279
|
+
} catch (error) {
|
|
13280
|
+
if (debug) console.error(`[${options.name}] hook error:`, error);
|
|
13281
|
+
emit(options.fallbackOutput);
|
|
13282
|
+
}
|
|
13283
|
+
}
|
|
13284
|
+
|
|
12930
13285
|
// src/adapters/claude/hooks/post-tool-use.ts
|
|
12931
13286
|
var DEFAULT_CONFIG6 = {
|
|
12932
13287
|
enabled: true,
|
|
@@ -12946,7 +13301,7 @@ var DEFAULT_CONFIG6 = {
|
|
|
12946
13301
|
"Skill",
|
|
12947
13302
|
"EnterPlanMode"
|
|
12948
13303
|
],
|
|
12949
|
-
minOutputLength:
|
|
13304
|
+
minOutputLength: readNumberEnv("CLAUDE_MEMORY_TOOL_MIN_OUTPUT_LEN", 100, { integer: true, min: 0 }),
|
|
12950
13305
|
maxOutputLength: 1e4,
|
|
12951
13306
|
maxOutputLines: 100,
|
|
12952
13307
|
storeOnlyOnSuccess: false
|
|
@@ -13021,8 +13376,12 @@ function isToolSuccess(response) {
|
|
|
13021
13376
|
return true;
|
|
13022
13377
|
}
|
|
13023
13378
|
async function main() {
|
|
13024
|
-
|
|
13025
|
-
|
|
13379
|
+
let input;
|
|
13380
|
+
try {
|
|
13381
|
+
input = JSON.parse(await readStdin());
|
|
13382
|
+
} catch {
|
|
13383
|
+
return JSON.stringify({});
|
|
13384
|
+
}
|
|
13026
13385
|
const config = { ...DEFAULT_CONFIG6 };
|
|
13027
13386
|
const privacyConfig = DEFAULT_PRIVACY_CONFIG;
|
|
13028
13387
|
const envBlocklist = process.env.CLAUDE_MEMORY_TOOL_BLOCKLIST;
|
|
@@ -13030,18 +13389,15 @@ async function main() {
|
|
|
13030
13389
|
config.excludedTools = envBlocklist.split(",").map((s) => s.trim()).filter(Boolean);
|
|
13031
13390
|
}
|
|
13032
13391
|
if (!config.enabled) {
|
|
13033
|
-
|
|
13034
|
-
return;
|
|
13392
|
+
return JSON.stringify({});
|
|
13035
13393
|
}
|
|
13036
13394
|
if (config.excludedTools?.includes(input.tool_name)) {
|
|
13037
|
-
|
|
13038
|
-
return;
|
|
13395
|
+
return JSON.stringify({});
|
|
13039
13396
|
}
|
|
13040
13397
|
const toolOutput = extractToolOutput(input.tool_response);
|
|
13041
13398
|
const success = isToolSuccess(input.tool_response);
|
|
13042
13399
|
if (!success && config.storeOnlyOnSuccess) {
|
|
13043
|
-
|
|
13044
|
-
return;
|
|
13400
|
+
return JSON.stringify({});
|
|
13045
13401
|
}
|
|
13046
13402
|
if (!hasSignificantOutput(
|
|
13047
13403
|
input.tool_name,
|
|
@@ -13049,8 +13405,7 @@ async function main() {
|
|
|
13049
13405
|
input.tool_response,
|
|
13050
13406
|
config.minOutputLength ?? 100
|
|
13051
13407
|
)) {
|
|
13052
|
-
|
|
13053
|
-
return;
|
|
13408
|
+
return JSON.stringify({});
|
|
13054
13409
|
}
|
|
13055
13410
|
try {
|
|
13056
13411
|
const memoryService = getLightweightMemoryService(input.session_id);
|
|
@@ -13082,27 +13437,15 @@ async function main() {
|
|
|
13082
13437
|
}
|
|
13083
13438
|
};
|
|
13084
13439
|
await memoryService.storeToolObservation(input.session_id, payload);
|
|
13085
|
-
|
|
13440
|
+
return JSON.stringify({});
|
|
13086
13441
|
} catch (error) {
|
|
13087
13442
|
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
13088
13443
|
console.error("PostToolUse hook error:", error);
|
|
13089
13444
|
}
|
|
13090
|
-
|
|
13445
|
+
return JSON.stringify({});
|
|
13091
13446
|
}
|
|
13092
13447
|
}
|
|
13093
|
-
function readStdin() {
|
|
13094
|
-
return new Promise((resolve2) => {
|
|
13095
|
-
let data = "";
|
|
13096
|
-
process.stdin.setEncoding("utf8");
|
|
13097
|
-
process.stdin.on("data", (chunk) => {
|
|
13098
|
-
data += chunk;
|
|
13099
|
-
});
|
|
13100
|
-
process.stdin.on("end", () => {
|
|
13101
|
-
resolve2(data);
|
|
13102
|
-
});
|
|
13103
|
-
});
|
|
13104
|
-
}
|
|
13105
13448
|
|
|
13106
13449
|
// src/hooks/post-tool-use.ts
|
|
13107
|
-
main
|
|
13450
|
+
void runHook({ name: "post-tool-use", fallbackOutput: "{}" }, main);
|
|
13108
13451
|
//# sourceMappingURL=post-tool-use.js.map
|