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/index.js
CHANGED
|
@@ -18,8 +18,8 @@ import { cors } from "hono/cors";
|
|
|
18
18
|
import { logger } from "hono/logger";
|
|
19
19
|
import { serve } from "@hono/node-server";
|
|
20
20
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
21
|
-
import * as
|
|
22
|
-
import * as
|
|
21
|
+
import * as path7 from "path";
|
|
22
|
+
import * as fs7 from "fs";
|
|
23
23
|
|
|
24
24
|
// src/server/api/index.ts
|
|
25
25
|
import { Hono as Hono10 } from "hono";
|
|
@@ -92,57 +92,57 @@ function toDate(value) {
|
|
|
92
92
|
return new Date(value);
|
|
93
93
|
return new Date(String(value));
|
|
94
94
|
}
|
|
95
|
-
function createDatabase(
|
|
95
|
+
function createDatabase(path8, options) {
|
|
96
96
|
if (options?.readOnly) {
|
|
97
|
-
return new duckdb.Database(
|
|
97
|
+
return new duckdb.Database(path8, { access_mode: "READ_ONLY" });
|
|
98
98
|
}
|
|
99
|
-
return new duckdb.Database(
|
|
99
|
+
return new duckdb.Database(path8);
|
|
100
100
|
}
|
|
101
101
|
function dbRun(db, sql, params = []) {
|
|
102
|
-
return new Promise((
|
|
102
|
+
return new Promise((resolve3, reject) => {
|
|
103
103
|
if (params.length === 0) {
|
|
104
104
|
db.run(sql, (err) => {
|
|
105
105
|
if (err)
|
|
106
106
|
reject(err);
|
|
107
107
|
else
|
|
108
|
-
|
|
108
|
+
resolve3();
|
|
109
109
|
});
|
|
110
110
|
} else {
|
|
111
111
|
db.run(sql, ...params, (err) => {
|
|
112
112
|
if (err)
|
|
113
113
|
reject(err);
|
|
114
114
|
else
|
|
115
|
-
|
|
115
|
+
resolve3();
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
function dbAll(db, sql, params = []) {
|
|
121
|
-
return new Promise((
|
|
121
|
+
return new Promise((resolve3, reject) => {
|
|
122
122
|
if (params.length === 0) {
|
|
123
123
|
db.all(sql, (err, rows) => {
|
|
124
124
|
if (err)
|
|
125
125
|
reject(err);
|
|
126
126
|
else
|
|
127
|
-
|
|
127
|
+
resolve3(convertBigInts(rows || []));
|
|
128
128
|
});
|
|
129
129
|
} else {
|
|
130
130
|
db.all(sql, ...params, (err, rows) => {
|
|
131
131
|
if (err)
|
|
132
132
|
reject(err);
|
|
133
133
|
else
|
|
134
|
-
|
|
134
|
+
resolve3(convertBigInts(rows || []));
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
139
|
function dbClose(db) {
|
|
140
|
-
return new Promise((
|
|
140
|
+
return new Promise((resolve3, reject) => {
|
|
141
141
|
db.close((err) => {
|
|
142
142
|
if (err)
|
|
143
143
|
reject(err);
|
|
144
144
|
else
|
|
145
|
-
|
|
145
|
+
resolve3();
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
148
|
}
|
|
@@ -780,12 +780,12 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
780
780
|
import Database from "better-sqlite3";
|
|
781
781
|
import * as fs from "fs";
|
|
782
782
|
import * as nodePath from "path";
|
|
783
|
-
function createSQLiteDatabase(
|
|
784
|
-
const dir = nodePath.dirname(
|
|
783
|
+
function createSQLiteDatabase(path8, options) {
|
|
784
|
+
const dir = nodePath.dirname(path8);
|
|
785
785
|
if (!fs.existsSync(dir)) {
|
|
786
786
|
fs.mkdirSync(dir, { recursive: true });
|
|
787
787
|
}
|
|
788
|
-
const db = new Database(
|
|
788
|
+
const db = new Database(path8, {
|
|
789
789
|
readonly: options?.readonly ?? false
|
|
790
790
|
});
|
|
791
791
|
if (!options?.readonly && (options?.walMode ?? true)) {
|
|
@@ -2431,7 +2431,7 @@ var SyncWorker = class {
|
|
|
2431
2431
|
* Sleep utility
|
|
2432
2432
|
*/
|
|
2433
2433
|
sleep(ms) {
|
|
2434
|
-
return new Promise((
|
|
2434
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2435
2435
|
}
|
|
2436
2436
|
/**
|
|
2437
2437
|
* Get sync statistics
|
|
@@ -3461,8 +3461,8 @@ _Context:_ ${sessionContext}`;
|
|
|
3461
3461
|
matchesMetadataScope(metadata, expected) {
|
|
3462
3462
|
if (!metadata)
|
|
3463
3463
|
return false;
|
|
3464
|
-
return Object.entries(expected).every(([
|
|
3465
|
-
const actual =
|
|
3464
|
+
return Object.entries(expected).every(([path8, value]) => {
|
|
3465
|
+
const actual = path8.split(".").reduce((acc, key) => {
|
|
3466
3466
|
if (typeof acc !== "object" || acc === null)
|
|
3467
3467
|
return void 0;
|
|
3468
3468
|
return acc[key];
|
|
@@ -6899,6 +6899,12 @@ var MemoryService = class {
|
|
|
6899
6899
|
recordMemoryAccess(eventId, sessionId, confidence = 1) {
|
|
6900
6900
|
this.graduation.recordAccess(eventId, sessionId, confidence);
|
|
6901
6901
|
}
|
|
6902
|
+
/**
|
|
6903
|
+
* Backward-compatible alias used by some hooks
|
|
6904
|
+
*/
|
|
6905
|
+
async close() {
|
|
6906
|
+
await this.shutdown();
|
|
6907
|
+
}
|
|
6902
6908
|
/**
|
|
6903
6909
|
* Shutdown service
|
|
6904
6910
|
*/
|
|
@@ -7115,6 +7121,7 @@ eventsRouter.get("/", async (c) => {
|
|
|
7115
7121
|
sessionId: e.sessionId,
|
|
7116
7122
|
preview: e.content.slice(0, 200) + (e.content.length > 200 ? "..." : ""),
|
|
7117
7123
|
contentLength: e.content.length,
|
|
7124
|
+
metadata: e.metadata,
|
|
7118
7125
|
accessCount: e.access_count || 0,
|
|
7119
7126
|
lastAccessedAt: e.last_accessed_at || null
|
|
7120
7127
|
})),
|
|
@@ -7241,7 +7248,175 @@ searchRouter.get("/", async (c) => {
|
|
|
7241
7248
|
|
|
7242
7249
|
// src/server/api/stats.ts
|
|
7243
7250
|
import { Hono as Hono4 } from "hono";
|
|
7251
|
+
import * as fs5 from "fs";
|
|
7252
|
+
import * as path5 from "path";
|
|
7244
7253
|
var statsRouter = new Hono4();
|
|
7254
|
+
var DEFAULT_KPI_THRESHOLDS = {
|
|
7255
|
+
usefulRecallRateMin: 0.45,
|
|
7256
|
+
reworkRateMax: 0.25,
|
|
7257
|
+
postChangeFailureRateMax: 0.2,
|
|
7258
|
+
avgCompletionTurnsMax: 12,
|
|
7259
|
+
memoryHitRateMin: 0.35
|
|
7260
|
+
};
|
|
7261
|
+
function loadKpiThresholds() {
|
|
7262
|
+
try {
|
|
7263
|
+
const filePath = path5.resolve(process.cwd(), "config", "kpi-thresholds.json");
|
|
7264
|
+
if (!fs5.existsSync(filePath))
|
|
7265
|
+
return DEFAULT_KPI_THRESHOLDS;
|
|
7266
|
+
const parsed = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
|
|
7267
|
+
return {
|
|
7268
|
+
usefulRecallRateMin: Number(parsed.usefulRecallRateMin ?? DEFAULT_KPI_THRESHOLDS.usefulRecallRateMin),
|
|
7269
|
+
reworkRateMax: Number(parsed.reworkRateMax ?? DEFAULT_KPI_THRESHOLDS.reworkRateMax),
|
|
7270
|
+
postChangeFailureRateMax: Number(parsed.postChangeFailureRateMax ?? DEFAULT_KPI_THRESHOLDS.postChangeFailureRateMax),
|
|
7271
|
+
avgCompletionTurnsMax: Number(parsed.avgCompletionTurnsMax ?? DEFAULT_KPI_THRESHOLDS.avgCompletionTurnsMax),
|
|
7272
|
+
memoryHitRateMin: Number(parsed.memoryHitRateMin ?? DEFAULT_KPI_THRESHOLDS.memoryHitRateMin)
|
|
7273
|
+
};
|
|
7274
|
+
} catch {
|
|
7275
|
+
return DEFAULT_KPI_THRESHOLDS;
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
function windowToMs(window) {
|
|
7279
|
+
if (window === "24h")
|
|
7280
|
+
return 24 * 60 * 60 * 1e3;
|
|
7281
|
+
if (window === "7d")
|
|
7282
|
+
return 7 * 24 * 60 * 60 * 1e3;
|
|
7283
|
+
return 30 * 24 * 60 * 60 * 1e3;
|
|
7284
|
+
}
|
|
7285
|
+
function inWindow(e, now, window) {
|
|
7286
|
+
return now - e.timestamp.getTime() <= windowToMs(window);
|
|
7287
|
+
}
|
|
7288
|
+
function isEditToolName(name) {
|
|
7289
|
+
return ["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(name);
|
|
7290
|
+
}
|
|
7291
|
+
function parseToolPayload(e) {
|
|
7292
|
+
if (e.eventType !== "tool_observation")
|
|
7293
|
+
return null;
|
|
7294
|
+
try {
|
|
7295
|
+
const payload = JSON.parse(e.content);
|
|
7296
|
+
return {
|
|
7297
|
+
toolName: payload?.toolName,
|
|
7298
|
+
success: payload?.success,
|
|
7299
|
+
filePath: payload?.metadata?.filePath,
|
|
7300
|
+
command: payload?.metadata?.command
|
|
7301
|
+
};
|
|
7302
|
+
} catch {
|
|
7303
|
+
return {
|
|
7304
|
+
toolName: e.metadata?.toolName,
|
|
7305
|
+
success: e.metadata?.success,
|
|
7306
|
+
filePath: e.metadata?.filePath,
|
|
7307
|
+
command: e.metadata?.command
|
|
7308
|
+
};
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
function isTestLikeCommand(command) {
|
|
7312
|
+
if (!command)
|
|
7313
|
+
return false;
|
|
7314
|
+
return /(test|jest|vitest|pytest|go test|cargo test|lint|eslint|build|tsc)/i.test(command);
|
|
7315
|
+
}
|
|
7316
|
+
function safeRatio(num, den) {
|
|
7317
|
+
if (!Number.isFinite(num) || !Number.isFinite(den) || den <= 0)
|
|
7318
|
+
return 0;
|
|
7319
|
+
return num / den;
|
|
7320
|
+
}
|
|
7321
|
+
function round(value, digits = 4) {
|
|
7322
|
+
const factor = 10 ** digits;
|
|
7323
|
+
return Math.round(value * factor) / factor;
|
|
7324
|
+
}
|
|
7325
|
+
function computeSessionTurnCount(sessionEvents) {
|
|
7326
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
7327
|
+
for (const e of sessionEvents) {
|
|
7328
|
+
const turnId = e.metadata?.turnId;
|
|
7329
|
+
if (typeof turnId === "string" && turnId.length > 0)
|
|
7330
|
+
turnIds.add(turnId);
|
|
7331
|
+
}
|
|
7332
|
+
if (turnIds.size > 0)
|
|
7333
|
+
return turnIds.size;
|
|
7334
|
+
return sessionEvents.filter((e) => e.eventType === "user_prompt").length;
|
|
7335
|
+
}
|
|
7336
|
+
function computeKpiMetrics(events, usefulRecallRate) {
|
|
7337
|
+
const prompts = events.filter((e) => e.eventType === "user_prompt");
|
|
7338
|
+
const promptCount = prompts.length;
|
|
7339
|
+
const memoryHitPrompts = prompts.filter((p) => p.metadata?.adherence?.checked).length;
|
|
7340
|
+
const memoryHitRate = round(safeRatio(memoryHitPrompts, promptCount));
|
|
7341
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
7342
|
+
for (const e of events) {
|
|
7343
|
+
const arr = sessions.get(e.sessionId) || [];
|
|
7344
|
+
arr.push(e);
|
|
7345
|
+
sessions.set(e.sessionId, arr);
|
|
7346
|
+
}
|
|
7347
|
+
let sessionTurnTotal = 0;
|
|
7348
|
+
let sessionTurnSamples = 0;
|
|
7349
|
+
let firstValidEditMinutesTotal = 0;
|
|
7350
|
+
let firstValidEditSamples = 0;
|
|
7351
|
+
for (const sessionEvents of sessions.values()) {
|
|
7352
|
+
sessionEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
7353
|
+
const turns = computeSessionTurnCount(sessionEvents);
|
|
7354
|
+
if (turns > 0) {
|
|
7355
|
+
sessionTurnTotal += turns;
|
|
7356
|
+
sessionTurnSamples++;
|
|
7357
|
+
}
|
|
7358
|
+
const firstPrompt = sessionEvents.find((e) => e.eventType === "user_prompt");
|
|
7359
|
+
const firstEdit = sessionEvents.find((e) => {
|
|
7360
|
+
const payload = parseToolPayload(e);
|
|
7361
|
+
return payload?.toolName && isEditToolName(payload.toolName) && payload.success === true;
|
|
7362
|
+
});
|
|
7363
|
+
if (firstPrompt && firstEdit) {
|
|
7364
|
+
const minutes = (firstEdit.timestamp.getTime() - firstPrompt.timestamp.getTime()) / 6e4;
|
|
7365
|
+
if (minutes >= 0) {
|
|
7366
|
+
firstValidEditMinutesTotal += minutes;
|
|
7367
|
+
firstValidEditSamples++;
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7370
|
+
}
|
|
7371
|
+
const avgCompletionTurns = round(safeRatio(sessionTurnTotal, sessionTurnSamples), 2);
|
|
7372
|
+
const timeToFirstValidEditMinutes = round(safeRatio(firstValidEditMinutesTotal, firstValidEditSamples), 2);
|
|
7373
|
+
const editActions = [];
|
|
7374
|
+
let testRunsAfterEdit = 0;
|
|
7375
|
+
let failedTestRunsAfterEdit = 0;
|
|
7376
|
+
for (const [sessionId, sessionEvents] of sessions.entries()) {
|
|
7377
|
+
const sorted = [...sessionEvents].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
7378
|
+
let seenEdit = false;
|
|
7379
|
+
for (const e of sorted) {
|
|
7380
|
+
const payload = parseToolPayload(e);
|
|
7381
|
+
if (!payload?.toolName)
|
|
7382
|
+
continue;
|
|
7383
|
+
if (isEditToolName(payload.toolName) && payload.success === true) {
|
|
7384
|
+
editActions.push({ sessionId, timestamp: e.timestamp.getTime(), filePath: payload.filePath });
|
|
7385
|
+
seenEdit = true;
|
|
7386
|
+
continue;
|
|
7387
|
+
}
|
|
7388
|
+
if (seenEdit && isTestLikeCommand(payload.command)) {
|
|
7389
|
+
testRunsAfterEdit++;
|
|
7390
|
+
if (payload.success === false)
|
|
7391
|
+
failedTestRunsAfterEdit++;
|
|
7392
|
+
}
|
|
7393
|
+
}
|
|
7394
|
+
}
|
|
7395
|
+
const THIRTY_MIN_MS = 30 * 60 * 1e3;
|
|
7396
|
+
let reworkCount = 0;
|
|
7397
|
+
const bySessionFile = /* @__PURE__ */ new Map();
|
|
7398
|
+
const sortedEdits = [...editActions].sort((a, b) => a.timestamp - b.timestamp);
|
|
7399
|
+
for (const edit of sortedEdits) {
|
|
7400
|
+
if (!edit.filePath)
|
|
7401
|
+
continue;
|
|
7402
|
+
const key = `${edit.sessionId}::${edit.filePath}`;
|
|
7403
|
+
const prev = bySessionFile.get(key);
|
|
7404
|
+
if (typeof prev === "number" && edit.timestamp - prev <= THIRTY_MIN_MS) {
|
|
7405
|
+
reworkCount++;
|
|
7406
|
+
}
|
|
7407
|
+
bySessionFile.set(key, edit.timestamp);
|
|
7408
|
+
}
|
|
7409
|
+
const reworkRate = round(safeRatio(reworkCount, editActions.length));
|
|
7410
|
+
const postChangeFailureRate = round(safeRatio(failedTestRunsAfterEdit, testRunsAfterEdit));
|
|
7411
|
+
return {
|
|
7412
|
+
memoryHitRate,
|
|
7413
|
+
usefulRecallRate,
|
|
7414
|
+
avgCompletionTurns,
|
|
7415
|
+
timeToFirstValidEditMinutes,
|
|
7416
|
+
reworkRate,
|
|
7417
|
+
postChangeFailureRate
|
|
7418
|
+
};
|
|
7419
|
+
}
|
|
7245
7420
|
statsRouter.get("/shared", async (c) => {
|
|
7246
7421
|
const memoryService = getServiceFromQuery(c);
|
|
7247
7422
|
try {
|
|
@@ -7521,6 +7696,129 @@ statsRouter.get("/retrieval-traces", async (c) => {
|
|
|
7521
7696
|
await memoryService.shutdown();
|
|
7522
7697
|
}
|
|
7523
7698
|
});
|
|
7699
|
+
statsRouter.get("/kpi", async (c) => {
|
|
7700
|
+
const rawWindow = c.req.query("window") || "7d";
|
|
7701
|
+
const window = rawWindow === "24h" || rawWindow === "30d" ? rawWindow : "7d";
|
|
7702
|
+
const memoryService = getServiceFromQuery(c);
|
|
7703
|
+
try {
|
|
7704
|
+
await memoryService.initialize();
|
|
7705
|
+
const now = Date.now();
|
|
7706
|
+
const thresholds = loadKpiThresholds();
|
|
7707
|
+
const allEvents = await memoryService.getRecentEvents(2e4);
|
|
7708
|
+
const events = allEvents.filter((e) => inWindow(e, now, window));
|
|
7709
|
+
const helpfulness = await memoryService.getHelpfulnessStats();
|
|
7710
|
+
const usefulRecallRate = helpfulness.totalEvaluated > 0 ? round(safeRatio(helpfulness.helpful, helpfulness.totalEvaluated)) : 0;
|
|
7711
|
+
const metrics = computeKpiMetrics(events, usefulRecallRate);
|
|
7712
|
+
const windowMs = windowToMs(window);
|
|
7713
|
+
const prevEvents = allEvents.filter((e) => {
|
|
7714
|
+
const age = now - e.timestamp.getTime();
|
|
7715
|
+
return age > windowMs && age <= windowMs * 2;
|
|
7716
|
+
});
|
|
7717
|
+
const previousMetrics = computeKpiMetrics(prevEvents, usefulRecallRate);
|
|
7718
|
+
const deltas = {
|
|
7719
|
+
memoryHitRate: round(metrics.memoryHitRate - previousMetrics.memoryHitRate),
|
|
7720
|
+
usefulRecallRate: round(metrics.usefulRecallRate - previousMetrics.usefulRecallRate),
|
|
7721
|
+
avgCompletionTurns: round(metrics.avgCompletionTurns - previousMetrics.avgCompletionTurns, 2),
|
|
7722
|
+
timeToFirstValidEditMinutes: round(metrics.timeToFirstValidEditMinutes - previousMetrics.timeToFirstValidEditMinutes, 2),
|
|
7723
|
+
reworkRate: round(metrics.reworkRate - previousMetrics.reworkRate),
|
|
7724
|
+
postChangeFailureRate: round(metrics.postChangeFailureRate - previousMetrics.postChangeFailureRate)
|
|
7725
|
+
};
|
|
7726
|
+
const THIRTY_MIN_MS = 30 * 60 * 1e3;
|
|
7727
|
+
const trendWindowMs = 30 * 24 * 60 * 60 * 1e3;
|
|
7728
|
+
const trendEvents = allEvents.filter((e) => now - e.timestamp.getTime() <= trendWindowMs);
|
|
7729
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
7730
|
+
for (const e of trendEvents) {
|
|
7731
|
+
const day = e.timestamp.toISOString().split("T")[0];
|
|
7732
|
+
const arr = buckets.get(day) || [];
|
|
7733
|
+
arr.push(e);
|
|
7734
|
+
buckets.set(day, arr);
|
|
7735
|
+
}
|
|
7736
|
+
const trendDaily = Array.from(buckets.entries()).sort((a, b) => a[0].localeCompare(b[0])).map(([date, dayEvents]) => {
|
|
7737
|
+
const dayPrompts = dayEvents.filter((e) => e.eventType === "user_prompt");
|
|
7738
|
+
const dayPromptCount = dayPrompts.length;
|
|
7739
|
+
const dayMemoryHit = dayPrompts.filter((p) => p.metadata?.adherence?.checked).length;
|
|
7740
|
+
const dayEdits = dayEvents.filter((e) => {
|
|
7741
|
+
const p = parseToolPayload(e);
|
|
7742
|
+
return Boolean(p?.toolName && isEditToolName(p.toolName) && p.success === true);
|
|
7743
|
+
});
|
|
7744
|
+
const dayEditActions = dayEdits.map((e) => {
|
|
7745
|
+
const p = parseToolPayload(e);
|
|
7746
|
+
return { sessionId: e.sessionId, timestamp: e.timestamp.getTime(), filePath: p?.filePath };
|
|
7747
|
+
}).filter((x) => Boolean(x.filePath));
|
|
7748
|
+
let dayReworkCount = 0;
|
|
7749
|
+
const dayBySessionFile = /* @__PURE__ */ new Map();
|
|
7750
|
+
for (const edit of dayEditActions) {
|
|
7751
|
+
const key = `${edit.sessionId}::${edit.filePath}`;
|
|
7752
|
+
const prev = dayBySessionFile.get(key);
|
|
7753
|
+
if (typeof prev === "number" && edit.timestamp - prev <= THIRTY_MIN_MS)
|
|
7754
|
+
dayReworkCount++;
|
|
7755
|
+
dayBySessionFile.set(key, edit.timestamp);
|
|
7756
|
+
}
|
|
7757
|
+
const dayTests = dayEvents.filter((e) => {
|
|
7758
|
+
const p = parseToolPayload(e);
|
|
7759
|
+
return Boolean(p?.toolName && isTestLikeCommand(p.command));
|
|
7760
|
+
});
|
|
7761
|
+
const dayFailedTests = dayEvents.filter((e) => {
|
|
7762
|
+
const p = parseToolPayload(e);
|
|
7763
|
+
return Boolean(p?.toolName && isTestLikeCommand(p.command) && p.success === false);
|
|
7764
|
+
});
|
|
7765
|
+
const turnsBySession = /* @__PURE__ */ new Map();
|
|
7766
|
+
for (const e of dayEvents) {
|
|
7767
|
+
const arr = turnsBySession.get(e.sessionId) || [];
|
|
7768
|
+
arr.push(e);
|
|
7769
|
+
turnsBySession.set(e.sessionId, arr);
|
|
7770
|
+
}
|
|
7771
|
+
let dayTurnsTotal = 0;
|
|
7772
|
+
let dayTurnsSamples = 0;
|
|
7773
|
+
for (const sessionEvents of turnsBySession.values()) {
|
|
7774
|
+
const turns = computeSessionTurnCount(sessionEvents);
|
|
7775
|
+
if (turns > 0) {
|
|
7776
|
+
dayTurnsTotal += turns;
|
|
7777
|
+
dayTurnsSamples++;
|
|
7778
|
+
}
|
|
7779
|
+
}
|
|
7780
|
+
return {
|
|
7781
|
+
date,
|
|
7782
|
+
memoryHitRate: round(safeRatio(dayMemoryHit, dayPromptCount)),
|
|
7783
|
+
usefulRecallRate,
|
|
7784
|
+
reworkRate: round(safeRatio(dayReworkCount, dayEditActions.length)),
|
|
7785
|
+
postChangeFailureRate: round(safeRatio(dayFailedTests.length, dayTests.length)),
|
|
7786
|
+
avgCompletionTurns: round(safeRatio(dayTurnsTotal, dayTurnsSamples), 2)
|
|
7787
|
+
};
|
|
7788
|
+
});
|
|
7789
|
+
const alerts = [];
|
|
7790
|
+
if (metrics.usefulRecallRate < thresholds.usefulRecallRateMin) {
|
|
7791
|
+
alerts.push({ metric: "usefulRecallRate", level: "warn", message: "Useful recall rate is below threshold", value: metrics.usefulRecallRate, threshold: thresholds.usefulRecallRateMin });
|
|
7792
|
+
}
|
|
7793
|
+
if (metrics.reworkRate > thresholds.reworkRateMax) {
|
|
7794
|
+
alerts.push({ metric: "reworkRate", level: "warn", message: "Rework rate is above threshold", value: metrics.reworkRate, threshold: thresholds.reworkRateMax });
|
|
7795
|
+
}
|
|
7796
|
+
if (metrics.postChangeFailureRate > thresholds.postChangeFailureRateMax) {
|
|
7797
|
+
alerts.push({ metric: "postChangeFailureRate", level: "warn", message: "Post-change failure rate is above threshold", value: metrics.postChangeFailureRate, threshold: thresholds.postChangeFailureRateMax });
|
|
7798
|
+
}
|
|
7799
|
+
if (metrics.avgCompletionTurns > thresholds.avgCompletionTurnsMax) {
|
|
7800
|
+
alerts.push({ metric: "avgCompletionTurns", level: "warn", message: "Average completion turns is above threshold", value: metrics.avgCompletionTurns, threshold: thresholds.avgCompletionTurnsMax });
|
|
7801
|
+
}
|
|
7802
|
+
if (metrics.memoryHitRate < thresholds.memoryHitRateMin) {
|
|
7803
|
+
alerts.push({ metric: "memoryHitRate", level: "warn", message: "Memory hit rate is below threshold", value: metrics.memoryHitRate, threshold: thresholds.memoryHitRateMin });
|
|
7804
|
+
}
|
|
7805
|
+
return c.json({
|
|
7806
|
+
window,
|
|
7807
|
+
metrics,
|
|
7808
|
+
previousMetrics,
|
|
7809
|
+
deltas,
|
|
7810
|
+
trend: {
|
|
7811
|
+
daily: trendDaily
|
|
7812
|
+
},
|
|
7813
|
+
thresholds,
|
|
7814
|
+
alerts
|
|
7815
|
+
});
|
|
7816
|
+
} catch (error) {
|
|
7817
|
+
return c.json({ error: error.message }, 500);
|
|
7818
|
+
} finally {
|
|
7819
|
+
await memoryService.shutdown();
|
|
7820
|
+
}
|
|
7821
|
+
});
|
|
7524
7822
|
statsRouter.post("/graduation/run", async (c) => {
|
|
7525
7823
|
const memoryService = getServiceFromQuery(c);
|
|
7526
7824
|
try {
|
|
@@ -7768,19 +8066,19 @@ turnsRouter.post("/backfill", async (c) => {
|
|
|
7768
8066
|
|
|
7769
8067
|
// src/server/api/projects.ts
|
|
7770
8068
|
import { Hono as Hono7 } from "hono";
|
|
7771
|
-
import * as
|
|
7772
|
-
import * as
|
|
8069
|
+
import * as fs6 from "fs";
|
|
8070
|
+
import * as path6 from "path";
|
|
7773
8071
|
import * as os3 from "os";
|
|
7774
8072
|
var projectsRouter = new Hono7();
|
|
7775
8073
|
projectsRouter.get("/", async (c) => {
|
|
7776
8074
|
try {
|
|
7777
|
-
const projectsDir =
|
|
7778
|
-
if (!
|
|
8075
|
+
const projectsDir = path6.join(os3.homedir(), ".claude-code", "memory", "projects");
|
|
8076
|
+
if (!fs6.existsSync(projectsDir)) {
|
|
7779
8077
|
return c.json({ projects: [] });
|
|
7780
8078
|
}
|
|
7781
|
-
const projectHashes =
|
|
7782
|
-
const fullPath =
|
|
7783
|
-
return
|
|
8079
|
+
const projectHashes = fs6.readdirSync(projectsDir).filter((name) => {
|
|
8080
|
+
const fullPath = path6.join(projectsDir, name);
|
|
8081
|
+
return fs6.statSync(fullPath).isDirectory();
|
|
7784
8082
|
});
|
|
7785
8083
|
const registry = loadSessionRegistry();
|
|
7786
8084
|
const hashToPath = /* @__PURE__ */ new Map();
|
|
@@ -7790,17 +8088,17 @@ projectsRouter.get("/", async (c) => {
|
|
|
7790
8088
|
}
|
|
7791
8089
|
}
|
|
7792
8090
|
const projects = projectHashes.map((hash) => {
|
|
7793
|
-
const dirPath =
|
|
7794
|
-
const dbPath =
|
|
8091
|
+
const dirPath = path6.join(projectsDir, hash);
|
|
8092
|
+
const dbPath = path6.join(dirPath, "events.sqlite");
|
|
7795
8093
|
let dbSize = 0;
|
|
7796
|
-
if (
|
|
7797
|
-
dbSize =
|
|
8094
|
+
if (fs6.existsSync(dbPath)) {
|
|
8095
|
+
dbSize = fs6.statSync(dbPath).size;
|
|
7798
8096
|
}
|
|
7799
8097
|
const projectPath = hashToPath.get(hash) || `unknown (${hash})`;
|
|
7800
8098
|
return {
|
|
7801
8099
|
hash,
|
|
7802
8100
|
projectPath,
|
|
7803
|
-
projectName:
|
|
8101
|
+
projectName: path6.basename(projectPath),
|
|
7804
8102
|
dbSize,
|
|
7805
8103
|
dbSizeHuman: formatBytes(dbSize)
|
|
7806
8104
|
};
|
|
@@ -7925,7 +8223,7 @@ function buildPrompt(statsContext, memoryContext, history, currentMessage) {
|
|
|
7925
8223
|
return parts.join("\n");
|
|
7926
8224
|
}
|
|
7927
8225
|
function streamClaudeResponse(prompt, stream) {
|
|
7928
|
-
return new Promise((
|
|
8226
|
+
return new Promise((resolve3, reject) => {
|
|
7929
8227
|
const proc = spawn("claude", [
|
|
7930
8228
|
"-p",
|
|
7931
8229
|
"--output-format",
|
|
@@ -7997,7 +8295,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
7997
8295
|
if (code !== 0 && code !== null) {
|
|
7998
8296
|
reject(new Error(`Claude CLI exited with code ${code}`));
|
|
7999
8297
|
} else {
|
|
8000
|
-
|
|
8298
|
+
resolve3();
|
|
8001
8299
|
}
|
|
8002
8300
|
});
|
|
8003
8301
|
});
|
|
@@ -8054,14 +8352,14 @@ app.use("/*", cors());
|
|
|
8054
8352
|
app.use("/*", logger());
|
|
8055
8353
|
app.route("/api", apiRouter);
|
|
8056
8354
|
app.get("/health", (c) => c.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
8057
|
-
var uiPath =
|
|
8058
|
-
if (
|
|
8355
|
+
var uiPath = path7.join(__dirname, "../../dist/ui");
|
|
8356
|
+
if (fs7.existsSync(uiPath)) {
|
|
8059
8357
|
app.use("/*", serveStatic({ root: uiPath }));
|
|
8060
8358
|
}
|
|
8061
8359
|
app.get("*", (c) => {
|
|
8062
|
-
const indexPath =
|
|
8063
|
-
if (
|
|
8064
|
-
return c.html(
|
|
8360
|
+
const indexPath = path7.join(uiPath, "index.html");
|
|
8361
|
+
if (fs7.existsSync(indexPath)) {
|
|
8362
|
+
return c.html(fs7.readFileSync(indexPath, "utf-8"));
|
|
8065
8363
|
}
|
|
8066
8364
|
return c.text('UI not built. Run "npm run build:ui" first.', 404);
|
|
8067
8365
|
});
|