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/cli/index.js
CHANGED
|
@@ -9,8 +9,8 @@ const __dirname = dirname(__filename);
|
|
|
9
9
|
// src/apps/cli/index.ts
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import { exec } from "child_process";
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
12
|
+
import * as fs20 from "fs";
|
|
13
|
+
import * as path26 from "path";
|
|
14
14
|
import * as os13 from "os";
|
|
15
15
|
|
|
16
16
|
// src/services/memory-service.ts
|
|
@@ -165,7 +165,13 @@ function toDate(value) {
|
|
|
165
165
|
return new Date(String(value));
|
|
166
166
|
}
|
|
167
167
|
function createDatabase(dbPath, options) {
|
|
168
|
-
|
|
168
|
+
const db = new BetterSqlite3(dbPath, { readonly: options?.readOnly });
|
|
169
|
+
db.pragma("busy_timeout = 5000");
|
|
170
|
+
if (!options?.readOnly) {
|
|
171
|
+
db.pragma("journal_mode = WAL");
|
|
172
|
+
db.pragma("synchronous = NORMAL");
|
|
173
|
+
}
|
|
174
|
+
return db;
|
|
169
175
|
}
|
|
170
176
|
function dbRun(db, sql, params = []) {
|
|
171
177
|
db.prepare(sql).run(...params);
|
|
@@ -398,19 +404,29 @@ var ConsolidatedStore = class {
|
|
|
398
404
|
*/
|
|
399
405
|
async create(input) {
|
|
400
406
|
const memoryId = randomUUID2();
|
|
401
|
-
|
|
402
|
-
|
|
407
|
+
const sourceEvents = Array.isArray(input.sourceEvents) ? input.sourceEvents : [];
|
|
408
|
+
const insertMemory = this.db.prepare(
|
|
403
409
|
`INSERT INTO consolidated_memories
|
|
404
410
|
(memory_id, summary, topics, source_events, confidence, created_at)
|
|
405
|
-
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
406
|
-
|
|
411
|
+
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`
|
|
412
|
+
);
|
|
413
|
+
const insertEvent = this.db.prepare(
|
|
414
|
+
`INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id) VALUES (?, ?)`
|
|
415
|
+
);
|
|
416
|
+
this.db.transaction(() => {
|
|
417
|
+
insertMemory.run(
|
|
407
418
|
memoryId,
|
|
408
419
|
input.summary,
|
|
409
420
|
JSON.stringify(input.topics),
|
|
410
|
-
JSON.stringify(
|
|
421
|
+
JSON.stringify(sourceEvents),
|
|
411
422
|
input.confidence
|
|
412
|
-
|
|
413
|
-
|
|
423
|
+
);
|
|
424
|
+
for (const eventId of sourceEvents) {
|
|
425
|
+
if (typeof eventId === "string" && eventId.length > 0) {
|
|
426
|
+
insertEvent.run(memoryId, eventId);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
})();
|
|
414
430
|
return memoryId;
|
|
415
431
|
}
|
|
416
432
|
/**
|
|
@@ -516,11 +532,12 @@ var ConsolidatedStore = class {
|
|
|
516
532
|
* Delete a consolidated memory
|
|
517
533
|
*/
|
|
518
534
|
async delete(memoryId) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
535
|
+
const deleteEvents = this.db.prepare(`DELETE FROM consolidated_memory_events WHERE memory_id = ?`);
|
|
536
|
+
const deleteMemory = this.db.prepare(`DELETE FROM consolidated_memories WHERE memory_id = ?`);
|
|
537
|
+
this.db.transaction(() => {
|
|
538
|
+
deleteEvents.run(memoryId);
|
|
539
|
+
deleteMemory.run(memoryId);
|
|
540
|
+
})();
|
|
524
541
|
}
|
|
525
542
|
/**
|
|
526
543
|
* Create a long-term rule promoted from stable summaries
|
|
@@ -633,16 +650,16 @@ var ConsolidatedStore = class {
|
|
|
633
650
|
* Check if source events are already consolidated
|
|
634
651
|
*/
|
|
635
652
|
async isAlreadyConsolidated(eventIds) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
return
|
|
653
|
+
if (eventIds.length === 0) return false;
|
|
654
|
+
const placeholders2 = eventIds.map(() => "?").join(",");
|
|
655
|
+
const rows = await dbAll(
|
|
656
|
+
this.db,
|
|
657
|
+
`SELECT memory_id FROM consolidated_memory_events
|
|
658
|
+
WHERE event_id IN (${placeholders2})
|
|
659
|
+
LIMIT 1`,
|
|
660
|
+
eventIds
|
|
661
|
+
);
|
|
662
|
+
return rows.length > 0;
|
|
646
663
|
}
|
|
647
664
|
/**
|
|
648
665
|
* Get the last consolidation time
|
|
@@ -2178,6 +2195,23 @@ function makeDedupeKey(content, sessionId) {
|
|
|
2178
2195
|
return `${sessionId}:${contentHash}`;
|
|
2179
2196
|
}
|
|
2180
2197
|
|
|
2198
|
+
// src/core/citation-generator.ts
|
|
2199
|
+
import { createHash as createHash2 } from "crypto";
|
|
2200
|
+
var ID_LENGTH = 6;
|
|
2201
|
+
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
2202
|
+
function generateCitationId(eventId) {
|
|
2203
|
+
const hash = createHash2("sha256").update(eventId).digest();
|
|
2204
|
+
let id = "";
|
|
2205
|
+
for (let i = 0; i < ID_LENGTH; i++) {
|
|
2206
|
+
id += CHARSET[hash[i] % CHARSET.length];
|
|
2207
|
+
}
|
|
2208
|
+
return id;
|
|
2209
|
+
}
|
|
2210
|
+
function parseCitationId(formatted) {
|
|
2211
|
+
const match = formatted.match(/\[?mem:([A-Za-z0-9]{6})\]?/);
|
|
2212
|
+
return match ? match[1] : null;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2181
2215
|
// src/core/sqlite-event-store.ts
|
|
2182
2216
|
import * as nodePath2 from "path";
|
|
2183
2217
|
|
|
@@ -2211,12 +2245,12 @@ function resolveProjectStoragePath(projectOrHash) {
|
|
|
2211
2245
|
import Database from "better-sqlite3";
|
|
2212
2246
|
import * as fs4 from "fs";
|
|
2213
2247
|
import * as nodePath from "path";
|
|
2214
|
-
function createSQLiteDatabase(
|
|
2215
|
-
const dir = nodePath.dirname(
|
|
2248
|
+
function createSQLiteDatabase(path27, options) {
|
|
2249
|
+
const dir = nodePath.dirname(path27);
|
|
2216
2250
|
if (!fs4.existsSync(dir)) {
|
|
2217
2251
|
fs4.mkdirSync(dir, { recursive: true });
|
|
2218
2252
|
}
|
|
2219
|
-
const db = new Database(
|
|
2253
|
+
const db = new Database(path27, {
|
|
2220
2254
|
readonly: options?.readonly ?? false
|
|
2221
2255
|
});
|
|
2222
2256
|
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
@@ -2226,17 +2260,28 @@ function createSQLiteDatabase(path25, options) {
|
|
|
2226
2260
|
}
|
|
2227
2261
|
return db;
|
|
2228
2262
|
}
|
|
2263
|
+
var statementCache = /* @__PURE__ */ new WeakMap();
|
|
2264
|
+
function prepareCached(db, sql) {
|
|
2265
|
+
let cache = statementCache.get(db);
|
|
2266
|
+
if (!cache) {
|
|
2267
|
+
cache = /* @__PURE__ */ new Map();
|
|
2268
|
+
statementCache.set(db, cache);
|
|
2269
|
+
}
|
|
2270
|
+
let stmt = cache.get(sql);
|
|
2271
|
+
if (!stmt) {
|
|
2272
|
+
stmt = db.prepare(sql);
|
|
2273
|
+
cache.set(sql, stmt);
|
|
2274
|
+
}
|
|
2275
|
+
return stmt;
|
|
2276
|
+
}
|
|
2229
2277
|
function sqliteRun(db, sql, params = []) {
|
|
2230
|
-
|
|
2231
|
-
return stmt.run(...params);
|
|
2278
|
+
return prepareCached(db, sql).run(...params);
|
|
2232
2279
|
}
|
|
2233
2280
|
function sqliteAll(db, sql, params = []) {
|
|
2234
|
-
|
|
2235
|
-
return stmt.all(...params);
|
|
2281
|
+
return prepareCached(db, sql).all(...params);
|
|
2236
2282
|
}
|
|
2237
2283
|
function sqliteGet(db, sql, params = []) {
|
|
2238
|
-
|
|
2239
|
-
return stmt.get(...params);
|
|
2284
|
+
return prepareCached(db, sql).get(...params);
|
|
2240
2285
|
}
|
|
2241
2286
|
function sqliteExec(db, sql) {
|
|
2242
2287
|
db.exec(sql);
|
|
@@ -2658,6 +2703,7 @@ function normalizeQueryRewriteKind(value) {
|
|
|
2658
2703
|
var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
|
|
2659
2704
|
var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2660
2705
|
var DEFAULT_OUTBOX_MAX_RETRIES = 3;
|
|
2706
|
+
var SQLITE_SCHEMA_VERSION = 1;
|
|
2661
2707
|
function emptyOutboxRecoveryResult() {
|
|
2662
2708
|
return {
|
|
2663
2709
|
embedding: { recoveredProcessing: 0, retriedFailed: 0 },
|
|
@@ -2667,17 +2713,17 @@ function emptyOutboxRecoveryResult() {
|
|
|
2667
2713
|
function isRecord(value) {
|
|
2668
2714
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2669
2715
|
}
|
|
2670
|
-
function getNestedRecord(root,
|
|
2716
|
+
function getNestedRecord(root, path27) {
|
|
2671
2717
|
let cursor = root;
|
|
2672
|
-
for (const key of
|
|
2718
|
+
for (const key of path27) {
|
|
2673
2719
|
if (!isRecord(cursor)) return void 0;
|
|
2674
2720
|
cursor = cursor[key];
|
|
2675
2721
|
}
|
|
2676
2722
|
return isRecord(cursor) ? cursor : void 0;
|
|
2677
2723
|
}
|
|
2678
|
-
function getNestedString(root,
|
|
2724
|
+
function getNestedString(root, path27) {
|
|
2679
2725
|
let cursor = root;
|
|
2680
|
-
for (const key of
|
|
2726
|
+
for (const key of path27) {
|
|
2681
2727
|
if (!isRecord(cursor)) return void 0;
|
|
2682
2728
|
cursor = cursor[key];
|
|
2683
2729
|
}
|
|
@@ -3002,6 +3048,15 @@ var SQLiteEventStore = class {
|
|
|
3002
3048
|
access_count INTEGER DEFAULT 0
|
|
3003
3049
|
);
|
|
3004
3050
|
|
|
3051
|
+
-- Junction: consolidated memory -> source event id (indexed lookup that
|
|
3052
|
+
-- replaces per-event source_events LIKE scans during consolidation).
|
|
3053
|
+
CREATE TABLE IF NOT EXISTS consolidated_memory_events (
|
|
3054
|
+
memory_id TEXT NOT NULL,
|
|
3055
|
+
event_id TEXT NOT NULL,
|
|
3056
|
+
PRIMARY KEY (memory_id, event_id)
|
|
3057
|
+
);
|
|
3058
|
+
CREATE INDEX IF NOT EXISTS idx_cme_event ON consolidated_memory_events(event_id);
|
|
3059
|
+
|
|
3005
3060
|
-- Continuity Log table (tracks context transitions)
|
|
3006
3061
|
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
3007
3062
|
log_id TEXT PRIMARY KEY,
|
|
@@ -3361,11 +3416,17 @@ var SQLiteEventStore = class {
|
|
|
3361
3416
|
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
3362
3417
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
3363
3418
|
END;
|
|
3419
|
+
|
|
3420
|
+
-- Reverse index for citation ids (a non-reversible hash of event id), so
|
|
3421
|
+
-- citation lookups are O(1) instead of scanning recent events and hashing
|
|
3422
|
+
-- each one. Populated lazily/self-healing by getEventByCitationId.
|
|
3423
|
+
CREATE TABLE IF NOT EXISTS event_citations (
|
|
3424
|
+
event_id TEXT PRIMARY KEY,
|
|
3425
|
+
citation_id TEXT NOT NULL
|
|
3426
|
+
);
|
|
3427
|
+
CREATE INDEX IF NOT EXISTS idx_event_citations_citation ON event_citations(citation_id);
|
|
3364
3428
|
`);
|
|
3365
|
-
|
|
3366
|
-
sqliteExec(this.db, `ALTER TABLE memory_action_edges ADD COLUMN source TEXT NOT NULL DEFAULT 'manual';`);
|
|
3367
|
-
} catch {
|
|
3368
|
-
}
|
|
3429
|
+
this.addColumnIfMissing("memory_action_edges", "source", `TEXT NOT NULL DEFAULT 'manual'`);
|
|
3369
3430
|
try {
|
|
3370
3431
|
const edgeIndexes = sqliteAll(this.db, `PRAGMA index_list(memory_action_edges)`, []);
|
|
3371
3432
|
const hasSourceAwareUnique = edgeIndexes.some((index) => {
|
|
@@ -3402,55 +3463,17 @@ var SQLiteEventStore = class {
|
|
|
3402
3463
|
}
|
|
3403
3464
|
} catch {
|
|
3404
3465
|
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
try {
|
|
3410
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN candidate_details_json TEXT;`);
|
|
3411
|
-
} catch {
|
|
3412
|
-
}
|
|
3413
|
-
try {
|
|
3414
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN raw_query_text TEXT;`);
|
|
3415
|
-
} catch {
|
|
3416
|
-
}
|
|
3417
|
-
try {
|
|
3418
|
-
sqliteExec(this.db, `ALTER TABLE retrieval_traces ADD COLUMN query_rewrite_kind TEXT;`);
|
|
3419
|
-
} catch {
|
|
3420
|
-
}
|
|
3466
|
+
this.addColumnIfMissing("retrieval_traces", "selected_details_json", "TEXT");
|
|
3467
|
+
this.addColumnIfMissing("retrieval_traces", "candidate_details_json", "TEXT");
|
|
3468
|
+
this.addColumnIfMissing("retrieval_traces", "raw_query_text", "TEXT");
|
|
3469
|
+
this.addColumnIfMissing("retrieval_traces", "query_rewrite_kind", "TEXT");
|
|
3421
3470
|
try {
|
|
3422
3471
|
sqliteExec(this.db, `CREATE INDEX IF NOT EXISTS idx_retrieval_traces_query_rewrite_kind ON retrieval_traces(query_rewrite_kind);`);
|
|
3423
3472
|
} catch {
|
|
3424
3473
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
try {
|
|
3429
|
-
sqliteExec(this.db, `
|
|
3430
|
-
ALTER TABLE events ADD COLUMN access_count INTEGER DEFAULT 0;
|
|
3431
|
-
`);
|
|
3432
|
-
} catch (err) {
|
|
3433
|
-
console.error("Error adding access_count column:", err);
|
|
3434
|
-
}
|
|
3435
|
-
}
|
|
3436
|
-
if (!columnNames.includes("last_accessed_at")) {
|
|
3437
|
-
try {
|
|
3438
|
-
sqliteExec(this.db, `
|
|
3439
|
-
ALTER TABLE events ADD COLUMN last_accessed_at TEXT;
|
|
3440
|
-
`);
|
|
3441
|
-
} catch (err) {
|
|
3442
|
-
console.error("Error adding last_accessed_at column:", err);
|
|
3443
|
-
}
|
|
3444
|
-
}
|
|
3445
|
-
if (!columnNames.includes("turn_id")) {
|
|
3446
|
-
try {
|
|
3447
|
-
sqliteExec(this.db, `
|
|
3448
|
-
ALTER TABLE events ADD COLUMN turn_id TEXT;
|
|
3449
|
-
`);
|
|
3450
|
-
} catch (err) {
|
|
3451
|
-
console.error("Error adding turn_id column:", err);
|
|
3452
|
-
}
|
|
3453
|
-
}
|
|
3474
|
+
this.addColumnIfMissing("events", "access_count", "INTEGER DEFAULT 0");
|
|
3475
|
+
this.addColumnIfMissing("events", "last_accessed_at", "TEXT");
|
|
3476
|
+
this.addColumnIfMissing("events", "turn_id", "TEXT");
|
|
3454
3477
|
try {
|
|
3455
3478
|
sqliteExec(this.db, `
|
|
3456
3479
|
CREATE INDEX IF NOT EXISTS idx_events_access_count ON events(access_count DESC);
|
|
@@ -3467,8 +3490,18 @@ var SQLiteEventStore = class {
|
|
|
3467
3490
|
sqliteExec(this.db, `
|
|
3468
3491
|
CREATE INDEX IF NOT EXISTS idx_events_turn_id ON events(turn_id);
|
|
3469
3492
|
`);
|
|
3470
|
-
} catch
|
|
3493
|
+
} catch {
|
|
3494
|
+
}
|
|
3495
|
+
const cmeCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM consolidated_memory_events`, []);
|
|
3496
|
+
if ((cmeCount?.c ?? 0) === 0) {
|
|
3497
|
+
sqliteExec(this.db, `
|
|
3498
|
+
INSERT OR IGNORE INTO consolidated_memory_events (memory_id, event_id)
|
|
3499
|
+
SELECT cm.memory_id, je.value
|
|
3500
|
+
FROM consolidated_memories cm, json_each(cm.source_events) je
|
|
3501
|
+
WHERE json_valid(cm.source_events);
|
|
3502
|
+
`);
|
|
3471
3503
|
}
|
|
3504
|
+
sqliteExec(this.db, `PRAGMA user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
3472
3505
|
this.initialized = true;
|
|
3473
3506
|
}
|
|
3474
3507
|
/**
|
|
@@ -3600,6 +3633,67 @@ var SQLiteEventStore = class {
|
|
|
3600
3633
|
);
|
|
3601
3634
|
return rows.map(this.rowToEvent);
|
|
3602
3635
|
}
|
|
3636
|
+
/**
|
|
3637
|
+
* Aggregate event counts by type via SQL GROUP BY, instead of loading rows
|
|
3638
|
+
* into memory and counting in JS. Counts all events (the old recent-window
|
|
3639
|
+
* scan also under-counted stores larger than its cap).
|
|
3640
|
+
*/
|
|
3641
|
+
async getEventTypeCounts(options) {
|
|
3642
|
+
await this.initialize();
|
|
3643
|
+
const rows = sqliteAll(
|
|
3644
|
+
this.db,
|
|
3645
|
+
`SELECT event_type, COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)} GROUP BY event_type`,
|
|
3646
|
+
[]
|
|
3647
|
+
);
|
|
3648
|
+
return rows.map((row) => ({ eventType: row.event_type, count: row.count }));
|
|
3649
|
+
}
|
|
3650
|
+
/**
|
|
3651
|
+
* Fetch every event at/after an ISO timestamp (ascending), with no row cap.
|
|
3652
|
+
* For window-scoped analytics (KPI/usefulness) this fetches exactly the events
|
|
3653
|
+
* the window needs instead of an arbitrary "most recent N" slice that both
|
|
3654
|
+
* over-fetches sparse windows and truncates very active ones.
|
|
3655
|
+
*/
|
|
3656
|
+
async getEventsAfter(sinceIso, options) {
|
|
3657
|
+
await this.initialize();
|
|
3658
|
+
const rows = sqliteAll(
|
|
3659
|
+
this.db,
|
|
3660
|
+
`SELECT * FROM events WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3661
|
+
[sinceIso]
|
|
3662
|
+
);
|
|
3663
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3664
|
+
}
|
|
3665
|
+
/** Count distinct sessions via SQL instead of materializing a Set in JS. */
|
|
3666
|
+
async getDistinctSessionCount(options) {
|
|
3667
|
+
await this.initialize();
|
|
3668
|
+
const row = sqliteGet(
|
|
3669
|
+
this.db,
|
|
3670
|
+
`SELECT COUNT(DISTINCT session_id) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`,
|
|
3671
|
+
[]
|
|
3672
|
+
);
|
|
3673
|
+
return row?.count ?? 0;
|
|
3674
|
+
}
|
|
3675
|
+
/**
|
|
3676
|
+
* Per-day event counts (with type breakdown) since an ISO timestamp, computed
|
|
3677
|
+
* by SQL GROUP BY on the date prefix. Timestamps are stored as ISO-8601
|
|
3678
|
+
* strings, so substr(...,1,10) is the UTC day and `timestamp >= ?` compares
|
|
3679
|
+
* lexicographically.
|
|
3680
|
+
*/
|
|
3681
|
+
async getDailyEventCounts(sinceIso, options) {
|
|
3682
|
+
await this.initialize();
|
|
3683
|
+
return sqliteAll(
|
|
3684
|
+
this.db,
|
|
3685
|
+
`SELECT substr(timestamp, 1, 10) as day,
|
|
3686
|
+
COUNT(*) as total,
|
|
3687
|
+
SUM(CASE WHEN event_type = 'user_prompt' THEN 1 ELSE 0 END) as prompts,
|
|
3688
|
+
SUM(CASE WHEN event_type = 'agent_response' THEN 1 ELSE 0 END) as responses,
|
|
3689
|
+
SUM(CASE WHEN event_type = 'tool_observation' THEN 1 ELSE 0 END) as tools
|
|
3690
|
+
FROM events
|
|
3691
|
+
WHERE timestamp >= ? AND ${maybeQuarantinePredicate(options)}
|
|
3692
|
+
GROUP BY day
|
|
3693
|
+
ORDER BY day ASC`,
|
|
3694
|
+
[sinceIso]
|
|
3695
|
+
);
|
|
3696
|
+
}
|
|
3603
3697
|
/**
|
|
3604
3698
|
* Get event by ID
|
|
3605
3699
|
*/
|
|
@@ -3613,6 +3707,71 @@ var SQLiteEventStore = class {
|
|
|
3613
3707
|
if (!row) return null;
|
|
3614
3708
|
return this.rowToEvent(row);
|
|
3615
3709
|
}
|
|
3710
|
+
/**
|
|
3711
|
+
* Batch-fetch events by id in a single query, applying the same quarantine
|
|
3712
|
+
* predicate as getEvent. Lets hot paths replace N sequential round-trips with
|
|
3713
|
+
* one. Missing ids are simply absent from the result (order not guaranteed).
|
|
3714
|
+
*/
|
|
3715
|
+
async getEvents(ids, options) {
|
|
3716
|
+
await this.initialize();
|
|
3717
|
+
if (ids.length === 0) return [];
|
|
3718
|
+
const placeholders2 = ids.map(() => "?").join(",");
|
|
3719
|
+
const rows = sqliteAll(
|
|
3720
|
+
this.db,
|
|
3721
|
+
`SELECT * FROM events WHERE id IN (${placeholders2}) AND ${maybeQuarantinePredicate(options)}`,
|
|
3722
|
+
ids
|
|
3723
|
+
);
|
|
3724
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
3725
|
+
}
|
|
3726
|
+
/**
|
|
3727
|
+
* Resolve an event from its citation id via the reverse index, instead of
|
|
3728
|
+
* scanning recent events and hashing each one. The index is self-healing: a
|
|
3729
|
+
* miss triggers a one-time backfill of any events not yet indexed (covering
|
|
3730
|
+
* freshly appended events) before retrying.
|
|
3731
|
+
*/
|
|
3732
|
+
async getEventByCitationId(citationId, options) {
|
|
3733
|
+
await this.initialize();
|
|
3734
|
+
if (!citationId) return null;
|
|
3735
|
+
let row = sqliteGet(
|
|
3736
|
+
this.db,
|
|
3737
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3738
|
+
[citationId]
|
|
3739
|
+
);
|
|
3740
|
+
if (!row) {
|
|
3741
|
+
this.indexMissingCitations();
|
|
3742
|
+
row = sqliteGet(
|
|
3743
|
+
this.db,
|
|
3744
|
+
`SELECT event_id FROM event_citations WHERE citation_id = ? LIMIT 1`,
|
|
3745
|
+
[citationId]
|
|
3746
|
+
);
|
|
3747
|
+
}
|
|
3748
|
+
if (!row) return null;
|
|
3749
|
+
return this.getEvent(row.event_id, options);
|
|
3750
|
+
}
|
|
3751
|
+
/**
|
|
3752
|
+
* Backfill citation ids for any events not yet present in event_citations.
|
|
3753
|
+
* Gated on a cheap count comparison so the common (fully-indexed) path does no
|
|
3754
|
+
* work, and the scan only runs when there are genuinely new events to index.
|
|
3755
|
+
*/
|
|
3756
|
+
indexMissingCitations() {
|
|
3757
|
+
const eventCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM events`, [])?.c ?? 0;
|
|
3758
|
+
const indexedCount = sqliteGet(this.db, `SELECT COUNT(*) as c FROM event_citations`, [])?.c ?? 0;
|
|
3759
|
+
if (indexedCount >= eventCount) return;
|
|
3760
|
+
const unindexed = sqliteAll(
|
|
3761
|
+
this.db,
|
|
3762
|
+
`SELECT e.id FROM events e
|
|
3763
|
+
LEFT JOIN event_citations ec ON ec.event_id = e.id
|
|
3764
|
+
WHERE ec.event_id IS NULL`,
|
|
3765
|
+
[]
|
|
3766
|
+
);
|
|
3767
|
+
if (unindexed.length === 0) return;
|
|
3768
|
+
const insert = this.db.prepare(`INSERT OR IGNORE INTO event_citations (event_id, citation_id) VALUES (?, ?)`);
|
|
3769
|
+
this.db.transaction(() => {
|
|
3770
|
+
for (const { id } of unindexed) {
|
|
3771
|
+
insert.run(id, generateCitationId(id));
|
|
3772
|
+
}
|
|
3773
|
+
})();
|
|
3774
|
+
}
|
|
3616
3775
|
/**
|
|
3617
3776
|
* Get events since a timestamp (for sync)
|
|
3618
3777
|
*/
|
|
@@ -3814,25 +3973,22 @@ var SQLiteEventStore = class {
|
|
|
3814
3973
|
*/
|
|
3815
3974
|
async getPendingOutboxItems(limit = 32) {
|
|
3816
3975
|
await this.initialize();
|
|
3817
|
-
const
|
|
3818
|
-
this.db,
|
|
3819
|
-
`SELECT * FROM embedding_outbox
|
|
3820
|
-
WHERE status = 'pending'
|
|
3821
|
-
ORDER BY created_at
|
|
3822
|
-
LIMIT ?`,
|
|
3823
|
-
[limit]
|
|
3824
|
-
);
|
|
3825
|
-
if (pending.length === 0) return [];
|
|
3826
|
-
const ids = pending.map((r) => r.id);
|
|
3827
|
-
const placeholders2 = ids.map(() => "?").join(",");
|
|
3828
|
-
sqliteRun(
|
|
3976
|
+
const claimed = sqliteAll(
|
|
3829
3977
|
this.db,
|
|
3830
3978
|
`UPDATE embedding_outbox
|
|
3831
3979
|
SET status = 'processing', processed_at = datetime('now'), error_message = NULL
|
|
3832
|
-
WHERE id IN (
|
|
3833
|
-
|
|
3980
|
+
WHERE id IN (
|
|
3981
|
+
SELECT id FROM embedding_outbox
|
|
3982
|
+
WHERE status = 'pending'
|
|
3983
|
+
ORDER BY created_at
|
|
3984
|
+
LIMIT ?
|
|
3985
|
+
)
|
|
3986
|
+
RETURNING *`,
|
|
3987
|
+
[limit]
|
|
3834
3988
|
);
|
|
3835
|
-
|
|
3989
|
+
if (claimed.length === 0) return [];
|
|
3990
|
+
claimed.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
|
|
3991
|
+
return claimed.map((row) => ({
|
|
3836
3992
|
id: row.id,
|
|
3837
3993
|
eventId: row.event_id,
|
|
3838
3994
|
content: row.content,
|
|
@@ -4436,7 +4592,7 @@ var SQLiteEventStore = class {
|
|
|
4436
4592
|
const promptEvents = sessionEvents.filter((e) => e.event_type === "user_prompt");
|
|
4437
4593
|
const toolEvents = sessionEvents.filter((e) => e.event_type === "tool_observation");
|
|
4438
4594
|
let toolSuccessCount = 0;
|
|
4439
|
-
|
|
4595
|
+
const toolTotalCount = toolEvents.length;
|
|
4440
4596
|
for (const t of toolEvents) {
|
|
4441
4597
|
try {
|
|
4442
4598
|
const content = JSON.parse(t.content);
|
|
@@ -4599,10 +4755,14 @@ var SQLiteEventStore = class {
|
|
|
4599
4755
|
* Rebuild FTS index from existing events
|
|
4600
4756
|
* Call this once after upgrading to FTS5
|
|
4601
4757
|
*/
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4758
|
+
/**
|
|
4759
|
+
* Drop and recreate the events_fts virtual table + sync triggers, repopulating
|
|
4760
|
+
* from the current events table. Recreating the table (instead of issuing
|
|
4761
|
+
* DELETEs against it) sidesteps a quirk where older migrated FTS5 tables fail
|
|
4762
|
+
* with `no such column: T.event_id` on synthetic deletes. Safe to call inside
|
|
4763
|
+
* a transaction (pure DDL + INSERT...SELECT, no transaction control).
|
|
4764
|
+
*/
|
|
4765
|
+
recreateEventsFtsTable() {
|
|
4606
4766
|
sqliteExec(this.db, `
|
|
4607
4767
|
DROP TRIGGER IF EXISTS events_fts_insert;
|
|
4608
4768
|
DROP TRIGGER IF EXISTS events_fts_delete;
|
|
@@ -4631,6 +4791,12 @@ var SQLiteEventStore = class {
|
|
|
4631
4791
|
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4632
4792
|
END;
|
|
4633
4793
|
`);
|
|
4794
|
+
}
|
|
4795
|
+
async rebuildFtsIndex() {
|
|
4796
|
+
await this.initialize();
|
|
4797
|
+
const countRow = sqliteGet(this.db, "SELECT COUNT(*) as count FROM events", []);
|
|
4798
|
+
const totalEvents = countRow?.count ?? 0;
|
|
4799
|
+
this.recreateEventsFtsTable();
|
|
4634
4800
|
return totalEvents;
|
|
4635
4801
|
}
|
|
4636
4802
|
/**
|
|
@@ -4648,6 +4814,35 @@ var SQLiteEventStore = class {
|
|
|
4648
4814
|
return false;
|
|
4649
4815
|
}
|
|
4650
4816
|
}
|
|
4817
|
+
hasTable(tableName) {
|
|
4818
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) return false;
|
|
4819
|
+
try {
|
|
4820
|
+
const rows = sqliteAll(
|
|
4821
|
+
this.db,
|
|
4822
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`,
|
|
4823
|
+
[tableName]
|
|
4824
|
+
);
|
|
4825
|
+
return rows.length > 0;
|
|
4826
|
+
} catch {
|
|
4827
|
+
return false;
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
/**
|
|
4831
|
+
* Add a column only when its table exists and the column is missing.
|
|
4832
|
+
*
|
|
4833
|
+
* Unlike a blind `try { ALTER } catch {}`, this distinguishes "already
|
|
4834
|
+
* migrated" / "table not present yet" (both safely skipped) from a genuine
|
|
4835
|
+
* failure (disk full, corruption), which is allowed to surface instead of
|
|
4836
|
+
* being silently swallowed as if the migration had succeeded.
|
|
4837
|
+
*/
|
|
4838
|
+
addColumnIfMissing(table, column, definition) {
|
|
4839
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
|
|
4840
|
+
throw new Error(`Invalid identifier in migration: ${table}.${column}`);
|
|
4841
|
+
}
|
|
4842
|
+
if (!this.hasTable(table)) return;
|
|
4843
|
+
if (this.hasTableColumn(table, column)) return;
|
|
4844
|
+
sqliteExec(this.db, `ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`);
|
|
4845
|
+
}
|
|
4651
4846
|
async recordRetrievalTrace(input) {
|
|
4652
4847
|
await this.initialize();
|
|
4653
4848
|
const traceId = randomUUID6();
|
|
@@ -4733,6 +4928,45 @@ var SQLiteEventStore = class {
|
|
|
4733
4928
|
FROM retrieval_traces`,
|
|
4734
4929
|
[]
|
|
4735
4930
|
);
|
|
4931
|
+
const strategyColumnSql = this.hasTableColumn("retrieval_traces", "strategy") ? "COALESCE(NULLIF(TRIM(strategy), ''), 'unknown')" : "'unknown'";
|
|
4932
|
+
const strategyRows = sqliteAll(
|
|
4933
|
+
this.db,
|
|
4934
|
+
`SELECT
|
|
4935
|
+
${strategyColumnSql} as strategy,
|
|
4936
|
+
COUNT(*) as total_queries,
|
|
4937
|
+
SUM(CASE WHEN selected_count > 0 THEN 1 ELSE 0 END) as queries_with_selection,
|
|
4938
|
+
SUM(CASE WHEN ${rewrittenQueryRewriteKindSql} THEN 1 ELSE 0 END) as rewritten_queries,
|
|
4939
|
+
SUM(candidate_count) as total_candidate_count,
|
|
4940
|
+
SUM(selected_count) as total_selected_count,
|
|
4941
|
+
AVG(candidate_count) as avg_candidate_count,
|
|
4942
|
+
AVG(selected_count) as avg_selected_count,
|
|
4943
|
+
CASE
|
|
4944
|
+
WHEN SUM(candidate_count) > 0 THEN (SUM(selected_count) * 1.0 / SUM(candidate_count))
|
|
4945
|
+
ELSE 0
|
|
4946
|
+
END as selection_rate
|
|
4947
|
+
FROM retrieval_traces
|
|
4948
|
+
GROUP BY ${strategyColumnSql}
|
|
4949
|
+
ORDER BY total_queries DESC, strategy ASC`,
|
|
4950
|
+
[]
|
|
4951
|
+
);
|
|
4952
|
+
const strategyBreakdown = strategyRows.map((strategyRow) => {
|
|
4953
|
+
const strategyTotalQueries = Number(strategyRow.total_queries || 0);
|
|
4954
|
+
const strategyQueriesWithSelection = Number(strategyRow.queries_with_selection || 0);
|
|
4955
|
+
const strategyRewrittenQueries = Number(strategyRow.rewritten_queries || 0);
|
|
4956
|
+
return {
|
|
4957
|
+
strategy: String(strategyRow.strategy || "unknown"),
|
|
4958
|
+
totalQueries: strategyTotalQueries,
|
|
4959
|
+
queriesWithSelection: strategyQueriesWithSelection,
|
|
4960
|
+
rewrittenQueries: strategyRewrittenQueries,
|
|
4961
|
+
rewriteRate: strategyTotalQueries > 0 ? strategyRewrittenQueries / strategyTotalQueries : 0,
|
|
4962
|
+
totalCandidateCount: Number(strategyRow.total_candidate_count || 0),
|
|
4963
|
+
totalSelectedCount: Number(strategyRow.total_selected_count || 0),
|
|
4964
|
+
avgCandidateCount: Number(strategyRow.avg_candidate_count || 0),
|
|
4965
|
+
avgSelectedCount: Number(strategyRow.avg_selected_count || 0),
|
|
4966
|
+
selectionRate: Number(strategyRow.selection_rate || 0),
|
|
4967
|
+
queryYieldRate: strategyTotalQueries > 0 ? strategyQueriesWithSelection / strategyTotalQueries : 0
|
|
4968
|
+
};
|
|
4969
|
+
});
|
|
4736
4970
|
const totalQueries = Number(row?.total_queries || 0);
|
|
4737
4971
|
const rewrittenQueries = Number(row?.rewritten_queries || 0);
|
|
4738
4972
|
const rawQueries = Math.max(0, totalQueries - rewrittenQueries);
|
|
@@ -4750,7 +4984,8 @@ var SQLiteEventStore = class {
|
|
|
4750
4984
|
rewrittenSelectionRate: rewrittenQueries > 0 ? rewrittenQueriesWithSelection / rewrittenQueries : 0,
|
|
4751
4985
|
rawSelectionRate: rawQueries > 0 ? rawQueriesWithSelection / rawQueries : 0,
|
|
4752
4986
|
avgSelectedCountForRewrittenQueries: Number(row?.avg_selected_count_for_rewritten_queries || 0),
|
|
4753
|
-
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0)
|
|
4987
|
+
avgSelectedCountForRawQueries: Number(row?.avg_selected_count_for_raw_queries || 0),
|
|
4988
|
+
strategyBreakdown
|
|
4754
4989
|
};
|
|
4755
4990
|
} catch (err) {
|
|
4756
4991
|
if (err?.message?.includes("no such table")) {
|
|
@@ -4766,7 +5001,8 @@ var SQLiteEventStore = class {
|
|
|
4766
5001
|
rewrittenSelectionRate: 0,
|
|
4767
5002
|
rawSelectionRate: 0,
|
|
4768
5003
|
avgSelectedCountForRewrittenQueries: 0,
|
|
4769
|
-
avgSelectedCountForRawQueries: 0
|
|
5004
|
+
avgSelectedCountForRawQueries: 0,
|
|
5005
|
+
strategyBreakdown: []
|
|
4770
5006
|
};
|
|
4771
5007
|
}
|
|
4772
5008
|
throw err;
|
|
@@ -4883,38 +5119,19 @@ var SQLiteEventStore = class {
|
|
|
4883
5119
|
if (events.length === 0) return 0;
|
|
4884
5120
|
const eventIds = events.map((e) => e.id);
|
|
4885
5121
|
const placeholders2 = eventIds.map(() => "?").join(",");
|
|
4886
|
-
const
|
|
4887
|
-
|
|
4888
|
-
|
|
5122
|
+
const relatedTables = ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox", "event_citations"].filter((table) => this.hasTable(table) && this.hasTableColumn(table, "event_id"));
|
|
5123
|
+
const runDelete = this.db.transaction(() => {
|
|
5124
|
+
for (const triggerName of ["events_fts_delete", "events_fts_update", "events_fts_insert"]) {
|
|
4889
5125
|
sqliteRun(this.db, `DROP TRIGGER IF EXISTS ${triggerName}`);
|
|
4890
|
-
ftsTriggersDropped.push(triggerName);
|
|
4891
|
-
} catch {
|
|
4892
5126
|
}
|
|
4893
|
-
|
|
4894
|
-
for (const table of ["event_dedup", "memory_levels", "embedding_queue", "embedding_outbox", "vector_outbox"]) {
|
|
4895
|
-
try {
|
|
5127
|
+
for (const table of relatedTables) {
|
|
4896
5128
|
sqliteRun(this.db, `DELETE FROM ${table} WHERE event_id IN (${placeholders2})`, eventIds);
|
|
4897
|
-
} catch {
|
|
4898
5129
|
}
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_insert AFTER INSERT ON events BEGIN
|
|
4905
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4906
|
-
END`);
|
|
4907
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_delete AFTER DELETE ON events BEGIN
|
|
4908
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4909
|
-
END`);
|
|
4910
|
-
sqliteRun(this.db, `CREATE TRIGGER IF NOT EXISTS events_fts_update AFTER UPDATE ON events BEGIN
|
|
4911
|
-
DELETE FROM events_fts WHERE rowid = OLD.rowid;
|
|
4912
|
-
INSERT INTO events_fts(rowid, content, event_id) VALUES (NEW.rowid, NEW.content, NEW.id);
|
|
4913
|
-
END`);
|
|
4914
|
-
} catch {
|
|
4915
|
-
}
|
|
4916
|
-
}
|
|
4917
|
-
return result.changes || 0;
|
|
5130
|
+
const result = sqliteRun(this.db, `DELETE FROM events WHERE session_id = ?`, [sessionId]);
|
|
5131
|
+
this.recreateEventsFtsTable();
|
|
5132
|
+
return result.changes || 0;
|
|
5133
|
+
});
|
|
5134
|
+
return runDelete();
|
|
4918
5135
|
}
|
|
4919
5136
|
/**
|
|
4920
5137
|
* Convert database row to MemoryEvent
|
|
@@ -7678,9 +7895,9 @@ function safeParseObject(value) {
|
|
|
7678
7895
|
return void 0;
|
|
7679
7896
|
}
|
|
7680
7897
|
}
|
|
7681
|
-
function nestedString(root,
|
|
7898
|
+
function nestedString(root, path27) {
|
|
7682
7899
|
let cursor = root;
|
|
7683
|
-
for (const key of
|
|
7900
|
+
for (const key of path27) {
|
|
7684
7901
|
if (!isRecord2(cursor)) return void 0;
|
|
7685
7902
|
cursor = cursor[key];
|
|
7686
7903
|
}
|
|
@@ -7755,12 +7972,12 @@ var GraphPathService = class {
|
|
|
7755
7972
|
}
|
|
7756
7973
|
}
|
|
7757
7974
|
}
|
|
7758
|
-
const paths = Array.from(bestByTarget.entries()).map(([key,
|
|
7975
|
+
const paths = Array.from(bestByTarget.entries()).map(([key, path27]) => ({
|
|
7759
7976
|
target: graph.node(nodeFromKey(key)),
|
|
7760
|
-
hops:
|
|
7761
|
-
totalCost:
|
|
7762
|
-
scoreContribution:
|
|
7763
|
-
steps:
|
|
7977
|
+
hops: path27.hops,
|
|
7978
|
+
totalCost: path27.totalCost,
|
|
7979
|
+
scoreContribution: path27.totalCost > 0 ? 1 / path27.totalCost : 0,
|
|
7980
|
+
steps: path27.steps
|
|
7764
7981
|
})).sort((a, b) => b.scoreContribution - a.scoreContribution || a.hops - b.hops || a.target.name.localeCompare(b.target.name)).slice(0, maxResults);
|
|
7765
7982
|
return { startNodes, effectiveMaxHops, paths };
|
|
7766
7983
|
}
|
|
@@ -8537,7 +8754,7 @@ var SessionActorRepository = class {
|
|
|
8537
8754
|
};
|
|
8538
8755
|
|
|
8539
8756
|
// src/core/operations/perspective-observation-repository.ts
|
|
8540
|
-
import { createHash as
|
|
8757
|
+
import { createHash as createHash4, randomUUID as randomUUID11 } from "crypto";
|
|
8541
8758
|
function projectHashToStorage4(projectHash) {
|
|
8542
8759
|
return projectHash ?? "";
|
|
8543
8760
|
}
|
|
@@ -8574,7 +8791,7 @@ function sanitizeStoredRecord(value) {
|
|
|
8574
8791
|
return sanitizeGovernanceAuditValue(value);
|
|
8575
8792
|
}
|
|
8576
8793
|
function stableHash(value) {
|
|
8577
|
-
return
|
|
8794
|
+
return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
|
|
8578
8795
|
}
|
|
8579
8796
|
function evidenceHash(sourceEventIds, sourceObservationIds) {
|
|
8580
8797
|
return stableHash({
|
|
@@ -9082,23 +9299,6 @@ function createPerspectiveDeriver(options) {
|
|
|
9082
9299
|
return new PerspectiveDeriver(options);
|
|
9083
9300
|
}
|
|
9084
9301
|
|
|
9085
|
-
// src/core/citation-generator.ts
|
|
9086
|
-
import { createHash as createHash4 } from "crypto";
|
|
9087
|
-
var ID_LENGTH = 6;
|
|
9088
|
-
var CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
9089
|
-
function generateCitationId(eventId) {
|
|
9090
|
-
const hash = createHash4("sha256").update(eventId).digest();
|
|
9091
|
-
let id = "";
|
|
9092
|
-
for (let i = 0; i < ID_LENGTH; i++) {
|
|
9093
|
-
id += CHARSET[hash[i] % CHARSET.length];
|
|
9094
|
-
}
|
|
9095
|
-
return id;
|
|
9096
|
-
}
|
|
9097
|
-
function parseCitationId(formatted) {
|
|
9098
|
-
const match = formatted.match(/\[?mem:([A-Za-z0-9]{6})\]?/);
|
|
9099
|
-
return match ? match[1] : null;
|
|
9100
|
-
}
|
|
9101
|
-
|
|
9102
9302
|
// src/core/operations/perspective-session-actor-backfill.ts
|
|
9103
9303
|
var DEFAULT_EVENT_LIMIT = 1e4;
|
|
9104
9304
|
var DEFAULT_SAMPLE_LIMIT2 = 10;
|
|
@@ -9892,6 +10092,30 @@ var MemoryQueryService = class {
|
|
|
9892
10092
|
score: 1 - (r.rank - minRank) / rankRange
|
|
9893
10093
|
})).filter((r) => !options?.minScore || r.score >= options.minScore);
|
|
9894
10094
|
}
|
|
10095
|
+
async getEvent(id) {
|
|
10096
|
+
await this.initialize();
|
|
10097
|
+
return this.queryStore.getEvent(id);
|
|
10098
|
+
}
|
|
10099
|
+
async getEventByCitationId(citationId) {
|
|
10100
|
+
await this.initialize();
|
|
10101
|
+
return this.getMaintenanceStore("getEventByCitationId").getEventByCitationId(citationId);
|
|
10102
|
+
}
|
|
10103
|
+
async getEventsAfter(sinceIso) {
|
|
10104
|
+
await this.initialize();
|
|
10105
|
+
return this.getMaintenanceStore("getEventsAfter").getEventsAfter(sinceIso);
|
|
10106
|
+
}
|
|
10107
|
+
async getEventTypeCounts() {
|
|
10108
|
+
await this.initialize();
|
|
10109
|
+
return this.getMaintenanceStore("getEventTypeCounts").getEventTypeCounts();
|
|
10110
|
+
}
|
|
10111
|
+
async getDistinctSessionCount() {
|
|
10112
|
+
await this.initialize();
|
|
10113
|
+
return this.getMaintenanceStore("getDistinctSessionCount").getDistinctSessionCount();
|
|
10114
|
+
}
|
|
10115
|
+
async getDailyEventCounts(sinceIso) {
|
|
10116
|
+
await this.initialize();
|
|
10117
|
+
return this.getMaintenanceStore("getDailyEventCounts").getDailyEventCounts(sinceIso);
|
|
10118
|
+
}
|
|
9895
10119
|
async getSessionHistory(sessionId) {
|
|
9896
10120
|
await this.initialize();
|
|
9897
10121
|
return this.queryStore.getSessionEvents(sessionId);
|
|
@@ -10716,12 +10940,12 @@ var Retriever = class {
|
|
|
10716
10940
|
direction: "both"
|
|
10717
10941
|
});
|
|
10718
10942
|
const titleByEntityId = new Map(startNodes.map((node) => [node.entityId, node.title]));
|
|
10719
|
-
for (const
|
|
10720
|
-
if (
|
|
10721
|
-
const target = await this.eventStore.getEvent(
|
|
10943
|
+
for (const path27 of expansion.paths) {
|
|
10944
|
+
if (path27.target.type !== "event") continue;
|
|
10945
|
+
const target = await this.eventStore.getEvent(path27.target.id);
|
|
10722
10946
|
if (!target) continue;
|
|
10723
|
-
const graphPath = toRetrievalGraphPathDebug(
|
|
10724
|
-
const score = graphPathScore(
|
|
10947
|
+
const graphPath = toRetrievalGraphPathDebug(path27, titleByEntityId);
|
|
10948
|
+
const score = graphPathScore(path27, opts.hopPenalty);
|
|
10725
10949
|
const existing = byId.get(target.id);
|
|
10726
10950
|
const graphPaths = mergeGraphPaths(existing?.graphPaths ?? [], [graphPath]);
|
|
10727
10951
|
const graphLane = {
|
|
@@ -10730,7 +10954,7 @@ var Retriever = class {
|
|
|
10730
10954
|
score
|
|
10731
10955
|
};
|
|
10732
10956
|
const row = {
|
|
10733
|
-
id: existing?.id ?? `graph-path-${
|
|
10957
|
+
id: existing?.id ?? `graph-path-${path27.hops}-${target.id}`,
|
|
10734
10958
|
eventId: target.id,
|
|
10735
10959
|
content: target.content,
|
|
10736
10960
|
score: Math.max(existing?.score ?? 0, score),
|
|
@@ -10860,11 +11084,12 @@ var Retriever = class {
|
|
|
10860
11084
|
if (!scope && projectScopeMode === "global" && facetFilters === null) return results;
|
|
10861
11085
|
const normalizedIncludes = (scope?.contentIncludes || []).map((s) => s.toLowerCase());
|
|
10862
11086
|
const filtered = [];
|
|
11087
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
10863
11088
|
for (const result of results) {
|
|
10864
11089
|
if (scope?.sessionId && result.sessionId !== scope.sessionId) continue;
|
|
10865
11090
|
if (scope?.sessionIdPrefix && !result.sessionId.startsWith(scope.sessionIdPrefix)) continue;
|
|
10866
11091
|
if (scope?.eventTypes && scope.eventTypes.length > 0 && !scope.eventTypes.includes(result.eventType)) continue;
|
|
10867
|
-
const event =
|
|
11092
|
+
const event = eventMap.get(result.eventId);
|
|
10868
11093
|
if (!event) continue;
|
|
10869
11094
|
if (scope?.canonicalKeyPrefix && !event.canonicalKey.startsWith(scope.canonicalKeyPrefix)) continue;
|
|
10870
11095
|
if (normalizedIncludes.length > 0) {
|
|
@@ -10973,10 +11198,30 @@ var Retriever = class {
|
|
|
10973
11198
|
async retrieveRecent(limit = 100) {
|
|
10974
11199
|
return this.eventStore.getRecentEvents(limit);
|
|
10975
11200
|
}
|
|
11201
|
+
/**
|
|
11202
|
+
* Batch-load events by id into a Map in a single query, de-duplicating ids.
|
|
11203
|
+
* Replaces per-result getEvent round-trips in the scope/enrich hot paths.
|
|
11204
|
+
*/
|
|
11205
|
+
async fetchEventsByIds(ids) {
|
|
11206
|
+
const map = /* @__PURE__ */ new Map();
|
|
11207
|
+
if (ids.length === 0) return map;
|
|
11208
|
+
const unique2 = [...new Set(ids)];
|
|
11209
|
+
if (typeof this.eventStore.getEvents === "function") {
|
|
11210
|
+
const events = await this.eventStore.getEvents(unique2);
|
|
11211
|
+
for (const event of events) map.set(event.id, event);
|
|
11212
|
+
} else {
|
|
11213
|
+
const events = await Promise.all(unique2.map((id) => this.eventStore.getEvent(id)));
|
|
11214
|
+
for (const event of events) {
|
|
11215
|
+
if (event) map.set(event.id, event);
|
|
11216
|
+
}
|
|
11217
|
+
}
|
|
11218
|
+
return map;
|
|
11219
|
+
}
|
|
10976
11220
|
async enrichResults(results, options, query) {
|
|
10977
11221
|
const memories = [];
|
|
11222
|
+
const eventMap = await this.fetchEventsByIds(results.map((r) => r.eventId));
|
|
10978
11223
|
for (const result of results) {
|
|
10979
|
-
const event =
|
|
11224
|
+
const event = eventMap.get(result.eventId);
|
|
10980
11225
|
if (!event) continue;
|
|
10981
11226
|
if (this.graduation) {
|
|
10982
11227
|
this.graduation.recordAccess(event.id, options.sessionId || "unknown", result.score);
|
|
@@ -11049,8 +11294,8 @@ _Context:_ ${sessionContext}`;
|
|
|
11049
11294
|
}
|
|
11050
11295
|
matchesMetadataScope(metadata, expected) {
|
|
11051
11296
|
if (!metadata) return false;
|
|
11052
|
-
return Object.entries(expected).every(([
|
|
11053
|
-
const actual =
|
|
11297
|
+
return Object.entries(expected).every(([path27, value]) => {
|
|
11298
|
+
const actual = path27.split(".").reduce((acc, key) => {
|
|
11054
11299
|
if (typeof acc !== "object" || acc === null) return void 0;
|
|
11055
11300
|
return acc[key];
|
|
11056
11301
|
}, metadata);
|
|
@@ -11101,22 +11346,22 @@ function uniqueEntityStartNodes(candidates) {
|
|
|
11101
11346
|
}
|
|
11102
11347
|
return nodes;
|
|
11103
11348
|
}
|
|
11104
|
-
function toRetrievalGraphPathDebug(
|
|
11105
|
-
const firstStep =
|
|
11349
|
+
function toRetrievalGraphPathDebug(path27, titleByEntityId) {
|
|
11350
|
+
const firstStep = path27.steps[0];
|
|
11106
11351
|
const startNode = firstStep?.direction === "incoming" ? firstStep.to : firstStep?.from;
|
|
11107
11352
|
const startEntityId = startNode?.type === "entity" ? startNode.id : "";
|
|
11108
11353
|
return {
|
|
11109
11354
|
startEntityId,
|
|
11110
11355
|
startEntityTitle: titleByEntityId.get(startEntityId) ?? startNode?.name,
|
|
11111
|
-
targetId:
|
|
11112
|
-
targetType:
|
|
11113
|
-
hops:
|
|
11114
|
-
relationPath:
|
|
11356
|
+
targetId: path27.target.id,
|
|
11357
|
+
targetType: path27.target.type,
|
|
11358
|
+
hops: path27.hops,
|
|
11359
|
+
relationPath: path27.steps.map((step) => step.relationType)
|
|
11115
11360
|
};
|
|
11116
11361
|
}
|
|
11117
|
-
function graphPathScore(
|
|
11118
|
-
const base = Math.min(0.95, Math.max(0,
|
|
11119
|
-
return Math.max(0.05, base - hopPenalty * Math.max(0,
|
|
11362
|
+
function graphPathScore(path27, hopPenalty) {
|
|
11363
|
+
const base = Math.min(0.95, Math.max(0, path27.scoreContribution));
|
|
11364
|
+
return Math.max(0.05, base - hopPenalty * Math.max(0, path27.hops - 1));
|
|
11120
11365
|
}
|
|
11121
11366
|
function clampGraphHops(maxHops) {
|
|
11122
11367
|
if (!Number.isFinite(maxHops)) return 2;
|
|
@@ -11124,14 +11369,14 @@ function clampGraphHops(maxHops) {
|
|
|
11124
11369
|
}
|
|
11125
11370
|
function mergeGraphPaths(existing, incoming) {
|
|
11126
11371
|
const byKey = /* @__PURE__ */ new Map();
|
|
11127
|
-
for (const
|
|
11128
|
-
const key = [
|
|
11129
|
-
if (!byKey.has(key)) byKey.set(key,
|
|
11372
|
+
for (const path27 of [...existing, ...incoming]) {
|
|
11373
|
+
const key = [path27.startEntityId, path27.targetType, path27.targetId, path27.hops, ...path27.relationPath].join("\0");
|
|
11374
|
+
if (!byKey.has(key)) byKey.set(key, path27);
|
|
11130
11375
|
}
|
|
11131
11376
|
return [...byKey.values()].sort((a, b) => a.hops - b.hops || compareStable(graphPathSignature(a), graphPathSignature(b))).slice(0, 3);
|
|
11132
11377
|
}
|
|
11133
|
-
function graphPathSignature(
|
|
11134
|
-
return [
|
|
11378
|
+
function graphPathSignature(path27) {
|
|
11379
|
+
return [path27.startEntityId, path27.targetType, path27.targetId, path27.hops, ...path27.relationPath].join("|");
|
|
11135
11380
|
}
|
|
11136
11381
|
function compareStable(a, b) {
|
|
11137
11382
|
if (a < b) return -1;
|
|
@@ -13730,19 +13975,47 @@ var MemoryService = class {
|
|
|
13730
13975
|
return this.queryService.rebuildFtsIndex();
|
|
13731
13976
|
}
|
|
13732
13977
|
/**
|
|
13733
|
-
* Get
|
|
13978
|
+
* Get a single event by id (indexed lookup).
|
|
13734
13979
|
*/
|
|
13735
|
-
async
|
|
13736
|
-
return this.queryService.
|
|
13980
|
+
async getEvent(id) {
|
|
13981
|
+
return this.queryService.getEvent(id);
|
|
13737
13982
|
}
|
|
13738
13983
|
/**
|
|
13739
|
-
*
|
|
13984
|
+
* Resolve an event from its citation id via the reverse index.
|
|
13740
13985
|
*/
|
|
13741
|
-
async
|
|
13742
|
-
return this.queryService.
|
|
13986
|
+
async getEventByCitationId(citationId) {
|
|
13987
|
+
return this.queryService.getEventByCitationId(citationId);
|
|
13743
13988
|
}
|
|
13744
|
-
/**
|
|
13745
|
-
|
|
13989
|
+
/** All events at/after an ISO timestamp (ascending, uncapped). */
|
|
13990
|
+
async getEventsAfter(sinceIso) {
|
|
13991
|
+
return this.queryService.getEventsAfter(sinceIso);
|
|
13992
|
+
}
|
|
13993
|
+
/** Aggregate event counts by type (SQL GROUP BY). */
|
|
13994
|
+
async getEventTypeCounts() {
|
|
13995
|
+
return this.queryService.getEventTypeCounts();
|
|
13996
|
+
}
|
|
13997
|
+
/** Count distinct sessions (SQL). */
|
|
13998
|
+
async getDistinctSessionCount() {
|
|
13999
|
+
return this.queryService.getDistinctSessionCount();
|
|
14000
|
+
}
|
|
14001
|
+
/** Per-day event counts with type breakdown since an ISO timestamp (SQL). */
|
|
14002
|
+
async getDailyEventCounts(sinceIso) {
|
|
14003
|
+
return this.queryService.getDailyEventCounts(sinceIso);
|
|
14004
|
+
}
|
|
14005
|
+
/**
|
|
14006
|
+
* Get session history
|
|
14007
|
+
*/
|
|
14008
|
+
async getSessionHistory(sessionId) {
|
|
14009
|
+
return this.queryService.getSessionHistory(sessionId);
|
|
14010
|
+
}
|
|
14011
|
+
/**
|
|
14012
|
+
* Get recent events
|
|
14013
|
+
*/
|
|
14014
|
+
async getRecentEvents(limit = 100) {
|
|
14015
|
+
return this.queryService.getRecentEvents(limit);
|
|
14016
|
+
}
|
|
14017
|
+
/**
|
|
14018
|
+
* Get memory statistics
|
|
13746
14019
|
*/
|
|
13747
14020
|
async getOutboxStats(options) {
|
|
13748
14021
|
return this.queryService.getOutboxStats(options);
|
|
@@ -14058,6 +14331,20 @@ import * as path11 from "path";
|
|
|
14058
14331
|
import * as os6 from "os";
|
|
14059
14332
|
import * as readline from "readline";
|
|
14060
14333
|
import { randomUUID as randomUUID14 } from "crypto";
|
|
14334
|
+
|
|
14335
|
+
// src/services/turn-buffering.ts
|
|
14336
|
+
var MIN_SUBSTANTIVE_RESPONSE_LEN = 100;
|
|
14337
|
+
var MAX_AGENT_RESPONSE_LEN = 1e4;
|
|
14338
|
+
function mergeAgentResponseBlocks(blocks) {
|
|
14339
|
+
const substantive = blocks.filter((block) => block.length >= MIN_SUBSTANTIVE_RESPONSE_LEN);
|
|
14340
|
+
if (substantive.length > 0) return substantive.join("\n\n");
|
|
14341
|
+
return blocks.reduce((longest, block) => longest.length >= block.length ? longest : block, "");
|
|
14342
|
+
}
|
|
14343
|
+
function truncateAgentResponse(text) {
|
|
14344
|
+
return text.length > MAX_AGENT_RESPONSE_LEN ? `${text.slice(0, MAX_AGENT_RESPONSE_LEN)}...[truncated]` : text;
|
|
14345
|
+
}
|
|
14346
|
+
|
|
14347
|
+
// src/services/session-history-importer.ts
|
|
14061
14348
|
function isClaudeLocalCommandArtifact(content) {
|
|
14062
14349
|
const trimmed = content.trim();
|
|
14063
14350
|
return /^<local-command-(stdout|stderr)>/.test(trimmed) || /^<command-(name|message)>/.test(trimmed) || trimmed.includes("<command-name>") && trimmed.includes("<local-command-stdout>");
|
|
@@ -14189,6 +14476,34 @@ var SessionHistoryImporter = class {
|
|
|
14189
14476
|
/**
|
|
14190
14477
|
* Import a specific session file
|
|
14191
14478
|
*/
|
|
14479
|
+
/**
|
|
14480
|
+
* Pre-flight check used before a destructive force-reimport: returns true only
|
|
14481
|
+
* if the file can be opened and contains at least one line that parses as JSON.
|
|
14482
|
+
* Streams the file and short-circuits on the first valid record.
|
|
14483
|
+
*/
|
|
14484
|
+
async hasParseableContent(filePath) {
|
|
14485
|
+
let stream;
|
|
14486
|
+
let rl;
|
|
14487
|
+
try {
|
|
14488
|
+
stream = fs9.createReadStream(filePath);
|
|
14489
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
14490
|
+
for await (const line of rl) {
|
|
14491
|
+
const trimmed = line.trim();
|
|
14492
|
+
if (!trimmed) continue;
|
|
14493
|
+
try {
|
|
14494
|
+
JSON.parse(trimmed);
|
|
14495
|
+
return true;
|
|
14496
|
+
} catch {
|
|
14497
|
+
}
|
|
14498
|
+
}
|
|
14499
|
+
return false;
|
|
14500
|
+
} catch {
|
|
14501
|
+
return false;
|
|
14502
|
+
} finally {
|
|
14503
|
+
rl?.close();
|
|
14504
|
+
stream?.close();
|
|
14505
|
+
}
|
|
14506
|
+
}
|
|
14192
14507
|
async importSessionFile(filePath, options = {}) {
|
|
14193
14508
|
const result = {
|
|
14194
14509
|
totalSessions: 1,
|
|
@@ -14204,6 +14519,10 @@ var SessionHistoryImporter = class {
|
|
|
14204
14519
|
}
|
|
14205
14520
|
const sessionId = path11.basename(filePath, ".jsonl");
|
|
14206
14521
|
if (options.force) {
|
|
14522
|
+
if (!await this.hasParseableContent(filePath)) {
|
|
14523
|
+
result.errors.push(`Skipped force reimport (no parseable records): ${filePath}`);
|
|
14524
|
+
return result;
|
|
14525
|
+
}
|
|
14207
14526
|
const deleted = await this.memoryService.deleteSessionEvents(sessionId);
|
|
14208
14527
|
if (options.verbose && deleted > 0) {
|
|
14209
14528
|
console.log(` Deleted ${deleted} existing events for session ${sessionId}`);
|
|
@@ -14225,13 +14544,12 @@ var SessionHistoryImporter = class {
|
|
|
14225
14544
|
let lastTimestamp;
|
|
14226
14545
|
const flushTextBuffer = async () => {
|
|
14227
14546
|
if (textBuffer.length === 0 || !currentTurnId) return;
|
|
14228
|
-
const
|
|
14229
|
-
const merged = substantive.length > 0 ? substantive.join("\n\n") : textBuffer.reduce((a, b) => a.length >= b.length ? a : b, "");
|
|
14547
|
+
const merged = mergeAgentResponseBlocks(textBuffer);
|
|
14230
14548
|
if (!merged) {
|
|
14231
14549
|
textBuffer = [];
|
|
14232
14550
|
return;
|
|
14233
14551
|
}
|
|
14234
|
-
const truncated = merged
|
|
14552
|
+
const truncated = truncateAgentResponse(merged);
|
|
14235
14553
|
const appendResult = await this.memoryService.storeAgentResponse(
|
|
14236
14554
|
sessionId,
|
|
14237
14555
|
truncated,
|
|
@@ -14245,55 +14563,60 @@ var SessionHistoryImporter = class {
|
|
|
14245
14563
|
lineCount++;
|
|
14246
14564
|
textBuffer = [];
|
|
14247
14565
|
};
|
|
14248
|
-
|
|
14249
|
-
|
|
14250
|
-
|
|
14251
|
-
|
|
14252
|
-
|
|
14253
|
-
|
|
14254
|
-
|
|
14255
|
-
|
|
14256
|
-
|
|
14257
|
-
|
|
14258
|
-
|
|
14259
|
-
|
|
14260
|
-
|
|
14261
|
-
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
|
|
14265
|
-
|
|
14266
|
-
|
|
14267
|
-
|
|
14268
|
-
|
|
14269
|
-
|
|
14270
|
-
|
|
14271
|
-
|
|
14566
|
+
try {
|
|
14567
|
+
for await (const line of rl) {
|
|
14568
|
+
if (lineCount >= limit) break;
|
|
14569
|
+
try {
|
|
14570
|
+
const entry = JSON.parse(line);
|
|
14571
|
+
result.totalMessages++;
|
|
14572
|
+
const msgClass = classifyEntry(entry);
|
|
14573
|
+
if (msgClass === "user_prompt") {
|
|
14574
|
+
await flushTextBuffer();
|
|
14575
|
+
const content = this.extractContent(entry);
|
|
14576
|
+
if (!content) continue;
|
|
14577
|
+
if (!isWorthStoringPrompt(content)) {
|
|
14578
|
+
result.skippedDuplicates++;
|
|
14579
|
+
continue;
|
|
14580
|
+
}
|
|
14581
|
+
currentTurnId = randomUUID14();
|
|
14582
|
+
const appendResult = await this.memoryService.storeUserPrompt(
|
|
14583
|
+
sessionId,
|
|
14584
|
+
content,
|
|
14585
|
+
{ importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
|
|
14586
|
+
);
|
|
14587
|
+
if (appendResult.success && appendResult.isDuplicate) {
|
|
14588
|
+
result.skippedDuplicates++;
|
|
14589
|
+
} else {
|
|
14590
|
+
result.importedPrompts++;
|
|
14591
|
+
}
|
|
14592
|
+
lineCount++;
|
|
14593
|
+
} else if (msgClass === "agent_text") {
|
|
14594
|
+
const content = this.extractContent(entry);
|
|
14595
|
+
if (content) {
|
|
14596
|
+
textBuffer.push(content);
|
|
14597
|
+
lastTimestamp = entry.timestamp;
|
|
14598
|
+
}
|
|
14272
14599
|
}
|
|
14273
|
-
|
|
14274
|
-
|
|
14275
|
-
|
|
14276
|
-
|
|
14277
|
-
|
|
14278
|
-
|
|
14600
|
+
const now = Date.now();
|
|
14601
|
+
if (now - lastProgressAt > 200) {
|
|
14602
|
+
lastProgressAt = now;
|
|
14603
|
+
onProgress?.({
|
|
14604
|
+
phase: "session-progress",
|
|
14605
|
+
sessionIndex,
|
|
14606
|
+
messagesProcessed: result.totalMessages,
|
|
14607
|
+
imported: result.importedPrompts + result.importedResponses,
|
|
14608
|
+
skipped: result.skippedDuplicates
|
|
14609
|
+
});
|
|
14279
14610
|
}
|
|
14611
|
+
} catch (parseError) {
|
|
14612
|
+
result.errors.push(`Parse error on line: ${parseError}`);
|
|
14280
14613
|
}
|
|
14281
|
-
const now = Date.now();
|
|
14282
|
-
if (now - lastProgressAt > 200) {
|
|
14283
|
-
lastProgressAt = now;
|
|
14284
|
-
onProgress?.({
|
|
14285
|
-
phase: "session-progress",
|
|
14286
|
-
sessionIndex,
|
|
14287
|
-
messagesProcessed: result.totalMessages,
|
|
14288
|
-
imported: result.importedPrompts + result.importedResponses,
|
|
14289
|
-
skipped: result.skippedDuplicates
|
|
14290
|
-
});
|
|
14291
|
-
}
|
|
14292
|
-
} catch (parseError) {
|
|
14293
|
-
result.errors.push(`Parse error on line: ${parseError}`);
|
|
14294
14614
|
}
|
|
14615
|
+
await flushTextBuffer();
|
|
14616
|
+
} finally {
|
|
14617
|
+
rl.close();
|
|
14618
|
+
fileStream.close();
|
|
14295
14619
|
}
|
|
14296
|
-
await flushTextBuffer();
|
|
14297
14620
|
await this.memoryService.endSession(sessionId);
|
|
14298
14621
|
if (options.projectPath) {
|
|
14299
14622
|
registerSession(sessionId, options.projectPath);
|
|
@@ -14804,29 +15127,6 @@ var CodexSessionHistoryImporter = class {
|
|
|
14804
15127
|
getSessionsRoot() {
|
|
14805
15128
|
return this.sessionsRoot;
|
|
14806
15129
|
}
|
|
14807
|
-
listSessionFilesRecursive(rootDir) {
|
|
14808
|
-
if (!fs10.existsSync(rootDir)) return [];
|
|
14809
|
-
const out = [];
|
|
14810
|
-
const stack = [rootDir];
|
|
14811
|
-
while (stack.length > 0) {
|
|
14812
|
-
const dir = stack.pop();
|
|
14813
|
-
let entries;
|
|
14814
|
-
try {
|
|
14815
|
-
entries = fs10.readdirSync(dir, { withFileTypes: true });
|
|
14816
|
-
} catch {
|
|
14817
|
-
continue;
|
|
14818
|
-
}
|
|
14819
|
-
for (const ent of entries) {
|
|
14820
|
-
const fullPath = path12.join(dir, ent.name);
|
|
14821
|
-
if (ent.isDirectory()) {
|
|
14822
|
-
stack.push(fullPath);
|
|
14823
|
-
} else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
|
|
14824
|
-
out.push(fullPath);
|
|
14825
|
-
}
|
|
14826
|
-
}
|
|
14827
|
-
}
|
|
14828
|
-
return out;
|
|
14829
|
-
}
|
|
14830
15130
|
async readSessionMeta(filePath) {
|
|
14831
15131
|
const fileStream = fs10.createReadStream(filePath, { encoding: "utf-8" });
|
|
14832
15132
|
const rl = readline2.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
@@ -14876,7 +15176,7 @@ var CodexSessionHistoryImporter = class {
|
|
|
14876
15176
|
}
|
|
14877
15177
|
const normalizedTarget = normalizeMaybeRealpath(projectPath);
|
|
14878
15178
|
onProgress?.({ phase: "scan", message: "Scanning Codex session files..." });
|
|
14879
|
-
const sessionFiles =
|
|
15179
|
+
const sessionFiles = listCodexSessionFilesRecursive(sessionsRoot);
|
|
14880
15180
|
const matchingFiles = [];
|
|
14881
15181
|
for (const filePath of sessionFiles) {
|
|
14882
15182
|
try {
|
|
@@ -14941,7 +15241,7 @@ var CodexSessionHistoryImporter = class {
|
|
|
14941
15241
|
return result;
|
|
14942
15242
|
}
|
|
14943
15243
|
onProgress?.({ phase: "scan", message: "Scanning all Codex sessions..." });
|
|
14944
|
-
const sessionFiles =
|
|
15244
|
+
const sessionFiles = listCodexSessionFilesRecursive(sessionsRoot);
|
|
14945
15245
|
const selectedFiles = selectRecentCodexSessionFiles(sessionFiles, options.sessionLimit);
|
|
14946
15246
|
onProgress?.({ phase: "scan", message: `Found ${sessionFiles.length} Codex session file(s)` });
|
|
14947
15247
|
const totalLimit = normalizePositiveImportLimit(options.limit);
|
|
@@ -15020,13 +15320,12 @@ var CodexSessionHistoryImporter = class {
|
|
|
15020
15320
|
return;
|
|
15021
15321
|
}
|
|
15022
15322
|
if (textBuffer.length === 0 || !currentTurnId) return;
|
|
15023
|
-
const
|
|
15024
|
-
const merged = substantive.length > 0 ? substantive.join("\n\n") : textBuffer.reduce((a, b) => a.length >= b.length ? a : b, "");
|
|
15323
|
+
const merged = mergeAgentResponseBlocks(textBuffer);
|
|
15025
15324
|
if (!merged) {
|
|
15026
15325
|
textBuffer = [];
|
|
15027
15326
|
return;
|
|
15028
15327
|
}
|
|
15029
|
-
const truncated = merged
|
|
15328
|
+
const truncated = truncateAgentResponse(merged);
|
|
15030
15329
|
const appendResult = await this.memoryService.storeAgentResponse(
|
|
15031
15330
|
sessionId,
|
|
15032
15331
|
truncated,
|
|
@@ -15110,7 +15409,7 @@ var CodexSessionHistoryImporter = class {
|
|
|
15110
15409
|
async listAvailableSessions(projectPath) {
|
|
15111
15410
|
const sessionsRoot = this.getSessionsRoot();
|
|
15112
15411
|
if (!fs10.existsSync(sessionsRoot)) return [];
|
|
15113
|
-
const files =
|
|
15412
|
+
const files = listCodexSessionFilesRecursive(sessionsRoot);
|
|
15114
15413
|
const sessions = [];
|
|
15115
15414
|
const normalizedTarget = projectPath ? normalizeMaybeRealpath(projectPath) : null;
|
|
15116
15415
|
for (const filePath of files) {
|
|
@@ -15551,9 +15850,7 @@ var HermesSessionHistoryImporter = class {
|
|
|
15551
15850
|
return;
|
|
15552
15851
|
}
|
|
15553
15852
|
if (textBuffer.length === 0 || !currentTurnId) return;
|
|
15554
|
-
const
|
|
15555
|
-
const substantive = contentBlocks.filter((content) => content.length >= 100);
|
|
15556
|
-
const merged = substantive.length > 0 ? substantive.join("\n\n") : contentBlocks.reduce((a, b) => a.length >= b.length ? a : b, "");
|
|
15853
|
+
const merged = mergeAgentResponseBlocks(textBuffer.map((item) => item.content));
|
|
15557
15854
|
if (!merged) {
|
|
15558
15855
|
textBuffer = [];
|
|
15559
15856
|
return;
|
|
@@ -16053,8 +16350,8 @@ import { getCookie, setCookie, deleteCookie } from "hono/cookie";
|
|
|
16053
16350
|
import { logger } from "hono/logger";
|
|
16054
16351
|
import { serve } from "@hono/node-server";
|
|
16055
16352
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
16056
|
-
import * as
|
|
16057
|
-
import * as
|
|
16353
|
+
import * as path19 from "path";
|
|
16354
|
+
import * as fs17 from "fs";
|
|
16058
16355
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
16059
16356
|
import { fileURLToPath as fileUrlToPath } from "url";
|
|
16060
16357
|
|
|
@@ -16065,6 +16362,11 @@ import { Hono as Hono11 } from "hono";
|
|
|
16065
16362
|
import { Hono } from "hono";
|
|
16066
16363
|
|
|
16067
16364
|
// src/apps/server/api/utils.ts
|
|
16365
|
+
function jsonError(c, error, options = {}) {
|
|
16366
|
+
const status = options.status ?? 500;
|
|
16367
|
+
console.error(`[api] ${c.req.method} ${c.req.path} failed:`, error);
|
|
16368
|
+
return c.json({ error: options.message ?? "Internal server error" }, status);
|
|
16369
|
+
}
|
|
16068
16370
|
function getServiceFromQuery(c) {
|
|
16069
16371
|
const project = c.req.query("project") || c.req.query("projectId");
|
|
16070
16372
|
if (project) {
|
|
@@ -16176,7 +16478,7 @@ sessionsRouter.get("/", async (c) => {
|
|
|
16176
16478
|
hasMore: end < total
|
|
16177
16479
|
});
|
|
16178
16480
|
} catch (error) {
|
|
16179
|
-
return c
|
|
16481
|
+
return jsonError(c, error);
|
|
16180
16482
|
} finally {
|
|
16181
16483
|
await memoryService.shutdown();
|
|
16182
16484
|
}
|
|
@@ -16212,7 +16514,7 @@ sessionsRouter.get("/:id", async (c) => {
|
|
|
16212
16514
|
stats: eventsByType
|
|
16213
16515
|
});
|
|
16214
16516
|
} catch (error) {
|
|
16215
|
-
return c
|
|
16517
|
+
return jsonError(c, error);
|
|
16216
16518
|
} finally {
|
|
16217
16519
|
await memoryService.shutdown();
|
|
16218
16520
|
}
|
|
@@ -16278,7 +16580,7 @@ eventsRouter.get("/", async (c) => {
|
|
|
16278
16580
|
hasMore: offset + limit < total
|
|
16279
16581
|
});
|
|
16280
16582
|
} catch (error) {
|
|
16281
|
-
return c
|
|
16583
|
+
return jsonError(c, error);
|
|
16282
16584
|
} finally {
|
|
16283
16585
|
await memoryService.shutdown();
|
|
16284
16586
|
}
|
|
@@ -16288,12 +16590,11 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
16288
16590
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
16289
16591
|
try {
|
|
16290
16592
|
await memoryService.initialize();
|
|
16291
|
-
const
|
|
16292
|
-
const event = recentEvents.find((e) => e.id === id);
|
|
16593
|
+
const event = await memoryService.getEvent(id);
|
|
16293
16594
|
if (!event) {
|
|
16294
16595
|
return c.json({ error: "Event not found" }, 404);
|
|
16295
16596
|
}
|
|
16296
|
-
const sessionEvents =
|
|
16597
|
+
const sessionEvents = (await memoryService.getSessionHistory(event.sessionId)).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
16297
16598
|
const eventIndex = sessionEvents.findIndex((e) => e.id === id);
|
|
16298
16599
|
const start = Math.max(0, eventIndex - 2);
|
|
16299
16600
|
const end = Math.min(sessionEvents.length, eventIndex + 3);
|
|
@@ -16315,7 +16616,7 @@ eventsRouter.get("/:id", async (c) => {
|
|
|
16315
16616
|
}))
|
|
16316
16617
|
});
|
|
16317
16618
|
} catch (error) {
|
|
16318
|
-
return c
|
|
16619
|
+
return jsonError(c, error);
|
|
16319
16620
|
} finally {
|
|
16320
16621
|
await memoryService.shutdown();
|
|
16321
16622
|
}
|
|
@@ -16362,7 +16663,7 @@ searchRouter.post("/", async (c) => {
|
|
|
16362
16663
|
}
|
|
16363
16664
|
});
|
|
16364
16665
|
} catch (error) {
|
|
16365
|
-
return c
|
|
16666
|
+
return jsonError(c, error);
|
|
16366
16667
|
} finally {
|
|
16367
16668
|
await memoryService.shutdown();
|
|
16368
16669
|
}
|
|
@@ -16405,7 +16706,7 @@ searchRouter.post("/disclosure", async (c) => {
|
|
|
16405
16706
|
throw error;
|
|
16406
16707
|
}
|
|
16407
16708
|
} catch (error) {
|
|
16408
|
-
return c
|
|
16709
|
+
return jsonError(c, error);
|
|
16409
16710
|
} finally {
|
|
16410
16711
|
await memoryService?.shutdown();
|
|
16411
16712
|
}
|
|
@@ -16425,7 +16726,7 @@ searchRouter.get("/disclosure/:resultId/expand", async (c) => {
|
|
|
16425
16726
|
}
|
|
16426
16727
|
return c.json(result);
|
|
16427
16728
|
} catch (error) {
|
|
16428
|
-
return c
|
|
16729
|
+
return jsonError(c, error);
|
|
16429
16730
|
} finally {
|
|
16430
16731
|
await memoryService.shutdown();
|
|
16431
16732
|
}
|
|
@@ -16440,7 +16741,7 @@ searchRouter.get("/disclosure/:resultId/source", async (c) => {
|
|
|
16440
16741
|
}
|
|
16441
16742
|
return c.json(result);
|
|
16442
16743
|
} catch (error) {
|
|
16443
|
-
return c
|
|
16744
|
+
return jsonError(c, error);
|
|
16444
16745
|
} finally {
|
|
16445
16746
|
await memoryService.shutdown();
|
|
16446
16747
|
}
|
|
@@ -16469,7 +16770,7 @@ searchRouter.get("/", async (c) => {
|
|
|
16469
16770
|
}
|
|
16470
16771
|
});
|
|
16471
16772
|
} catch (error) {
|
|
16472
|
-
return c
|
|
16773
|
+
return jsonError(c, error);
|
|
16473
16774
|
} finally {
|
|
16474
16775
|
await memoryService.shutdown();
|
|
16475
16776
|
}
|
|
@@ -16477,9 +16778,89 @@ searchRouter.get("/", async (c) => {
|
|
|
16477
16778
|
|
|
16478
16779
|
// src/apps/server/api/stats.ts
|
|
16479
16780
|
import { Hono as Hono4 } from "hono";
|
|
16480
|
-
import * as
|
|
16781
|
+
import * as fs14 from "fs";
|
|
16481
16782
|
import * as os9 from "os";
|
|
16783
|
+
import * as path16 from "path";
|
|
16784
|
+
|
|
16785
|
+
// src/apps/server/api/stats-metrics.ts
|
|
16786
|
+
import * as fs13 from "fs";
|
|
16482
16787
|
import * as path15 from "path";
|
|
16788
|
+
var DEFAULT_KPI_THRESHOLDS = {
|
|
16789
|
+
usefulRecallRateMin: 0.45,
|
|
16790
|
+
reworkRateMax: 0.25,
|
|
16791
|
+
postChangeFailureRateMax: 0.2,
|
|
16792
|
+
avgCompletionTurnsMax: 12,
|
|
16793
|
+
memoryHitRateMin: 0.35
|
|
16794
|
+
};
|
|
16795
|
+
function loadKpiThresholds() {
|
|
16796
|
+
try {
|
|
16797
|
+
const filePath = path15.resolve(process.cwd(), "config", "kpi-thresholds.json");
|
|
16798
|
+
if (!fs13.existsSync(filePath)) return DEFAULT_KPI_THRESHOLDS;
|
|
16799
|
+
const parsed = JSON.parse(fs13.readFileSync(filePath, "utf-8"));
|
|
16800
|
+
return {
|
|
16801
|
+
usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
|
|
16802
|
+
reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
|
|
16803
|
+
postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
|
|
16804
|
+
avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
|
|
16805
|
+
memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
|
|
16806
|
+
};
|
|
16807
|
+
} catch {
|
|
16808
|
+
return DEFAULT_KPI_THRESHOLDS;
|
|
16809
|
+
}
|
|
16810
|
+
}
|
|
16811
|
+
function windowToMs(window) {
|
|
16812
|
+
if (window === "24h") return 24 * 60 * 60 * 1e3;
|
|
16813
|
+
if (window === "7d") return 7 * 24 * 60 * 60 * 1e3;
|
|
16814
|
+
return 30 * 24 * 60 * 60 * 1e3;
|
|
16815
|
+
}
|
|
16816
|
+
function inWindow(e, now, window) {
|
|
16817
|
+
return now - e.timestamp.getTime() <= windowToMs(window);
|
|
16818
|
+
}
|
|
16819
|
+
function isEditToolName(name) {
|
|
16820
|
+
return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
|
|
16821
|
+
}
|
|
16822
|
+
function parseToolPayload(e) {
|
|
16823
|
+
if (e.eventType !== "tool_observation") return null;
|
|
16824
|
+
try {
|
|
16825
|
+
const payload = JSON.parse(e.content);
|
|
16826
|
+
return {
|
|
16827
|
+
toolName: payload?.toolName,
|
|
16828
|
+
success: payload?.success,
|
|
16829
|
+
filePath: payload?.metadata?.filePath,
|
|
16830
|
+
command: payload?.metadata?.command
|
|
16831
|
+
};
|
|
16832
|
+
} catch {
|
|
16833
|
+
return {
|
|
16834
|
+
toolName: e.metadata?.toolName,
|
|
16835
|
+
success: e.metadata?.success,
|
|
16836
|
+
filePath: e.metadata?.filePath,
|
|
16837
|
+
command: e.metadata?.command
|
|
16838
|
+
};
|
|
16839
|
+
}
|
|
16840
|
+
}
|
|
16841
|
+
function isTestLikeCommand(command) {
|
|
16842
|
+
if (!command) return false;
|
|
16843
|
+
return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
|
|
16844
|
+
}
|
|
16845
|
+
function safeRatio(num, den) {
|
|
16846
|
+
if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) return 0;
|
|
16847
|
+
return num / den;
|
|
16848
|
+
}
|
|
16849
|
+
function round2(value, digits = 4) {
|
|
16850
|
+
const factor = 10 ** digits;
|
|
16851
|
+
return Math.round(value * factor) / factor;
|
|
16852
|
+
}
|
|
16853
|
+
function computeSessionTurnCount(sessionEvents) {
|
|
16854
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
16855
|
+
for (const e of sessionEvents) {
|
|
16856
|
+
const turnId = e.metadata?.turnId;
|
|
16857
|
+
if (typeof turnId === "string" && turnId.length > 0) turnIds.add(turnId);
|
|
16858
|
+
}
|
|
16859
|
+
if (turnIds.size > 0) return turnIds.size;
|
|
16860
|
+
return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
|
|
16861
|
+
}
|
|
16862
|
+
|
|
16863
|
+
// src/apps/server/api/stats.ts
|
|
16483
16864
|
var statsRouter = new Hono4();
|
|
16484
16865
|
var OPERATION_STATS_TABLES = [
|
|
16485
16866
|
"memory_facets",
|
|
@@ -16506,7 +16887,7 @@ function operationsStatsHomeDir() {
|
|
|
16506
16887
|
}
|
|
16507
16888
|
function projectStoragePathForOperationsStats(projectOrHash) {
|
|
16508
16889
|
const projectHash = /^[a-f0-9]{8}$/.test(projectOrHash) ? projectOrHash : hashProjectPath(projectOrHash);
|
|
16509
|
-
return
|
|
16890
|
+
return path16.join(operationsStatsHomeDir(), ".claude-code", "memory", "projects", projectHash);
|
|
16510
16891
|
}
|
|
16511
16892
|
function getOperationsStatsContext(project) {
|
|
16512
16893
|
if (project && project.trim().length > 0) {
|
|
@@ -16516,13 +16897,13 @@ function getOperationsStatsContext(project) {
|
|
|
16516
16897
|
return {
|
|
16517
16898
|
projectHash,
|
|
16518
16899
|
storagePath: storagePath2,
|
|
16519
|
-
dbPath:
|
|
16900
|
+
dbPath: path16.join(storagePath2, "events.sqlite")
|
|
16520
16901
|
};
|
|
16521
16902
|
}
|
|
16522
|
-
const storagePath =
|
|
16903
|
+
const storagePath = path16.join(operationsStatsHomeDir(), ".claude-code", "memory");
|
|
16523
16904
|
return {
|
|
16524
16905
|
storagePath,
|
|
16525
|
-
dbPath:
|
|
16906
|
+
dbPath: path16.join(storagePath, "events.sqlite")
|
|
16526
16907
|
};
|
|
16527
16908
|
}
|
|
16528
16909
|
function countRowValue(db, sql, params = []) {
|
|
@@ -16540,6 +16921,23 @@ function sanitizeAggregateLabel(value) {
|
|
|
16540
16921
|
const trimmed = sanitized.trim();
|
|
16541
16922
|
return trimmed.length > 0 ? trimmed.slice(0, 96) : "unknown";
|
|
16542
16923
|
}
|
|
16924
|
+
var SAFE_RETRIEVAL_TRACE_STRATEGIES = /* @__PURE__ */ new Set([
|
|
16925
|
+
"auto",
|
|
16926
|
+
"deep",
|
|
16927
|
+
"fast",
|
|
16928
|
+
"hybrid",
|
|
16929
|
+
"keyword",
|
|
16930
|
+
"semantic",
|
|
16931
|
+
"mcp-context-pack",
|
|
16932
|
+
"session-start-hook",
|
|
16933
|
+
"unknown"
|
|
16934
|
+
]);
|
|
16935
|
+
function normalizeRetrievalTraceStrategy(value) {
|
|
16936
|
+
const label = sanitizeAggregateLabel(value);
|
|
16937
|
+
if (label === "[REDACTED]") return "unknown";
|
|
16938
|
+
const normalized = label.trim().toLowerCase();
|
|
16939
|
+
return SAFE_RETRIEVAL_TRACE_STRATEGIES.has(normalized) ? normalized : "unknown";
|
|
16940
|
+
}
|
|
16543
16941
|
function emptyOperationsStatsPayload(context, databaseExists, missingTables, windowDays) {
|
|
16544
16942
|
return {
|
|
16545
16943
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -16953,80 +17351,6 @@ function buildLessonConfidenceBuckets(db, projectHash) {
|
|
|
16953
17351
|
}).length
|
|
16954
17352
|
}));
|
|
16955
17353
|
}
|
|
16956
|
-
var DEFAULT_KPI_THRESHOLDS = {
|
|
16957
|
-
usefulRecallRateMin: 0.45,
|
|
16958
|
-
reworkRateMax: 0.25,
|
|
16959
|
-
postChangeFailureRateMax: 0.2,
|
|
16960
|
-
avgCompletionTurnsMax: 12,
|
|
16961
|
-
memoryHitRateMin: 0.35
|
|
16962
|
-
};
|
|
16963
|
-
function loadKpiThresholds() {
|
|
16964
|
-
try {
|
|
16965
|
-
const filePath = path15.resolve(process.cwd(), "config", "kpi-thresholds.json");
|
|
16966
|
-
if (!fs13.existsSync(filePath)) return DEFAULT_KPI_THRESHOLDS;
|
|
16967
|
-
const parsed = JSON.parse(fs13.readFileSync(filePath, "utf-8"));
|
|
16968
|
-
return {
|
|
16969
|
-
usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
|
|
16970
|
-
reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
|
|
16971
|
-
postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
|
|
16972
|
-
avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
|
|
16973
|
-
memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
|
|
16974
|
-
};
|
|
16975
|
-
} catch {
|
|
16976
|
-
return DEFAULT_KPI_THRESHOLDS;
|
|
16977
|
-
}
|
|
16978
|
-
}
|
|
16979
|
-
function windowToMs(window) {
|
|
16980
|
-
if (window === "24h") return 24 * 60 * 60 * 1e3;
|
|
16981
|
-
if (window === "7d") return 7 * 24 * 60 * 60 * 1e3;
|
|
16982
|
-
return 30 * 24 * 60 * 60 * 1e3;
|
|
16983
|
-
}
|
|
16984
|
-
function inWindow(e, now, window) {
|
|
16985
|
-
return now - e.timestamp.getTime() <= windowToMs(window);
|
|
16986
|
-
}
|
|
16987
|
-
function isEditToolName(name) {
|
|
16988
|
-
return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
|
|
16989
|
-
}
|
|
16990
|
-
function parseToolPayload(e) {
|
|
16991
|
-
if (e.eventType !== "tool_observation") return null;
|
|
16992
|
-
try {
|
|
16993
|
-
const payload = JSON.parse(e.content);
|
|
16994
|
-
return {
|
|
16995
|
-
toolName: payload?.toolName,
|
|
16996
|
-
success: payload?.success,
|
|
16997
|
-
filePath: payload?.metadata?.filePath,
|
|
16998
|
-
command: payload?.metadata?.command
|
|
16999
|
-
};
|
|
17000
|
-
} catch {
|
|
17001
|
-
return {
|
|
17002
|
-
toolName: e.metadata?.toolName,
|
|
17003
|
-
success: e.metadata?.success,
|
|
17004
|
-
filePath: e.metadata?.filePath,
|
|
17005
|
-
command: e.metadata?.command
|
|
17006
|
-
};
|
|
17007
|
-
}
|
|
17008
|
-
}
|
|
17009
|
-
function isTestLikeCommand(command) {
|
|
17010
|
-
if (!command) return false;
|
|
17011
|
-
return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
|
|
17012
|
-
}
|
|
17013
|
-
function safeRatio(num, den) {
|
|
17014
|
-
if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0) return 0;
|
|
17015
|
-
return num / den;
|
|
17016
|
-
}
|
|
17017
|
-
function round2(value, digits = 4) {
|
|
17018
|
-
const factor = 10 ** digits;
|
|
17019
|
-
return Math.round(value * factor) / factor;
|
|
17020
|
-
}
|
|
17021
|
-
function computeSessionTurnCount(sessionEvents) {
|
|
17022
|
-
const turnIds = /* @__PURE__ */ new Set();
|
|
17023
|
-
for (const e of sessionEvents) {
|
|
17024
|
-
const turnId = e.metadata?.turnId;
|
|
17025
|
-
if (typeof turnId === "string" && turnId.length > 0) turnIds.add(turnId);
|
|
17026
|
-
}
|
|
17027
|
-
if (turnIds.size > 0) return turnIds.size;
|
|
17028
|
-
return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
|
|
17029
|
-
}
|
|
17030
17354
|
function normalizeQueryRewriteKind2(value) {
|
|
17031
17355
|
const normalized = (value || "").trim().toLowerCase();
|
|
17032
17356
|
if (normalized === "follow-up-context" || normalized === "intent-rewrite") return normalized;
|
|
@@ -17054,6 +17378,63 @@ function getTraceSelectedCount(trace) {
|
|
|
17054
17378
|
function getTraceCandidateCount(trace) {
|
|
17055
17379
|
return Number(trace.candidateCount ?? trace.candidateEventIds?.length ?? trace.candidateDetails?.length ?? 0);
|
|
17056
17380
|
}
|
|
17381
|
+
function normalizeStatsNumber(value) {
|
|
17382
|
+
const numberValue = Number(value || 0);
|
|
17383
|
+
return Number.isFinite(numberValue) ? Math.max(0, numberValue) : 0;
|
|
17384
|
+
}
|
|
17385
|
+
function sanitizeRetrievalTraceStrategyBreakdown(value) {
|
|
17386
|
+
if (!Array.isArray(value)) return [];
|
|
17387
|
+
const byStrategy = /* @__PURE__ */ new Map();
|
|
17388
|
+
for (const item of value) {
|
|
17389
|
+
const row = item && typeof item === "object" ? item : {};
|
|
17390
|
+
const strategy = normalizeRetrievalTraceStrategy(row.strategy ?? "unknown");
|
|
17391
|
+
const bucket = byStrategy.get(strategy) ?? {
|
|
17392
|
+
strategy,
|
|
17393
|
+
totalQueries: 0,
|
|
17394
|
+
queriesWithSelection: 0,
|
|
17395
|
+
rewrittenQueries: 0,
|
|
17396
|
+
totalCandidateCount: 0,
|
|
17397
|
+
totalSelectedCount: 0
|
|
17398
|
+
};
|
|
17399
|
+
bucket.totalQueries += normalizeStatsNumber(row.totalQueries);
|
|
17400
|
+
bucket.queriesWithSelection += normalizeStatsNumber(row.queriesWithSelection);
|
|
17401
|
+
bucket.rewrittenQueries += normalizeStatsNumber(row.rewrittenQueries);
|
|
17402
|
+
bucket.totalCandidateCount += normalizeStatsNumber(row.totalCandidateCount);
|
|
17403
|
+
bucket.totalSelectedCount += normalizeStatsNumber(row.totalSelectedCount);
|
|
17404
|
+
byStrategy.set(strategy, bucket);
|
|
17405
|
+
}
|
|
17406
|
+
return Array.from(byStrategy.values()).map((row) => ({
|
|
17407
|
+
strategy: row.strategy,
|
|
17408
|
+
totalQueries: row.totalQueries,
|
|
17409
|
+
queriesWithSelection: row.queriesWithSelection,
|
|
17410
|
+
rewrittenQueries: row.rewrittenQueries,
|
|
17411
|
+
rewriteRate: normalizeMetric(safeRatio(row.rewrittenQueries, row.totalQueries)),
|
|
17412
|
+
totalCandidateCount: row.totalCandidateCount,
|
|
17413
|
+
totalSelectedCount: row.totalSelectedCount,
|
|
17414
|
+
avgCandidateCount: safeRatio(row.totalCandidateCount, row.totalQueries),
|
|
17415
|
+
avgSelectedCount: safeRatio(row.totalSelectedCount, row.totalQueries),
|
|
17416
|
+
selectionRate: normalizeMetric(safeRatio(row.totalSelectedCount, row.totalCandidateCount)),
|
|
17417
|
+
queryYieldRate: normalizeMetric(safeRatio(row.queriesWithSelection, row.totalQueries))
|
|
17418
|
+
}));
|
|
17419
|
+
}
|
|
17420
|
+
function sanitizeRetrievalTraceStats(stats) {
|
|
17421
|
+
const s = stats && typeof stats === "object" ? stats : {};
|
|
17422
|
+
return {
|
|
17423
|
+
totalQueries: Number(s.totalQueries || 0),
|
|
17424
|
+
avgCandidateCount: Number(s.avgCandidateCount || 0),
|
|
17425
|
+
avgSelectedCount: Number(s.avgSelectedCount || 0),
|
|
17426
|
+
selectionRate: normalizeMetric(s.selectionRate),
|
|
17427
|
+
rewrittenQueries: Number(s.rewrittenQueries || 0),
|
|
17428
|
+
rewriteRate: normalizeMetric(s.rewriteRate),
|
|
17429
|
+
rewrittenQueriesWithSelection: Number(s.rewrittenQueriesWithSelection || 0),
|
|
17430
|
+
rawQueriesWithSelection: Number(s.rawQueriesWithSelection || 0),
|
|
17431
|
+
rewrittenSelectionRate: normalizeMetric(s.rewrittenSelectionRate),
|
|
17432
|
+
rawSelectionRate: normalizeMetric(s.rawSelectionRate),
|
|
17433
|
+
avgSelectedCountForRewrittenQueries: Number(s.avgSelectedCountForRewrittenQueries || 0),
|
|
17434
|
+
avgSelectedCountForRawQueries: Number(s.avgSelectedCountForRawQueries || 0),
|
|
17435
|
+
strategyBreakdown: sanitizeRetrievalTraceStrategyBreakdown(s.strategyBreakdown)
|
|
17436
|
+
};
|
|
17437
|
+
}
|
|
17057
17438
|
function makeRetrievalReviewItem(trace) {
|
|
17058
17439
|
const candidateCount = getTraceCandidateCount(trace);
|
|
17059
17440
|
const selectedCount = getTraceSelectedCount(trace);
|
|
@@ -17107,7 +17488,7 @@ function makeRetrievalReviewItem(trace) {
|
|
|
17107
17488
|
action,
|
|
17108
17489
|
queryRewriteKind,
|
|
17109
17490
|
rewritten,
|
|
17110
|
-
strategy: trace.strategy
|
|
17491
|
+
strategy: normalizeRetrievalTraceStrategy(trace.strategy ?? "unknown"),
|
|
17111
17492
|
candidateCount,
|
|
17112
17493
|
selectedCount,
|
|
17113
17494
|
candidateEventIds: (trace.candidateEventIds || []).slice(0, 5),
|
|
@@ -17534,7 +17915,7 @@ statsRouter.get("/levels/:level", async (c) => {
|
|
|
17534
17915
|
hasMore: events.length === limit
|
|
17535
17916
|
});
|
|
17536
17917
|
} catch (error) {
|
|
17537
|
-
return c
|
|
17918
|
+
return jsonError(c, error);
|
|
17538
17919
|
} finally {
|
|
17539
17920
|
await memoryService.shutdown();
|
|
17540
17921
|
}
|
|
@@ -17543,7 +17924,7 @@ statsRouter.get("/operations", async (c) => {
|
|
|
17543
17924
|
const context = getOperationsStatsContext(c.req.query("project") || c.req.query("projectId"));
|
|
17544
17925
|
const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
|
|
17545
17926
|
const topFacetValues = parseStatsLimit(c.req.query("topFacetValues"), 5, 25);
|
|
17546
|
-
const databaseExists =
|
|
17927
|
+
const databaseExists = fs14.existsSync(context.dbPath);
|
|
17547
17928
|
if (!databaseExists) {
|
|
17548
17929
|
return c.json(emptyOperationsStatsPayload(context, false, [...OPERATION_STATS_TABLES], windowDays));
|
|
17549
17930
|
}
|
|
@@ -17631,7 +18012,7 @@ statsRouter.get("/perspective", async (c) => {
|
|
|
17631
18012
|
const windowDays = parseStatsLimit(c.req.query("windowDays"), 30, 365);
|
|
17632
18013
|
const contradictionLimit = parseStatsLimit(c.req.query("contradictionLimit"), 10, 50);
|
|
17633
18014
|
const graphLimit = parseStatsLimit(c.req.query("graphLimit"), 10, 50);
|
|
17634
|
-
const databaseExists =
|
|
18015
|
+
const databaseExists = fs14.existsSync(context.dbPath);
|
|
17635
18016
|
if (!databaseExists) {
|
|
17636
18017
|
return c.json(emptyPerspectiveStatsPayload(context, false, [...PERSPECTIVE_STATS_TABLES], windowDays));
|
|
17637
18018
|
}
|
|
@@ -17707,33 +18088,29 @@ statsRouter.get("/", async (c) => {
|
|
|
17707
18088
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
17708
18089
|
try {
|
|
17709
18090
|
await memoryService.initialize();
|
|
17710
|
-
const
|
|
17711
|
-
const
|
|
17712
|
-
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
|
|
17716
|
-
|
|
17717
|
-
|
|
17718
|
-
const
|
|
17719
|
-
const eventsByDay =
|
|
17720
|
-
|
|
17721
|
-
acc[day] = (acc[day] || 0) + 1;
|
|
17722
|
-
return acc;
|
|
17723
|
-
}, {});
|
|
17724
|
-
const retrievalTrace = await memoryService.getRetrievalTraceStats();
|
|
18091
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString();
|
|
18092
|
+
const [stats, typeCounts, sessionTotal, dailyCounts, rawTrace] = await Promise.all([
|
|
18093
|
+
memoryService.getStats(),
|
|
18094
|
+
memoryService.getEventTypeCounts(),
|
|
18095
|
+
memoryService.getDistinctSessionCount(),
|
|
18096
|
+
memoryService.getDailyEventCounts(sevenDaysAgo),
|
|
18097
|
+
memoryService.getRetrievalTraceStats()
|
|
18098
|
+
]);
|
|
18099
|
+
const eventsByType = Object.fromEntries(typeCounts.map((t) => [t.eventType, t.count]));
|
|
18100
|
+
const eventsByDay = Object.fromEntries(dailyCounts.map((d) => [d.day, d.total]));
|
|
18101
|
+
const retrievalTrace = sanitizeRetrievalTraceStats(rawTrace);
|
|
17725
18102
|
return c.json({
|
|
17726
18103
|
storage: {
|
|
17727
18104
|
eventCount: stats.totalEvents,
|
|
17728
18105
|
vectorCount: stats.vectorCount
|
|
17729
18106
|
},
|
|
17730
18107
|
sessions: {
|
|
17731
|
-
total:
|
|
18108
|
+
total: sessionTotal
|
|
17732
18109
|
},
|
|
17733
18110
|
eventsByType,
|
|
17734
18111
|
activity: {
|
|
17735
18112
|
daily: eventsByDay,
|
|
17736
|
-
total7Days:
|
|
18113
|
+
total7Days: dailyCounts.reduce((sum, d) => sum + d.total, 0)
|
|
17737
18114
|
},
|
|
17738
18115
|
memory: {
|
|
17739
18116
|
heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
@@ -17743,7 +18120,7 @@ statsRouter.get("/", async (c) => {
|
|
|
17743
18120
|
retrievalTrace
|
|
17744
18121
|
});
|
|
17745
18122
|
} catch (error) {
|
|
17746
|
-
return c
|
|
18123
|
+
return jsonError(c, error);
|
|
17747
18124
|
} finally {
|
|
17748
18125
|
await memoryService.shutdown();
|
|
17749
18126
|
}
|
|
@@ -17780,30 +18157,22 @@ statsRouter.get("/most-accessed", async (c) => {
|
|
|
17780
18157
|
}
|
|
17781
18158
|
});
|
|
17782
18159
|
statsRouter.get("/timeline", async (c) => {
|
|
17783
|
-
const
|
|
18160
|
+
const parsedDays = parseInt(c.req.query("days") || "7", 10);
|
|
18161
|
+
const days = Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : 7;
|
|
17784
18162
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
17785
18163
|
try {
|
|
17786
18164
|
await memoryService.initialize();
|
|
17787
|
-
const
|
|
17788
|
-
const
|
|
17789
|
-
|
|
17790
|
-
|
|
17791
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
if (e.eventType === "user_prompt") acc[day].prompts++;
|
|
17797
|
-
if (e.eventType === "agent_response") acc[day].responses++;
|
|
17798
|
-
if (e.eventType === "tool_observation") acc[day].tools++;
|
|
17799
|
-
return acc;
|
|
17800
|
-
}, {});
|
|
17801
|
-
return c.json({
|
|
17802
|
-
days,
|
|
17803
|
-
daily: Object.values(daily).sort((a, b) => a.date.localeCompare(b.date))
|
|
17804
|
-
});
|
|
18165
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
18166
|
+
const daily = (await memoryService.getDailyEventCounts(cutoff)).map((d) => ({
|
|
18167
|
+
date: d.day,
|
|
18168
|
+
total: d.total,
|
|
18169
|
+
prompts: d.prompts,
|
|
18170
|
+
responses: d.responses,
|
|
18171
|
+
tools: d.tools
|
|
18172
|
+
}));
|
|
18173
|
+
return c.json({ days, daily });
|
|
17805
18174
|
} catch (error) {
|
|
17806
|
-
return c
|
|
18175
|
+
return jsonError(c, error);
|
|
17807
18176
|
} finally {
|
|
17808
18177
|
await memoryService.shutdown();
|
|
17809
18178
|
}
|
|
@@ -17846,16 +18215,14 @@ statsRouter.get("/usefulness", async (c) => {
|
|
|
17846
18215
|
try {
|
|
17847
18216
|
await memoryService.initialize();
|
|
17848
18217
|
const now = Date.now();
|
|
17849
|
-
const eventLimit = 2e4;
|
|
17850
18218
|
const traceLimit = 5e3;
|
|
17851
18219
|
const windowStart = new Date(now - windowToMs(window));
|
|
17852
18220
|
const [events, helpfulness, traces] = await Promise.all([
|
|
17853
|
-
memoryService.
|
|
18221
|
+
memoryService.getEventsAfter(windowStart.toISOString()),
|
|
17854
18222
|
memoryService.getHelpfulnessStats(windowStart),
|
|
17855
18223
|
memoryService.getRecentRetrievalTraces(traceLimit)
|
|
17856
18224
|
]);
|
|
17857
18225
|
return c.json(computeMemoryUsefulnessSummary(events, helpfulness, traces, now, window, {
|
|
17858
|
-
eventsLimit: eventLimit,
|
|
17859
18226
|
tracesLimit: traceLimit
|
|
17860
18227
|
}));
|
|
17861
18228
|
} catch (error) {
|
|
@@ -17873,7 +18240,7 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
17873
18240
|
const traces = await memoryService.getRecentRetrievalTraces(limit);
|
|
17874
18241
|
const traceStats = await memoryService.getRetrievalTraceStats();
|
|
17875
18242
|
return c.json({
|
|
17876
|
-
stats: traceStats,
|
|
18243
|
+
stats: sanitizeRetrievalTraceStats(traceStats),
|
|
17877
18244
|
traces: traces.map((t) => {
|
|
17878
18245
|
const queryRewriteKind = normalizeQueryRewriteKind2(t.queryRewriteKind);
|
|
17879
18246
|
return {
|
|
@@ -17882,7 +18249,7 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
17882
18249
|
projectHash: t.projectHash || null,
|
|
17883
18250
|
queryRewriteKind,
|
|
17884
18251
|
rewritten: queryRewriteKind !== "none",
|
|
17885
|
-
strategy: t.strategy
|
|
18252
|
+
strategy: normalizeRetrievalTraceStrategy(t.strategy ?? "unknown"),
|
|
17886
18253
|
candidateEventIds: t.candidateEventIds,
|
|
17887
18254
|
selectedEventIds: t.selectedEventIds,
|
|
17888
18255
|
candidateDetails: t.candidateDetails || [],
|
|
@@ -17909,7 +18276,8 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
17909
18276
|
rewrittenSelectionRate: 0,
|
|
17910
18277
|
rawSelectionRate: 0,
|
|
17911
18278
|
avgSelectedCountForRewrittenQueries: 0,
|
|
17912
|
-
avgSelectedCountForRawQueries: 0
|
|
18279
|
+
avgSelectedCountForRawQueries: 0,
|
|
18280
|
+
strategyBreakdown: []
|
|
17913
18281
|
},
|
|
17914
18282
|
traces: [],
|
|
17915
18283
|
error: error.message
|
|
@@ -17960,7 +18328,8 @@ statsRouter.get("/kpi", async (c) => {
|
|
|
17960
18328
|
await memoryService.initialize();
|
|
17961
18329
|
const now = Date.now();
|
|
17962
18330
|
const thresholds = loadKpiThresholds();
|
|
17963
|
-
const
|
|
18331
|
+
const lookbackMs = Math.max(2 * windowToMs(window), 30 * 24 * 60 * 60 * 1e3);
|
|
18332
|
+
const allEvents = await memoryService.getEventsAfter(new Date(now - lookbackMs).toISOString());
|
|
17964
18333
|
const events = allEvents.filter((e) => inWindow(e, now, window));
|
|
17965
18334
|
const helpfulness = await memoryService.getHelpfulnessStats();
|
|
17966
18335
|
const usefulRecallRate = helpfulness.totalEvaluated > 0 ? round2(safeRatio(helpfulness.helpful, helpfulness.totalEvaluated)) : 0;
|
|
@@ -18069,7 +18438,7 @@ statsRouter.get("/kpi", async (c) => {
|
|
|
18069
18438
|
alerts
|
|
18070
18439
|
});
|
|
18071
18440
|
} catch (error) {
|
|
18072
|
-
return c
|
|
18441
|
+
return jsonError(c, error);
|
|
18073
18442
|
} finally {
|
|
18074
18443
|
await memoryService.shutdown();
|
|
18075
18444
|
}
|
|
@@ -18120,11 +18489,7 @@ citationsRouter.get("/:id", async (c) => {
|
|
|
18120
18489
|
const memoryService = getServiceFromQuery(c);
|
|
18121
18490
|
try {
|
|
18122
18491
|
await memoryService.initialize();
|
|
18123
|
-
const
|
|
18124
|
-
const event = recentEvents.find((e) => {
|
|
18125
|
-
const eventCitationId = generateCitationId(e.id);
|
|
18126
|
-
return eventCitationId === citationId;
|
|
18127
|
-
});
|
|
18492
|
+
const event = await memoryService.getEventByCitationId(citationId);
|
|
18128
18493
|
if (!event) {
|
|
18129
18494
|
return c.json({ error: "Citation not found" }, 404);
|
|
18130
18495
|
}
|
|
@@ -18143,7 +18508,7 @@ citationsRouter.get("/:id", async (c) => {
|
|
|
18143
18508
|
}
|
|
18144
18509
|
});
|
|
18145
18510
|
} catch (error) {
|
|
18146
|
-
return c
|
|
18511
|
+
return jsonError(c, error);
|
|
18147
18512
|
} finally {
|
|
18148
18513
|
await memoryService.shutdown();
|
|
18149
18514
|
}
|
|
@@ -18154,15 +18519,11 @@ citationsRouter.get("/:id/related", async (c) => {
|
|
|
18154
18519
|
const memoryService = getServiceFromQuery(c);
|
|
18155
18520
|
try {
|
|
18156
18521
|
await memoryService.initialize();
|
|
18157
|
-
const
|
|
18158
|
-
const event = recentEvents.find((e) => {
|
|
18159
|
-
const eventCitationId = generateCitationId(e.id);
|
|
18160
|
-
return eventCitationId === citationId;
|
|
18161
|
-
});
|
|
18522
|
+
const event = await memoryService.getEventByCitationId(citationId);
|
|
18162
18523
|
if (!event) {
|
|
18163
18524
|
return c.json({ error: "Citation not found" }, 404);
|
|
18164
18525
|
}
|
|
18165
|
-
const sessionEvents =
|
|
18526
|
+
const sessionEvents = (await memoryService.getSessionHistory(event.sessionId)).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
18166
18527
|
const eventIndex = sessionEvents.findIndex((e) => e.id === event.id);
|
|
18167
18528
|
const prev = eventIndex > 0 ? sessionEvents[eventIndex - 1] : null;
|
|
18168
18529
|
const next = eventIndex < sessionEvents.length - 1 ? sessionEvents[eventIndex + 1] : null;
|
|
@@ -18181,7 +18542,7 @@ citationsRouter.get("/:id/related", async (c) => {
|
|
|
18181
18542
|
} : null
|
|
18182
18543
|
});
|
|
18183
18544
|
} catch (error) {
|
|
18184
|
-
return c
|
|
18545
|
+
return jsonError(c, error);
|
|
18185
18546
|
} finally {
|
|
18186
18547
|
await memoryService.shutdown();
|
|
18187
18548
|
}
|
|
@@ -18224,7 +18585,7 @@ turnsRouter.get("/", async (c) => {
|
|
|
18224
18585
|
hasMore: offset + limit < totalTurns
|
|
18225
18586
|
});
|
|
18226
18587
|
} catch (error) {
|
|
18227
|
-
return c
|
|
18588
|
+
return jsonError(c, error);
|
|
18228
18589
|
} finally {
|
|
18229
18590
|
await memoryService.shutdown();
|
|
18230
18591
|
}
|
|
@@ -18275,7 +18636,7 @@ turnsRouter.get("/:turnId", async (c) => {
|
|
|
18275
18636
|
totalEvents: events.length
|
|
18276
18637
|
});
|
|
18277
18638
|
} catch (error) {
|
|
18278
|
-
return c
|
|
18639
|
+
return jsonError(c, error);
|
|
18279
18640
|
} finally {
|
|
18280
18641
|
await memoryService.shutdown();
|
|
18281
18642
|
}
|
|
@@ -18302,8 +18663,8 @@ turnsRouter.post("/backfill", async (c) => {
|
|
|
18302
18663
|
|
|
18303
18664
|
// src/apps/server/api/projects.ts
|
|
18304
18665
|
import { Hono as Hono7 } from "hono";
|
|
18305
|
-
import * as
|
|
18306
|
-
import * as
|
|
18666
|
+
import * as fs15 from "fs";
|
|
18667
|
+
import * as path17 from "path";
|
|
18307
18668
|
import * as os10 from "os";
|
|
18308
18669
|
var projectsRouter = new Hono7();
|
|
18309
18670
|
projectsRouter.get("/:hash/detail", async (c) => {
|
|
@@ -18332,7 +18693,7 @@ projectsRouter.get("/:hash/detail", async (c) => {
|
|
|
18332
18693
|
return c.json({
|
|
18333
18694
|
project: {
|
|
18334
18695
|
hash,
|
|
18335
|
-
projectName: projectPath ?
|
|
18696
|
+
projectName: projectPath ? path17.basename(projectPath) : `unknown (${hash})`,
|
|
18336
18697
|
registered: Boolean(projectPath)
|
|
18337
18698
|
},
|
|
18338
18699
|
storage: {
|
|
@@ -18360,13 +18721,13 @@ projectsRouter.get("/:hash/detail", async (c) => {
|
|
|
18360
18721
|
});
|
|
18361
18722
|
projectsRouter.get("/", async (c) => {
|
|
18362
18723
|
try {
|
|
18363
|
-
const projectsDir =
|
|
18364
|
-
if (!
|
|
18724
|
+
const projectsDir = path17.join(os10.homedir(), ".claude-code", "memory", "projects");
|
|
18725
|
+
if (!fs15.existsSync(projectsDir)) {
|
|
18365
18726
|
return c.json({ projects: [] });
|
|
18366
18727
|
}
|
|
18367
|
-
const projectHashes =
|
|
18368
|
-
const fullPath =
|
|
18369
|
-
return
|
|
18728
|
+
const projectHashes = fs15.readdirSync(projectsDir).filter((name) => {
|
|
18729
|
+
const fullPath = path17.join(projectsDir, name);
|
|
18730
|
+
return fs15.statSync(fullPath).isDirectory();
|
|
18370
18731
|
});
|
|
18371
18732
|
const registry = loadSessionRegistry();
|
|
18372
18733
|
const hashToPath = /* @__PURE__ */ new Map();
|
|
@@ -18376,17 +18737,17 @@ projectsRouter.get("/", async (c) => {
|
|
|
18376
18737
|
}
|
|
18377
18738
|
}
|
|
18378
18739
|
const projects = projectHashes.map((hash) => {
|
|
18379
|
-
const dirPath =
|
|
18380
|
-
const dbPath =
|
|
18740
|
+
const dirPath = path17.join(projectsDir, hash);
|
|
18741
|
+
const dbPath = path17.join(dirPath, "events.sqlite");
|
|
18381
18742
|
let dbSize = 0;
|
|
18382
|
-
if (
|
|
18383
|
-
dbSize =
|
|
18743
|
+
if (fs15.existsSync(dbPath)) {
|
|
18744
|
+
dbSize = fs15.statSync(dbPath).size;
|
|
18384
18745
|
}
|
|
18385
18746
|
const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
|
|
18386
18747
|
return {
|
|
18387
18748
|
hash,
|
|
18388
18749
|
projectPath,
|
|
18389
|
-
projectName:
|
|
18750
|
+
projectName: path17.basename(projectPath),
|
|
18390
18751
|
dbSize,
|
|
18391
18752
|
dbSizeHuman: formatBytes(dbSize)
|
|
18392
18753
|
};
|
|
@@ -18515,7 +18876,7 @@ chatRouter.post("/", async (c) => {
|
|
|
18515
18876
|
}
|
|
18516
18877
|
});
|
|
18517
18878
|
} catch (error) {
|
|
18518
|
-
return c
|
|
18879
|
+
return jsonError(c, error);
|
|
18519
18880
|
} finally {
|
|
18520
18881
|
await memoryService.shutdown();
|
|
18521
18882
|
}
|
|
@@ -18739,6 +19100,150 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
18739
19100
|
// src/apps/server/api/health.ts
|
|
18740
19101
|
import { Hono as Hono9 } from "hono";
|
|
18741
19102
|
import { spawnSync } from "child_process";
|
|
19103
|
+
import * as fs16 from "fs";
|
|
19104
|
+
import * as path18 from "path";
|
|
19105
|
+
|
|
19106
|
+
// src/core/productivity-health-report.ts
|
|
19107
|
+
var PRODUCTIVITY_HEALTH_SCHEMA_VERSION = "agent-productivity-health-v1";
|
|
19108
|
+
var PRODUCTIVITY_HEALTH_PROFILES = ["coder", "reviewer", "pm", "support", "researcher", "team"];
|
|
19109
|
+
var PRODUCTIVITY_HEALTH_MODES = ["observe", "preview", "enforce"];
|
|
19110
|
+
function parseProductivityHealthProfile(value) {
|
|
19111
|
+
const normalized = typeof value === "string" && value.trim().length > 0 ? value.trim() : "coder";
|
|
19112
|
+
if (PRODUCTIVITY_HEALTH_PROFILES.includes(normalized)) {
|
|
19113
|
+
return normalized;
|
|
19114
|
+
}
|
|
19115
|
+
throw new Error(`Invalid --profile: expected one of ${PRODUCTIVITY_HEALTH_PROFILES.join(", ")}`);
|
|
19116
|
+
}
|
|
19117
|
+
function parseProductivityHealthMode(value) {
|
|
19118
|
+
const normalized = typeof value === "string" && value.trim().length > 0 ? value.trim() : "preview";
|
|
19119
|
+
if (PRODUCTIVITY_HEALTH_MODES.includes(normalized)) {
|
|
19120
|
+
return normalized;
|
|
19121
|
+
}
|
|
19122
|
+
throw new Error(`Invalid --mode: expected one of ${PRODUCTIVITY_HEALTH_MODES.join(", ")}`);
|
|
19123
|
+
}
|
|
19124
|
+
function buildProductivityHealthReport(input) {
|
|
19125
|
+
const profile = input.profile ?? "coder";
|
|
19126
|
+
const mode = input.mode ?? "preview";
|
|
19127
|
+
const embedding = normalizeQueue(input.outbox.embedding);
|
|
19128
|
+
const vector = normalizeQueue(input.outbox.vector);
|
|
19129
|
+
const totals = sumQueues(embedding, vector);
|
|
19130
|
+
const storage = {
|
|
19131
|
+
totalEvents: numberOrZero(input.stats.totalEvents),
|
|
19132
|
+
vectorCount: numberOrZero(input.stats.vectorCount),
|
|
19133
|
+
levelStats: normalizeLevelStats(input.stats.levelStats)
|
|
19134
|
+
};
|
|
19135
|
+
const warningReasons = [];
|
|
19136
|
+
const projectGate = buildProjectScopeGate(input.project);
|
|
19137
|
+
const outboxGate = buildOutboxGate(totals);
|
|
19138
|
+
const densityGate = buildMemoryDensityGate(storage.totalEvents);
|
|
19139
|
+
const riskGates = [projectGate, outboxGate, densityGate];
|
|
19140
|
+
if (projectGate.status === "warn") warningReasons.push("project_scope_unknown");
|
|
19141
|
+
if (outboxGate.status === "warn") warningReasons.push("outbox_requires_attention");
|
|
19142
|
+
if (densityGate.status === "warn") warningReasons.push("memory_density_low");
|
|
19143
|
+
const status = riskGates.some((gate) => gate.status === "warn") ? "needs-attention" : "ok";
|
|
19144
|
+
return {
|
|
19145
|
+
schemaVersion: PRODUCTIVITY_HEALTH_SCHEMA_VERSION,
|
|
19146
|
+
generatedAt: (input.generatedAt ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
19147
|
+
status,
|
|
19148
|
+
profile,
|
|
19149
|
+
mode,
|
|
19150
|
+
project: input.project,
|
|
19151
|
+
summary: { warningReasons },
|
|
19152
|
+
signals: {
|
|
19153
|
+
storage,
|
|
19154
|
+
outbox: { embedding, vector, totals }
|
|
19155
|
+
},
|
|
19156
|
+
riskGates,
|
|
19157
|
+
nextBestAction: selectNextBestAction({ projectGate, outboxGate, densityGate })
|
|
19158
|
+
};
|
|
19159
|
+
}
|
|
19160
|
+
function buildProjectScopeGate(project) {
|
|
19161
|
+
if (project.scope === "project") {
|
|
19162
|
+
return { id: "project-scope-known", severity: "blocker", status: "pass" };
|
|
19163
|
+
}
|
|
19164
|
+
return {
|
|
19165
|
+
id: "project-scope-known",
|
|
19166
|
+
severity: "blocker",
|
|
19167
|
+
status: "warn",
|
|
19168
|
+
message: "No project parameter supplied; report uses global memory scope."
|
|
19169
|
+
};
|
|
19170
|
+
}
|
|
19171
|
+
function buildOutboxGate(totals) {
|
|
19172
|
+
if (totals.failed > 0 || totals.stuckProcessing > 0) {
|
|
19173
|
+
return {
|
|
19174
|
+
id: "outbox-healthy",
|
|
19175
|
+
severity: "warning",
|
|
19176
|
+
status: "warn",
|
|
19177
|
+
message: "Outbox has failed or stuck processing items; run recovery before trusting retrieval freshness."
|
|
19178
|
+
};
|
|
19179
|
+
}
|
|
19180
|
+
return { id: "outbox-healthy", severity: "warning", status: "pass" };
|
|
19181
|
+
}
|
|
19182
|
+
function buildMemoryDensityGate(totalEvents) {
|
|
19183
|
+
if (totalEvents <= 0) {
|
|
19184
|
+
return {
|
|
19185
|
+
id: "memory-density",
|
|
19186
|
+
severity: "warning",
|
|
19187
|
+
status: "warn",
|
|
19188
|
+
message: "No memories are available for this scope yet."
|
|
19189
|
+
};
|
|
19190
|
+
}
|
|
19191
|
+
return { id: "memory-density", severity: "warning", status: "pass" };
|
|
19192
|
+
}
|
|
19193
|
+
function selectNextBestAction(input) {
|
|
19194
|
+
if (input.outboxGate.status === "warn") {
|
|
19195
|
+
return "Run claude-memory-layer process --dry-run-recovery, then process pending embeddings.";
|
|
19196
|
+
}
|
|
19197
|
+
if (input.densityGate.status === "warn") {
|
|
19198
|
+
return "Import or capture project context before relying on productivity memory guidance.";
|
|
19199
|
+
}
|
|
19200
|
+
if (input.projectGate.status === "warn") {
|
|
19201
|
+
return "Provide a project path or project hash before relying on project-scoped productivity guidance.";
|
|
19202
|
+
}
|
|
19203
|
+
return "No immediate maintenance action required.";
|
|
19204
|
+
}
|
|
19205
|
+
function normalizeLevelStats(levelStats) {
|
|
19206
|
+
if (!Array.isArray(levelStats)) return [];
|
|
19207
|
+
return levelStats.filter((entry) => typeof entry.level === "string" && entry.level.trim().length > 0).map((entry) => ({ level: entry.level.trim().slice(0, 20), count: numberOrZero(entry.count) }));
|
|
19208
|
+
}
|
|
19209
|
+
function normalizeQueue(queue) {
|
|
19210
|
+
return {
|
|
19211
|
+
pending: numberOrZero(queue.pending),
|
|
19212
|
+
processing: numberOrZero(queue.processing),
|
|
19213
|
+
failed: numberOrZero(queue.failed),
|
|
19214
|
+
retryableFailed: numberOrZero(queue.retryableFailed),
|
|
19215
|
+
quarantinedFailed: numberOrZero(queue.quarantinedFailed),
|
|
19216
|
+
total: numberOrZero(queue.total),
|
|
19217
|
+
stuckProcessing: numberOrZero(queue.stuckProcessing),
|
|
19218
|
+
oldestProcessingAgeMs: normalizeNullableAge(queue.oldestProcessingAgeMs)
|
|
19219
|
+
};
|
|
19220
|
+
}
|
|
19221
|
+
function sumQueues(a, b) {
|
|
19222
|
+
return {
|
|
19223
|
+
pending: a.pending + b.pending,
|
|
19224
|
+
processing: a.processing + b.processing,
|
|
19225
|
+
failed: a.failed + b.failed,
|
|
19226
|
+
retryableFailed: (a.retryableFailed ?? 0) + (b.retryableFailed ?? 0),
|
|
19227
|
+
quarantinedFailed: (a.quarantinedFailed ?? 0) + (b.quarantinedFailed ?? 0),
|
|
19228
|
+
total: a.total + b.total,
|
|
19229
|
+
stuckProcessing: a.stuckProcessing + b.stuckProcessing,
|
|
19230
|
+
oldestProcessingAgeMs: maxNullable(a.oldestProcessingAgeMs, b.oldestProcessingAgeMs)
|
|
19231
|
+
};
|
|
19232
|
+
}
|
|
19233
|
+
function normalizeNullableAge(value) {
|
|
19234
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
|
|
19235
|
+
}
|
|
19236
|
+
function maxNullable(a, b) {
|
|
19237
|
+
if (a === null) return b;
|
|
19238
|
+
if (b === null) return a;
|
|
19239
|
+
return Math.max(a, b);
|
|
19240
|
+
}
|
|
19241
|
+
function numberOrZero(value) {
|
|
19242
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
19243
|
+
return Math.floor(value);
|
|
19244
|
+
}
|
|
19245
|
+
|
|
19246
|
+
// src/apps/server/api/health.ts
|
|
18742
19247
|
var healthRouter = new Hono9();
|
|
18743
19248
|
function hasEnvSignal(name) {
|
|
18744
19249
|
const value = process.env[name];
|
|
@@ -18779,6 +19284,33 @@ function aggregateOutbox(outbox) {
|
|
|
18779
19284
|
const stuckProcessing = (outbox.embedding?.stuckProcessing || 0) + (outbox.vector?.stuckProcessing || 0);
|
|
18780
19285
|
return { pending, processing, failed, retryableFailed, quarantinedFailed, stuckProcessing };
|
|
18781
19286
|
}
|
|
19287
|
+
function resolveProductivityProjectIdentity(project) {
|
|
19288
|
+
if (!project || project.trim().length === 0) return { scope: "global", id: "global" };
|
|
19289
|
+
const normalized = project.trim();
|
|
19290
|
+
return { scope: "project", id: /^[a-f0-9]{8}$/.test(normalized) ? normalized : hashProjectPath(normalized) };
|
|
19291
|
+
}
|
|
19292
|
+
function explicitProjectStoreExists(project) {
|
|
19293
|
+
if (!project || project.trim().length === 0) return true;
|
|
19294
|
+
return fs16.existsSync(path18.join(resolveProjectStoragePath(project.trim()), "events.sqlite"));
|
|
19295
|
+
}
|
|
19296
|
+
function emptyProductivityStats() {
|
|
19297
|
+
return { totalEvents: 0, vectorCount: 0, levelStats: [] };
|
|
19298
|
+
}
|
|
19299
|
+
function emptyProductivityQueue() {
|
|
19300
|
+
return {
|
|
19301
|
+
pending: 0,
|
|
19302
|
+
processing: 0,
|
|
19303
|
+
failed: 0,
|
|
19304
|
+
retryableFailed: 0,
|
|
19305
|
+
quarantinedFailed: 0,
|
|
19306
|
+
total: 0,
|
|
19307
|
+
stuckProcessing: 0,
|
|
19308
|
+
oldestProcessingAgeMs: null
|
|
19309
|
+
};
|
|
19310
|
+
}
|
|
19311
|
+
function emptyProductivityOutbox() {
|
|
19312
|
+
return { embedding: emptyProductivityQueue(), vector: emptyProductivityQueue() };
|
|
19313
|
+
}
|
|
18782
19314
|
healthRouter.get("/setup", async (c) => {
|
|
18783
19315
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
18784
19316
|
try {
|
|
@@ -18835,6 +19367,40 @@ healthRouter.get("/setup", async (c) => {
|
|
|
18835
19367
|
await memoryService.shutdown();
|
|
18836
19368
|
}
|
|
18837
19369
|
});
|
|
19370
|
+
healthRouter.get("/productivity", async (c) => {
|
|
19371
|
+
let memoryService;
|
|
19372
|
+
try {
|
|
19373
|
+
const profile = parseProductivityHealthProfile(c.req.query("profile"));
|
|
19374
|
+
const mode = parseProductivityHealthMode(c.req.query("mode"));
|
|
19375
|
+
const projectQuery = c.req.query("project") || c.req.query("projectId");
|
|
19376
|
+
const project = resolveProductivityProjectIdentity(projectQuery);
|
|
19377
|
+
if (!explicitProjectStoreExists(projectQuery)) {
|
|
19378
|
+
return c.json(buildProductivityHealthReport({
|
|
19379
|
+
stats: emptyProductivityStats(),
|
|
19380
|
+
outbox: emptyProductivityOutbox(),
|
|
19381
|
+
project,
|
|
19382
|
+
profile,
|
|
19383
|
+
mode
|
|
19384
|
+
}));
|
|
19385
|
+
}
|
|
19386
|
+
memoryService = getLightweightServiceFromQuery(c);
|
|
19387
|
+
await memoryService.initialize();
|
|
19388
|
+
const [stats, outbox] = await Promise.all([
|
|
19389
|
+
memoryService.getStats(),
|
|
19390
|
+
memoryService.getOutboxStats()
|
|
19391
|
+
]);
|
|
19392
|
+
return c.json(buildProductivityHealthReport({ stats, outbox, project, profile, mode }));
|
|
19393
|
+
} catch (error) {
|
|
19394
|
+
const status = error instanceof Error && error.message.startsWith("Invalid --") ? 400 : 500;
|
|
19395
|
+
return c.json({
|
|
19396
|
+
status: "error",
|
|
19397
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19398
|
+
error: status === 400 ? "Invalid productivity health option" : "Productivity health check failed"
|
|
19399
|
+
}, status);
|
|
19400
|
+
} finally {
|
|
19401
|
+
if (memoryService) await memoryService.shutdown();
|
|
19402
|
+
}
|
|
19403
|
+
});
|
|
18838
19404
|
healthRouter.get("/", async (c) => {
|
|
18839
19405
|
const memoryService = getLightweightServiceFromQuery(c);
|
|
18840
19406
|
try {
|
|
@@ -19156,20 +19722,20 @@ playgroundRouter.post("/dry-run", async (c) => {
|
|
|
19156
19722
|
var apiRouter = new Hono11().route("/sessions", sessionsRouter).route("/events", eventsRouter).route("/search", searchRouter).route("/stats", statsRouter).route("/citations", citationsRouter).route("/turns", turnsRouter).route("/projects", projectsRouter).route("/chat", chatRouter).route("/health", healthRouter).route("/playground", playgroundRouter);
|
|
19157
19723
|
|
|
19158
19724
|
// src/apps/server/index.ts
|
|
19159
|
-
var moduleDir =
|
|
19725
|
+
var moduleDir = path19.dirname(fileUrlToPath(import.meta.url));
|
|
19160
19726
|
var DASHBOARD_SESSION_COOKIE = "cml_dashboard_session";
|
|
19161
19727
|
var DEFAULT_SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
|
|
19162
19728
|
function resolveUiPath() {
|
|
19163
19729
|
const candidates = [
|
|
19164
19730
|
// Built server: dist/server/index.js -> dist/ui
|
|
19165
|
-
|
|
19731
|
+
path19.resolve(moduleDir, "../ui"),
|
|
19166
19732
|
// Source/dev server: src/apps/server/index.ts -> src/apps/dashboard
|
|
19167
|
-
|
|
19733
|
+
path19.resolve(moduleDir, "../dashboard"),
|
|
19168
19734
|
// Fallback when running from a repository root.
|
|
19169
|
-
|
|
19170
|
-
|
|
19735
|
+
path19.resolve(process.cwd(), "dist/ui"),
|
|
19736
|
+
path19.resolve(process.cwd(), "src/apps/dashboard")
|
|
19171
19737
|
];
|
|
19172
|
-
return candidates.find((candidate) =>
|
|
19738
|
+
return candidates.find((candidate) => fs17.existsSync(path19.join(candidate, "index.html"))) ?? candidates[0];
|
|
19173
19739
|
}
|
|
19174
19740
|
function normalizeDashboardHost(host = "localhost") {
|
|
19175
19741
|
const normalized = host.trim().toLowerCase();
|
|
@@ -19310,14 +19876,51 @@ function createAuthOptions(options) {
|
|
|
19310
19876
|
return {
|
|
19311
19877
|
password: options.password ?? "",
|
|
19312
19878
|
cookieMaxAgeSeconds: options.cookieMaxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
19313
|
-
now: options.now ?? Date.now
|
|
19879
|
+
now: options.now ?? Date.now,
|
|
19880
|
+
allowedOrigins: options.allowedOrigins ?? []
|
|
19314
19881
|
};
|
|
19315
19882
|
}
|
|
19883
|
+
function resolveAllowedOrigins(explicit) {
|
|
19884
|
+
if (explicit && explicit.length > 0) return explicit;
|
|
19885
|
+
const raw = process.env.DASHBOARD_ALLOWED_ORIGINS;
|
|
19886
|
+
if (!raw) return [];
|
|
19887
|
+
return raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
19888
|
+
}
|
|
19889
|
+
function selfOrigins(c) {
|
|
19890
|
+
const origins = [];
|
|
19891
|
+
try {
|
|
19892
|
+
origins.push(new URL(c.req.url).origin);
|
|
19893
|
+
} catch {
|
|
19894
|
+
}
|
|
19895
|
+
const host = c.req.header("host");
|
|
19896
|
+
if (host) origins.push(`${isSecureRequest(c) ? "https" : "http"}://${host}`);
|
|
19897
|
+
return origins;
|
|
19898
|
+
}
|
|
19899
|
+
function isAllowedMutationOrigin(c, origin, allowedOrigins) {
|
|
19900
|
+
return allowedOrigins.includes(origin) || selfOrigins(c).includes(origin);
|
|
19901
|
+
}
|
|
19316
19902
|
function createDashboardApp(options = {}) {
|
|
19317
19903
|
const app2 = new Hono12();
|
|
19318
19904
|
const authOptions = createAuthOptions(options);
|
|
19319
|
-
|
|
19905
|
+
const allowedOrigins = resolveAllowedOrigins(options.allowedOrigins);
|
|
19906
|
+
if (allowedOrigins.length > 0) {
|
|
19907
|
+
app2.use("*", cors({
|
|
19908
|
+
origin: (origin) => allowedOrigins.includes(origin) ? origin : null,
|
|
19909
|
+
credentials: true
|
|
19910
|
+
}));
|
|
19911
|
+
}
|
|
19320
19912
|
app2.use("*", logger());
|
|
19913
|
+
app2.use("/api/*", async (c, next) => {
|
|
19914
|
+
const method = c.req.method;
|
|
19915
|
+
if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
|
|
19916
|
+
return next();
|
|
19917
|
+
}
|
|
19918
|
+
const origin = c.req.header("origin");
|
|
19919
|
+
if (origin && !isAllowedMutationOrigin(c, origin, allowedOrigins)) {
|
|
19920
|
+
return c.json({ error: "Cross-origin request blocked" }, 403);
|
|
19921
|
+
}
|
|
19922
|
+
return next();
|
|
19923
|
+
});
|
|
19321
19924
|
app2.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
19322
19925
|
app2.get("/api/auth/status", (c) => c.json({
|
|
19323
19926
|
enabled: Boolean(authOptions.password),
|
|
@@ -19360,13 +19963,13 @@ function createDashboardApp(options = {}) {
|
|
|
19360
19963
|
}
|
|
19361
19964
|
app2.route("/api", apiRouter);
|
|
19362
19965
|
const uiPath = resolveUiPath();
|
|
19363
|
-
if (
|
|
19966
|
+
if (fs17.existsSync(uiPath)) {
|
|
19364
19967
|
app2.use("/*", serveStatic({ root: uiPath }));
|
|
19365
19968
|
}
|
|
19366
19969
|
app2.get("*", (c) => {
|
|
19367
|
-
const indexPath =
|
|
19368
|
-
if (
|
|
19369
|
-
return c.html(
|
|
19970
|
+
const indexPath = path19.join(uiPath, "index.html");
|
|
19971
|
+
if (fs17.existsSync(indexPath)) {
|
|
19972
|
+
return c.html(fs17.readFileSync(indexPath, "utf-8"));
|
|
19370
19973
|
}
|
|
19371
19974
|
return c.text('UI not built. Run "npm run build:ui" first.', 404);
|
|
19372
19975
|
});
|
|
@@ -19824,20 +20427,20 @@ function formatMetadataValue(value) {
|
|
|
19824
20427
|
}
|
|
19825
20428
|
|
|
19826
20429
|
// src/apps/cli/mcp-install.ts
|
|
19827
|
-
import { existsSync as
|
|
19828
|
-
import { dirname as dirname7, join as
|
|
20430
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync7, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
20431
|
+
import { dirname as dirname7, join as join19 } from "node:path";
|
|
19829
20432
|
import { homedir as homedir11 } from "node:os";
|
|
19830
20433
|
function getDefaultClaudeDesktopConfigPath(platform = process.platform, env = process.env, home = homedir11()) {
|
|
19831
20434
|
if (platform === "darwin") {
|
|
19832
|
-
return
|
|
20435
|
+
return join19(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
19833
20436
|
}
|
|
19834
20437
|
if (platform === "win32") {
|
|
19835
|
-
return
|
|
20438
|
+
return join19(env.APPDATA ?? join19(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
19836
20439
|
}
|
|
19837
|
-
return
|
|
20440
|
+
return join19(env.XDG_CONFIG_HOME ?? join19(home, ".config"), "claude", "claude_desktop_config.json");
|
|
19838
20441
|
}
|
|
19839
20442
|
function readJsonConfig(configPath) {
|
|
19840
|
-
if (!
|
|
20443
|
+
if (!existsSync15(configPath)) {
|
|
19841
20444
|
return {};
|
|
19842
20445
|
}
|
|
19843
20446
|
const content = readFileSync6(configPath, "utf8").trim();
|
|
@@ -19886,7 +20489,7 @@ function installMcpServer(options = {}) {
|
|
|
19886
20489
|
}
|
|
19887
20490
|
|
|
19888
20491
|
// src/apps/cli/claude-settings-hooks.ts
|
|
19889
|
-
import * as
|
|
20492
|
+
import * as path20 from "path";
|
|
19890
20493
|
var REQUIRED_HOOK_FILES = [
|
|
19891
20494
|
"user-prompt-submit.js",
|
|
19892
20495
|
"post-tool-use.js",
|
|
@@ -19905,7 +20508,7 @@ function shellQuotePathForNode(filePath) {
|
|
|
19905
20508
|
return `'${filePath.replace(/'/g, `'\\''`)}'`;
|
|
19906
20509
|
}
|
|
19907
20510
|
function buildHookCommand(pluginPath, fileName) {
|
|
19908
|
-
return `node ${shellQuotePathForNode(
|
|
20511
|
+
return `node ${shellQuotePathForNode(path20.join(pluginPath, "hooks", fileName))}`;
|
|
19909
20512
|
}
|
|
19910
20513
|
function getHooksConfig(pluginPath) {
|
|
19911
20514
|
const makeHook = (fileName) => [
|
|
@@ -19976,8 +20579,8 @@ function mergePluginHooksIntoSettings(settings, pluginPath) {
|
|
|
19976
20579
|
}
|
|
19977
20580
|
|
|
19978
20581
|
// src/apps/cli/codex-validation-output.ts
|
|
19979
|
-
import * as
|
|
19980
|
-
import * as
|
|
20582
|
+
import * as fs18 from "fs";
|
|
20583
|
+
import * as path21 from "path";
|
|
19981
20584
|
function formatNumber(value) {
|
|
19982
20585
|
return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
|
|
19983
20586
|
}
|
|
@@ -20051,14 +20654,14 @@ function formatCodexValidationReport(report, format = "markdown") {
|
|
|
20051
20654
|
return lines.join("\n");
|
|
20052
20655
|
}
|
|
20053
20656
|
function writeCodexValidationReport(outputPath, report, format = "markdown") {
|
|
20054
|
-
const dir =
|
|
20055
|
-
|
|
20056
|
-
|
|
20657
|
+
const dir = path21.dirname(outputPath);
|
|
20658
|
+
fs18.mkdirSync(dir, { recursive: true });
|
|
20659
|
+
fs18.writeFileSync(outputPath, formatCodexValidationReport(report, format), "utf8");
|
|
20057
20660
|
}
|
|
20058
20661
|
|
|
20059
20662
|
// src/apps/cli/hermes-validation-output.ts
|
|
20060
|
-
import * as
|
|
20061
|
-
import * as
|
|
20663
|
+
import * as fs19 from "fs";
|
|
20664
|
+
import * as path22 from "path";
|
|
20062
20665
|
function formatNumber2(value) {
|
|
20063
20666
|
return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
|
|
20064
20667
|
}
|
|
@@ -20128,9 +20731,9 @@ function formatHermesValidationReport(report, format = "markdown") {
|
|
|
20128
20731
|
return lines.join("\n");
|
|
20129
20732
|
}
|
|
20130
20733
|
function writeHermesValidationReport(outputPath, report, format = "markdown") {
|
|
20131
|
-
const dir =
|
|
20132
|
-
|
|
20133
|
-
|
|
20734
|
+
const dir = path22.dirname(outputPath);
|
|
20735
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
20736
|
+
fs19.writeFileSync(outputPath, formatHermesValidationReport(report, format), "utf8");
|
|
20134
20737
|
}
|
|
20135
20738
|
|
|
20136
20739
|
// src/apps/cli/codex-import-runner.ts
|
|
@@ -20817,7 +21420,7 @@ function redactSecrets(input) {
|
|
|
20817
21420
|
}
|
|
20818
21421
|
|
|
20819
21422
|
// src/apps/cli/process-command.ts
|
|
20820
|
-
import * as
|
|
21423
|
+
import * as path23 from "node:path";
|
|
20821
21424
|
function resolveProcessCommandOptions(options, cwd = process.cwd(), deps = {}) {
|
|
20822
21425
|
const explicitProject = options.project;
|
|
20823
21426
|
if (explicitProject !== void 0 && explicitProject.trim().length === 0) {
|
|
@@ -20838,7 +21441,7 @@ function resolveProcessCommandOptions(options, cwd = process.cwd(), deps = {}) {
|
|
|
20838
21441
|
projectPath,
|
|
20839
21442
|
recoverStuck,
|
|
20840
21443
|
dryRunRecovery,
|
|
20841
|
-
lockPath: explicitLockPath ??
|
|
21444
|
+
lockPath: explicitLockPath ?? path23.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
|
|
20842
21445
|
};
|
|
20843
21446
|
}
|
|
20844
21447
|
function formatProcessLockBusy(input) {
|
|
@@ -20858,7 +21461,7 @@ function formatProcessRecoveryPreview(input) {
|
|
|
20858
21461
|
const embeddingRetried = input.recovery.embedding.retriedFailed;
|
|
20859
21462
|
const vectorRetried = input.recovery.vector.retriedFailed;
|
|
20860
21463
|
const retriedTotal = embeddingRetried + vectorRetried;
|
|
20861
|
-
const oldestProcessingAge =
|
|
21464
|
+
const oldestProcessingAge = maxNullable2(
|
|
20862
21465
|
input.stats.embedding.oldestProcessingAgeMs,
|
|
20863
21466
|
input.stats.vector.oldestProcessingAgeMs
|
|
20864
21467
|
);
|
|
@@ -20873,7 +21476,7 @@ function formatProcessRecoveryPreview(input) {
|
|
|
20873
21476
|
`Next command: claude-memory-layer process --project ${quoteCliArg(input.projectPath)}`
|
|
20874
21477
|
].join("\n");
|
|
20875
21478
|
}
|
|
20876
|
-
function
|
|
21479
|
+
function maxNullable2(a, b) {
|
|
20877
21480
|
if (a === null) return b;
|
|
20878
21481
|
if (b === null) return a;
|
|
20879
21482
|
return Math.max(a, b);
|
|
@@ -20933,15 +21536,15 @@ function formatVectorStatusJsonReport(input) {
|
|
|
20933
21536
|
return JSON.stringify(buildVectorStatusReport(input), null, 2);
|
|
20934
21537
|
}
|
|
20935
21538
|
function buildVectorStatusReport(input) {
|
|
20936
|
-
const embedding =
|
|
20937
|
-
const vector =
|
|
20938
|
-
const totals =
|
|
21539
|
+
const embedding = normalizeQueue2(input.outbox.embedding);
|
|
21540
|
+
const vector = normalizeQueue2(input.outbox.vector);
|
|
21541
|
+
const totals = sumQueues2(embedding, vector);
|
|
20939
21542
|
const status = totals.failed > 0 || totals.stuckProcessing > 0 ? "needs-attention" : "ok";
|
|
20940
|
-
const oldestProcessingAgeMs =
|
|
21543
|
+
const oldestProcessingAgeMs = maxNullable3(embedding.oldestProcessingAgeMs, vector.oldestProcessingAgeMs);
|
|
20941
21544
|
return {
|
|
20942
21545
|
storage: {
|
|
20943
|
-
totalEvents:
|
|
20944
|
-
vectorCount:
|
|
21546
|
+
totalEvents: numberOrZero2(input.stats.totalEvents),
|
|
21547
|
+
vectorCount: numberOrZero2(input.stats.vectorCount)
|
|
20945
21548
|
},
|
|
20946
21549
|
outbox: { embedding, vector, totals },
|
|
20947
21550
|
status,
|
|
@@ -20954,19 +21557,19 @@ function selectRecommendedAction(totals) {
|
|
|
20954
21557
|
if ((totals.quarantinedFailed ?? 0) > 0) return "inspect-quarantined";
|
|
20955
21558
|
return "none";
|
|
20956
21559
|
}
|
|
20957
|
-
function
|
|
21560
|
+
function normalizeQueue2(queue) {
|
|
20958
21561
|
return {
|
|
20959
|
-
pending:
|
|
20960
|
-
processing:
|
|
20961
|
-
failed:
|
|
20962
|
-
retryableFailed:
|
|
20963
|
-
quarantinedFailed:
|
|
20964
|
-
total:
|
|
20965
|
-
stuckProcessing:
|
|
21562
|
+
pending: numberOrZero2(queue.pending),
|
|
21563
|
+
processing: numberOrZero2(queue.processing),
|
|
21564
|
+
failed: numberOrZero2(queue.failed),
|
|
21565
|
+
retryableFailed: numberOrZero2(queue.retryableFailed),
|
|
21566
|
+
quarantinedFailed: numberOrZero2(queue.quarantinedFailed),
|
|
21567
|
+
total: numberOrZero2(queue.total),
|
|
21568
|
+
stuckProcessing: numberOrZero2(queue.stuckProcessing),
|
|
20966
21569
|
oldestProcessingAgeMs: Number.isFinite(queue.oldestProcessingAgeMs ?? Number.NaN) ? queue.oldestProcessingAgeMs : null
|
|
20967
21570
|
};
|
|
20968
21571
|
}
|
|
20969
|
-
function
|
|
21572
|
+
function sumQueues2(a, b) {
|
|
20970
21573
|
return {
|
|
20971
21574
|
pending: a.pending + b.pending,
|
|
20972
21575
|
processing: a.processing + b.processing,
|
|
@@ -20975,7 +21578,7 @@ function sumQueues(a, b) {
|
|
|
20975
21578
|
quarantinedFailed: (a.quarantinedFailed ?? 0) + (b.quarantinedFailed ?? 0),
|
|
20976
21579
|
stuckProcessing: a.stuckProcessing + b.stuckProcessing,
|
|
20977
21580
|
total: a.total + b.total,
|
|
20978
|
-
oldestProcessingAgeMs:
|
|
21581
|
+
oldestProcessingAgeMs: maxNullable3(a.oldestProcessingAgeMs, b.oldestProcessingAgeMs)
|
|
20979
21582
|
};
|
|
20980
21583
|
}
|
|
20981
21584
|
function formatQueueRow(label, queue) {
|
|
@@ -20991,12 +21594,12 @@ function formatQueueRow(label, queue) {
|
|
|
20991
21594
|
formatDuration2(queue.oldestProcessingAgeMs).padStart(7)
|
|
20992
21595
|
].join(" ");
|
|
20993
21596
|
}
|
|
20994
|
-
function
|
|
21597
|
+
function maxNullable3(a, b) {
|
|
20995
21598
|
if (a === null) return b;
|
|
20996
21599
|
if (b === null) return a;
|
|
20997
21600
|
return Math.max(a, b);
|
|
20998
21601
|
}
|
|
20999
|
-
function
|
|
21602
|
+
function numberOrZero2(value) {
|
|
21000
21603
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
21001
21604
|
return Math.floor(value);
|
|
21002
21605
|
}
|
|
@@ -21016,12 +21619,12 @@ function formatDuration2(ms) {
|
|
|
21016
21619
|
}
|
|
21017
21620
|
|
|
21018
21621
|
// src/apps/cli/mongo-sync-command.ts
|
|
21019
|
-
import * as
|
|
21622
|
+
import * as path25 from "node:path";
|
|
21020
21623
|
|
|
21021
21624
|
// src/core/worker-lock.ts
|
|
21022
|
-
import { closeSync, existsSync as
|
|
21625
|
+
import { closeSync, existsSync as existsSync16, mkdirSync as mkdirSync10, openSync, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
21023
21626
|
import * as os12 from "node:os";
|
|
21024
|
-
import * as
|
|
21627
|
+
import * as path24 from "node:path";
|
|
21025
21628
|
import { randomUUID as randomUUID18 } from "node:crypto";
|
|
21026
21629
|
var WorkerLock = class {
|
|
21027
21630
|
lockPath;
|
|
@@ -21030,7 +21633,7 @@ var WorkerLock = class {
|
|
|
21030
21633
|
now;
|
|
21031
21634
|
isProcessRunning;
|
|
21032
21635
|
acquired = false;
|
|
21033
|
-
constructor(lockPath =
|
|
21636
|
+
constructor(lockPath = path24.join(os12.tmpdir(), "claude-memory-layer", "vector-worker.lock"), options = {}) {
|
|
21034
21637
|
this.lockPath = lockPath;
|
|
21035
21638
|
this.pid = options.pid ?? process.pid;
|
|
21036
21639
|
this.ownerId = options.ownerId ?? randomUUID18();
|
|
@@ -21043,7 +21646,7 @@ var WorkerLock = class {
|
|
|
21043
21646
|
acquire() {
|
|
21044
21647
|
let staleRecovered = false;
|
|
21045
21648
|
for (; ; ) {
|
|
21046
|
-
mkdirSync10(
|
|
21649
|
+
mkdirSync10(path24.dirname(this.lockPath), { recursive: true });
|
|
21047
21650
|
const createResult = this.tryCreateLockFile();
|
|
21048
21651
|
if (createResult === "created") {
|
|
21049
21652
|
this.acquired = true;
|
|
@@ -21077,7 +21680,7 @@ var WorkerLock = class {
|
|
|
21077
21680
|
}
|
|
21078
21681
|
}
|
|
21079
21682
|
release() {
|
|
21080
|
-
if (!this.acquired || !
|
|
21683
|
+
if (!this.acquired || !existsSync16(this.lockPath)) return false;
|
|
21081
21684
|
const existing = this.readPayload();
|
|
21082
21685
|
if (!this.isOwnedPayload(existing)) return false;
|
|
21083
21686
|
try {
|
|
@@ -21175,7 +21778,7 @@ function resolveMongoSyncProcessOptions(options, cwd = process.cwd(), deps = {})
|
|
|
21175
21778
|
projectPath,
|
|
21176
21779
|
processAfterSync: options.processAfterSync === true,
|
|
21177
21780
|
processIntervalMs,
|
|
21178
|
-
lockPath: explicitLockPath ??
|
|
21781
|
+
lockPath: explicitLockPath ?? path25.join(getProjectStoragePath2(projectPath), "vector-worker.lock")
|
|
21179
21782
|
};
|
|
21180
21783
|
}
|
|
21181
21784
|
function createMongoSyncPostProcessor(options, deps = {}) {
|
|
@@ -21258,28 +21861,28 @@ function parsePositiveIntegerOption2(raw, flagName) {
|
|
|
21258
21861
|
}
|
|
21259
21862
|
|
|
21260
21863
|
// src/apps/cli/index.ts
|
|
21261
|
-
var CLAUDE_SETTINGS_PATH =
|
|
21864
|
+
var CLAUDE_SETTINGS_PATH = path26.join(os13.homedir(), ".claude", "settings.json");
|
|
21262
21865
|
function getPluginPath() {
|
|
21263
21866
|
const possiblePaths = [
|
|
21264
|
-
|
|
21867
|
+
path26.join(__dirname, ".."),
|
|
21265
21868
|
// When running from dist/cli
|
|
21266
|
-
|
|
21869
|
+
path26.join(__dirname, "../..", "dist"),
|
|
21267
21870
|
// When running from src
|
|
21268
|
-
|
|
21871
|
+
path26.join(process.cwd(), "dist")
|
|
21269
21872
|
// Current working directory
|
|
21270
21873
|
];
|
|
21271
21874
|
for (const p of possiblePaths) {
|
|
21272
|
-
const hooksPath =
|
|
21273
|
-
if (
|
|
21875
|
+
const hooksPath = path26.join(p, "hooks", "user-prompt-submit.js");
|
|
21876
|
+
if (fs20.existsSync(hooksPath)) {
|
|
21274
21877
|
return p;
|
|
21275
21878
|
}
|
|
21276
21879
|
}
|
|
21277
|
-
return
|
|
21880
|
+
return path26.join(os13.homedir(), ".npm-global", "lib", "node_modules", "claude-memory-layer", "dist");
|
|
21278
21881
|
}
|
|
21279
21882
|
function loadClaudeSettings() {
|
|
21280
21883
|
try {
|
|
21281
|
-
if (
|
|
21282
|
-
const content =
|
|
21884
|
+
if (fs20.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
21885
|
+
const content = fs20.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
21283
21886
|
return JSON.parse(content);
|
|
21284
21887
|
}
|
|
21285
21888
|
} catch (error) {
|
|
@@ -21288,13 +21891,52 @@ function loadClaudeSettings() {
|
|
|
21288
21891
|
return {};
|
|
21289
21892
|
}
|
|
21290
21893
|
function saveClaudeSettings(settings) {
|
|
21291
|
-
const dir =
|
|
21292
|
-
if (!
|
|
21293
|
-
|
|
21894
|
+
const dir = path26.dirname(CLAUDE_SETTINGS_PATH);
|
|
21895
|
+
if (!fs20.existsSync(dir)) {
|
|
21896
|
+
fs20.mkdirSync(dir, { recursive: true });
|
|
21294
21897
|
}
|
|
21295
21898
|
const tempPath = CLAUDE_SETTINGS_PATH + ".tmp";
|
|
21296
|
-
|
|
21297
|
-
|
|
21899
|
+
fs20.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
|
|
21900
|
+
fs20.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
|
|
21901
|
+
}
|
|
21902
|
+
function resolveHealthProjectIdentity(projectPath) {
|
|
21903
|
+
if (!projectPath || projectPath.trim().length === 0) return { scope: "global", id: "global" };
|
|
21904
|
+
const normalized = projectPath.trim();
|
|
21905
|
+
return { scope: "project", id: /^[a-f0-9]{8}$/.test(normalized) ? normalized : hashProjectPath(normalized) };
|
|
21906
|
+
}
|
|
21907
|
+
function resolveHealthStoragePath(projectPathOrHash) {
|
|
21908
|
+
const normalized = projectPathOrHash?.trim();
|
|
21909
|
+
return normalized && normalized.length > 0 ? resolveProjectStoragePath(normalized) : path26.join(os13.homedir(), ".claude-code", "memory");
|
|
21910
|
+
}
|
|
21911
|
+
function createHealthMemoryService(projectPathOrHash) {
|
|
21912
|
+
return new MemoryService({
|
|
21913
|
+
storagePath: resolveHealthStoragePath(projectPathOrHash),
|
|
21914
|
+
readOnly: true,
|
|
21915
|
+
lightweightMode: true,
|
|
21916
|
+
analyticsEnabled: false,
|
|
21917
|
+
sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
|
|
21918
|
+
});
|
|
21919
|
+
}
|
|
21920
|
+
function healthStoreExists(projectPathOrHash) {
|
|
21921
|
+
return fs20.existsSync(path26.join(resolveHealthStoragePath(projectPathOrHash), "events.sqlite"));
|
|
21922
|
+
}
|
|
21923
|
+
function emptyHealthStats() {
|
|
21924
|
+
return { totalEvents: 0, vectorCount: 0, levelStats: [] };
|
|
21925
|
+
}
|
|
21926
|
+
function emptyHealthQueue() {
|
|
21927
|
+
return {
|
|
21928
|
+
pending: 0,
|
|
21929
|
+
processing: 0,
|
|
21930
|
+
failed: 0,
|
|
21931
|
+
retryableFailed: 0,
|
|
21932
|
+
quarantinedFailed: 0,
|
|
21933
|
+
total: 0,
|
|
21934
|
+
stuckProcessing: 0,
|
|
21935
|
+
oldestProcessingAgeMs: null
|
|
21936
|
+
};
|
|
21937
|
+
}
|
|
21938
|
+
function emptyHealthOutbox() {
|
|
21939
|
+
return { embedding: emptyHealthQueue(), vector: emptyHealthQueue() };
|
|
21298
21940
|
}
|
|
21299
21941
|
function parseCommaList(value) {
|
|
21300
21942
|
if (!value) return void 0;
|
|
@@ -21354,7 +21996,7 @@ async function runCodexValidationCommand(options) {
|
|
|
21354
21996
|
process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}
|
|
21355
21997
|
`);
|
|
21356
21998
|
if (options.output) {
|
|
21357
|
-
const outputPath =
|
|
21999
|
+
const outputPath = path26.resolve(options.output);
|
|
21358
22000
|
writeCodexValidationReport(outputPath, report, format);
|
|
21359
22001
|
console.log(`
|
|
21360
22002
|
Report written to ${outputPath}`);
|
|
@@ -21374,7 +22016,7 @@ async function runHermesValidationCommand(options) {
|
|
|
21374
22016
|
process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}
|
|
21375
22017
|
`);
|
|
21376
22018
|
if (options.output) {
|
|
21377
|
-
const outputPath =
|
|
22019
|
+
const outputPath = path26.resolve(options.output);
|
|
21378
22020
|
writeHermesValidationReport(outputPath, report, format);
|
|
21379
22021
|
console.log(`
|
|
21380
22022
|
Report written to ${outputPath}`);
|
|
@@ -21408,18 +22050,18 @@ var OPERATION_PRIVACY_CONFIG = {
|
|
|
21408
22050
|
}
|
|
21409
22051
|
};
|
|
21410
22052
|
function resolveOperationProject(project) {
|
|
21411
|
-
const projectPath =
|
|
22053
|
+
const projectPath = path26.resolve(project ?? process.cwd());
|
|
21412
22054
|
const projectHash = hashProjectPath(projectPath);
|
|
21413
22055
|
const storagePath = getProjectStoragePath(projectPath);
|
|
21414
22056
|
return {
|
|
21415
22057
|
projectPath,
|
|
21416
22058
|
projectHash,
|
|
21417
22059
|
storagePath,
|
|
21418
|
-
dbPath:
|
|
22060
|
+
dbPath: path26.join(storagePath, "events.sqlite")
|
|
21419
22061
|
};
|
|
21420
22062
|
}
|
|
21421
22063
|
function openOperationReadDatabase(context) {
|
|
21422
|
-
if (!
|
|
22064
|
+
if (!fs20.existsSync(context.dbPath)) return null;
|
|
21423
22065
|
return createSQLiteDatabase(context.dbPath, { readonly: true, walMode: false });
|
|
21424
22066
|
}
|
|
21425
22067
|
async function withOperationWriteDatabase(context, callback) {
|
|
@@ -21629,7 +22271,7 @@ async function runOperationCli(action, label) {
|
|
|
21629
22271
|
}
|
|
21630
22272
|
}
|
|
21631
22273
|
var program = new Command();
|
|
21632
|
-
program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.
|
|
22274
|
+
program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.55");
|
|
21633
22275
|
program.command("market-context").description("Fetch read-only DART/FRED/Finnhub context with structured MarketContextSnapshot bull/bear/risk/catalyst analysis").option("--company <name>", "Company name for DART fallback search and report subject").option("--dart-corp-code <code>", "Exact DART corp_code for issuer-specific filings").option("--symbol <ticker>", "Listed ticker for Finnhub company profile").option("--providers <list>", "Comma-separated providers: dart,fred,finnhub").option("--fred-series <list>", "Comma-separated FRED series IDs").option("--json", "Print structured JSON including analysis.marketSnapshot").option("--no-snapshot", "Disable MarketContextSnapshot and DART company snapshot analysis").action(async (options) => {
|
|
21634
22276
|
try {
|
|
21635
22277
|
await runMarketContextCommand(options);
|
|
@@ -21642,7 +22284,7 @@ program.command("install").description("Install hooks into Claude Code settings"
|
|
|
21642
22284
|
try {
|
|
21643
22285
|
const pluginPath = options.path || getPluginPath();
|
|
21644
22286
|
const missingHooks = REQUIRED_HOOK_FILES.filter(
|
|
21645
|
-
(file) => !
|
|
22287
|
+
(file) => !fs20.existsSync(path26.join(pluginPath, "hooks", file))
|
|
21646
22288
|
);
|
|
21647
22289
|
if (missingHooks.length > 0) {
|
|
21648
22290
|
console.error(`
|
|
@@ -21708,7 +22350,7 @@ program.command("status").description("Check plugin installation status").action
|
|
|
21708
22350
|
console.log(` PostToolUse: ${hasPostToolHook ? "\u2705 Installed" : "\u274C Not installed"}`);
|
|
21709
22351
|
console.log(` Stop: ${hasStopHook ? "\u2705 Installed" : "\u274C Not installed"}`);
|
|
21710
22352
|
console.log(` SessionEnd: ${hasSessionEndHook ? "\u2705 Installed" : "\u274C Not installed"}`);
|
|
21711
|
-
const hooksExist = REQUIRED_HOOK_FILES.every((file) =>
|
|
22353
|
+
const hooksExist = REQUIRED_HOOK_FILES.every((file) => fs20.existsSync(path26.join(pluginPath, "hooks", file)));
|
|
21712
22354
|
console.log(`
|
|
21713
22355
|
Plugin files: ${hooksExist ? "\u2705 Found" : "\u274C Not found"}`);
|
|
21714
22356
|
console.log(` Path: ${pluginPath}`);
|
|
@@ -21843,6 +22485,40 @@ program.command("stats").description("View memory statistics").option("-p, --pro
|
|
|
21843
22485
|
process.exit(1);
|
|
21844
22486
|
}
|
|
21845
22487
|
});
|
|
22488
|
+
program.command("health").description("Show aggregate health reports for agent productivity workflows").option("--productivity", "Print Project Health Report for agent productivity workflows").option("--json", "Print health report as JSON").option("-p, --project <path>", "Project path or 8-character project hash").option("--profile <profile>", "Agent profile: coder, reviewer, pm, support, researcher, team", "coder").option("--mode <mode>", "Report mode: observe, preview, enforce", "preview").action(async (options) => {
|
|
22489
|
+
let service;
|
|
22490
|
+
try {
|
|
22491
|
+
if (options.productivity !== true) {
|
|
22492
|
+
throw new Error("Use --productivity to select the Project Health Report");
|
|
22493
|
+
}
|
|
22494
|
+
if (options.json !== true) {
|
|
22495
|
+
throw new Error("Only --json output is currently supported for productivity health");
|
|
22496
|
+
}
|
|
22497
|
+
const profile = parseProductivityHealthProfile(options.profile);
|
|
22498
|
+
const mode = parseProductivityHealthMode(options.mode);
|
|
22499
|
+
const projectPath = options.project ?? process.cwd();
|
|
22500
|
+
const project = resolveHealthProjectIdentity(projectPath);
|
|
22501
|
+
let stats;
|
|
22502
|
+
let outbox;
|
|
22503
|
+
if (healthStoreExists(projectPath)) {
|
|
22504
|
+
service = createHealthMemoryService(projectPath);
|
|
22505
|
+
[stats, outbox] = await Promise.all([
|
|
22506
|
+
service.getStats(),
|
|
22507
|
+
service.getOutboxStats()
|
|
22508
|
+
]);
|
|
22509
|
+
} else {
|
|
22510
|
+
stats = emptyHealthStats();
|
|
22511
|
+
outbox = emptyHealthOutbox();
|
|
22512
|
+
}
|
|
22513
|
+
console.log(JSON.stringify(buildProductivityHealthReport({ stats, outbox, project, profile, mode }), null, 2));
|
|
22514
|
+
} catch (error) {
|
|
22515
|
+
const message = error instanceof Error ? error.message : "Health report failed";
|
|
22516
|
+
console.error(message.startsWith("Invalid --") || message.startsWith("Use --") || message.startsWith("Only --") ? message : "Health report failed");
|
|
22517
|
+
process.exit(1);
|
|
22518
|
+
} finally {
|
|
22519
|
+
await service?.shutdown().catch(() => void 0);
|
|
22520
|
+
}
|
|
22521
|
+
});
|
|
21846
22522
|
program.command("vector-status").description("Show aggregate vector outbox status").option("-p, --project <path>", "Project path (defaults to cwd)").option("--json", "Print aggregate vector outbox status as JSON for automation").action(async (options) => {
|
|
21847
22523
|
let service;
|
|
21848
22524
|
try {
|
|
@@ -21943,8 +22619,8 @@ repairCommand.command("legacy-project-scope").description("Dry-run or apply proj
|
|
|
21943
22619
|
apply: options.apply
|
|
21944
22620
|
});
|
|
21945
22621
|
const storagePath = projectPath ? getProjectStoragePath(projectPath) : resolveProjectStoragePath(repairOptions.projectHash);
|
|
21946
|
-
const dbPath =
|
|
21947
|
-
if (repairOptions.dryRun && !
|
|
22622
|
+
const dbPath = path26.join(storagePath, "events.sqlite");
|
|
22623
|
+
if (repairOptions.dryRun && !fs20.existsSync(dbPath)) {
|
|
21948
22624
|
const projectHash = repairOptions.projectHash || hashProjectPath(repairOptions.projectPath);
|
|
21949
22625
|
console.log(formatLegacyProjectScopeRepairResult({
|
|
21950
22626
|
dryRun: true,
|
|
@@ -22191,8 +22867,8 @@ retentionCommand.command("audit").description("Run a dry-run retention audit for
|
|
|
22191
22867
|
});
|
|
22192
22868
|
const projectHash = auditOptions.projectHash ?? hashProjectPath(auditOptions.projectPath);
|
|
22193
22869
|
const storagePath = auditOptions.projectPath ? getProjectStoragePath(auditOptions.projectPath) : resolveProjectStoragePath(projectHash);
|
|
22194
|
-
const dbPath =
|
|
22195
|
-
if (!
|
|
22870
|
+
const dbPath = path26.join(storagePath, "events.sqlite");
|
|
22871
|
+
if (!fs20.existsSync(dbPath)) {
|
|
22196
22872
|
const emptyReport = emptyRetentionAuditReport(projectHash, auditOptions.limit);
|
|
22197
22873
|
console.log(formatRetentionAuditReport(emptyReport, { json: auditOptions.json }));
|
|
22198
22874
|
return;
|
|
@@ -22219,7 +22895,7 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
|
|
|
22219
22895
|
const projectPath = options.project || process.cwd();
|
|
22220
22896
|
const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;
|
|
22221
22897
|
const mongoDb = options.mongoDb || process.env.CLAUDE_MEMORY_MONGO_DB;
|
|
22222
|
-
const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT ||
|
|
22898
|
+
const projectKey = options.mongoProject || process.env.CLAUDE_MEMORY_MONGO_PROJECT || path26.basename(projectPath);
|
|
22223
22899
|
const direction = String(options.direction || "both").toLowerCase();
|
|
22224
22900
|
if (!mongoUri || !mongoDb) {
|
|
22225
22901
|
console.error("\n\u274C MongoDB sync is not configured.");
|
|
@@ -22249,14 +22925,14 @@ program.command("mongo-sync").description("Sync events with MongoDB for multi-se
|
|
|
22249
22925
|
}
|
|
22250
22926
|
})();
|
|
22251
22927
|
const storagePath = getProjectStoragePath(projectPath);
|
|
22252
|
-
if (!
|
|
22253
|
-
|
|
22928
|
+
if (!fs20.existsSync(storagePath)) {
|
|
22929
|
+
fs20.mkdirSync(storagePath, { recursive: true });
|
|
22254
22930
|
}
|
|
22255
22931
|
const postProcessor = createMongoSyncPostProcessor(processOptions, {
|
|
22256
22932
|
log: (message) => process.stdout.write(`${message}
|
|
22257
22933
|
`)
|
|
22258
22934
|
});
|
|
22259
|
-
const sqliteStore = new SQLiteEventStore(
|
|
22935
|
+
const sqliteStore = new SQLiteEventStore(path26.join(storagePath, "events.sqlite"));
|
|
22260
22936
|
const worker = new MongoSyncWorker(sqliteStore, {
|
|
22261
22937
|
uri: mongoUri,
|
|
22262
22938
|
dbName: mongoDb,
|
|
@@ -22316,7 +22992,7 @@ function renderProgress(event) {
|
|
|
22316
22992
|
break;
|
|
22317
22993
|
case "session-start": {
|
|
22318
22994
|
const pct = Math.round(event.sessionIndex / event.totalSessions * 100);
|
|
22319
|
-
const sessionName =
|
|
22995
|
+
const sessionName = path26.basename(event.filePath, ".jsonl").slice(0, 8);
|
|
22320
22996
|
process.stdout.write(
|
|
22321
22997
|
`\r \u{1F4C4} [${event.sessionIndex + 1}/${event.totalSessions}] ${pct}% | Session ${sessionName}... `
|
|
22322
22998
|
);
|
|
@@ -22391,9 +23067,9 @@ async function listMarkdownFiles(root) {
|
|
|
22391
23067
|
const stack = [root];
|
|
22392
23068
|
while (stack.length > 0) {
|
|
22393
23069
|
const dir = stack.pop();
|
|
22394
|
-
const entries = await
|
|
23070
|
+
const entries = await fs20.promises.readdir(dir, { withFileTypes: true });
|
|
22395
23071
|
for (const e of entries) {
|
|
22396
|
-
const full =
|
|
23072
|
+
const full = path26.join(dir, e.name);
|
|
22397
23073
|
if (e.isDirectory()) stack.push(full);
|
|
22398
23074
|
else if (e.isFile() && e.name.endsWith(".md") && e.name !== "_index.md") out.push(full);
|
|
22399
23075
|
}
|
|
@@ -22401,8 +23077,8 @@ async function listMarkdownFiles(root) {
|
|
|
22401
23077
|
return out.sort();
|
|
22402
23078
|
}
|
|
22403
23079
|
function deriveNamespaceCategory(sourceRoot, filePath) {
|
|
22404
|
-
const rel =
|
|
22405
|
-
const dirSeg =
|
|
23080
|
+
const rel = path26.relative(sourceRoot, filePath);
|
|
23081
|
+
const dirSeg = path26.dirname(rel).split(path26.sep).filter(Boolean);
|
|
22406
23082
|
if (dirSeg.length >= 2) {
|
|
22407
23083
|
const namespace = sanitizeSegment3(dirSeg[0], "default");
|
|
22408
23084
|
const categoryPath = dirSeg.slice(1).map((s) => sanitizeSegment3(s, "uncategorized"));
|
|
@@ -22421,10 +23097,10 @@ function extractImportEvidence(markdown) {
|
|
|
22421
23097
|
program.command("organize-import [sourceDir]").description("Import existing markdown memory files, or bootstrap knowledge docs from codebase/git when markdown is missing").option("-p, --project <path>", "Project path (defaults to cwd)").option("--session <id>", "Session id for imported events (default: import:organized)").option("--limit <n>", "Limit number of files to import").option("--dry-run", "Preview mapping without writing").option("--bootstrap", "Force-generate structured markdown from codebase + git history before import").option("--bootstrap-if-empty", "Auto-bootstrap when source has no markdown files (default: true)", true).option("--no-bootstrap-if-empty", "Disable auto-bootstrap when source has no markdown files").option("--force-bootstrap", "Run bootstrap even when markdown files exist").option("--repo <path>", "Repository root for bootstrap analysis (default: project path)").option("--out <path>", "Output directory for generated bootstrap markdown (default: <sourceDir>/bootstrap-kb)").option("--since <range>", 'Git history range for bootstrap (default: "180 days ago")').option("--max-commits <n>", "Max commits to analyze for bootstrap (default: 1000)").option("--incremental", "Use previous bootstrap manifest as baseline for incremental updates (default: true)", true).option("--no-incremental", "Disable incremental bootstrap; regenerate full snapshot").action(async (sourceDir, options) => {
|
|
22422
23098
|
const projectPath = options.project || process.cwd();
|
|
22423
23099
|
const sessionId = options.session || "import:organized";
|
|
22424
|
-
const sourceRoot =
|
|
22425
|
-
const repoPath =
|
|
22426
|
-
if (!
|
|
22427
|
-
|
|
23100
|
+
const sourceRoot = path26.resolve(sourceDir || options.out || projectPath);
|
|
23101
|
+
const repoPath = path26.resolve(options.repo || projectPath);
|
|
23102
|
+
if (!fs20.existsSync(sourceRoot)) {
|
|
23103
|
+
fs20.mkdirSync(sourceRoot, { recursive: true });
|
|
22428
23104
|
}
|
|
22429
23105
|
const service = getMemoryServiceForProject(projectPath);
|
|
22430
23106
|
try {
|
|
@@ -22434,7 +23110,7 @@ program.command("organize-import [sourceDir]").description("Import existing mark
|
|
|
22434
23110
|
const hasMarkdown = files.length > 0;
|
|
22435
23111
|
const shouldBootstrap = Boolean(options.forceBootstrap || options.bootstrap || !hasMarkdown && options.bootstrapIfEmpty);
|
|
22436
23112
|
if (shouldBootstrap) {
|
|
22437
|
-
const outDir =
|
|
23113
|
+
const outDir = path26.resolve(options.out || path26.join(sourceRoot, "bootstrap-kb"));
|
|
22438
23114
|
const since = options.since || "180 days ago";
|
|
22439
23115
|
const maxCommits = options.maxCommits ? Math.max(1, parseInt(options.maxCommits, 10)) : 1e3;
|
|
22440
23116
|
console.log("\n\u{1F9E0} Bootstrapping markdown knowledge base...");
|
|
@@ -22473,13 +23149,13 @@ program.command("organize-import [sourceDir]").description("Import existing mark
|
|
|
22473
23149
|
let imported = 0;
|
|
22474
23150
|
let skipped = 0;
|
|
22475
23151
|
for (const file of targets) {
|
|
22476
|
-
const text = await
|
|
23152
|
+
const text = await fs20.promises.readFile(file, "utf8");
|
|
22477
23153
|
if (!text.trim()) {
|
|
22478
23154
|
skipped += 1;
|
|
22479
23155
|
continue;
|
|
22480
23156
|
}
|
|
22481
23157
|
const { namespace, categoryPath } = deriveNamespaceCategory(activeSourceRoot, file);
|
|
22482
|
-
const rel =
|
|
23158
|
+
const rel = path26.relative(activeSourceRoot, file);
|
|
22483
23159
|
const evidence = extractImportEvidence(text);
|
|
22484
23160
|
if (options.dryRun) {
|
|
22485
23161
|
console.log(`- ${rel} -> namespace=${namespace} category=${categoryPath.join("/")} confidence=${evidence.confidence || "n/a"} sources=${evidence.sources.length}`);
|