claude-memory-layer 1.0.18 → 1.0.20
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/config/kpi-thresholds.json +7 -0
- package/dist/cli/index.js +532 -79
- package/dist/cli/index.js.map +3 -3
- package/dist/core/index.js +49 -4
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +140 -3
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +140 -3
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +140 -3
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +140 -3
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +379 -34
- package/dist/hooks/user-prompt-submit.js.map +3 -3
- package/dist/server/api/index.js +467 -34
- package/dist/server/api/index.js.map +3 -3
- package/dist/server/index.js +474 -41
- package/dist/server/index.js.map +3 -3
- package/dist/services/memory-service.js +140 -3
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +362 -4
- package/dist/ui/index.html +90 -0
- package/dist/ui/style.css +41 -0
- package/memory/_index.md +3 -0
- package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
- package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
- package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
- package/package.json +3 -2
- package/scripts/delete-unknown-projects.js +154 -0
- package/src/cli/index.ts +23 -1
- package/src/core/embedder.ts +3 -2
- package/src/core/sqlite-event-store.ts +32 -0
- package/src/core/types.ts +2 -2
- package/src/core/vector-store.ts +20 -0
- package/src/hooks/user-prompt-submit.ts +225 -29
- package/src/server/api/events.ts +7 -0
- package/src/server/api/stats.ts +346 -0
- package/src/services/memory-service.ts +119 -2
- package/src/ui/app.js +362 -4
- package/src/ui/index.html +90 -0
- package/src/ui/style.css +41 -0
package/dist/server/api/index.js
CHANGED
|
@@ -83,57 +83,57 @@ function toDate(value) {
|
|
|
83
83
|
return new Date(value);
|
|
84
84
|
return new Date(String(value));
|
|
85
85
|
}
|
|
86
|
-
function createDatabase(
|
|
86
|
+
function createDatabase(path7, options) {
|
|
87
87
|
if (options?.readOnly) {
|
|
88
|
-
return new duckdb.Database(
|
|
88
|
+
return new duckdb.Database(path7, { access_mode: "READ_ONLY" });
|
|
89
89
|
}
|
|
90
|
-
return new duckdb.Database(
|
|
90
|
+
return new duckdb.Database(path7);
|
|
91
91
|
}
|
|
92
92
|
function dbRun(db, sql, params = []) {
|
|
93
|
-
return new Promise((
|
|
93
|
+
return new Promise((resolve3, reject) => {
|
|
94
94
|
if (params.length === 0) {
|
|
95
95
|
db.run(sql, (err) => {
|
|
96
96
|
if (err)
|
|
97
97
|
reject(err);
|
|
98
98
|
else
|
|
99
|
-
|
|
99
|
+
resolve3();
|
|
100
100
|
});
|
|
101
101
|
} else {
|
|
102
102
|
db.run(sql, ...params, (err) => {
|
|
103
103
|
if (err)
|
|
104
104
|
reject(err);
|
|
105
105
|
else
|
|
106
|
-
|
|
106
|
+
resolve3();
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
function dbAll(db, sql, params = []) {
|
|
112
|
-
return new Promise((
|
|
112
|
+
return new Promise((resolve3, reject) => {
|
|
113
113
|
if (params.length === 0) {
|
|
114
114
|
db.all(sql, (err, rows) => {
|
|
115
115
|
if (err)
|
|
116
116
|
reject(err);
|
|
117
117
|
else
|
|
118
|
-
|
|
118
|
+
resolve3(convertBigInts(rows || []));
|
|
119
119
|
});
|
|
120
120
|
} else {
|
|
121
121
|
db.all(sql, ...params, (err, rows) => {
|
|
122
122
|
if (err)
|
|
123
123
|
reject(err);
|
|
124
124
|
else
|
|
125
|
-
|
|
125
|
+
resolve3(convertBigInts(rows || []));
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
function dbClose(db) {
|
|
131
|
-
return new Promise((
|
|
131
|
+
return new Promise((resolve3, reject) => {
|
|
132
132
|
db.close((err) => {
|
|
133
133
|
if (err)
|
|
134
134
|
reject(err);
|
|
135
135
|
else
|
|
136
|
-
|
|
136
|
+
resolve3();
|
|
137
137
|
});
|
|
138
138
|
});
|
|
139
139
|
}
|
|
@@ -771,12 +771,12 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
771
771
|
import Database from "better-sqlite3";
|
|
772
772
|
import * as fs from "fs";
|
|
773
773
|
import * as nodePath from "path";
|
|
774
|
-
function createSQLiteDatabase(
|
|
775
|
-
const dir = nodePath.dirname(
|
|
774
|
+
function createSQLiteDatabase(path7, options) {
|
|
775
|
+
const dir = nodePath.dirname(path7);
|
|
776
776
|
if (!fs.existsSync(dir)) {
|
|
777
777
|
fs.mkdirSync(dir, { recursive: true });
|
|
778
778
|
}
|
|
779
|
-
const db = new Database(
|
|
779
|
+
const db = new Database(path7, {
|
|
780
780
|
readonly: options?.readonly ?? false
|
|
781
781
|
});
|
|
782
782
|
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
@@ -1615,6 +1615,33 @@ var SQLiteEventStore = class {
|
|
|
1615
1615
|
ids
|
|
1616
1616
|
);
|
|
1617
1617
|
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Clear embedding outbox (used for embedding model migration)
|
|
1620
|
+
*/
|
|
1621
|
+
async clearEmbeddingOutbox() {
|
|
1622
|
+
await this.initialize();
|
|
1623
|
+
sqliteRun(this.db, `DELETE FROM embedding_outbox`);
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Count total events
|
|
1627
|
+
*/
|
|
1628
|
+
async countEvents() {
|
|
1629
|
+
await this.initialize();
|
|
1630
|
+
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
|
|
1631
|
+
return row?.count || 0;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Get events page in timestamp ascending order (stable migration/reindex scans)
|
|
1635
|
+
*/
|
|
1636
|
+
async getEventsPage(limit = 1e3, offset = 0) {
|
|
1637
|
+
await this.initialize();
|
|
1638
|
+
const rows = sqliteAll(
|
|
1639
|
+
this.db,
|
|
1640
|
+
`SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
1641
|
+
[limit, offset]
|
|
1642
|
+
);
|
|
1643
|
+
return rows.map(this.rowToEvent);
|
|
1644
|
+
}
|
|
1618
1645
|
/**
|
|
1619
1646
|
* Mark outbox items as failed
|
|
1620
1647
|
*/
|
|
@@ -2422,7 +2449,7 @@ var SyncWorker = class {
|
|
|
2422
2449
|
* Sleep utility
|
|
2423
2450
|
*/
|
|
2424
2451
|
sleep(ms) {
|
|
2425
|
-
return new Promise((
|
|
2452
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2426
2453
|
}
|
|
2427
2454
|
/**
|
|
2428
2455
|
* Get sync statistics
|
|
@@ -2580,6 +2607,23 @@ var VectorStore = class {
|
|
|
2580
2607
|
const result = await this.table.countRows();
|
|
2581
2608
|
return result;
|
|
2582
2609
|
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Clear all vectors (used for embedding model migration)
|
|
2612
|
+
*/
|
|
2613
|
+
async clearAll() {
|
|
2614
|
+
await this.initialize();
|
|
2615
|
+
if (!this.db)
|
|
2616
|
+
return;
|
|
2617
|
+
try {
|
|
2618
|
+
if (typeof this.db.dropTable === "function") {
|
|
2619
|
+
await this.db.dropTable(this.tableName);
|
|
2620
|
+
} else if (typeof this.db.drop_table === "function") {
|
|
2621
|
+
await this.db.drop_table(this.tableName);
|
|
2622
|
+
}
|
|
2623
|
+
} catch {
|
|
2624
|
+
}
|
|
2625
|
+
this.table = null;
|
|
2626
|
+
}
|
|
2583
2627
|
/**
|
|
2584
2628
|
* Check if vector exists for event
|
|
2585
2629
|
*/
|
|
@@ -2597,7 +2641,7 @@ var Embedder = class {
|
|
|
2597
2641
|
pipeline = null;
|
|
2598
2642
|
modelName;
|
|
2599
2643
|
initialized = false;
|
|
2600
|
-
constructor(modelName = "
|
|
2644
|
+
constructor(modelName = "jinaai/jina-embeddings-v5-text-nano") {
|
|
2601
2645
|
this.modelName = modelName;
|
|
2602
2646
|
}
|
|
2603
2647
|
/**
|
|
@@ -2677,8 +2721,9 @@ var Embedder = class {
|
|
|
2677
2721
|
};
|
|
2678
2722
|
var defaultEmbedder = null;
|
|
2679
2723
|
function getDefaultEmbedder() {
|
|
2724
|
+
const envModel = process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
|
|
2680
2725
|
if (!defaultEmbedder) {
|
|
2681
|
-
defaultEmbedder = new Embedder();
|
|
2726
|
+
defaultEmbedder = new Embedder(envModel || void 0);
|
|
2682
2727
|
}
|
|
2683
2728
|
return defaultEmbedder;
|
|
2684
2729
|
}
|
|
@@ -3452,8 +3497,8 @@ _Context:_ ${sessionContext}`;
|
|
|
3452
3497
|
matchesMetadataScope(metadata, expected) {
|
|
3453
3498
|
if (!metadata)
|
|
3454
3499
|
return false;
|
|
3455
|
-
return Object.entries(expected).every(([
|
|
3456
|
-
const actual =
|
|
3500
|
+
return Object.entries(expected).every(([path7, value]) => {
|
|
3501
|
+
const actual = path7.split(".").reduce((acc, key) => {
|
|
3457
3502
|
if (typeof acc !== "object" || acc === null)
|
|
3458
3503
|
return void 0;
|
|
3459
3504
|
return acc[key];
|
|
@@ -5909,8 +5954,10 @@ var MemoryService = class {
|
|
|
5909
5954
|
readOnly;
|
|
5910
5955
|
lightweightMode;
|
|
5911
5956
|
mdMirror;
|
|
5957
|
+
storagePath;
|
|
5912
5958
|
constructor(config) {
|
|
5913
5959
|
const storagePath = this.expandPath(config.storagePath);
|
|
5960
|
+
this.storagePath = storagePath;
|
|
5914
5961
|
this.readOnly = config.readOnly ?? false;
|
|
5915
5962
|
this.lightweightMode = config.lightweightMode ?? false;
|
|
5916
5963
|
this.mdMirror = new MarkdownMirror2(process.cwd());
|
|
@@ -5946,7 +5993,8 @@ var MemoryService = class {
|
|
|
5946
5993
|
);
|
|
5947
5994
|
}
|
|
5948
5995
|
this.vectorStore = new VectorStore(path3.join(storagePath, "vectors"));
|
|
5949
|
-
|
|
5996
|
+
const embeddingModel = config.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
|
|
5997
|
+
this.embedder = embeddingModel ? new Embedder(embeddingModel) : getDefaultEmbedder();
|
|
5950
5998
|
this.matcher = getDefaultMatcher();
|
|
5951
5999
|
this.retriever = createRetriever(
|
|
5952
6000
|
this.sqliteStore,
|
|
@@ -6890,6 +6938,95 @@ var MemoryService = class {
|
|
|
6890
6938
|
recordMemoryAccess(eventId, sessionId, confidence = 1) {
|
|
6891
6939
|
this.graduation.recordAccess(eventId, sessionId, confidence);
|
|
6892
6940
|
}
|
|
6941
|
+
getEmbeddingModelName() {
|
|
6942
|
+
return this.embedder.getModelName();
|
|
6943
|
+
}
|
|
6944
|
+
/**
|
|
6945
|
+
* Ensure embedding model metadata is in sync and optionally migrate vectors.
|
|
6946
|
+
* Migration strategy: clear vector index + clear embedding outbox + re-enqueue all events.
|
|
6947
|
+
*/
|
|
6948
|
+
async ensureEmbeddingModelForImport(options) {
|
|
6949
|
+
await this.initialize();
|
|
6950
|
+
const currentModel = this.getEmbeddingModelName();
|
|
6951
|
+
const metaPath = path3.join(this.storagePath, "embedding-meta.json");
|
|
6952
|
+
let previousModel = null;
|
|
6953
|
+
try {
|
|
6954
|
+
if (fs4.existsSync(metaPath)) {
|
|
6955
|
+
const parsed = JSON.parse(fs4.readFileSync(metaPath, "utf-8"));
|
|
6956
|
+
previousModel = parsed?.model || null;
|
|
6957
|
+
}
|
|
6958
|
+
} catch {
|
|
6959
|
+
previousModel = null;
|
|
6960
|
+
}
|
|
6961
|
+
const stats = await this.getStats();
|
|
6962
|
+
const hasExistingVectors = (stats.vectorCount || 0) > 0;
|
|
6963
|
+
if (!previousModel && !hasExistingVectors) {
|
|
6964
|
+
fs4.writeFileSync(metaPath, JSON.stringify({ model: currentModel, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
6965
|
+
return { changed: false, previousModel: null, currentModel, enqueued: 0, reason: "initialized-meta" };
|
|
6966
|
+
}
|
|
6967
|
+
const modelChanged = previousModel !== currentModel;
|
|
6968
|
+
const legacyUnknownButVectorsExist = !previousModel && hasExistingVectors;
|
|
6969
|
+
if (!modelChanged && !legacyUnknownButVectorsExist) {
|
|
6970
|
+
return { changed: false, previousModel, currentModel, enqueued: 0 };
|
|
6971
|
+
}
|
|
6972
|
+
if (options?.autoMigrate === false) {
|
|
6973
|
+
return {
|
|
6974
|
+
changed: true,
|
|
6975
|
+
previousModel,
|
|
6976
|
+
currentModel,
|
|
6977
|
+
enqueued: 0,
|
|
6978
|
+
reason: legacyUnknownButVectorsExist ? "legacy-vectors-without-meta" : "model-mismatch"
|
|
6979
|
+
};
|
|
6980
|
+
}
|
|
6981
|
+
const wasRunning = this.vectorWorker?.isRunning() || false;
|
|
6982
|
+
if (wasRunning)
|
|
6983
|
+
this.vectorWorker?.stop();
|
|
6984
|
+
await this.vectorStore.clearAll();
|
|
6985
|
+
await this.sqliteStore.clearEmbeddingOutbox();
|
|
6986
|
+
const pageSize = 1e3;
|
|
6987
|
+
let offset = 0;
|
|
6988
|
+
let enqueued = 0;
|
|
6989
|
+
while (true) {
|
|
6990
|
+
const page = await this.sqliteStore.getEventsPage(pageSize, offset);
|
|
6991
|
+
if (page.length === 0)
|
|
6992
|
+
break;
|
|
6993
|
+
for (const event of page) {
|
|
6994
|
+
await this.sqliteStore.enqueueForEmbedding(event.id, event.content);
|
|
6995
|
+
enqueued += 1;
|
|
6996
|
+
}
|
|
6997
|
+
offset += page.length;
|
|
6998
|
+
if (page.length < pageSize)
|
|
6999
|
+
break;
|
|
7000
|
+
}
|
|
7001
|
+
fs4.writeFileSync(
|
|
7002
|
+
metaPath,
|
|
7003
|
+
JSON.stringify(
|
|
7004
|
+
{
|
|
7005
|
+
model: currentModel,
|
|
7006
|
+
previousModel,
|
|
7007
|
+
migratedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7008
|
+
enqueued
|
|
7009
|
+
},
|
|
7010
|
+
null,
|
|
7011
|
+
2
|
|
7012
|
+
)
|
|
7013
|
+
);
|
|
7014
|
+
if (wasRunning)
|
|
7015
|
+
this.vectorWorker?.start();
|
|
7016
|
+
return {
|
|
7017
|
+
changed: true,
|
|
7018
|
+
previousModel,
|
|
7019
|
+
currentModel,
|
|
7020
|
+
enqueued,
|
|
7021
|
+
reason: legacyUnknownButVectorsExist ? "legacy-vectors-without-meta" : "model-mismatch"
|
|
7022
|
+
};
|
|
7023
|
+
}
|
|
7024
|
+
/**
|
|
7025
|
+
* Backward-compatible alias used by some hooks
|
|
7026
|
+
*/
|
|
7027
|
+
async close() {
|
|
7028
|
+
await this.shutdown();
|
|
7029
|
+
}
|
|
6893
7030
|
/**
|
|
6894
7031
|
* Shutdown service
|
|
6895
7032
|
*/
|
|
@@ -7068,6 +7205,7 @@ eventsRouter.get("/", async (c) => {
|
|
|
7068
7205
|
const eventType = c.req.query("type");
|
|
7069
7206
|
const level = c.req.query("level");
|
|
7070
7207
|
const sort = c.req.query("sort") || "recent";
|
|
7208
|
+
const q = (c.req.query("q") || "").trim().toLowerCase();
|
|
7071
7209
|
const limit = parseInt(c.req.query("limit") || "100", 10);
|
|
7072
7210
|
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
7073
7211
|
const memoryService = getServiceFromQuery(c);
|
|
@@ -7085,6 +7223,9 @@ eventsRouter.get("/", async (c) => {
|
|
|
7085
7223
|
if (eventType) {
|
|
7086
7224
|
events = events.filter((e) => e.eventType === eventType);
|
|
7087
7225
|
}
|
|
7226
|
+
if (q) {
|
|
7227
|
+
events = events.filter((e) => (e.content || "").toLowerCase().includes(q));
|
|
7228
|
+
}
|
|
7088
7229
|
if (sort === "accessed") {
|
|
7089
7230
|
events.sort((a, b) => {
|
|
7090
7231
|
const aTime = a.last_accessed_at || "";
|
|
@@ -7106,6 +7247,7 @@ eventsRouter.get("/", async (c) => {
|
|
|
7106
7247
|
sessionId: e.sessionId,
|
|
7107
7248
|
preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
|
|
7108
7249
|
contentLength: e.content.length,
|
|
7250
|
+
metadata: e.metadata,
|
|
7109
7251
|
accessCount: e.access_count || 0,
|
|
7110
7252
|
lastAccessedAt: e.last_accessed_at || null
|
|
7111
7253
|
})),
|
|
@@ -7232,7 +7374,175 @@ searchRouter.get("/", async (c) => {
|
|
|
7232
7374
|
|
|
7233
7375
|
// src/server/api/stats.ts
|
|
7234
7376
|
import { Hono as Hono4 } from "hono";
|
|
7377
|
+
import * as fs5 from "fs";
|
|
7378
|
+
import * as path5 from "path";
|
|
7235
7379
|
var statsRouter = new Hono4();
|
|
7380
|
+
var DEFAULT_KPI_THRESHOLDS = {
|
|
7381
|
+
usefulRecallRateMin: 0.45,
|
|
7382
|
+
reworkRateMax: 0.25,
|
|
7383
|
+
postChangeFailureRateMax: 0.2,
|
|
7384
|
+
avgCompletionTurnsMax: 12,
|
|
7385
|
+
memoryHitRateMin: 0.35
|
|
7386
|
+
};
|
|
7387
|
+
function loadKpiThresholds() {
|
|
7388
|
+
try {
|
|
7389
|
+
const filePath = path5.resolve(process.cwd(), "config", "kpi-thresholds.json");
|
|
7390
|
+
if (!fs5.existsSync(filePath))
|
|
7391
|
+
return DEFAULT_KPI_THRESHOLDS;
|
|
7392
|
+
const parsed = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
|
|
7393
|
+
return {
|
|
7394
|
+
usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
|
|
7395
|
+
reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
|
|
7396
|
+
postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
|
|
7397
|
+
avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
|
|
7398
|
+
memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
|
|
7399
|
+
};
|
|
7400
|
+
} catch {
|
|
7401
|
+
return DEFAULT_KPI_THRESHOLDS;
|
|
7402
|
+
}
|
|
7403
|
+
}
|
|
7404
|
+
function windowToMs(window) {
|
|
7405
|
+
if (window === "24h")
|
|
7406
|
+
return 24 * 60 * 60 * 1e3;
|
|
7407
|
+
if (window === "7d")
|
|
7408
|
+
return 7 * 24 * 60 * 60 * 1e3;
|
|
7409
|
+
return 30 * 24 * 60 * 60 * 1e3;
|
|
7410
|
+
}
|
|
7411
|
+
function inWindow(e, now, window) {
|
|
7412
|
+
return now - e.timestamp.getTime() <= windowToMs(window);
|
|
7413
|
+
}
|
|
7414
|
+
function isEditToolName(name) {
|
|
7415
|
+
return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
|
|
7416
|
+
}
|
|
7417
|
+
function parseToolPayload(e) {
|
|
7418
|
+
if (e.eventType !== "tool_observation")
|
|
7419
|
+
return null;
|
|
7420
|
+
try {
|
|
7421
|
+
const payload = JSON.parse(e.content);
|
|
7422
|
+
return {
|
|
7423
|
+
toolName: payload?.toolName,
|
|
7424
|
+
success: payload?.success,
|
|
7425
|
+
filePath: payload?.metadata?.filePath,
|
|
7426
|
+
command: payload?.metadata?.command
|
|
7427
|
+
};
|
|
7428
|
+
} catch {
|
|
7429
|
+
return {
|
|
7430
|
+
toolName: e.metadata?.toolName,
|
|
7431
|
+
success: e.metadata?.success,
|
|
7432
|
+
filePath: e.metadata?.filePath,
|
|
7433
|
+
command: e.metadata?.command
|
|
7434
|
+
};
|
|
7435
|
+
}
|
|
7436
|
+
}
|
|
7437
|
+
function isTestLikeCommand(command) {
|
|
7438
|
+
if (!command)
|
|
7439
|
+
return false;
|
|
7440
|
+
return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
|
|
7441
|
+
}
|
|
7442
|
+
function safeRatio(num, den) {
|
|
7443
|
+
if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0)
|
|
7444
|
+
return 0;
|
|
7445
|
+
return num / den;
|
|
7446
|
+
}
|
|
7447
|
+
function round(value, digits = 4) {
|
|
7448
|
+
const factor = 10 ** digits;
|
|
7449
|
+
return Math.round(value * factor) / factor;
|
|
7450
|
+
}
|
|
7451
|
+
function computeSessionTurnCount(sessionEvents) {
|
|
7452
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
7453
|
+
for (const e of sessionEvents) {
|
|
7454
|
+
const turnId = e.metadata?.turnId;
|
|
7455
|
+
if (typeof turnId === "string" && turnId.length > 0)
|
|
7456
|
+
turnIds.add(turnId);
|
|
7457
|
+
}
|
|
7458
|
+
if (turnIds.size > 0)
|
|
7459
|
+
return turnIds.size;
|
|
7460
|
+
return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
|
|
7461
|
+
}
|
|
7462
|
+
function computeKpiMetrics(events, usefulRecallRate) {
|
|
7463
|
+
const prompts = events.filter((e) => e.eventType === "user_prompt");
|
|
7464
|
+
const promptCount = prompts.length;
|
|
7465
|
+
const memoryHitPrompts = prompts.filter((p) => p.metadata?.adherence?.checked).length;
|
|
7466
|
+
const memoryHitRate = round(safeRatio(memoryHitPrompts, promptCount));
|
|
7467
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
7468
|
+
for (const e of events) {
|
|
7469
|
+
const arr = sessions.get(e.sessionId) || [];
|
|
7470
|
+
arr.push(e);
|
|
7471
|
+
sessions.set(e.sessionId, arr);
|
|
7472
|
+
}
|
|
7473
|
+
let sessionTurnTotal = 0;
|
|
7474
|
+
let sessionTurnSamples = 0;
|
|
7475
|
+
let firstValidEditMinutesTotal = 0;
|
|
7476
|
+
let firstValidEditSamples = 0;
|
|
7477
|
+
for (const sessionEvents of sessions.values()) {
|
|
7478
|
+
sessionEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
7479
|
+
const turns = computeSessionTurnCount(sessionEvents);
|
|
7480
|
+
if (turns > 0) {
|
|
7481
|
+
sessionTurnTotal += turns;
|
|
7482
|
+
sessionTurnSamples++;
|
|
7483
|
+
}
|
|
7484
|
+
const firstPrompt = sessionEvents.find((e) => e.eventType === "user_prompt");
|
|
7485
|
+
const firstEdit = sessionEvents.find((e) => {
|
|
7486
|
+
const payload = parseToolPayload(e);
|
|
7487
|
+
return payload?.toolName && isEditToolName(payload.toolName) && payload.success === true;
|
|
7488
|
+
});
|
|
7489
|
+
if (firstPrompt && firstEdit) {
|
|
7490
|
+
const minutes = (firstEdit.timestamp.getTime() - firstPrompt.timestamp.getTime()) / 6e4;
|
|
7491
|
+
if (minutes >= 0) {
|
|
7492
|
+
firstValidEditMinutesTotal += minutes;
|
|
7493
|
+
firstValidEditSamples++;
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
}
|
|
7497
|
+
const avgCompletionTurns = round(safeRatio(sessionTurnTotal, sessionTurnSamples), 2);
|
|
7498
|
+
const timeToFirstValidEditMinutes = round(safeRatio(firstValidEditMinutesTotal, firstValidEditSamples), 2);
|
|
7499
|
+
const editActions = [];
|
|
7500
|
+
let testRunsAfterEdit = 0;
|
|
7501
|
+
let failedTestRunsAfterEdit = 0;
|
|
7502
|
+
for (const [sessionId, sessionEvents] of sessions.entries()) {
|
|
7503
|
+
const sorted = [...sessionEvents].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
7504
|
+
let seenEdit = false;
|
|
7505
|
+
for (const e of sorted) {
|
|
7506
|
+
const payload = parseToolPayload(e);
|
|
7507
|
+
if (!payload?.toolName)
|
|
7508
|
+
continue;
|
|
7509
|
+
if (isEditToolName(payload.toolName) && payload.success === true) {
|
|
7510
|
+
editActions.push({ sessionId, timestamp: e.timestamp.getTime(), filePath: payload.filePath });
|
|
7511
|
+
seenEdit = true;
|
|
7512
|
+
continue;
|
|
7513
|
+
}
|
|
7514
|
+
if (seenEdit && isTestLikeCommand(payload.command)) {
|
|
7515
|
+
testRunsAfterEdit++;
|
|
7516
|
+
if (payload.success === false)
|
|
7517
|
+
failedTestRunsAfterEdit++;
|
|
7518
|
+
}
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
const THIRTY_MIN_MS = 30 * 60 * 1e3;
|
|
7522
|
+
let reworkCount = 0;
|
|
7523
|
+
const bySessionFile = /* @__PURE__ */ new Map();
|
|
7524
|
+
const sortedEdits = [...editActions].sort((a, b) => a.timestamp - b.timestamp);
|
|
7525
|
+
for (const edit of sortedEdits) {
|
|
7526
|
+
if (!edit.filePath)
|
|
7527
|
+
continue;
|
|
7528
|
+
const key = `${edit.sessionId}::${edit.filePath}`;
|
|
7529
|
+
const prev = bySessionFile.get(key);
|
|
7530
|
+
if (typeof prev === "number" && edit.timestamp - prev <= THIRTY_MIN_MS) {
|
|
7531
|
+
reworkCount++;
|
|
7532
|
+
}
|
|
7533
|
+
bySessionFile.set(key, edit.timestamp);
|
|
7534
|
+
}
|
|
7535
|
+
const reworkRate = round(safeRatio(reworkCount, editActions.length));
|
|
7536
|
+
const postChangeFailureRate = round(safeRatio(failedTestRunsAfterEdit, testRunsAfterEdit));
|
|
7537
|
+
return {
|
|
7538
|
+
memoryHitRate,
|
|
7539
|
+
usefulRecallRate,
|
|
7540
|
+
avgCompletionTurns,
|
|
7541
|
+
timeToFirstValidEditMinutes,
|
|
7542
|
+
reworkRate,
|
|
7543
|
+
postChangeFailureRate
|
|
7544
|
+
};
|
|
7545
|
+
}
|
|
7236
7546
|
statsRouter.get("/shared", async (c) => {
|
|
7237
7547
|
const memoryService = getServiceFromQuery(c);
|
|
7238
7548
|
try {
|
|
@@ -7512,6 +7822,129 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
7512
7822
|
await memoryService.shutdown();
|
|
7513
7823
|
}
|
|
7514
7824
|
});
|
|
7825
|
+
statsRouter.get("/kpi", async (c) => {
|
|
7826
|
+
const rawWindow = c.req.query("window") || "7d";
|
|
7827
|
+
const window = rawWindow === "24h" || rawWindow === "30d" ? rawWindow : "7d";
|
|
7828
|
+
const memoryService = getServiceFromQuery(c);
|
|
7829
|
+
try {
|
|
7830
|
+
await memoryService.initialize();
|
|
7831
|
+
const now = Date.now();
|
|
7832
|
+
const thresholds = loadKpiThresholds();
|
|
7833
|
+
const allEvents = await memoryService.getRecentEvents(2e4);
|
|
7834
|
+
const events = allEvents.filter((e) => inWindow(e, now, window));
|
|
7835
|
+
const helpfulness = await memoryService.getHelpfulnessStats();
|
|
7836
|
+
const usefulRecallRate = helpfulness.totalEvaluated > 0 ? round(safeRatio(helpfulness.helpful, helpfulness.totalEvaluated)) : 0;
|
|
7837
|
+
const metrics = computeKpiMetrics(events, usefulRecallRate);
|
|
7838
|
+
const windowMs = windowToMs(window);
|
|
7839
|
+
const prevEvents = allEvents.filter((e) => {
|
|
7840
|
+
const age = now - e.timestamp.getTime();
|
|
7841
|
+
return age > windowMs && age <= windowMs * 2;
|
|
7842
|
+
});
|
|
7843
|
+
const previousMetrics = computeKpiMetrics(prevEvents, usefulRecallRate);
|
|
7844
|
+
const deltas = {
|
|
7845
|
+
memoryHitRate: round(metrics.memoryHitRate - previousMetrics.memoryHitRate),
|
|
7846
|
+
usefulRecallRate: round(metrics.usefulRecallRate - previousMetrics.usefulRecallRate),
|
|
7847
|
+
avgCompletionTurns: round(metrics.avgCompletionTurns - previousMetrics.avgCompletionTurns, 2),
|
|
7848
|
+
timeToFirstValidEditMinutes: round(metrics.timeToFirstValidEditMinutes - previousMetrics.timeToFirstValidEditMinutes, 2),
|
|
7849
|
+
reworkRate: round(metrics.reworkRate - previousMetrics.reworkRate),
|
|
7850
|
+
postChangeFailureRate: round(metrics.postChangeFailureRate - previousMetrics.postChangeFailureRate)
|
|
7851
|
+
};
|
|
7852
|
+
const THIRTY_MIN_MS = 30 * 60 * 1e3;
|
|
7853
|
+
const trendWindowMs = 30 * 24 * 60 * 60 * 1e3;
|
|
7854
|
+
const trendEvents = allEvents.filter((e) => now - e.timestamp.getTime() <= trendWindowMs);
|
|
7855
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
7856
|
+
for (const e of trendEvents) {
|
|
7857
|
+
const day = e.timestamp.toISOString().split("T")[0];
|
|
7858
|
+
const arr = buckets.get(day) || [];
|
|
7859
|
+
arr.push(e);
|
|
7860
|
+
buckets.set(day, arr);
|
|
7861
|
+
}
|
|
7862
|
+
const trendDaily = Array.from(buckets.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([date, dayEvents]) => {
|
|
7863
|
+
const dayPrompts = dayEvents.filter((e) => e.eventType === "user_prompt");
|
|
7864
|
+
const dayPromptCount = dayPrompts.length;
|
|
7865
|
+
const dayMemoryHit = dayPrompts.filter((p) => p.metadata?.adherence?.checked).length;
|
|
7866
|
+
const dayEdits = dayEvents.filter((e) => {
|
|
7867
|
+
const p = parseToolPayload(e);
|
|
7868
|
+
return Boolean(p?.toolName && isEditToolName(p.toolName) && p.success === true);
|
|
7869
|
+
});
|
|
7870
|
+
const dayEditActions = dayEdits.map((e) => {
|
|
7871
|
+
const p = parseToolPayload(e);
|
|
7872
|
+
return { sessionId: e.sessionId, timestamp: e.timestamp.getTime(), filePath: p?.filePath };
|
|
7873
|
+
}).filter((x) => Boolean(x.filePath));
|
|
7874
|
+
let dayReworkCount = 0;
|
|
7875
|
+
const dayBySessionFile = /* @__PURE__ */ new Map();
|
|
7876
|
+
for (const edit of dayEditActions) {
|
|
7877
|
+
const key = `${edit.sessionId}::${edit.filePath}`;
|
|
7878
|
+
const prev = dayBySessionFile.get(key);
|
|
7879
|
+
if (typeof prev === "number" && edit.timestamp - prev <= THIRTY_MIN_MS)
|
|
7880
|
+
dayReworkCount++;
|
|
7881
|
+
dayBySessionFile.set(key, edit.timestamp);
|
|
7882
|
+
}
|
|
7883
|
+
const dayTests = dayEvents.filter((e) => {
|
|
7884
|
+
const p = parseToolPayload(e);
|
|
7885
|
+
return Boolean(p?.toolName && isTestLikeCommand(p.command));
|
|
7886
|
+
});
|
|
7887
|
+
const dayFailedTests = dayEvents.filter((e) => {
|
|
7888
|
+
const p = parseToolPayload(e);
|
|
7889
|
+
return Boolean(p?.toolName && isTestLikeCommand(p.command) && p.success === false);
|
|
7890
|
+
});
|
|
7891
|
+
const turnsBySession = /* @__PURE__ */ new Map();
|
|
7892
|
+
for (const e of dayEvents) {
|
|
7893
|
+
const arr = turnsBySession.get(e.sessionId) || [];
|
|
7894
|
+
arr.push(e);
|
|
7895
|
+
turnsBySession.set(e.sessionId, arr);
|
|
7896
|
+
}
|
|
7897
|
+
let dayTurnsTotal = 0;
|
|
7898
|
+
let dayTurnsSamples = 0;
|
|
7899
|
+
for (const sessionEvents of turnsBySession.values()) {
|
|
7900
|
+
const turns = computeSessionTurnCount(sessionEvents);
|
|
7901
|
+
if (turns > 0) {
|
|
7902
|
+
dayTurnsTotal += turns;
|
|
7903
|
+
dayTurnsSamples++;
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
return {
|
|
7907
|
+
date,
|
|
7908
|
+
memoryHitRate: round(safeRatio(dayMemoryHit, dayPromptCount)),
|
|
7909
|
+
usefulRecallRate,
|
|
7910
|
+
reworkRate: round(safeRatio(dayReworkCount, dayEditActions.length)),
|
|
7911
|
+
postChangeFailureRate: round(safeRatio(dayFailedTests.length, dayTests.length)),
|
|
7912
|
+
avgCompletionTurns: round(safeRatio(dayTurnsTotal, dayTurnsSamples), 2)
|
|
7913
|
+
};
|
|
7914
|
+
});
|
|
7915
|
+
const alerts = [];
|
|
7916
|
+
if (metrics.usefulRecallRate < thresholds.usefulRecallRateMin) {
|
|
7917
|
+
alerts.push({ metric: "usefulRecallRate", level: "warn", message: "Useful recall rate is below threshold", value: metrics.usefulRecallRate, threshold: thresholds.usefulRecallRateMin });
|
|
7918
|
+
}
|
|
7919
|
+
if (metrics.reworkRate > thresholds.reworkRateMax) {
|
|
7920
|
+
alerts.push({ metric: "reworkRate", level: "warn", message: "Rework rate is above threshold", value: metrics.reworkRate, threshold: thresholds.reworkRateMax });
|
|
7921
|
+
}
|
|
7922
|
+
if (metrics.postChangeFailureRate > thresholds.postChangeFailureRateMax) {
|
|
7923
|
+
alerts.push({ metric: "postChangeFailureRate", level: "warn", message: "Post-change failure rate is above threshold", value: metrics.postChangeFailureRate, threshold: thresholds.postChangeFailureRateMax });
|
|
7924
|
+
}
|
|
7925
|
+
if (metrics.avgCompletionTurns > thresholds.avgCompletionTurnsMax) {
|
|
7926
|
+
alerts.push({ metric: "avgCompletionTurns", level: "warn", message: "Average completion turns is above threshold", value: metrics.avgCompletionTurns, threshold: thresholds.avgCompletionTurnsMax });
|
|
7927
|
+
}
|
|
7928
|
+
if (metrics.memoryHitRate < thresholds.memoryHitRateMin) {
|
|
7929
|
+
alerts.push({ metric: "memoryHitRate", level: "warn", message: "Memory hit rate is below threshold", value: metrics.memoryHitRate, threshold: thresholds.memoryHitRateMin });
|
|
7930
|
+
}
|
|
7931
|
+
return c.json({
|
|
7932
|
+
window,
|
|
7933
|
+
metrics,
|
|
7934
|
+
previousMetrics,
|
|
7935
|
+
deltas,
|
|
7936
|
+
trend: {
|
|
7937
|
+
daily: trendDaily
|
|
7938
|
+
},
|
|
7939
|
+
thresholds,
|
|
7940
|
+
alerts
|
|
7941
|
+
});
|
|
7942
|
+
} catch (error) {
|
|
7943
|
+
return c.json({ error: error.message }, 500);
|
|
7944
|
+
} finally {
|
|
7945
|
+
await memoryService.shutdown();
|
|
7946
|
+
}
|
|
7947
|
+
});
|
|
7515
7948
|
statsRouter.post("/graduation/run", async (c) => {
|
|
7516
7949
|
const memoryService = getServiceFromQuery(c);
|
|
7517
7950
|
try {
|
|
@@ -7759,19 +8192,19 @@ turnsRouter.post("/backfill", async (c) => {
|
|
|
7759
8192
|
|
|
7760
8193
|
// src/server/api/projects.ts
|
|
7761
8194
|
import { Hono as Hono7 } from "hono";
|
|
7762
|
-
import * as
|
|
7763
|
-
import * as
|
|
8195
|
+
import * as fs6 from "fs";
|
|
8196
|
+
import * as path6 from "path";
|
|
7764
8197
|
import * as os3 from "os";
|
|
7765
8198
|
var projectsRouter = new Hono7();
|
|
7766
8199
|
projectsRouter.get("/", async (c) => {
|
|
7767
8200
|
try {
|
|
7768
|
-
const projectsDir =
|
|
7769
|
-
if (!
|
|
8201
|
+
const projectsDir = path6.join(os3.homedir(), ".claude-code", "memory", "projects");
|
|
8202
|
+
if (!fs6.existsSync(projectsDir)) {
|
|
7770
8203
|
return c.json({ projects: [] });
|
|
7771
8204
|
}
|
|
7772
|
-
const projectHashes =
|
|
7773
|
-
const fullPath =
|
|
7774
|
-
return
|
|
8205
|
+
const projectHashes = fs6.readdirSync(projectsDir).filter((name) => {
|
|
8206
|
+
const fullPath = path6.join(projectsDir, name);
|
|
8207
|
+
return fs6.statSync(fullPath).isDirectory();
|
|
7775
8208
|
});
|
|
7776
8209
|
const registry = loadSessionRegistry();
|
|
7777
8210
|
const hashToPath = /* @__PURE__ */ new Map();
|
|
@@ -7781,17 +8214,17 @@ projectsRouter.get("/", async (c) => {
|
|
|
7781
8214
|
}
|
|
7782
8215
|
}
|
|
7783
8216
|
const projects = projectHashes.map((hash) => {
|
|
7784
|
-
const dirPath =
|
|
7785
|
-
const dbPath =
|
|
8217
|
+
const dirPath = path6.join(projectsDir, hash);
|
|
8218
|
+
const dbPath = path6.join(dirPath, "events.sqlite");
|
|
7786
8219
|
let dbSize = 0;
|
|
7787
|
-
if (
|
|
7788
|
-
dbSize =
|
|
8220
|
+
if (fs6.existsSync(dbPath)) {
|
|
8221
|
+
dbSize = fs6.statSync(dbPath).size;
|
|
7789
8222
|
}
|
|
7790
8223
|
const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
|
|
7791
8224
|
return {
|
|
7792
8225
|
hash,
|
|
7793
8226
|
projectPath,
|
|
7794
|
-
projectName:
|
|
8227
|
+
projectName: path6.basename(projectPath),
|
|
7795
8228
|
dbSize,
|
|
7796
8229
|
dbSizeHuman: formatBytes(dbSize)
|
|
7797
8230
|
};
|
|
@@ -7916,7 +8349,7 @@ function buildPrompt(statsContext, memoryContext, history, currentMessage) {
|
|
|
7916
8349
|
return parts.join("\n");
|
|
7917
8350
|
}
|
|
7918
8351
|
function streamClaudeResponse(prompt, stream) {
|
|
7919
|
-
return new Promise((
|
|
8352
|
+
return new Promise((resolve3, reject) => {
|
|
7920
8353
|
const proc = spawn("claude", [
|
|
7921
8354
|
"-p",
|
|
7922
8355
|
"--output-format",
|
|
@@ -7988,7 +8421,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
7988
8421
|
if (code !== 0 && code !== null) {
|
|
7989
8422
|
reject(new Error(`Claude CLI exited with code ${code}`));
|
|
7990
8423
|
} else {
|
|
7991
|
-
|
|
8424
|
+
resolve3();
|
|
7992
8425
|
}
|
|
7993
8426
|
});
|
|
7994
8427
|
});
|