claude-memory-layer 1.0.17 → 1.0.19
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 +372 -74
- package/dist/cli/index.js.map +3 -3
- package/dist/hooks/post-tool-use.js +6 -0
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +6 -0
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +29 -13
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +6 -0
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +245 -31
- package/dist/hooks/user-prompt-submit.js.map +3 -3
- package/dist/server/api/index.js +329 -31
- package/dist/server/api/index.js.map +3 -3
- package/dist/server/index.js +336 -38
- package/dist/server/index.js.map +3 -3
- package/dist/services/memory-service.js +6 -0
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +236 -4
- package/dist/ui/index.html +51 -0
- package/dist/ui/style.css +34 -0
- package/memory/_index.md +4 -0
- package/memory/agent_response/uncategorized/2026-02-26.md +151 -1
- package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
- package/memory/session_summary/uncategorized/2026-02-26.md +13 -0
- package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
- package/memory/tool_observation/uncategorized/2026-02-26.md +9 -1
- package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
- package/memory/user_prompt/uncategorized/2026-02-26.md +9 -0
- package/package.json +3 -2
- package/scripts/delete-unknown-projects.js +154 -0
- package/src/hooks/session-start.ts +9 -3
- package/src/hooks/user-prompt-submit.ts +225 -29
- package/src/server/api/events.ts +1 -0
- package/src/server/api/stats.ts +346 -0
- package/src/services/memory-service.ts +7 -0
- package/src/ui/app.js +236 -4
- package/src/ui/index.html +51 -0
- package/src/ui/style.css +34 -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)) {
|
|
@@ -2422,7 +2422,7 @@ var SyncWorker = class {
|
|
|
2422
2422
|
* Sleep utility
|
|
2423
2423
|
*/
|
|
2424
2424
|
sleep(ms) {
|
|
2425
|
-
return new Promise((
|
|
2425
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2426
2426
|
}
|
|
2427
2427
|
/**
|
|
2428
2428
|
* Get sync statistics
|
|
@@ -3452,8 +3452,8 @@ _Context:_ ${sessionContext}`;
|
|
|
3452
3452
|
matchesMetadataScope(metadata, expected) {
|
|
3453
3453
|
if (!metadata)
|
|
3454
3454
|
return false;
|
|
3455
|
-
return Object.entries(expected).every(([
|
|
3456
|
-
const actual =
|
|
3455
|
+
return Object.entries(expected).every(([path7, value]) => {
|
|
3456
|
+
const actual = path7.split(".").reduce((acc, key) => {
|
|
3457
3457
|
if (typeof acc !== "object" || acc === null)
|
|
3458
3458
|
return void 0;
|
|
3459
3459
|
return acc[key];
|
|
@@ -6890,6 +6890,12 @@ var MemoryService = class {
|
|
|
6890
6890
|
recordMemoryAccess(eventId, sessionId, confidence = 1) {
|
|
6891
6891
|
this.graduation.recordAccess(eventId, sessionId, confidence);
|
|
6892
6892
|
}
|
|
6893
|
+
/**
|
|
6894
|
+
* Backward-compatible alias used by some hooks
|
|
6895
|
+
*/
|
|
6896
|
+
async close() {
|
|
6897
|
+
await this.shutdown();
|
|
6898
|
+
}
|
|
6893
6899
|
/**
|
|
6894
6900
|
* Shutdown service
|
|
6895
6901
|
*/
|
|
@@ -7106,6 +7112,7 @@ eventsRouter.get("/", async (c) => {
|
|
|
7106
7112
|
sessionId: e.sessionId,
|
|
7107
7113
|
preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
|
|
7108
7114
|
contentLength: e.content.length,
|
|
7115
|
+
metadata: e.metadata,
|
|
7109
7116
|
accessCount: e.access_count || 0,
|
|
7110
7117
|
lastAccessedAt: e.last_accessed_at || null
|
|
7111
7118
|
})),
|
|
@@ -7232,7 +7239,175 @@ searchRouter.get("/", async (c) => {
|
|
|
7232
7239
|
|
|
7233
7240
|
// src/server/api/stats.ts
|
|
7234
7241
|
import { Hono as Hono4 } from "hono";
|
|
7242
|
+
import * as fs5 from "fs";
|
|
7243
|
+
import * as path5 from "path";
|
|
7235
7244
|
var statsRouter = new Hono4();
|
|
7245
|
+
var DEFAULT_KPI_THRESHOLDS = {
|
|
7246
|
+
usefulRecallRateMin: 0.45,
|
|
7247
|
+
reworkRateMax: 0.25,
|
|
7248
|
+
postChangeFailureRateMax: 0.2,
|
|
7249
|
+
avgCompletionTurnsMax: 12,
|
|
7250
|
+
memoryHitRateMin: 0.35
|
|
7251
|
+
};
|
|
7252
|
+
function loadKpiThresholds() {
|
|
7253
|
+
try {
|
|
7254
|
+
const filePath = path5.resolve(process.cwd(), "config", "kpi-thresholds.json");
|
|
7255
|
+
if (!fs5.existsSync(filePath))
|
|
7256
|
+
return DEFAULT_KPI_THRESHOLDS;
|
|
7257
|
+
const parsed = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
|
|
7258
|
+
return {
|
|
7259
|
+
usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
|
|
7260
|
+
reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
|
|
7261
|
+
postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
|
|
7262
|
+
avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
|
|
7263
|
+
memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
|
|
7264
|
+
};
|
|
7265
|
+
} catch {
|
|
7266
|
+
return DEFAULT_KPI_THRESHOLDS;
|
|
7267
|
+
}
|
|
7268
|
+
}
|
|
7269
|
+
function windowToMs(window) {
|
|
7270
|
+
if (window === "24h")
|
|
7271
|
+
return 24 * 60 * 60 * 1e3;
|
|
7272
|
+
if (window === "7d")
|
|
7273
|
+
return 7 * 24 * 60 * 60 * 1e3;
|
|
7274
|
+
return 30 * 24 * 60 * 60 * 1e3;
|
|
7275
|
+
}
|
|
7276
|
+
function inWindow(e, now, window) {
|
|
7277
|
+
return now - e.timestamp.getTime() <= windowToMs(window);
|
|
7278
|
+
}
|
|
7279
|
+
function isEditToolName(name) {
|
|
7280
|
+
return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
|
|
7281
|
+
}
|
|
7282
|
+
function parseToolPayload(e) {
|
|
7283
|
+
if (e.eventType !== "tool_observation")
|
|
7284
|
+
return null;
|
|
7285
|
+
try {
|
|
7286
|
+
const payload = JSON.parse(e.content);
|
|
7287
|
+
return {
|
|
7288
|
+
toolName: payload?.toolName,
|
|
7289
|
+
success: payload?.success,
|
|
7290
|
+
filePath: payload?.metadata?.filePath,
|
|
7291
|
+
command: payload?.metadata?.command
|
|
7292
|
+
};
|
|
7293
|
+
} catch {
|
|
7294
|
+
return {
|
|
7295
|
+
toolName: e.metadata?.toolName,
|
|
7296
|
+
success: e.metadata?.success,
|
|
7297
|
+
filePath: e.metadata?.filePath,
|
|
7298
|
+
command: e.metadata?.command
|
|
7299
|
+
};
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
function isTestLikeCommand(command) {
|
|
7303
|
+
if (!command)
|
|
7304
|
+
return false;
|
|
7305
|
+
return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
|
|
7306
|
+
}
|
|
7307
|
+
function safeRatio(num, den) {
|
|
7308
|
+
if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0)
|
|
7309
|
+
return 0;
|
|
7310
|
+
return num / den;
|
|
7311
|
+
}
|
|
7312
|
+
function round(value, digits = 4) {
|
|
7313
|
+
const factor = 10 ** digits;
|
|
7314
|
+
return Math.round(value * factor) / factor;
|
|
7315
|
+
}
|
|
7316
|
+
function computeSessionTurnCount(sessionEvents) {
|
|
7317
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
7318
|
+
for (const e of sessionEvents) {
|
|
7319
|
+
const turnId = e.metadata?.turnId;
|
|
7320
|
+
if (typeof turnId === "string" && turnId.length > 0)
|
|
7321
|
+
turnIds.add(turnId);
|
|
7322
|
+
}
|
|
7323
|
+
if (turnIds.size > 0)
|
|
7324
|
+
return turnIds.size;
|
|
7325
|
+
return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
|
|
7326
|
+
}
|
|
7327
|
+
function computeKpiMetrics(events, usefulRecallRate) {
|
|
7328
|
+
const prompts = events.filter((e) => e.eventType === "user_prompt");
|
|
7329
|
+
const promptCount = prompts.length;
|
|
7330
|
+
const memoryHitPrompts = prompts.filter((p) => p.metadata?.adherence?.checked).length;
|
|
7331
|
+
const memoryHitRate = round(safeRatio(memoryHitPrompts, promptCount));
|
|
7332
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
7333
|
+
for (const e of events) {
|
|
7334
|
+
const arr = sessions.get(e.sessionId) || [];
|
|
7335
|
+
arr.push(e);
|
|
7336
|
+
sessions.set(e.sessionId, arr);
|
|
7337
|
+
}
|
|
7338
|
+
let sessionTurnTotal = 0;
|
|
7339
|
+
let sessionTurnSamples = 0;
|
|
7340
|
+
let firstValidEditMinutesTotal = 0;
|
|
7341
|
+
let firstValidEditSamples = 0;
|
|
7342
|
+
for (const sessionEvents of sessions.values()) {
|
|
7343
|
+
sessionEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
7344
|
+
const turns = computeSessionTurnCount(sessionEvents);
|
|
7345
|
+
if (turns > 0) {
|
|
7346
|
+
sessionTurnTotal += turns;
|
|
7347
|
+
sessionTurnSamples++;
|
|
7348
|
+
}
|
|
7349
|
+
const firstPrompt = sessionEvents.find((e) => e.eventType === "user_prompt");
|
|
7350
|
+
const firstEdit = sessionEvents.find((e) => {
|
|
7351
|
+
const payload = parseToolPayload(e);
|
|
7352
|
+
return payload?.toolName && isEditToolName(payload.toolName) && payload.success === true;
|
|
7353
|
+
});
|
|
7354
|
+
if (firstPrompt && firstEdit) {
|
|
7355
|
+
const minutes = (firstEdit.timestamp.getTime() - firstPrompt.timestamp.getTime()) / 6e4;
|
|
7356
|
+
if (minutes >= 0) {
|
|
7357
|
+
firstValidEditMinutesTotal += minutes;
|
|
7358
|
+
firstValidEditSamples++;
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
}
|
|
7362
|
+
const avgCompletionTurns = round(safeRatio(sessionTurnTotal, sessionTurnSamples), 2);
|
|
7363
|
+
const timeToFirstValidEditMinutes = round(safeRatio(firstValidEditMinutesTotal, firstValidEditSamples), 2);
|
|
7364
|
+
const editActions = [];
|
|
7365
|
+
let testRunsAfterEdit = 0;
|
|
7366
|
+
let failedTestRunsAfterEdit = 0;
|
|
7367
|
+
for (const [sessionId, sessionEvents] of sessions.entries()) {
|
|
7368
|
+
const sorted = [...sessionEvents].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
7369
|
+
let seenEdit = false;
|
|
7370
|
+
for (const e of sorted) {
|
|
7371
|
+
const payload = parseToolPayload(e);
|
|
7372
|
+
if (!payload?.toolName)
|
|
7373
|
+
continue;
|
|
7374
|
+
if (isEditToolName(payload.toolName) && payload.success === true) {
|
|
7375
|
+
editActions.push({ sessionId, timestamp: e.timestamp.getTime(), filePath: payload.filePath });
|
|
7376
|
+
seenEdit = true;
|
|
7377
|
+
continue;
|
|
7378
|
+
}
|
|
7379
|
+
if (seenEdit && isTestLikeCommand(payload.command)) {
|
|
7380
|
+
testRunsAfterEdit++;
|
|
7381
|
+
if (payload.success === false)
|
|
7382
|
+
failedTestRunsAfterEdit++;
|
|
7383
|
+
}
|
|
7384
|
+
}
|
|
7385
|
+
}
|
|
7386
|
+
const THIRTY_MIN_MS = 30 * 60 * 1e3;
|
|
7387
|
+
let reworkCount = 0;
|
|
7388
|
+
const bySessionFile = /* @__PURE__ */ new Map();
|
|
7389
|
+
const sortedEdits = [...editActions].sort((a, b) => a.timestamp - b.timestamp);
|
|
7390
|
+
for (const edit of sortedEdits) {
|
|
7391
|
+
if (!edit.filePath)
|
|
7392
|
+
continue;
|
|
7393
|
+
const key = `${edit.sessionId}::${edit.filePath}`;
|
|
7394
|
+
const prev = bySessionFile.get(key);
|
|
7395
|
+
if (typeof prev === "number" && edit.timestamp - prev <= THIRTY_MIN_MS) {
|
|
7396
|
+
reworkCount++;
|
|
7397
|
+
}
|
|
7398
|
+
bySessionFile.set(key, edit.timestamp);
|
|
7399
|
+
}
|
|
7400
|
+
const reworkRate = round(safeRatio(reworkCount, editActions.length));
|
|
7401
|
+
const postChangeFailureRate = round(safeRatio(failedTestRunsAfterEdit, testRunsAfterEdit));
|
|
7402
|
+
return {
|
|
7403
|
+
memoryHitRate,
|
|
7404
|
+
usefulRecallRate,
|
|
7405
|
+
avgCompletionTurns,
|
|
7406
|
+
timeToFirstValidEditMinutes,
|
|
7407
|
+
reworkRate,
|
|
7408
|
+
postChangeFailureRate
|
|
7409
|
+
};
|
|
7410
|
+
}
|
|
7236
7411
|
statsRouter.get("/shared", async (c) => {
|
|
7237
7412
|
const memoryService = getServiceFromQuery(c);
|
|
7238
7413
|
try {
|
|
@@ -7512,6 +7687,129 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
7512
7687
|
await memoryService.shutdown();
|
|
7513
7688
|
}
|
|
7514
7689
|
});
|
|
7690
|
+
statsRouter.get("/kpi", async (c) => {
|
|
7691
|
+
const rawWindow = c.req.query("window") || "7d";
|
|
7692
|
+
const window = rawWindow === "24h" || rawWindow === "30d" ? rawWindow : "7d";
|
|
7693
|
+
const memoryService = getServiceFromQuery(c);
|
|
7694
|
+
try {
|
|
7695
|
+
await memoryService.initialize();
|
|
7696
|
+
const now = Date.now();
|
|
7697
|
+
const thresholds = loadKpiThresholds();
|
|
7698
|
+
const allEvents = await memoryService.getRecentEvents(2e4);
|
|
7699
|
+
const events = allEvents.filter((e) => inWindow(e, now, window));
|
|
7700
|
+
const helpfulness = await memoryService.getHelpfulnessStats();
|
|
7701
|
+
const usefulRecallRate = helpfulness.totalEvaluated > 0 ? round(safeRatio(helpfulness.helpful, helpfulness.totalEvaluated)) : 0;
|
|
7702
|
+
const metrics = computeKpiMetrics(events, usefulRecallRate);
|
|
7703
|
+
const windowMs = windowToMs(window);
|
|
7704
|
+
const prevEvents = allEvents.filter((e) => {
|
|
7705
|
+
const age = now - e.timestamp.getTime();
|
|
7706
|
+
return age > windowMs && age <= windowMs * 2;
|
|
7707
|
+
});
|
|
7708
|
+
const previousMetrics = computeKpiMetrics(prevEvents, usefulRecallRate);
|
|
7709
|
+
const deltas = {
|
|
7710
|
+
memoryHitRate: round(metrics.memoryHitRate - previousMetrics.memoryHitRate),
|
|
7711
|
+
usefulRecallRate: round(metrics.usefulRecallRate - previousMetrics.usefulRecallRate),
|
|
7712
|
+
avgCompletionTurns: round(metrics.avgCompletionTurns - previousMetrics.avgCompletionTurns, 2),
|
|
7713
|
+
timeToFirstValidEditMinutes: round(metrics.timeToFirstValidEditMinutes - previousMetrics.timeToFirstValidEditMinutes, 2),
|
|
7714
|
+
reworkRate: round(metrics.reworkRate - previousMetrics.reworkRate),
|
|
7715
|
+
postChangeFailureRate: round(metrics.postChangeFailureRate - previousMetrics.postChangeFailureRate)
|
|
7716
|
+
};
|
|
7717
|
+
const THIRTY_MIN_MS = 30 * 60 * 1e3;
|
|
7718
|
+
const trendWindowMs = 30 * 24 * 60 * 60 * 1e3;
|
|
7719
|
+
const trendEvents = allEvents.filter((e) => now - e.timestamp.getTime() <= trendWindowMs);
|
|
7720
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
7721
|
+
for (const e of trendEvents) {
|
|
7722
|
+
const day = e.timestamp.toISOString().split("T")[0];
|
|
7723
|
+
const arr = buckets.get(day) || [];
|
|
7724
|
+
arr.push(e);
|
|
7725
|
+
buckets.set(day, arr);
|
|
7726
|
+
}
|
|
7727
|
+
const trendDaily = Array.from(buckets.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([date, dayEvents]) => {
|
|
7728
|
+
const dayPrompts = dayEvents.filter((e) => e.eventType === "user_prompt");
|
|
7729
|
+
const dayPromptCount = dayPrompts.length;
|
|
7730
|
+
const dayMemoryHit = dayPrompts.filter((p) => p.metadata?.adherence?.checked).length;
|
|
7731
|
+
const dayEdits = dayEvents.filter((e) => {
|
|
7732
|
+
const p = parseToolPayload(e);
|
|
7733
|
+
return Boolean(p?.toolName && isEditToolName(p.toolName) && p.success === true);
|
|
7734
|
+
});
|
|
7735
|
+
const dayEditActions = dayEdits.map((e) => {
|
|
7736
|
+
const p = parseToolPayload(e);
|
|
7737
|
+
return { sessionId: e.sessionId, timestamp: e.timestamp.getTime(), filePath: p?.filePath };
|
|
7738
|
+
}).filter((x) => Boolean(x.filePath));
|
|
7739
|
+
let dayReworkCount = 0;
|
|
7740
|
+
const dayBySessionFile = /* @__PURE__ */ new Map();
|
|
7741
|
+
for (const edit of dayEditActions) {
|
|
7742
|
+
const key = `${edit.sessionId}::${edit.filePath}`;
|
|
7743
|
+
const prev = dayBySessionFile.get(key);
|
|
7744
|
+
if (typeof prev === "number" && edit.timestamp - prev <= THIRTY_MIN_MS)
|
|
7745
|
+
dayReworkCount++;
|
|
7746
|
+
dayBySessionFile.set(key, edit.timestamp);
|
|
7747
|
+
}
|
|
7748
|
+
const dayTests = dayEvents.filter((e) => {
|
|
7749
|
+
const p = parseToolPayload(e);
|
|
7750
|
+
return Boolean(p?.toolName && isTestLikeCommand(p.command));
|
|
7751
|
+
});
|
|
7752
|
+
const dayFailedTests = dayEvents.filter((e) => {
|
|
7753
|
+
const p = parseToolPayload(e);
|
|
7754
|
+
return Boolean(p?.toolName && isTestLikeCommand(p.command) && p.success === false);
|
|
7755
|
+
});
|
|
7756
|
+
const turnsBySession = /* @__PURE__ */ new Map();
|
|
7757
|
+
for (const e of dayEvents) {
|
|
7758
|
+
const arr = turnsBySession.get(e.sessionId) || [];
|
|
7759
|
+
arr.push(e);
|
|
7760
|
+
turnsBySession.set(e.sessionId, arr);
|
|
7761
|
+
}
|
|
7762
|
+
let dayTurnsTotal = 0;
|
|
7763
|
+
let dayTurnsSamples = 0;
|
|
7764
|
+
for (const sessionEvents of turnsBySession.values()) {
|
|
7765
|
+
const turns = computeSessionTurnCount(sessionEvents);
|
|
7766
|
+
if (turns > 0) {
|
|
7767
|
+
dayTurnsTotal += turns;
|
|
7768
|
+
dayTurnsSamples++;
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7771
|
+
return {
|
|
7772
|
+
date,
|
|
7773
|
+
memoryHitRate: round(safeRatio(dayMemoryHit, dayPromptCount)),
|
|
7774
|
+
usefulRecallRate,
|
|
7775
|
+
reworkRate: round(safeRatio(dayReworkCount, dayEditActions.length)),
|
|
7776
|
+
postChangeFailureRate: round(safeRatio(dayFailedTests.length, dayTests.length)),
|
|
7777
|
+
avgCompletionTurns: round(safeRatio(dayTurnsTotal, dayTurnsSamples), 2)
|
|
7778
|
+
};
|
|
7779
|
+
});
|
|
7780
|
+
const alerts = [];
|
|
7781
|
+
if (metrics.usefulRecallRate < thresholds.usefulRecallRateMin) {
|
|
7782
|
+
alerts.push({ metric: "usefulRecallRate", level: "warn", message: "Useful recall rate is below threshold", value: metrics.usefulRecallRate, threshold: thresholds.usefulRecallRateMin });
|
|
7783
|
+
}
|
|
7784
|
+
if (metrics.reworkRate > thresholds.reworkRateMax) {
|
|
7785
|
+
alerts.push({ metric: "reworkRate", level: "warn", message: "Rework rate is above threshold", value: metrics.reworkRate, threshold: thresholds.reworkRateMax });
|
|
7786
|
+
}
|
|
7787
|
+
if (metrics.postChangeFailureRate > thresholds.postChangeFailureRateMax) {
|
|
7788
|
+
alerts.push({ metric: "postChangeFailureRate", level: "warn", message: "Post-change failure rate is above threshold", value: metrics.postChangeFailureRate, threshold: thresholds.postChangeFailureRateMax });
|
|
7789
|
+
}
|
|
7790
|
+
if (metrics.avgCompletionTurns > thresholds.avgCompletionTurnsMax) {
|
|
7791
|
+
alerts.push({ metric: "avgCompletionTurns", level: "warn", message: "Average completion turns is above threshold", value: metrics.avgCompletionTurns, threshold: thresholds.avgCompletionTurnsMax });
|
|
7792
|
+
}
|
|
7793
|
+
if (metrics.memoryHitRate < thresholds.memoryHitRateMin) {
|
|
7794
|
+
alerts.push({ metric: "memoryHitRate", level: "warn", message: "Memory hit rate is below threshold", value: metrics.memoryHitRate, threshold: thresholds.memoryHitRateMin });
|
|
7795
|
+
}
|
|
7796
|
+
return c.json({
|
|
7797
|
+
window,
|
|
7798
|
+
metrics,
|
|
7799
|
+
previousMetrics,
|
|
7800
|
+
deltas,
|
|
7801
|
+
trend: {
|
|
7802
|
+
daily: trendDaily
|
|
7803
|
+
},
|
|
7804
|
+
thresholds,
|
|
7805
|
+
alerts
|
|
7806
|
+
});
|
|
7807
|
+
} catch (error) {
|
|
7808
|
+
return c.json({ error: error.message }, 500);
|
|
7809
|
+
} finally {
|
|
7810
|
+
await memoryService.shutdown();
|
|
7811
|
+
}
|
|
7812
|
+
});
|
|
7515
7813
|
statsRouter.post("/graduation/run", async (c) => {
|
|
7516
7814
|
const memoryService = getServiceFromQuery(c);
|
|
7517
7815
|
try {
|
|
@@ -7759,19 +8057,19 @@ turnsRouter.post("/backfill", async (c) => {
|
|
|
7759
8057
|
|
|
7760
8058
|
// src/server/api/projects.ts
|
|
7761
8059
|
import { Hono as Hono7 } from "hono";
|
|
7762
|
-
import * as
|
|
7763
|
-
import * as
|
|
8060
|
+
import * as fs6 from "fs";
|
|
8061
|
+
import * as path6 from "path";
|
|
7764
8062
|
import * as os3 from "os";
|
|
7765
8063
|
var projectsRouter = new Hono7();
|
|
7766
8064
|
projectsRouter.get("/", async (c) => {
|
|
7767
8065
|
try {
|
|
7768
|
-
const projectsDir =
|
|
7769
|
-
if (!
|
|
8066
|
+
const projectsDir = path6.join(os3.homedir(), ".claude-code", "memory", "projects");
|
|
8067
|
+
if (!fs6.existsSync(projectsDir)) {
|
|
7770
8068
|
return c.json({ projects: [] });
|
|
7771
8069
|
}
|
|
7772
|
-
const projectHashes =
|
|
7773
|
-
const fullPath =
|
|
7774
|
-
return
|
|
8070
|
+
const projectHashes = fs6.readdirSync(projectsDir).filter((name) => {
|
|
8071
|
+
const fullPath = path6.join(projectsDir, name);
|
|
8072
|
+
return fs6.statSync(fullPath).isDirectory();
|
|
7775
8073
|
});
|
|
7776
8074
|
const registry = loadSessionRegistry();
|
|
7777
8075
|
const hashToPath = /* @__PURE__ */ new Map();
|
|
@@ -7781,17 +8079,17 @@ projectsRouter.get("/", async (c) => {
|
|
|
7781
8079
|
}
|
|
7782
8080
|
}
|
|
7783
8081
|
const projects = projectHashes.map((hash) => {
|
|
7784
|
-
const dirPath =
|
|
7785
|
-
const dbPath =
|
|
8082
|
+
const dirPath = path6.join(projectsDir, hash);
|
|
8083
|
+
const dbPath = path6.join(dirPath, "events.sqlite");
|
|
7786
8084
|
let dbSize = 0;
|
|
7787
|
-
if (
|
|
7788
|
-
dbSize =
|
|
8085
|
+
if (fs6.existsSync(dbPath)) {
|
|
8086
|
+
dbSize = fs6.statSync(dbPath).size;
|
|
7789
8087
|
}
|
|
7790
8088
|
const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
|
|
7791
8089
|
return {
|
|
7792
8090
|
hash,
|
|
7793
8091
|
projectPath,
|
|
7794
|
-
projectName:
|
|
8092
|
+
projectName: path6.basename(projectPath),
|
|
7795
8093
|
dbSize,
|
|
7796
8094
|
dbSizeHuman: formatBytes(dbSize)
|
|
7797
8095
|
};
|
|
@@ -7916,7 +8214,7 @@ function buildPrompt(statsContext, memoryContext, history, currentMessage) {
|
|
|
7916
8214
|
return parts.join("\n");
|
|
7917
8215
|
}
|
|
7918
8216
|
function streamClaudeResponse(prompt, stream) {
|
|
7919
|
-
return new Promise((
|
|
8217
|
+
return new Promise((resolve3, reject) => {
|
|
7920
8218
|
const proc = spawn("claude", [
|
|
7921
8219
|
"-p",
|
|
7922
8220
|
"--output-format",
|
|
@@ -7988,7 +8286,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
7988
8286
|
if (code !== 0 && code !== null) {
|
|
7989
8287
|
reject(new Error(`Claude CLI exited with code ${code}`));
|
|
7990
8288
|
} else {
|
|
7991
|
-
|
|
8289
|
+
resolve3();
|
|
7992
8290
|
}
|
|
7993
8291
|
});
|
|
7994
8292
|
});
|