claude-memory-layer 1.0.36 → 1.0.38
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/dist/cli/index.js +776 -126
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +377 -23
- package/dist/core/index.js.map +3 -3
- package/dist/hooks/post-tool-use.js +470 -75
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/semantic-daemon.js +428 -68
- package/dist/hooks/semantic-daemon.js.map +4 -4
- package/dist/hooks/session-end.js +428 -68
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +428 -68
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +469 -74
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +428 -68
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/index.js +389 -29
- package/dist/index.js.map +3 -3
- package/dist/mcp/index.js +474 -79
- package/dist/mcp/index.js.map +4 -4
- package/dist/server/api/index.js +632 -112
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +632 -112
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +428 -68
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/assets/js/chat.js +21 -3
- package/package.json +1 -1
package/dist/server/api/index.js
CHANGED
|
@@ -15,8 +15,8 @@ import { Hono } from "hono";
|
|
|
15
15
|
import * as os5 from "os";
|
|
16
16
|
|
|
17
17
|
// src/core/engine/memory-service-composition.ts
|
|
18
|
-
import * as
|
|
19
|
-
import * as
|
|
18
|
+
import * as os2 from "os";
|
|
19
|
+
import * as path7 from "path";
|
|
20
20
|
|
|
21
21
|
// src/core/metadata-extractor.ts
|
|
22
22
|
function createToolObservationEmbedding(toolName, metadata, success) {
|
|
@@ -1527,8 +1527,8 @@ function createEndlessMemoryServices(options) {
|
|
|
1527
1527
|
}
|
|
1528
1528
|
|
|
1529
1529
|
// src/core/engine/memory-engine-services.ts
|
|
1530
|
-
import * as
|
|
1531
|
-
import * as
|
|
1530
|
+
import * as fs6 from "fs";
|
|
1531
|
+
import * as path5 from "path";
|
|
1532
1532
|
|
|
1533
1533
|
// src/extensions/vector/embedder.ts
|
|
1534
1534
|
var DEFAULT_EMBEDDING_MODEL = "Xenova/multilingual-e5-small";
|
|
@@ -2206,14 +2206,43 @@ function makeDedupeKey(content, sessionId) {
|
|
|
2206
2206
|
return `${sessionId}:${contentHash}`;
|
|
2207
2207
|
}
|
|
2208
2208
|
|
|
2209
|
+
// src/core/sqlite-event-store.ts
|
|
2210
|
+
import * as nodePath2 from "path";
|
|
2211
|
+
|
|
2212
|
+
// src/core/registry/project-path.ts
|
|
2213
|
+
import * as crypto2 from "crypto";
|
|
2214
|
+
import * as fs3 from "fs";
|
|
2215
|
+
import * as os from "os";
|
|
2216
|
+
import * as path3 from "path";
|
|
2217
|
+
function normalizeProjectPath(projectPath) {
|
|
2218
|
+
const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
|
|
2219
|
+
try {
|
|
2220
|
+
return fs3.realpathSync(expanded);
|
|
2221
|
+
} catch {
|
|
2222
|
+
return path3.resolve(expanded);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
function hashProjectPath(projectPath) {
|
|
2226
|
+
const normalizedPath = normalizeProjectPath(projectPath);
|
|
2227
|
+
return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
|
|
2228
|
+
}
|
|
2229
|
+
function getProjectStoragePath(projectPath) {
|
|
2230
|
+
const hash = hashProjectPath(projectPath);
|
|
2231
|
+
return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
|
|
2232
|
+
}
|
|
2233
|
+
function resolveProjectStoragePath(projectOrHash) {
|
|
2234
|
+
const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
|
|
2235
|
+
return isHash ? path3.join(os.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2209
2238
|
// src/core/sqlite-wrapper.ts
|
|
2210
2239
|
import Database from "better-sqlite3";
|
|
2211
|
-
import * as
|
|
2240
|
+
import * as fs4 from "fs";
|
|
2212
2241
|
import * as nodePath from "path";
|
|
2213
2242
|
function createSQLiteDatabase(path13, options) {
|
|
2214
2243
|
const dir = nodePath.dirname(path13);
|
|
2215
|
-
if (!
|
|
2216
|
-
|
|
2244
|
+
if (!fs4.existsSync(dir)) {
|
|
2245
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
2217
2246
|
}
|
|
2218
2247
|
const db = new Database(path13, {
|
|
2219
2248
|
readonly: options?.readonly ?? false
|
|
@@ -2262,8 +2291,8 @@ function toSQLiteTimestamp(date) {
|
|
|
2262
2291
|
}
|
|
2263
2292
|
|
|
2264
2293
|
// src/core/markdown-mirror.ts
|
|
2265
|
-
import * as
|
|
2266
|
-
import * as
|
|
2294
|
+
import * as fs5 from "fs/promises";
|
|
2295
|
+
import * as path4 from "path";
|
|
2267
2296
|
var DEFAULT_NAMESPACE = "default";
|
|
2268
2297
|
var DEFAULT_CATEGORY = "uncategorized";
|
|
2269
2298
|
function sanitizeSegment2(input, fallback) {
|
|
@@ -2292,7 +2321,7 @@ function buildMirrorPath2(rootDir, event) {
|
|
|
2292
2321
|
const yyyy = d.getFullYear();
|
|
2293
2322
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
2294
2323
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
2295
|
-
return
|
|
2324
|
+
return path4.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
|
|
2296
2325
|
}
|
|
2297
2326
|
function formatMirrorEntry(event) {
|
|
2298
2327
|
const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
|
|
@@ -2313,8 +2342,8 @@ var MarkdownMirror2 = class {
|
|
|
2313
2342
|
}
|
|
2314
2343
|
async append(event) {
|
|
2315
2344
|
const outPath = buildMirrorPath2(this.rootDir, event);
|
|
2316
|
-
await
|
|
2317
|
-
await
|
|
2345
|
+
await fs5.mkdir(path4.dirname(outPath), { recursive: true });
|
|
2346
|
+
await fs5.appendFile(outPath, formatMirrorEntry(event), "utf8");
|
|
2318
2347
|
return outPath;
|
|
2319
2348
|
}
|
|
2320
2349
|
};
|
|
@@ -2327,6 +2356,143 @@ function normalizeQueryRewriteKind(value) {
|
|
|
2327
2356
|
return "none";
|
|
2328
2357
|
}
|
|
2329
2358
|
var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
|
|
2359
|
+
var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2360
|
+
var DEFAULT_OUTBOX_MAX_RETRIES = 3;
|
|
2361
|
+
function emptyOutboxRecoveryResult() {
|
|
2362
|
+
return {
|
|
2363
|
+
embedding: { recoveredProcessing: 0, retriedFailed: 0 },
|
|
2364
|
+
vector: { recoveredProcessing: 0, retriedFailed: 0 }
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
function isRecord(value) {
|
|
2368
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2369
|
+
}
|
|
2370
|
+
function getNestedRecord(root, path13) {
|
|
2371
|
+
let cursor = root;
|
|
2372
|
+
for (const key of path13) {
|
|
2373
|
+
if (!isRecord(cursor))
|
|
2374
|
+
return void 0;
|
|
2375
|
+
cursor = cursor[key];
|
|
2376
|
+
}
|
|
2377
|
+
return isRecord(cursor) ? cursor : void 0;
|
|
2378
|
+
}
|
|
2379
|
+
function getNestedString(root, path13) {
|
|
2380
|
+
let cursor = root;
|
|
2381
|
+
for (const key of path13) {
|
|
2382
|
+
if (!isRecord(cursor))
|
|
2383
|
+
return void 0;
|
|
2384
|
+
cursor = cursor[key];
|
|
2385
|
+
}
|
|
2386
|
+
return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
|
|
2387
|
+
}
|
|
2388
|
+
function metadataProjectHash(metadata) {
|
|
2389
|
+
return getNestedString(metadata, ["scope", "project", "hash"]);
|
|
2390
|
+
}
|
|
2391
|
+
function metadataProjectPaths(metadata) {
|
|
2392
|
+
const candidates = [
|
|
2393
|
+
getNestedString(metadata, ["projectPath"]),
|
|
2394
|
+
getNestedString(metadata, ["sourceProjectPath"]),
|
|
2395
|
+
getNestedString(metadata, ["scope", "project", "path"])
|
|
2396
|
+
];
|
|
2397
|
+
const paths = [];
|
|
2398
|
+
for (const value of candidates) {
|
|
2399
|
+
if (value && !paths.includes(value))
|
|
2400
|
+
paths.push(value);
|
|
2401
|
+
}
|
|
2402
|
+
return paths;
|
|
2403
|
+
}
|
|
2404
|
+
function metadataProjectPath(metadata) {
|
|
2405
|
+
return metadataProjectPaths(metadata)[0];
|
|
2406
|
+
}
|
|
2407
|
+
function isActiveQuarantinedMetadata(metadata) {
|
|
2408
|
+
const quarantine = getNestedRecord(metadata, ["quarantine"]);
|
|
2409
|
+
return quarantine?.status === "active";
|
|
2410
|
+
}
|
|
2411
|
+
function activeQuarantineStatusExpression(column = "metadata") {
|
|
2412
|
+
return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
|
|
2413
|
+
}
|
|
2414
|
+
function notActiveQuarantinedSql(column = "metadata") {
|
|
2415
|
+
return `${activeQuarantineStatusExpression(column)} != 'active'`;
|
|
2416
|
+
}
|
|
2417
|
+
function maybeQuarantinePredicate(options, column = "metadata") {
|
|
2418
|
+
return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
|
|
2419
|
+
}
|
|
2420
|
+
function safeParseMetadataValue(value) {
|
|
2421
|
+
if (!value)
|
|
2422
|
+
return void 0;
|
|
2423
|
+
if (typeof value === "object")
|
|
2424
|
+
return isRecord(value) ? value : void 0;
|
|
2425
|
+
if (typeof value !== "string")
|
|
2426
|
+
return void 0;
|
|
2427
|
+
try {
|
|
2428
|
+
const parsed = JSON.parse(value);
|
|
2429
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
2430
|
+
} catch {
|
|
2431
|
+
return void 0;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
function isImportedOrLegacyScopedMetadata(metadata) {
|
|
2435
|
+
if (!metadata)
|
|
2436
|
+
return false;
|
|
2437
|
+
return Boolean(
|
|
2438
|
+
metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
function addMetadataTag(metadata, tag) {
|
|
2442
|
+
const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
|
|
2443
|
+
if (!current.includes(tag))
|
|
2444
|
+
metadata.tags = [...current, tag];
|
|
2445
|
+
}
|
|
2446
|
+
function buildRepairResult(projectHash, dryRun) {
|
|
2447
|
+
return {
|
|
2448
|
+
dryRun,
|
|
2449
|
+
projectHash,
|
|
2450
|
+
scanned: 0,
|
|
2451
|
+
repaired: 0,
|
|
2452
|
+
quarantined: 0,
|
|
2453
|
+
alreadyScoped: 0,
|
|
2454
|
+
skipped: 0,
|
|
2455
|
+
samples: []
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
function normalizeRepoName(value) {
|
|
2459
|
+
return value.replace(/\.git$/i, "").trim().toLowerCase();
|
|
2460
|
+
}
|
|
2461
|
+
function projectBasename(projectPath) {
|
|
2462
|
+
if (!projectPath)
|
|
2463
|
+
return void 0;
|
|
2464
|
+
const trimmed = projectPath.replace(/[\\/]+$/, "");
|
|
2465
|
+
const basename3 = nodePath2.basename(trimmed);
|
|
2466
|
+
return basename3 ? normalizeRepoName(basename3) : void 0;
|
|
2467
|
+
}
|
|
2468
|
+
function isProjectScopeRepairExplanation(content) {
|
|
2469
|
+
const normalized = content.toLowerCase();
|
|
2470
|
+
const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
|
|
2471
|
+
const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
|
|
2472
|
+
return hasRepairContext && hasExplanationContext;
|
|
2473
|
+
}
|
|
2474
|
+
function hasConflictingContentProjectHint(content, projectPath) {
|
|
2475
|
+
const currentName = projectBasename(projectPath);
|
|
2476
|
+
if (!currentName)
|
|
2477
|
+
return false;
|
|
2478
|
+
if (isProjectScopeRepairExplanation(content))
|
|
2479
|
+
return false;
|
|
2480
|
+
const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
|
|
2481
|
+
let githubMatch;
|
|
2482
|
+
while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
|
|
2483
|
+
const repo = normalizeRepoName(githubMatch[2] || "");
|
|
2484
|
+
if (repo && repo !== currentName)
|
|
2485
|
+
return true;
|
|
2486
|
+
}
|
|
2487
|
+
const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
|
|
2488
|
+
let workspaceMatch;
|
|
2489
|
+
while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
|
|
2490
|
+
const repo = normalizeRepoName(workspaceMatch[1] || "");
|
|
2491
|
+
if (repo && repo !== currentName)
|
|
2492
|
+
return true;
|
|
2493
|
+
}
|
|
2494
|
+
return false;
|
|
2495
|
+
}
|
|
2330
2496
|
var SQLiteEventStore = class {
|
|
2331
2497
|
db;
|
|
2332
2498
|
initialized = false;
|
|
@@ -2825,11 +2991,11 @@ var SQLiteEventStore = class {
|
|
|
2825
2991
|
/**
|
|
2826
2992
|
* Get events by session ID
|
|
2827
2993
|
*/
|
|
2828
|
-
async getSessionEvents(sessionId) {
|
|
2994
|
+
async getSessionEvents(sessionId, options) {
|
|
2829
2995
|
await this.initialize();
|
|
2830
2996
|
const rows = sqliteAll(
|
|
2831
2997
|
this.db,
|
|
2832
|
-
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
2998
|
+
`SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
2833
2999
|
[sessionId]
|
|
2834
3000
|
);
|
|
2835
3001
|
return rows.map(this.rowToEvent);
|
|
@@ -2837,11 +3003,11 @@ var SQLiteEventStore = class {
|
|
|
2837
3003
|
/**
|
|
2838
3004
|
* Get recent events
|
|
2839
3005
|
*/
|
|
2840
|
-
async getRecentEvents(limit = 100) {
|
|
3006
|
+
async getRecentEvents(limit = 100, options) {
|
|
2841
3007
|
await this.initialize();
|
|
2842
3008
|
const rows = sqliteAll(
|
|
2843
3009
|
this.db,
|
|
2844
|
-
`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
|
|
3010
|
+
`SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
|
|
2845
3011
|
[limit]
|
|
2846
3012
|
);
|
|
2847
3013
|
return rows.map(this.rowToEvent);
|
|
@@ -2849,11 +3015,11 @@ var SQLiteEventStore = class {
|
|
|
2849
3015
|
/**
|
|
2850
3016
|
* Get event by ID
|
|
2851
3017
|
*/
|
|
2852
|
-
async getEvent(id) {
|
|
3018
|
+
async getEvent(id, options) {
|
|
2853
3019
|
await this.initialize();
|
|
2854
3020
|
const row = sqliteGet(
|
|
2855
3021
|
this.db,
|
|
2856
|
-
`SELECT * FROM events WHERE id =
|
|
3022
|
+
`SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
|
|
2857
3023
|
[id]
|
|
2858
3024
|
);
|
|
2859
3025
|
if (!row)
|
|
@@ -2863,11 +3029,11 @@ var SQLiteEventStore = class {
|
|
|
2863
3029
|
/**
|
|
2864
3030
|
* Get events since a timestamp (for sync)
|
|
2865
3031
|
*/
|
|
2866
|
-
async getEventsSince(timestamp, limit = 1e3) {
|
|
3032
|
+
async getEventsSince(timestamp, limit = 1e3, options) {
|
|
2867
3033
|
await this.initialize();
|
|
2868
3034
|
const rows = sqliteAll(
|
|
2869
3035
|
this.db,
|
|
2870
|
-
`SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
|
|
3036
|
+
`SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
|
|
2871
3037
|
[timestamp, limit]
|
|
2872
3038
|
);
|
|
2873
3039
|
return rows.map(this.rowToEvent);
|
|
@@ -2876,11 +3042,11 @@ var SQLiteEventStore = class {
|
|
|
2876
3042
|
* Get events since a SQLite rowid (for robust incremental replication).
|
|
2877
3043
|
* Rowid is monotonic for append-only tables, independent of client timestamps.
|
|
2878
3044
|
*/
|
|
2879
|
-
async getEventsSinceRowid(lastRowid, limit = 1e3) {
|
|
3045
|
+
async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
|
|
2880
3046
|
await this.initialize();
|
|
2881
3047
|
const rows = sqliteAll(
|
|
2882
3048
|
this.db,
|
|
2883
|
-
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
|
|
3049
|
+
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
|
|
2884
3050
|
[lastRowid, limit]
|
|
2885
3051
|
);
|
|
2886
3052
|
return rows.map((row) => ({
|
|
@@ -3077,7 +3243,9 @@ var SQLiteEventStore = class {
|
|
|
3077
3243
|
const placeholders = ids.map(() => "?").join(",");
|
|
3078
3244
|
sqliteRun(
|
|
3079
3245
|
this.db,
|
|
3080
|
-
`UPDATE embedding_outbox
|
|
3246
|
+
`UPDATE embedding_outbox
|
|
3247
|
+
SET status = 'processing', processed_at = datetime('now'), error_message = NULL
|
|
3248
|
+
WHERE id IN (${placeholders})`,
|
|
3081
3249
|
ids
|
|
3082
3250
|
);
|
|
3083
3251
|
return pending.map((row) => ({
|
|
@@ -3113,19 +3281,19 @@ var SQLiteEventStore = class {
|
|
|
3113
3281
|
/**
|
|
3114
3282
|
* Count total events
|
|
3115
3283
|
*/
|
|
3116
|
-
async countEvents() {
|
|
3284
|
+
async countEvents(options) {
|
|
3117
3285
|
await this.initialize();
|
|
3118
|
-
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
|
|
3286
|
+
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
|
|
3119
3287
|
return row?.count || 0;
|
|
3120
3288
|
}
|
|
3121
3289
|
/**
|
|
3122
3290
|
* Get events page in timestamp ascending order (stable migration/reindex scans)
|
|
3123
3291
|
*/
|
|
3124
|
-
async getEventsPage(limit = 1e3, offset = 0) {
|
|
3292
|
+
async getEventsPage(limit = 1e3, offset = 0, options) {
|
|
3125
3293
|
await this.initialize();
|
|
3126
3294
|
const rows = sqliteAll(
|
|
3127
3295
|
this.db,
|
|
3128
|
-
`SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
3296
|
+
`SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
3129
3297
|
[limit, offset]
|
|
3130
3298
|
);
|
|
3131
3299
|
return rows.map(this.rowToEvent);
|
|
@@ -3147,6 +3315,197 @@ var SQLiteEventStore = class {
|
|
|
3147
3315
|
[error, ...ids]
|
|
3148
3316
|
);
|
|
3149
3317
|
}
|
|
3318
|
+
/**
|
|
3319
|
+
* Recover abandoned outbox work after a worker/process crash.
|
|
3320
|
+
*
|
|
3321
|
+
* Rows in `processing` are claimed work. If the process exits before marking
|
|
3322
|
+
* them done/failed, they otherwise remain invisible to future processing.
|
|
3323
|
+
* Recovery is deliberately age-gated so an active worker is not disturbed.
|
|
3324
|
+
*/
|
|
3325
|
+
async recoverStuckOutboxItems(options = {}) {
|
|
3326
|
+
await this.initialize();
|
|
3327
|
+
const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
|
|
3328
|
+
const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
|
|
3329
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
3330
|
+
const threshold = new Date(now.getTime() - thresholdMs).toISOString();
|
|
3331
|
+
const result = emptyOutboxRecoveryResult();
|
|
3332
|
+
const embeddingRecovered = sqliteRun(
|
|
3333
|
+
this.db,
|
|
3334
|
+
`UPDATE embedding_outbox
|
|
3335
|
+
SET status = 'pending', processed_at = NULL, error_message = NULL
|
|
3336
|
+
WHERE status = 'processing'
|
|
3337
|
+
AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
|
|
3338
|
+
[threshold]
|
|
3339
|
+
);
|
|
3340
|
+
result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
|
|
3341
|
+
const embeddingRetried = sqliteRun(
|
|
3342
|
+
this.db,
|
|
3343
|
+
`UPDATE embedding_outbox
|
|
3344
|
+
SET status = 'pending', error_message = NULL
|
|
3345
|
+
WHERE status = 'failed'
|
|
3346
|
+
AND retry_count < ?`,
|
|
3347
|
+
[maxRetries]
|
|
3348
|
+
);
|
|
3349
|
+
result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
|
|
3350
|
+
const vectorRecovered = sqliteRun(
|
|
3351
|
+
this.db,
|
|
3352
|
+
`UPDATE vector_outbox
|
|
3353
|
+
SET status = 'pending', updated_at = ?, error = NULL
|
|
3354
|
+
WHERE status = 'processing'
|
|
3355
|
+
AND datetime(updated_at) < datetime(?)`,
|
|
3356
|
+
[now.toISOString(), threshold]
|
|
3357
|
+
);
|
|
3358
|
+
result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
|
|
3359
|
+
const vectorRetried = sqliteRun(
|
|
3360
|
+
this.db,
|
|
3361
|
+
`UPDATE vector_outbox
|
|
3362
|
+
SET status = 'pending', updated_at = ?, error = NULL
|
|
3363
|
+
WHERE status = 'failed'
|
|
3364
|
+
AND retry_count < ?`,
|
|
3365
|
+
[now.toISOString(), maxRetries]
|
|
3366
|
+
);
|
|
3367
|
+
result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
|
|
3368
|
+
return result;
|
|
3369
|
+
}
|
|
3370
|
+
/**
|
|
3371
|
+
* Repair legacy imported events that predate canonical project scope metadata.
|
|
3372
|
+
*
|
|
3373
|
+
* Same-project legacy rows are tagged with scope.project.hash. Rows that look
|
|
3374
|
+
* imported but cannot be proven to belong to this project are quarantined so
|
|
3375
|
+
* dashboard default reads/search do not surface cross-project contamination.
|
|
3376
|
+
*/
|
|
3377
|
+
async repairLegacyProjectScope(options = {}) {
|
|
3378
|
+
await this.initialize();
|
|
3379
|
+
const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
|
|
3380
|
+
if (!projectHash) {
|
|
3381
|
+
throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
|
|
3382
|
+
}
|
|
3383
|
+
if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
|
|
3384
|
+
throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
|
|
3385
|
+
}
|
|
3386
|
+
const dryRun = options.dryRun === true;
|
|
3387
|
+
const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
|
|
3388
|
+
const result = buildRepairResult(projectHash, dryRun);
|
|
3389
|
+
const rows = sqliteAll(
|
|
3390
|
+
this.db,
|
|
3391
|
+
`SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
|
|
3392
|
+
FROM events e
|
|
3393
|
+
LEFT JOIN sessions s ON s.id = e.session_id
|
|
3394
|
+
ORDER BY e.timestamp ASC`,
|
|
3395
|
+
[]
|
|
3396
|
+
);
|
|
3397
|
+
const sample = (entry) => {
|
|
3398
|
+
if (result.samples.length < 20)
|
|
3399
|
+
result.samples.push(entry);
|
|
3400
|
+
};
|
|
3401
|
+
for (const row of rows) {
|
|
3402
|
+
result.scanned++;
|
|
3403
|
+
let metadata = {};
|
|
3404
|
+
let metadataParseInvalid = false;
|
|
3405
|
+
if (row.metadata) {
|
|
3406
|
+
const parsed = safeParseMetadataValue(row.metadata);
|
|
3407
|
+
if (parsed) {
|
|
3408
|
+
metadata = parsed;
|
|
3409
|
+
} else {
|
|
3410
|
+
metadataParseInvalid = true;
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
if (isActiveQuarantinedMetadata(metadata)) {
|
|
3414
|
+
result.skipped++;
|
|
3415
|
+
continue;
|
|
3416
|
+
}
|
|
3417
|
+
const currentHash = metadataProjectHash(metadata);
|
|
3418
|
+
const explicitPath = metadataProjectPath(metadata);
|
|
3419
|
+
const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
|
|
3420
|
+
const candidatePaths = metadataProjectPaths(metadata);
|
|
3421
|
+
if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
|
|
3422
|
+
candidatePaths.push(sessionProjectPath);
|
|
3423
|
+
}
|
|
3424
|
+
const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
|
|
3425
|
+
const pathHashes = candidatePaths.map((candidate) => {
|
|
3426
|
+
try {
|
|
3427
|
+
return { path: candidate, hash: hashProjectPath(candidate) };
|
|
3428
|
+
} catch {
|
|
3429
|
+
return { path: candidate, hash: void 0 };
|
|
3430
|
+
}
|
|
3431
|
+
});
|
|
3432
|
+
const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
|
|
3433
|
+
const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
|
|
3434
|
+
let action = "skipped";
|
|
3435
|
+
let reason;
|
|
3436
|
+
let observedProjectHash;
|
|
3437
|
+
if (foreignPath) {
|
|
3438
|
+
action = "quarantined";
|
|
3439
|
+
reason = "project-path-mismatch";
|
|
3440
|
+
observedProjectHash = foreignPath.hash;
|
|
3441
|
+
} else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
|
|
3442
|
+
action = "quarantined";
|
|
3443
|
+
reason = "content-project-mismatch";
|
|
3444
|
+
} else if (currentHash === projectHash) {
|
|
3445
|
+
result.alreadyScoped++;
|
|
3446
|
+
continue;
|
|
3447
|
+
} else if (currentHash && currentHash !== projectHash) {
|
|
3448
|
+
action = "quarantined";
|
|
3449
|
+
reason = "scope-hash-mismatch";
|
|
3450
|
+
observedProjectHash = currentHash;
|
|
3451
|
+
} else if (matchingPath) {
|
|
3452
|
+
action = "repaired";
|
|
3453
|
+
reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
|
|
3454
|
+
} else if (candidatePaths.length > 0) {
|
|
3455
|
+
action = "quarantined";
|
|
3456
|
+
reason = "project-path-mismatch";
|
|
3457
|
+
} else if (importedOrLegacy) {
|
|
3458
|
+
action = "quarantined";
|
|
3459
|
+
reason = "missing-project-scope";
|
|
3460
|
+
}
|
|
3461
|
+
if (action === "skipped" || !reason) {
|
|
3462
|
+
result.skipped++;
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
if (action === "repaired") {
|
|
3466
|
+
const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
|
|
3467
|
+
const project = isRecord(scope.project) ? { ...scope.project } : {};
|
|
3468
|
+
project.hash = projectHash;
|
|
3469
|
+
scope.project = project;
|
|
3470
|
+
metadata.scope = scope;
|
|
3471
|
+
metadata.repair = {
|
|
3472
|
+
...isRecord(metadata.repair) ? metadata.repair : {},
|
|
3473
|
+
legacyProjectScope: {
|
|
3474
|
+
action,
|
|
3475
|
+
reason,
|
|
3476
|
+
repairedAt: nowIso
|
|
3477
|
+
}
|
|
3478
|
+
};
|
|
3479
|
+
addMetadataTag(metadata, `proj:${projectHash}`);
|
|
3480
|
+
result.repaired++;
|
|
3481
|
+
} else {
|
|
3482
|
+
metadata.quarantine = {
|
|
3483
|
+
...isRecord(metadata.quarantine) ? metadata.quarantine : {},
|
|
3484
|
+
status: "active",
|
|
3485
|
+
category: "project-scope",
|
|
3486
|
+
reason,
|
|
3487
|
+
detectedAt: nowIso,
|
|
3488
|
+
expectedProjectHash: projectHash,
|
|
3489
|
+
...observedProjectHash ? { observedProjectHash } : {}
|
|
3490
|
+
};
|
|
3491
|
+
metadata.repair = {
|
|
3492
|
+
...isRecord(metadata.repair) ? metadata.repair : {},
|
|
3493
|
+
legacyProjectScope: {
|
|
3494
|
+
action,
|
|
3495
|
+
reason,
|
|
3496
|
+
repairedAt: nowIso
|
|
3497
|
+
}
|
|
3498
|
+
};
|
|
3499
|
+
addMetadataTag(metadata, "quarantine:project-scope");
|
|
3500
|
+
result.quarantined++;
|
|
3501
|
+
}
|
|
3502
|
+
sample({ eventId: row.id, action, reason });
|
|
3503
|
+
if (!dryRun) {
|
|
3504
|
+
sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
return result;
|
|
3508
|
+
}
|
|
3150
3509
|
/**
|
|
3151
3510
|
* Get embedding/vector outbox health statistics
|
|
3152
3511
|
*/
|
|
@@ -3194,7 +3553,11 @@ var SQLiteEventStore = class {
|
|
|
3194
3553
|
await this.initialize();
|
|
3195
3554
|
const rows = sqliteAll(
|
|
3196
3555
|
this.db,
|
|
3197
|
-
`SELECT level, COUNT(*) as count
|
|
3556
|
+
`SELECT ml.level, COUNT(*) as count
|
|
3557
|
+
FROM memory_levels ml
|
|
3558
|
+
INNER JOIN events e ON e.id = ml.event_id
|
|
3559
|
+
WHERE ${notActiveQuarantinedSql("e.metadata")}
|
|
3560
|
+
GROUP BY ml.level`
|
|
3198
3561
|
);
|
|
3199
3562
|
return rows;
|
|
3200
3563
|
}
|
|
@@ -3210,6 +3573,7 @@ var SQLiteEventStore = class {
|
|
|
3210
3573
|
`SELECT e.* FROM events e
|
|
3211
3574
|
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
3212
3575
|
WHERE ml.level = ?
|
|
3576
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3213
3577
|
ORDER BY e.timestamp DESC
|
|
3214
3578
|
LIMIT ? OFFSET ?`,
|
|
3215
3579
|
[level, limit, offset]
|
|
@@ -3302,12 +3666,13 @@ var SQLiteEventStore = class {
|
|
|
3302
3666
|
/**
|
|
3303
3667
|
* Get most accessed memories (falls back to recent events if none accessed)
|
|
3304
3668
|
*/
|
|
3305
|
-
async getMostAccessed(limit = 10) {
|
|
3669
|
+
async getMostAccessed(limit = 10, options) {
|
|
3306
3670
|
await this.initialize();
|
|
3307
3671
|
let rows = sqliteAll(
|
|
3308
3672
|
this.db,
|
|
3309
3673
|
`SELECT * FROM events
|
|
3310
3674
|
WHERE access_count > 0
|
|
3675
|
+
AND ${maybeQuarantinePredicate(options)}
|
|
3311
3676
|
ORDER BY access_count DESC, last_accessed_at DESC
|
|
3312
3677
|
LIMIT ?`,
|
|
3313
3678
|
[limit]
|
|
@@ -3316,6 +3681,7 @@ var SQLiteEventStore = class {
|
|
|
3316
3681
|
rows = sqliteAll(
|
|
3317
3682
|
this.db,
|
|
3318
3683
|
`SELECT * FROM events
|
|
3684
|
+
WHERE ${maybeQuarantinePredicate(options)}
|
|
3319
3685
|
ORDER BY timestamp DESC
|
|
3320
3686
|
LIMIT ?`,
|
|
3321
3687
|
[limit]
|
|
@@ -3446,6 +3812,7 @@ var SQLiteEventStore = class {
|
|
|
3446
3812
|
FROM memory_helpfulness mh
|
|
3447
3813
|
JOIN events e ON e.id = mh.event_id
|
|
3448
3814
|
WHERE mh.measured_at IS NOT NULL
|
|
3815
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3449
3816
|
GROUP BY mh.event_id
|
|
3450
3817
|
ORDER BY avg_score DESC
|
|
3451
3818
|
LIMIT ?`,
|
|
@@ -3510,6 +3877,7 @@ var SQLiteEventStore = class {
|
|
|
3510
3877
|
FROM events_fts fts
|
|
3511
3878
|
JOIN events e ON e.id = fts.event_id
|
|
3512
3879
|
WHERE events_fts MATCH ?
|
|
3880
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3513
3881
|
ORDER BY fts.rank
|
|
3514
3882
|
LIMIT ?`,
|
|
3515
3883
|
[searchTerms, limit]
|
|
@@ -3524,6 +3892,7 @@ var SQLiteEventStore = class {
|
|
|
3524
3892
|
this.db,
|
|
3525
3893
|
`SELECT *, 0 as rank FROM events
|
|
3526
3894
|
WHERE content LIKE ?
|
|
3895
|
+
AND ${notActiveQuarantinedSql()}
|
|
3527
3896
|
ORDER BY timestamp DESC
|
|
3528
3897
|
LIMIT ?`,
|
|
3529
3898
|
[likePattern, limit]
|
|
@@ -3730,6 +4099,7 @@ var SQLiteEventStore = class {
|
|
|
3730
4099
|
`SELECT turn_id, MIN(timestamp) as min_ts
|
|
3731
4100
|
FROM events
|
|
3732
4101
|
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4102
|
+
AND ${maybeQuarantinePredicate(options)}
|
|
3733
4103
|
GROUP BY turn_id
|
|
3734
4104
|
ORDER BY min_ts DESC
|
|
3735
4105
|
LIMIT ? OFFSET ?`,
|
|
@@ -3737,7 +4107,7 @@ var SQLiteEventStore = class {
|
|
|
3737
4107
|
);
|
|
3738
4108
|
const turns = [];
|
|
3739
4109
|
for (const turnRow of turnRows) {
|
|
3740
|
-
const events = await this.getEventsByTurn(turnRow.turn_id);
|
|
4110
|
+
const events = await this.getEventsByTurn(turnRow.turn_id, options);
|
|
3741
4111
|
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
3742
4112
|
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
3743
4113
|
const hasResponse = events.some((e) => e.eventType === "agent_response");
|
|
@@ -3756,11 +4126,11 @@ var SQLiteEventStore = class {
|
|
|
3756
4126
|
/**
|
|
3757
4127
|
* Get all events for a specific turn_id
|
|
3758
4128
|
*/
|
|
3759
|
-
async getEventsByTurn(turnId) {
|
|
4129
|
+
async getEventsByTurn(turnId, options) {
|
|
3760
4130
|
await this.initialize();
|
|
3761
4131
|
const rows = sqliteAll(
|
|
3762
4132
|
this.db,
|
|
3763
|
-
`SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
|
|
4133
|
+
`SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3764
4134
|
[turnId]
|
|
3765
4135
|
);
|
|
3766
4136
|
return rows.map(this.rowToEvent);
|
|
@@ -3768,13 +4138,14 @@ var SQLiteEventStore = class {
|
|
|
3768
4138
|
/**
|
|
3769
4139
|
* Count total turns for a session
|
|
3770
4140
|
*/
|
|
3771
|
-
async countSessionTurns(sessionId) {
|
|
4141
|
+
async countSessionTurns(sessionId, options) {
|
|
3772
4142
|
await this.initialize();
|
|
3773
4143
|
const row = sqliteGet(
|
|
3774
4144
|
this.db,
|
|
3775
4145
|
`SELECT COUNT(DISTINCT turn_id) as count
|
|
3776
4146
|
FROM events
|
|
3777
|
-
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4147
|
+
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4148
|
+
AND ${maybeQuarantinePredicate(options)}`,
|
|
3778
4149
|
[sessionId]
|
|
3779
4150
|
);
|
|
3780
4151
|
return row?.count || 0;
|
|
@@ -3866,7 +4237,7 @@ var SQLiteEventStore = class {
|
|
|
3866
4237
|
content: row.content,
|
|
3867
4238
|
canonicalKey: row.canonical_key,
|
|
3868
4239
|
dedupeKey: row.dedupe_key,
|
|
3869
|
-
metadata:
|
|
4240
|
+
metadata: safeParseMetadataValue(row.metadata)
|
|
3870
4241
|
};
|
|
3871
4242
|
if (row.access_count !== void 0) {
|
|
3872
4243
|
event.access_count = row.access_count;
|
|
@@ -4018,6 +4389,7 @@ var VectorStore = class {
|
|
|
4018
4389
|
* Get total count of vectors
|
|
4019
4390
|
*/
|
|
4020
4391
|
async count() {
|
|
4392
|
+
await this.initialize();
|
|
4021
4393
|
if (!this.table)
|
|
4022
4394
|
return 0;
|
|
4023
4395
|
const result = await this.table.countRows();
|
|
@@ -4456,6 +4828,14 @@ var MemoryQueryService = class {
|
|
|
4456
4828
|
await this.initialize();
|
|
4457
4829
|
return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
|
|
4458
4830
|
}
|
|
4831
|
+
async recoverStuckOutboxItems(options) {
|
|
4832
|
+
await this.initialize();
|
|
4833
|
+
return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
|
|
4834
|
+
}
|
|
4835
|
+
async repairLegacyProjectScope(options) {
|
|
4836
|
+
await this.initialize();
|
|
4837
|
+
return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
|
|
4838
|
+
}
|
|
4459
4839
|
async getStats() {
|
|
4460
4840
|
await this.initialize();
|
|
4461
4841
|
const deps = this.getStatsDeps();
|
|
@@ -6078,18 +6458,18 @@ function assertDefaultRetrieverStore(eventStore) {
|
|
|
6078
6458
|
function createMemoryEngineServices(options) {
|
|
6079
6459
|
const factories = options.factories ?? {};
|
|
6080
6460
|
const storagePath = options.storagePath;
|
|
6081
|
-
if (!options.readOnly && !
|
|
6082
|
-
|
|
6461
|
+
if (!options.readOnly && !fs6.existsSync(storagePath)) {
|
|
6462
|
+
fs6.mkdirSync(storagePath, { recursive: true });
|
|
6083
6463
|
}
|
|
6084
6464
|
const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
|
|
6085
|
-
|
|
6465
|
+
path5.join(storagePath, "events.sqlite"),
|
|
6086
6466
|
{
|
|
6087
6467
|
readonly: options.readOnly,
|
|
6088
6468
|
markdownMirrorRoot: storagePath
|
|
6089
6469
|
}
|
|
6090
6470
|
);
|
|
6091
6471
|
const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
|
|
6092
|
-
|
|
6472
|
+
path5.join(storagePath, "vectors")
|
|
6093
6473
|
);
|
|
6094
6474
|
const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
|
|
6095
6475
|
const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
|
|
@@ -6500,8 +6880,8 @@ function createMemoryRuntimeService(deps) {
|
|
|
6500
6880
|
}
|
|
6501
6881
|
|
|
6502
6882
|
// src/extensions/shared-memory/shared-memory-services.ts
|
|
6503
|
-
import * as
|
|
6504
|
-
import * as
|
|
6883
|
+
import * as fs7 from "fs";
|
|
6884
|
+
import * as path6 from "path";
|
|
6505
6885
|
|
|
6506
6886
|
// src/core/shared-event-store.ts
|
|
6507
6887
|
var SharedEventStore = class {
|
|
@@ -7189,7 +7569,7 @@ var SharedMemoryServices = class {
|
|
|
7189
7569
|
this.ensureDirectory(sharedPath, { allowCreate: true });
|
|
7190
7570
|
const store = await this.openStore(sharedPath);
|
|
7191
7571
|
this.sharedVectorStore = this.factories.createSharedVectorStore(
|
|
7192
|
-
|
|
7572
|
+
path6.join(sharedPath, "vectors")
|
|
7193
7573
|
);
|
|
7194
7574
|
await this.sharedVectorStore.initialize();
|
|
7195
7575
|
this.sharedPromoter = this.factories.createSharedPromoter(
|
|
@@ -7262,7 +7642,7 @@ var SharedMemoryServices = class {
|
|
|
7262
7642
|
async createOpenStorePromise(sharedPath) {
|
|
7263
7643
|
if (!this.sharedEventStore) {
|
|
7264
7644
|
const sharedEventStore = this.factories.createSharedEventStore(
|
|
7265
|
-
|
|
7645
|
+
path6.join(sharedPath, "shared.duckdb")
|
|
7266
7646
|
);
|
|
7267
7647
|
await sharedEventStore.initialize();
|
|
7268
7648
|
this.sharedEventStore = sharedEventStore;
|
|
@@ -7282,9 +7662,9 @@ var SharedMemoryServices = class {
|
|
|
7282
7662
|
}
|
|
7283
7663
|
get factories() {
|
|
7284
7664
|
return {
|
|
7285
|
-
existsSync: this.options.factories?.existsSync ??
|
|
7665
|
+
existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
|
|
7286
7666
|
mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
|
|
7287
|
-
|
|
7667
|
+
fs7.mkdirSync(targetPath, { recursive: true });
|
|
7288
7668
|
}),
|
|
7289
7669
|
createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
|
|
7290
7670
|
createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
|
|
@@ -7399,37 +7779,11 @@ function createMemoryServiceComposition(options) {
|
|
|
7399
7779
|
}
|
|
7400
7780
|
function defaultExpandPath(targetPath) {
|
|
7401
7781
|
if (targetPath.startsWith("~")) {
|
|
7402
|
-
return
|
|
7782
|
+
return path7.join(os2.homedir(), targetPath.slice(1));
|
|
7403
7783
|
}
|
|
7404
7784
|
return targetPath;
|
|
7405
7785
|
}
|
|
7406
7786
|
|
|
7407
|
-
// src/core/registry/project-path.ts
|
|
7408
|
-
import * as crypto2 from "crypto";
|
|
7409
|
-
import * as fs7 from "fs";
|
|
7410
|
-
import * as os2 from "os";
|
|
7411
|
-
import * as path7 from "path";
|
|
7412
|
-
function normalizeProjectPath(projectPath) {
|
|
7413
|
-
const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
|
|
7414
|
-
try {
|
|
7415
|
-
return fs7.realpathSync(expanded);
|
|
7416
|
-
} catch {
|
|
7417
|
-
return path7.resolve(expanded);
|
|
7418
|
-
}
|
|
7419
|
-
}
|
|
7420
|
-
function hashProjectPath(projectPath) {
|
|
7421
|
-
const normalizedPath = normalizeProjectPath(projectPath);
|
|
7422
|
-
return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
|
|
7423
|
-
}
|
|
7424
|
-
function getProjectStoragePath(projectPath) {
|
|
7425
|
-
const hash = hashProjectPath(projectPath);
|
|
7426
|
-
return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
|
|
7427
|
-
}
|
|
7428
|
-
function resolveProjectStoragePath(projectOrHash) {
|
|
7429
|
-
const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
|
|
7430
|
-
return isHash ? path7.join(os2.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
|
|
7431
|
-
}
|
|
7432
|
-
|
|
7433
7787
|
// src/core/registry/session-registry.ts
|
|
7434
7788
|
import * as fs8 from "fs";
|
|
7435
7789
|
import * as os3 from "os";
|
|
@@ -7728,6 +8082,12 @@ var MemoryService = class {
|
|
|
7728
8082
|
async getOutboxStats() {
|
|
7729
8083
|
return this.queryService.getOutboxStats();
|
|
7730
8084
|
}
|
|
8085
|
+
async recoverStuckOutboxItems(options) {
|
|
8086
|
+
return this.queryService.recoverStuckOutboxItems(options);
|
|
8087
|
+
}
|
|
8088
|
+
async repairLegacyProjectScope(options) {
|
|
8089
|
+
return this.queryService.repairLegacyProjectScope(options);
|
|
8090
|
+
}
|
|
7731
8091
|
async getRetrievalTraceStats() {
|
|
7732
8092
|
return this.retrievalAnalyticsService.getRetrievalTraceStats();
|
|
7733
8093
|
}
|
|
@@ -8041,6 +8401,26 @@ function getServiceFromQuery(c) {
|
|
|
8041
8401
|
}
|
|
8042
8402
|
return getReadOnlyMemoryService();
|
|
8043
8403
|
}
|
|
8404
|
+
function getWritableServiceFromQuery(c) {
|
|
8405
|
+
const project = c.req.query("project") || c.req.query("projectId");
|
|
8406
|
+
if (project) {
|
|
8407
|
+
const storagePath = resolveProjectStoragePath(project);
|
|
8408
|
+
return new MemoryService({
|
|
8409
|
+
storagePath,
|
|
8410
|
+
readOnly: false,
|
|
8411
|
+
lightweightMode: true,
|
|
8412
|
+
analyticsEnabled: false,
|
|
8413
|
+
sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
|
|
8414
|
+
});
|
|
8415
|
+
}
|
|
8416
|
+
return new MemoryService({
|
|
8417
|
+
storagePath: "~/.claude-code/memory",
|
|
8418
|
+
readOnly: false,
|
|
8419
|
+
lightweightMode: true,
|
|
8420
|
+
analyticsEnabled: false,
|
|
8421
|
+
sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
|
|
8422
|
+
});
|
|
8423
|
+
}
|
|
8044
8424
|
function getLightweightServiceFromQuery(c) {
|
|
8045
8425
|
const project = c.req.query("project") || c.req.query("projectId");
|
|
8046
8426
|
if (project) {
|
|
@@ -9696,6 +10076,13 @@ import { Hono as Hono8 } from "hono";
|
|
|
9696
10076
|
import { streamSSE } from "hono/streaming";
|
|
9697
10077
|
import { spawn } from "child_process";
|
|
9698
10078
|
var chatRouter = new Hono8();
|
|
10079
|
+
var ProviderFailure = class extends Error {
|
|
10080
|
+
constructor(code, message) {
|
|
10081
|
+
super(message);
|
|
10082
|
+
this.code = code;
|
|
10083
|
+
this.name = "ProviderFailure";
|
|
10084
|
+
}
|
|
10085
|
+
};
|
|
9699
10086
|
var CLAUDE_TIMEOUT_MS = 12e4;
|
|
9700
10087
|
chatRouter.post("/", async (c) => {
|
|
9701
10088
|
let body;
|
|
@@ -9707,43 +10094,12 @@ chatRouter.post("/", async (c) => {
|
|
|
9707
10094
|
if (!body.message?.trim()) {
|
|
9708
10095
|
return c.json({ error: "Message is required" }, 400);
|
|
9709
10096
|
}
|
|
9710
|
-
const
|
|
10097
|
+
const memoryOnly = body.mode === "memory-only" || body.memoryOnly === true;
|
|
10098
|
+
const memoryService = memoryOnly ? getLightweightServiceFromQuery(c) : getServiceFromQuery(c);
|
|
9711
10099
|
try {
|
|
9712
10100
|
await memoryService.initialize();
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
try {
|
|
9716
|
-
const result = await memoryService.retrieveMemories(body.message, {
|
|
9717
|
-
topK: 8,
|
|
9718
|
-
minScore: 0.5
|
|
9719
|
-
});
|
|
9720
|
-
if (result.memories.length > 0) {
|
|
9721
|
-
const parts = ["## Relevant Memories\n"];
|
|
9722
|
-
for (const m of result.memories) {
|
|
9723
|
-
const date = new Date(m.event.timestamp).toISOString().split("T")[0];
|
|
9724
|
-
const content = m.event.content.slice(0, 500);
|
|
9725
|
-
parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
|
|
9726
|
-
parts.push(content);
|
|
9727
|
-
if (m.sessionContext) {
|
|
9728
|
-
parts.push(`_Context: ${m.sessionContext}_`);
|
|
9729
|
-
}
|
|
9730
|
-
parts.push("");
|
|
9731
|
-
}
|
|
9732
|
-
memoryContext = parts.join("\n");
|
|
9733
|
-
}
|
|
9734
|
-
} catch {
|
|
9735
|
-
}
|
|
9736
|
-
try {
|
|
9737
|
-
const stats = await memoryService.getStats();
|
|
9738
|
-
const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
|
|
9739
|
-
statsContext = [
|
|
9740
|
-
"## Memory Stats",
|
|
9741
|
-
`- Total events: ${stats.totalEvents}`,
|
|
9742
|
-
`- Vector nodes: ${stats.vectorCount}`,
|
|
9743
|
-
`- By level: ${levels}`
|
|
9744
|
-
].join("\n");
|
|
9745
|
-
} catch {
|
|
9746
|
-
}
|
|
10101
|
+
const { memoryContext, memoryHits } = await collectMemoryContext(memoryService, body.message);
|
|
10102
|
+
const statsContext = await collectStatsContext(memoryService);
|
|
9747
10103
|
const fullPrompt = buildPrompt(
|
|
9748
10104
|
statsContext,
|
|
9749
10105
|
memoryContext,
|
|
@@ -9751,13 +10107,23 @@ chatRouter.post("/", async (c) => {
|
|
|
9751
10107
|
body.message
|
|
9752
10108
|
);
|
|
9753
10109
|
return streamSSE(c, async (stream) => {
|
|
10110
|
+
if (memoryOnly) {
|
|
10111
|
+
await streamMemoryOnlyResponse(stream, {
|
|
10112
|
+
memoryContext,
|
|
10113
|
+
memoryHits,
|
|
10114
|
+
reason: "memory-only-mode"
|
|
10115
|
+
});
|
|
10116
|
+
return;
|
|
10117
|
+
}
|
|
9754
10118
|
try {
|
|
9755
10119
|
await streamClaudeResponse(fullPrompt, stream);
|
|
9756
10120
|
} catch (err) {
|
|
10121
|
+
const diagnostic = providerDiagnostic(err);
|
|
9757
10122
|
await stream.writeSSE({
|
|
9758
|
-
event: "
|
|
9759
|
-
data: JSON.stringify(
|
|
10123
|
+
event: "provider_error",
|
|
10124
|
+
data: JSON.stringify(diagnostic)
|
|
9760
10125
|
});
|
|
10126
|
+
await streamMemoryOnlyFallback(stream, memoryContext, memoryHits);
|
|
9761
10127
|
}
|
|
9762
10128
|
});
|
|
9763
10129
|
} catch (error) {
|
|
@@ -9766,6 +10132,115 @@ chatRouter.post("/", async (c) => {
|
|
|
9766
10132
|
await memoryService.shutdown();
|
|
9767
10133
|
}
|
|
9768
10134
|
});
|
|
10135
|
+
async function collectMemoryContext(memoryService, query) {
|
|
10136
|
+
let memoryHits = [];
|
|
10137
|
+
try {
|
|
10138
|
+
const result = await memoryService.retrieveMemories?.(query, {
|
|
10139
|
+
topK: 8,
|
|
10140
|
+
minScore: 0.5
|
|
10141
|
+
});
|
|
10142
|
+
memoryHits = result?.memories ?? [];
|
|
10143
|
+
} catch {
|
|
10144
|
+
memoryHits = [];
|
|
10145
|
+
}
|
|
10146
|
+
if (memoryHits.length === 0) {
|
|
10147
|
+
try {
|
|
10148
|
+
memoryHits = await memoryService.keywordSearch?.(query, { topK: 8, minScore: 0.05 }) ?? [];
|
|
10149
|
+
} catch {
|
|
10150
|
+
memoryHits = [];
|
|
10151
|
+
}
|
|
10152
|
+
}
|
|
10153
|
+
return {
|
|
10154
|
+
memoryContext: formatMemoryContext(memoryHits),
|
|
10155
|
+
memoryHits
|
|
10156
|
+
};
|
|
10157
|
+
}
|
|
10158
|
+
async function collectStatsContext(memoryService) {
|
|
10159
|
+
try {
|
|
10160
|
+
const stats = await memoryService.getStats?.();
|
|
10161
|
+
if (!stats)
|
|
10162
|
+
return "";
|
|
10163
|
+
const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
|
|
10164
|
+
return [
|
|
10165
|
+
"## Memory Stats",
|
|
10166
|
+
`- Total events: ${stats.totalEvents}`,
|
|
10167
|
+
`- Vector nodes: ${stats.vectorCount}`,
|
|
10168
|
+
`- By level: ${levels}`
|
|
10169
|
+
].join("\n");
|
|
10170
|
+
} catch {
|
|
10171
|
+
return "";
|
|
10172
|
+
}
|
|
10173
|
+
}
|
|
10174
|
+
function formatMemoryContext(memoryHits) {
|
|
10175
|
+
if (memoryHits.length === 0)
|
|
10176
|
+
return "";
|
|
10177
|
+
const parts = ["## Relevant Memories\n"];
|
|
10178
|
+
for (const m of memoryHits) {
|
|
10179
|
+
const date = m.event.timestamp ? new Date(m.event.timestamp).toISOString().split("T")[0] : "unknown-date";
|
|
10180
|
+
const content = (m.event.content ?? "").slice(0, 500);
|
|
10181
|
+
parts.push(`### [${m.event.eventType ?? "memory"}] ${date} (score: ${m.score.toFixed(2)})`);
|
|
10182
|
+
parts.push(content);
|
|
10183
|
+
if (m.sessionContext) {
|
|
10184
|
+
parts.push(`_Context: ${m.sessionContext}_`);
|
|
10185
|
+
}
|
|
10186
|
+
parts.push("");
|
|
10187
|
+
}
|
|
10188
|
+
return parts.join("\n");
|
|
10189
|
+
}
|
|
10190
|
+
async function streamMemoryOnlyResponse(stream, options) {
|
|
10191
|
+
await stream.writeSSE({
|
|
10192
|
+
event: "diagnostic",
|
|
10193
|
+
data: JSON.stringify({
|
|
10194
|
+
provider: "claude-cli",
|
|
10195
|
+
status: "skipped",
|
|
10196
|
+
mode: "memory-only",
|
|
10197
|
+
reason: options.reason,
|
|
10198
|
+
retrievedMemories: options.memoryHits.length
|
|
10199
|
+
})
|
|
10200
|
+
});
|
|
10201
|
+
await streamMemoryOnlyFallback(stream, options.memoryContext, options.memoryHits);
|
|
10202
|
+
}
|
|
10203
|
+
async function streamMemoryOnlyFallback(stream, memoryContext, memoryHits) {
|
|
10204
|
+
const content = memoryHits.length > 0 ? [
|
|
10205
|
+
"Provider unavailable or skipped; showing retrieved memory context directly.",
|
|
10206
|
+
"",
|
|
10207
|
+
memoryContext
|
|
10208
|
+
].join("\n") : "Provider unavailable or skipped, and no directly relevant memories were found for this query.";
|
|
10209
|
+
await stream.writeSSE({
|
|
10210
|
+
event: "message",
|
|
10211
|
+
data: JSON.stringify({ content, mode: "memory-only" })
|
|
10212
|
+
});
|
|
10213
|
+
await stream.writeSSE({ event: "done", data: "{}" });
|
|
10214
|
+
}
|
|
10215
|
+
function providerDiagnostic(err) {
|
|
10216
|
+
if (err instanceof ProviderFailure) {
|
|
10217
|
+
return {
|
|
10218
|
+
provider: "claude-cli",
|
|
10219
|
+
code: err.code,
|
|
10220
|
+
message: err.message,
|
|
10221
|
+
fallback: "memory-only"
|
|
10222
|
+
};
|
|
10223
|
+
}
|
|
10224
|
+
return {
|
|
10225
|
+
provider: "claude-cli",
|
|
10226
|
+
code: "claude-cli-error",
|
|
10227
|
+
message: err instanceof Error ? err.message : "Unknown Claude CLI failure",
|
|
10228
|
+
fallback: "memory-only"
|
|
10229
|
+
};
|
|
10230
|
+
}
|
|
10231
|
+
function classifyProviderFailure(message) {
|
|
10232
|
+
const normalized = message.toLowerCase();
|
|
10233
|
+
if (normalized.includes("401") || normalized.includes("unauthorized") || normalized.includes("auth")) {
|
|
10234
|
+
return new ProviderFailure("claude-cli-auth", "Claude CLI authentication failed; showing memory-only context.");
|
|
10235
|
+
}
|
|
10236
|
+
if (normalized.includes("not found") || normalized.includes("enoent")) {
|
|
10237
|
+
return new ProviderFailure("claude-cli-not-found", "Claude CLI was not found; showing memory-only context.");
|
|
10238
|
+
}
|
|
10239
|
+
if (normalized.includes("timed out")) {
|
|
10240
|
+
return new ProviderFailure("claude-cli-timeout", "Claude CLI timed out; showing memory-only context.");
|
|
10241
|
+
}
|
|
10242
|
+
return new ProviderFailure("claude-cli-error", "Claude CLI failed; showing memory-only context.");
|
|
10243
|
+
}
|
|
9769
10244
|
function buildPrompt(statsContext, memoryContext, history, currentMessage) {
|
|
9770
10245
|
const parts = [];
|
|
9771
10246
|
parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
|
|
@@ -9808,12 +10283,13 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
9808
10283
|
});
|
|
9809
10284
|
const timeout = setTimeout(() => {
|
|
9810
10285
|
proc.kill("SIGTERM");
|
|
9811
|
-
reject(
|
|
10286
|
+
reject(classifyProviderFailure("timed out"));
|
|
9812
10287
|
}, CLAUDE_TIMEOUT_MS);
|
|
9813
10288
|
proc.stdin.write(prompt);
|
|
9814
10289
|
proc.stdin.end();
|
|
9815
10290
|
let buffer = "";
|
|
9816
10291
|
let lastSentText = "";
|
|
10292
|
+
let stderrText = "";
|
|
9817
10293
|
proc.stdout.on("data", async (chunk) => {
|
|
9818
10294
|
buffer += chunk.toString();
|
|
9819
10295
|
const lines = buffer.split("\n");
|
|
@@ -9842,6 +10318,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
9842
10318
|
}
|
|
9843
10319
|
});
|
|
9844
10320
|
proc.stderr.on("data", (chunk) => {
|
|
10321
|
+
stderrText += chunk.toString();
|
|
9845
10322
|
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
9846
10323
|
console.error("[chat] claude stderr:", chunk.toString());
|
|
9847
10324
|
}
|
|
@@ -9849,7 +10326,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
9849
10326
|
proc.on("error", (err) => {
|
|
9850
10327
|
clearTimeout(timeout);
|
|
9851
10328
|
if (err.code === "ENOENT") {
|
|
9852
|
-
reject(
|
|
10329
|
+
reject(classifyProviderFailure("ENOENT not found"));
|
|
9853
10330
|
} else {
|
|
9854
10331
|
reject(err);
|
|
9855
10332
|
}
|
|
@@ -9866,7 +10343,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
9866
10343
|
}
|
|
9867
10344
|
}
|
|
9868
10345
|
if (code !== 0 && code !== null) {
|
|
9869
|
-
reject(
|
|
10346
|
+
reject(classifyProviderFailure(stderrText || `Claude CLI exited with code ${code}`));
|
|
9870
10347
|
} else {
|
|
9871
10348
|
resolve3();
|
|
9872
10349
|
}
|
|
@@ -9915,6 +10392,49 @@ healthRouter.get("/", async (c) => {
|
|
|
9915
10392
|
await memoryService.shutdown();
|
|
9916
10393
|
}
|
|
9917
10394
|
});
|
|
10395
|
+
healthRouter.post("/recover", async (c) => {
|
|
10396
|
+
const memoryService = getWritableServiceFromQuery(c);
|
|
10397
|
+
try {
|
|
10398
|
+
await memoryService.initialize();
|
|
10399
|
+
const body = await c.req.json().catch(() => ({}));
|
|
10400
|
+
const options = {};
|
|
10401
|
+
if (typeof body.stuckThresholdMs === "number" && Number.isFinite(body.stuckThresholdMs)) {
|
|
10402
|
+
options.stuckThresholdMs = body.stuckThresholdMs;
|
|
10403
|
+
}
|
|
10404
|
+
if (typeof body.maxRetries === "number" && Number.isFinite(body.maxRetries)) {
|
|
10405
|
+
options.maxRetries = body.maxRetries;
|
|
10406
|
+
}
|
|
10407
|
+
const before = await memoryService.getOutboxStats();
|
|
10408
|
+
const recovered = await memoryService.recoverStuckOutboxItems(options);
|
|
10409
|
+
const [stats, outbox] = await Promise.all([
|
|
10410
|
+
memoryService.getStats(),
|
|
10411
|
+
memoryService.getOutboxStats()
|
|
10412
|
+
]);
|
|
10413
|
+
return c.json({
|
|
10414
|
+
status: "ok",
|
|
10415
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10416
|
+
recovered,
|
|
10417
|
+
before: {
|
|
10418
|
+
outbox: before
|
|
10419
|
+
},
|
|
10420
|
+
after: {
|
|
10421
|
+
storage: {
|
|
10422
|
+
totalEvents: stats.totalEvents,
|
|
10423
|
+
vectorCount: stats.vectorCount
|
|
10424
|
+
},
|
|
10425
|
+
outbox
|
|
10426
|
+
}
|
|
10427
|
+
});
|
|
10428
|
+
} catch (error) {
|
|
10429
|
+
return c.json({
|
|
10430
|
+
status: "error",
|
|
10431
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10432
|
+
error: error.message
|
|
10433
|
+
}, 500);
|
|
10434
|
+
} finally {
|
|
10435
|
+
await memoryService.shutdown();
|
|
10436
|
+
}
|
|
10437
|
+
});
|
|
9918
10438
|
|
|
9919
10439
|
// src/apps/server/api/index.ts
|
|
9920
10440
|
var apiRouter = new Hono10().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);
|