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/index.js
CHANGED
|
@@ -27,8 +27,8 @@ import { Hono } from "hono";
|
|
|
27
27
|
import * as os5 from "os";
|
|
28
28
|
|
|
29
29
|
// src/core/engine/memory-service-composition.ts
|
|
30
|
-
import * as
|
|
31
|
-
import * as
|
|
30
|
+
import * as os2 from "os";
|
|
31
|
+
import * as path7 from "path";
|
|
32
32
|
|
|
33
33
|
// src/core/metadata-extractor.ts
|
|
34
34
|
function createToolObservationEmbedding(toolName, metadata, success) {
|
|
@@ -1539,8 +1539,8 @@ function createEndlessMemoryServices(options) {
|
|
|
1539
1539
|
}
|
|
1540
1540
|
|
|
1541
1541
|
// src/core/engine/memory-engine-services.ts
|
|
1542
|
-
import * as
|
|
1543
|
-
import * as
|
|
1542
|
+
import * as fs6 from "fs";
|
|
1543
|
+
import * as path5 from "path";
|
|
1544
1544
|
|
|
1545
1545
|
// src/extensions/vector/embedder.ts
|
|
1546
1546
|
var DEFAULT_EMBEDDING_MODEL = "Xenova/multilingual-e5-small";
|
|
@@ -2218,14 +2218,43 @@ function makeDedupeKey(content, sessionId) {
|
|
|
2218
2218
|
return `${sessionId}:${contentHash}`;
|
|
2219
2219
|
}
|
|
2220
2220
|
|
|
2221
|
+
// src/core/sqlite-event-store.ts
|
|
2222
|
+
import * as nodePath2 from "path";
|
|
2223
|
+
|
|
2224
|
+
// src/core/registry/project-path.ts
|
|
2225
|
+
import * as crypto2 from "crypto";
|
|
2226
|
+
import * as fs3 from "fs";
|
|
2227
|
+
import * as os from "os";
|
|
2228
|
+
import * as path3 from "path";
|
|
2229
|
+
function normalizeProjectPath(projectPath) {
|
|
2230
|
+
const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
|
|
2231
|
+
try {
|
|
2232
|
+
return fs3.realpathSync(expanded);
|
|
2233
|
+
} catch {
|
|
2234
|
+
return path3.resolve(expanded);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
function hashProjectPath(projectPath) {
|
|
2238
|
+
const normalizedPath = normalizeProjectPath(projectPath);
|
|
2239
|
+
return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
|
|
2240
|
+
}
|
|
2241
|
+
function getProjectStoragePath(projectPath) {
|
|
2242
|
+
const hash = hashProjectPath(projectPath);
|
|
2243
|
+
return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
|
|
2244
|
+
}
|
|
2245
|
+
function resolveProjectStoragePath(projectOrHash) {
|
|
2246
|
+
const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
|
|
2247
|
+
return isHash ? path3.join(os.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2221
2250
|
// src/core/sqlite-wrapper.ts
|
|
2222
2251
|
import Database from "better-sqlite3";
|
|
2223
|
-
import * as
|
|
2252
|
+
import * as fs4 from "fs";
|
|
2224
2253
|
import * as nodePath from "path";
|
|
2225
2254
|
function createSQLiteDatabase(path14, options) {
|
|
2226
2255
|
const dir = nodePath.dirname(path14);
|
|
2227
|
-
if (!
|
|
2228
|
-
|
|
2256
|
+
if (!fs4.existsSync(dir)) {
|
|
2257
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
2229
2258
|
}
|
|
2230
2259
|
const db = new Database(path14, {
|
|
2231
2260
|
readonly: options?.readonly ?? false
|
|
@@ -2274,8 +2303,8 @@ function toSQLiteTimestamp(date) {
|
|
|
2274
2303
|
}
|
|
2275
2304
|
|
|
2276
2305
|
// src/core/markdown-mirror.ts
|
|
2277
|
-
import * as
|
|
2278
|
-
import * as
|
|
2306
|
+
import * as fs5 from "fs/promises";
|
|
2307
|
+
import * as path4 from "path";
|
|
2279
2308
|
var DEFAULT_NAMESPACE = "default";
|
|
2280
2309
|
var DEFAULT_CATEGORY = "uncategorized";
|
|
2281
2310
|
function sanitizeSegment2(input, fallback) {
|
|
@@ -2304,7 +2333,7 @@ function buildMirrorPath2(rootDir, event) {
|
|
|
2304
2333
|
const yyyy = d.getFullYear();
|
|
2305
2334
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
2306
2335
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
2307
|
-
return
|
|
2336
|
+
return path4.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
|
|
2308
2337
|
}
|
|
2309
2338
|
function formatMirrorEntry(event) {
|
|
2310
2339
|
const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
|
|
@@ -2325,8 +2354,8 @@ var MarkdownMirror2 = class {
|
|
|
2325
2354
|
}
|
|
2326
2355
|
async append(event) {
|
|
2327
2356
|
const outPath = buildMirrorPath2(this.rootDir, event);
|
|
2328
|
-
await
|
|
2329
|
-
await
|
|
2357
|
+
await fs5.mkdir(path4.dirname(outPath), { recursive: true });
|
|
2358
|
+
await fs5.appendFile(outPath, formatMirrorEntry(event), "utf8");
|
|
2330
2359
|
return outPath;
|
|
2331
2360
|
}
|
|
2332
2361
|
};
|
|
@@ -2339,6 +2368,143 @@ function normalizeQueryRewriteKind(value) {
|
|
|
2339
2368
|
return "none";
|
|
2340
2369
|
}
|
|
2341
2370
|
var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
|
|
2371
|
+
var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2372
|
+
var DEFAULT_OUTBOX_MAX_RETRIES = 3;
|
|
2373
|
+
function emptyOutboxRecoveryResult() {
|
|
2374
|
+
return {
|
|
2375
|
+
embedding: { recoveredProcessing: 0, retriedFailed: 0 },
|
|
2376
|
+
vector: { recoveredProcessing: 0, retriedFailed: 0 }
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
function isRecord(value) {
|
|
2380
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2381
|
+
}
|
|
2382
|
+
function getNestedRecord(root, path14) {
|
|
2383
|
+
let cursor = root;
|
|
2384
|
+
for (const key of path14) {
|
|
2385
|
+
if (!isRecord(cursor))
|
|
2386
|
+
return void 0;
|
|
2387
|
+
cursor = cursor[key];
|
|
2388
|
+
}
|
|
2389
|
+
return isRecord(cursor) ? cursor : void 0;
|
|
2390
|
+
}
|
|
2391
|
+
function getNestedString(root, path14) {
|
|
2392
|
+
let cursor = root;
|
|
2393
|
+
for (const key of path14) {
|
|
2394
|
+
if (!isRecord(cursor))
|
|
2395
|
+
return void 0;
|
|
2396
|
+
cursor = cursor[key];
|
|
2397
|
+
}
|
|
2398
|
+
return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
|
|
2399
|
+
}
|
|
2400
|
+
function metadataProjectHash(metadata) {
|
|
2401
|
+
return getNestedString(metadata, ["scope", "project", "hash"]);
|
|
2402
|
+
}
|
|
2403
|
+
function metadataProjectPaths(metadata) {
|
|
2404
|
+
const candidates = [
|
|
2405
|
+
getNestedString(metadata, ["projectPath"]),
|
|
2406
|
+
getNestedString(metadata, ["sourceProjectPath"]),
|
|
2407
|
+
getNestedString(metadata, ["scope", "project", "path"])
|
|
2408
|
+
];
|
|
2409
|
+
const paths = [];
|
|
2410
|
+
for (const value of candidates) {
|
|
2411
|
+
if (value && !paths.includes(value))
|
|
2412
|
+
paths.push(value);
|
|
2413
|
+
}
|
|
2414
|
+
return paths;
|
|
2415
|
+
}
|
|
2416
|
+
function metadataProjectPath(metadata) {
|
|
2417
|
+
return metadataProjectPaths(metadata)[0];
|
|
2418
|
+
}
|
|
2419
|
+
function isActiveQuarantinedMetadata(metadata) {
|
|
2420
|
+
const quarantine = getNestedRecord(metadata, ["quarantine"]);
|
|
2421
|
+
return quarantine?.status === "active";
|
|
2422
|
+
}
|
|
2423
|
+
function activeQuarantineStatusExpression(column = "metadata") {
|
|
2424
|
+
return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
|
|
2425
|
+
}
|
|
2426
|
+
function notActiveQuarantinedSql(column = "metadata") {
|
|
2427
|
+
return `${activeQuarantineStatusExpression(column)} != 'active'`;
|
|
2428
|
+
}
|
|
2429
|
+
function maybeQuarantinePredicate(options, column = "metadata") {
|
|
2430
|
+
return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
|
|
2431
|
+
}
|
|
2432
|
+
function safeParseMetadataValue(value) {
|
|
2433
|
+
if (!value)
|
|
2434
|
+
return void 0;
|
|
2435
|
+
if (typeof value === "object")
|
|
2436
|
+
return isRecord(value) ? value : void 0;
|
|
2437
|
+
if (typeof value !== "string")
|
|
2438
|
+
return void 0;
|
|
2439
|
+
try {
|
|
2440
|
+
const parsed = JSON.parse(value);
|
|
2441
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
2442
|
+
} catch {
|
|
2443
|
+
return void 0;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
function isImportedOrLegacyScopedMetadata(metadata) {
|
|
2447
|
+
if (!metadata)
|
|
2448
|
+
return false;
|
|
2449
|
+
return Boolean(
|
|
2450
|
+
metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
function addMetadataTag(metadata, tag) {
|
|
2454
|
+
const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
|
|
2455
|
+
if (!current.includes(tag))
|
|
2456
|
+
metadata.tags = [...current, tag];
|
|
2457
|
+
}
|
|
2458
|
+
function buildRepairResult(projectHash, dryRun) {
|
|
2459
|
+
return {
|
|
2460
|
+
dryRun,
|
|
2461
|
+
projectHash,
|
|
2462
|
+
scanned: 0,
|
|
2463
|
+
repaired: 0,
|
|
2464
|
+
quarantined: 0,
|
|
2465
|
+
alreadyScoped: 0,
|
|
2466
|
+
skipped: 0,
|
|
2467
|
+
samples: []
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
function normalizeRepoName(value) {
|
|
2471
|
+
return value.replace(/\.git$/i, "").trim().toLowerCase();
|
|
2472
|
+
}
|
|
2473
|
+
function projectBasename(projectPath) {
|
|
2474
|
+
if (!projectPath)
|
|
2475
|
+
return void 0;
|
|
2476
|
+
const trimmed = projectPath.replace(/[\\/]+$/, "");
|
|
2477
|
+
const basename3 = nodePath2.basename(trimmed);
|
|
2478
|
+
return basename3 ? normalizeRepoName(basename3) : void 0;
|
|
2479
|
+
}
|
|
2480
|
+
function isProjectScopeRepairExplanation(content) {
|
|
2481
|
+
const normalized = content.toLowerCase();
|
|
2482
|
+
const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
|
|
2483
|
+
const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
|
|
2484
|
+
return hasRepairContext && hasExplanationContext;
|
|
2485
|
+
}
|
|
2486
|
+
function hasConflictingContentProjectHint(content, projectPath) {
|
|
2487
|
+
const currentName = projectBasename(projectPath);
|
|
2488
|
+
if (!currentName)
|
|
2489
|
+
return false;
|
|
2490
|
+
if (isProjectScopeRepairExplanation(content))
|
|
2491
|
+
return false;
|
|
2492
|
+
const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
|
|
2493
|
+
let githubMatch;
|
|
2494
|
+
while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
|
|
2495
|
+
const repo = normalizeRepoName(githubMatch[2] || "");
|
|
2496
|
+
if (repo && repo !== currentName)
|
|
2497
|
+
return true;
|
|
2498
|
+
}
|
|
2499
|
+
const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
|
|
2500
|
+
let workspaceMatch;
|
|
2501
|
+
while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
|
|
2502
|
+
const repo = normalizeRepoName(workspaceMatch[1] || "");
|
|
2503
|
+
if (repo && repo !== currentName)
|
|
2504
|
+
return true;
|
|
2505
|
+
}
|
|
2506
|
+
return false;
|
|
2507
|
+
}
|
|
2342
2508
|
var SQLiteEventStore = class {
|
|
2343
2509
|
db;
|
|
2344
2510
|
initialized = false;
|
|
@@ -2837,11 +3003,11 @@ var SQLiteEventStore = class {
|
|
|
2837
3003
|
/**
|
|
2838
3004
|
* Get events by session ID
|
|
2839
3005
|
*/
|
|
2840
|
-
async getSessionEvents(sessionId) {
|
|
3006
|
+
async getSessionEvents(sessionId, options) {
|
|
2841
3007
|
await this.initialize();
|
|
2842
3008
|
const rows = sqliteAll(
|
|
2843
3009
|
this.db,
|
|
2844
|
-
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
3010
|
+
`SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
2845
3011
|
[sessionId]
|
|
2846
3012
|
);
|
|
2847
3013
|
return rows.map(this.rowToEvent);
|
|
@@ -2849,11 +3015,11 @@ var SQLiteEventStore = class {
|
|
|
2849
3015
|
/**
|
|
2850
3016
|
* Get recent events
|
|
2851
3017
|
*/
|
|
2852
|
-
async getRecentEvents(limit = 100) {
|
|
3018
|
+
async getRecentEvents(limit = 100, options) {
|
|
2853
3019
|
await this.initialize();
|
|
2854
3020
|
const rows = sqliteAll(
|
|
2855
3021
|
this.db,
|
|
2856
|
-
`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
|
|
3022
|
+
`SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
|
|
2857
3023
|
[limit]
|
|
2858
3024
|
);
|
|
2859
3025
|
return rows.map(this.rowToEvent);
|
|
@@ -2861,11 +3027,11 @@ var SQLiteEventStore = class {
|
|
|
2861
3027
|
/**
|
|
2862
3028
|
* Get event by ID
|
|
2863
3029
|
*/
|
|
2864
|
-
async getEvent(id) {
|
|
3030
|
+
async getEvent(id, options) {
|
|
2865
3031
|
await this.initialize();
|
|
2866
3032
|
const row = sqliteGet(
|
|
2867
3033
|
this.db,
|
|
2868
|
-
`SELECT * FROM events WHERE id =
|
|
3034
|
+
`SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
|
|
2869
3035
|
[id]
|
|
2870
3036
|
);
|
|
2871
3037
|
if (!row)
|
|
@@ -2875,11 +3041,11 @@ var SQLiteEventStore = class {
|
|
|
2875
3041
|
/**
|
|
2876
3042
|
* Get events since a timestamp (for sync)
|
|
2877
3043
|
*/
|
|
2878
|
-
async getEventsSince(timestamp, limit = 1e3) {
|
|
3044
|
+
async getEventsSince(timestamp, limit = 1e3, options) {
|
|
2879
3045
|
await this.initialize();
|
|
2880
3046
|
const rows = sqliteAll(
|
|
2881
3047
|
this.db,
|
|
2882
|
-
`SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
|
|
3048
|
+
`SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
|
|
2883
3049
|
[timestamp, limit]
|
|
2884
3050
|
);
|
|
2885
3051
|
return rows.map(this.rowToEvent);
|
|
@@ -2888,11 +3054,11 @@ var SQLiteEventStore = class {
|
|
|
2888
3054
|
* Get events since a SQLite rowid (for robust incremental replication).
|
|
2889
3055
|
* Rowid is monotonic for append-only tables, independent of client timestamps.
|
|
2890
3056
|
*/
|
|
2891
|
-
async getEventsSinceRowid(lastRowid, limit = 1e3) {
|
|
3057
|
+
async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
|
|
2892
3058
|
await this.initialize();
|
|
2893
3059
|
const rows = sqliteAll(
|
|
2894
3060
|
this.db,
|
|
2895
|
-
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
|
|
3061
|
+
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
|
|
2896
3062
|
[lastRowid, limit]
|
|
2897
3063
|
);
|
|
2898
3064
|
return rows.map((row) => ({
|
|
@@ -3089,7 +3255,9 @@ var SQLiteEventStore = class {
|
|
|
3089
3255
|
const placeholders = ids.map(() => "?").join(",");
|
|
3090
3256
|
sqliteRun(
|
|
3091
3257
|
this.db,
|
|
3092
|
-
`UPDATE embedding_outbox
|
|
3258
|
+
`UPDATE embedding_outbox
|
|
3259
|
+
SET status = 'processing', processed_at = datetime('now'), error_message = NULL
|
|
3260
|
+
WHERE id IN (${placeholders})`,
|
|
3093
3261
|
ids
|
|
3094
3262
|
);
|
|
3095
3263
|
return pending.map((row) => ({
|
|
@@ -3125,19 +3293,19 @@ var SQLiteEventStore = class {
|
|
|
3125
3293
|
/**
|
|
3126
3294
|
* Count total events
|
|
3127
3295
|
*/
|
|
3128
|
-
async countEvents() {
|
|
3296
|
+
async countEvents(options) {
|
|
3129
3297
|
await this.initialize();
|
|
3130
|
-
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
|
|
3298
|
+
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
|
|
3131
3299
|
return row?.count || 0;
|
|
3132
3300
|
}
|
|
3133
3301
|
/**
|
|
3134
3302
|
* Get events page in timestamp ascending order (stable migration/reindex scans)
|
|
3135
3303
|
*/
|
|
3136
|
-
async getEventsPage(limit = 1e3, offset = 0) {
|
|
3304
|
+
async getEventsPage(limit = 1e3, offset = 0, options) {
|
|
3137
3305
|
await this.initialize();
|
|
3138
3306
|
const rows = sqliteAll(
|
|
3139
3307
|
this.db,
|
|
3140
|
-
`SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
3308
|
+
`SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
3141
3309
|
[limit, offset]
|
|
3142
3310
|
);
|
|
3143
3311
|
return rows.map(this.rowToEvent);
|
|
@@ -3159,6 +3327,197 @@ var SQLiteEventStore = class {
|
|
|
3159
3327
|
[error, ...ids]
|
|
3160
3328
|
);
|
|
3161
3329
|
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Recover abandoned outbox work after a worker/process crash.
|
|
3332
|
+
*
|
|
3333
|
+
* Rows in `processing` are claimed work. If the process exits before marking
|
|
3334
|
+
* them done/failed, they otherwise remain invisible to future processing.
|
|
3335
|
+
* Recovery is deliberately age-gated so an active worker is not disturbed.
|
|
3336
|
+
*/
|
|
3337
|
+
async recoverStuckOutboxItems(options = {}) {
|
|
3338
|
+
await this.initialize();
|
|
3339
|
+
const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
|
|
3340
|
+
const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
|
|
3341
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
3342
|
+
const threshold = new Date(now.getTime() - thresholdMs).toISOString();
|
|
3343
|
+
const result = emptyOutboxRecoveryResult();
|
|
3344
|
+
const embeddingRecovered = sqliteRun(
|
|
3345
|
+
this.db,
|
|
3346
|
+
`UPDATE embedding_outbox
|
|
3347
|
+
SET status = 'pending', processed_at = NULL, error_message = NULL
|
|
3348
|
+
WHERE status = 'processing'
|
|
3349
|
+
AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
|
|
3350
|
+
[threshold]
|
|
3351
|
+
);
|
|
3352
|
+
result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
|
|
3353
|
+
const embeddingRetried = sqliteRun(
|
|
3354
|
+
this.db,
|
|
3355
|
+
`UPDATE embedding_outbox
|
|
3356
|
+
SET status = 'pending', error_message = NULL
|
|
3357
|
+
WHERE status = 'failed'
|
|
3358
|
+
AND retry_count < ?`,
|
|
3359
|
+
[maxRetries]
|
|
3360
|
+
);
|
|
3361
|
+
result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
|
|
3362
|
+
const vectorRecovered = sqliteRun(
|
|
3363
|
+
this.db,
|
|
3364
|
+
`UPDATE vector_outbox
|
|
3365
|
+
SET status = 'pending', updated_at = ?, error = NULL
|
|
3366
|
+
WHERE status = 'processing'
|
|
3367
|
+
AND datetime(updated_at) < datetime(?)`,
|
|
3368
|
+
[now.toISOString(), threshold]
|
|
3369
|
+
);
|
|
3370
|
+
result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
|
|
3371
|
+
const vectorRetried = sqliteRun(
|
|
3372
|
+
this.db,
|
|
3373
|
+
`UPDATE vector_outbox
|
|
3374
|
+
SET status = 'pending', updated_at = ?, error = NULL
|
|
3375
|
+
WHERE status = 'failed'
|
|
3376
|
+
AND retry_count < ?`,
|
|
3377
|
+
[now.toISOString(), maxRetries]
|
|
3378
|
+
);
|
|
3379
|
+
result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
|
|
3380
|
+
return result;
|
|
3381
|
+
}
|
|
3382
|
+
/**
|
|
3383
|
+
* Repair legacy imported events that predate canonical project scope metadata.
|
|
3384
|
+
*
|
|
3385
|
+
* Same-project legacy rows are tagged with scope.project.hash. Rows that look
|
|
3386
|
+
* imported but cannot be proven to belong to this project are quarantined so
|
|
3387
|
+
* dashboard default reads/search do not surface cross-project contamination.
|
|
3388
|
+
*/
|
|
3389
|
+
async repairLegacyProjectScope(options = {}) {
|
|
3390
|
+
await this.initialize();
|
|
3391
|
+
const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
|
|
3392
|
+
if (!projectHash) {
|
|
3393
|
+
throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
|
|
3394
|
+
}
|
|
3395
|
+
if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
|
|
3396
|
+
throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
|
|
3397
|
+
}
|
|
3398
|
+
const dryRun = options.dryRun === true;
|
|
3399
|
+
const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
|
|
3400
|
+
const result = buildRepairResult(projectHash, dryRun);
|
|
3401
|
+
const rows = sqliteAll(
|
|
3402
|
+
this.db,
|
|
3403
|
+
`SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
|
|
3404
|
+
FROM events e
|
|
3405
|
+
LEFT JOIN sessions s ON s.id = e.session_id
|
|
3406
|
+
ORDER BY e.timestamp ASC`,
|
|
3407
|
+
[]
|
|
3408
|
+
);
|
|
3409
|
+
const sample = (entry) => {
|
|
3410
|
+
if (result.samples.length < 20)
|
|
3411
|
+
result.samples.push(entry);
|
|
3412
|
+
};
|
|
3413
|
+
for (const row of rows) {
|
|
3414
|
+
result.scanned++;
|
|
3415
|
+
let metadata = {};
|
|
3416
|
+
let metadataParseInvalid = false;
|
|
3417
|
+
if (row.metadata) {
|
|
3418
|
+
const parsed = safeParseMetadataValue(row.metadata);
|
|
3419
|
+
if (parsed) {
|
|
3420
|
+
metadata = parsed;
|
|
3421
|
+
} else {
|
|
3422
|
+
metadataParseInvalid = true;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
if (isActiveQuarantinedMetadata(metadata)) {
|
|
3426
|
+
result.skipped++;
|
|
3427
|
+
continue;
|
|
3428
|
+
}
|
|
3429
|
+
const currentHash = metadataProjectHash(metadata);
|
|
3430
|
+
const explicitPath = metadataProjectPath(metadata);
|
|
3431
|
+
const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
|
|
3432
|
+
const candidatePaths = metadataProjectPaths(metadata);
|
|
3433
|
+
if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
|
|
3434
|
+
candidatePaths.push(sessionProjectPath);
|
|
3435
|
+
}
|
|
3436
|
+
const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
|
|
3437
|
+
const pathHashes = candidatePaths.map((candidate) => {
|
|
3438
|
+
try {
|
|
3439
|
+
return { path: candidate, hash: hashProjectPath(candidate) };
|
|
3440
|
+
} catch {
|
|
3441
|
+
return { path: candidate, hash: void 0 };
|
|
3442
|
+
}
|
|
3443
|
+
});
|
|
3444
|
+
const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
|
|
3445
|
+
const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
|
|
3446
|
+
let action = "skipped";
|
|
3447
|
+
let reason;
|
|
3448
|
+
let observedProjectHash;
|
|
3449
|
+
if (foreignPath) {
|
|
3450
|
+
action = "quarantined";
|
|
3451
|
+
reason = "project-path-mismatch";
|
|
3452
|
+
observedProjectHash = foreignPath.hash;
|
|
3453
|
+
} else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
|
|
3454
|
+
action = "quarantined";
|
|
3455
|
+
reason = "content-project-mismatch";
|
|
3456
|
+
} else if (currentHash === projectHash) {
|
|
3457
|
+
result.alreadyScoped++;
|
|
3458
|
+
continue;
|
|
3459
|
+
} else if (currentHash && currentHash !== projectHash) {
|
|
3460
|
+
action = "quarantined";
|
|
3461
|
+
reason = "scope-hash-mismatch";
|
|
3462
|
+
observedProjectHash = currentHash;
|
|
3463
|
+
} else if (matchingPath) {
|
|
3464
|
+
action = "repaired";
|
|
3465
|
+
reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
|
|
3466
|
+
} else if (candidatePaths.length > 0) {
|
|
3467
|
+
action = "quarantined";
|
|
3468
|
+
reason = "project-path-mismatch";
|
|
3469
|
+
} else if (importedOrLegacy) {
|
|
3470
|
+
action = "quarantined";
|
|
3471
|
+
reason = "missing-project-scope";
|
|
3472
|
+
}
|
|
3473
|
+
if (action === "skipped" || !reason) {
|
|
3474
|
+
result.skipped++;
|
|
3475
|
+
continue;
|
|
3476
|
+
}
|
|
3477
|
+
if (action === "repaired") {
|
|
3478
|
+
const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
|
|
3479
|
+
const project = isRecord(scope.project) ? { ...scope.project } : {};
|
|
3480
|
+
project.hash = projectHash;
|
|
3481
|
+
scope.project = project;
|
|
3482
|
+
metadata.scope = scope;
|
|
3483
|
+
metadata.repair = {
|
|
3484
|
+
...isRecord(metadata.repair) ? metadata.repair : {},
|
|
3485
|
+
legacyProjectScope: {
|
|
3486
|
+
action,
|
|
3487
|
+
reason,
|
|
3488
|
+
repairedAt: nowIso
|
|
3489
|
+
}
|
|
3490
|
+
};
|
|
3491
|
+
addMetadataTag(metadata, `proj:${projectHash}`);
|
|
3492
|
+
result.repaired++;
|
|
3493
|
+
} else {
|
|
3494
|
+
metadata.quarantine = {
|
|
3495
|
+
...isRecord(metadata.quarantine) ? metadata.quarantine : {},
|
|
3496
|
+
status: "active",
|
|
3497
|
+
category: "project-scope",
|
|
3498
|
+
reason,
|
|
3499
|
+
detectedAt: nowIso,
|
|
3500
|
+
expectedProjectHash: projectHash,
|
|
3501
|
+
...observedProjectHash ? { observedProjectHash } : {}
|
|
3502
|
+
};
|
|
3503
|
+
metadata.repair = {
|
|
3504
|
+
...isRecord(metadata.repair) ? metadata.repair : {},
|
|
3505
|
+
legacyProjectScope: {
|
|
3506
|
+
action,
|
|
3507
|
+
reason,
|
|
3508
|
+
repairedAt: nowIso
|
|
3509
|
+
}
|
|
3510
|
+
};
|
|
3511
|
+
addMetadataTag(metadata, "quarantine:project-scope");
|
|
3512
|
+
result.quarantined++;
|
|
3513
|
+
}
|
|
3514
|
+
sample({ eventId: row.id, action, reason });
|
|
3515
|
+
if (!dryRun) {
|
|
3516
|
+
sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
return result;
|
|
3520
|
+
}
|
|
3162
3521
|
/**
|
|
3163
3522
|
* Get embedding/vector outbox health statistics
|
|
3164
3523
|
*/
|
|
@@ -3206,7 +3565,11 @@ var SQLiteEventStore = class {
|
|
|
3206
3565
|
await this.initialize();
|
|
3207
3566
|
const rows = sqliteAll(
|
|
3208
3567
|
this.db,
|
|
3209
|
-
`SELECT level, COUNT(*) as count
|
|
3568
|
+
`SELECT ml.level, COUNT(*) as count
|
|
3569
|
+
FROM memory_levels ml
|
|
3570
|
+
INNER JOIN events e ON e.id = ml.event_id
|
|
3571
|
+
WHERE ${notActiveQuarantinedSql("e.metadata")}
|
|
3572
|
+
GROUP BY ml.level`
|
|
3210
3573
|
);
|
|
3211
3574
|
return rows;
|
|
3212
3575
|
}
|
|
@@ -3222,6 +3585,7 @@ var SQLiteEventStore = class {
|
|
|
3222
3585
|
`SELECT e.* FROM events e
|
|
3223
3586
|
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
3224
3587
|
WHERE ml.level = ?
|
|
3588
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3225
3589
|
ORDER BY e.timestamp DESC
|
|
3226
3590
|
LIMIT ? OFFSET ?`,
|
|
3227
3591
|
[level, limit, offset]
|
|
@@ -3314,12 +3678,13 @@ var SQLiteEventStore = class {
|
|
|
3314
3678
|
/**
|
|
3315
3679
|
* Get most accessed memories (falls back to recent events if none accessed)
|
|
3316
3680
|
*/
|
|
3317
|
-
async getMostAccessed(limit = 10) {
|
|
3681
|
+
async getMostAccessed(limit = 10, options) {
|
|
3318
3682
|
await this.initialize();
|
|
3319
3683
|
let rows = sqliteAll(
|
|
3320
3684
|
this.db,
|
|
3321
3685
|
`SELECT * FROM events
|
|
3322
3686
|
WHERE access_count > 0
|
|
3687
|
+
AND ${maybeQuarantinePredicate(options)}
|
|
3323
3688
|
ORDER BY access_count DESC, last_accessed_at DESC
|
|
3324
3689
|
LIMIT ?`,
|
|
3325
3690
|
[limit]
|
|
@@ -3328,6 +3693,7 @@ var SQLiteEventStore = class {
|
|
|
3328
3693
|
rows = sqliteAll(
|
|
3329
3694
|
this.db,
|
|
3330
3695
|
`SELECT * FROM events
|
|
3696
|
+
WHERE ${maybeQuarantinePredicate(options)}
|
|
3331
3697
|
ORDER BY timestamp DESC
|
|
3332
3698
|
LIMIT ?`,
|
|
3333
3699
|
[limit]
|
|
@@ -3458,6 +3824,7 @@ var SQLiteEventStore = class {
|
|
|
3458
3824
|
FROM memory_helpfulness mh
|
|
3459
3825
|
JOIN events e ON e.id = mh.event_id
|
|
3460
3826
|
WHERE mh.measured_at IS NOT NULL
|
|
3827
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3461
3828
|
GROUP BY mh.event_id
|
|
3462
3829
|
ORDER BY avg_score DESC
|
|
3463
3830
|
LIMIT ?`,
|
|
@@ -3522,6 +3889,7 @@ var SQLiteEventStore = class {
|
|
|
3522
3889
|
FROM events_fts fts
|
|
3523
3890
|
JOIN events e ON e.id = fts.event_id
|
|
3524
3891
|
WHERE events_fts MATCH ?
|
|
3892
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3525
3893
|
ORDER BY fts.rank
|
|
3526
3894
|
LIMIT ?`,
|
|
3527
3895
|
[searchTerms, limit]
|
|
@@ -3536,6 +3904,7 @@ var SQLiteEventStore = class {
|
|
|
3536
3904
|
this.db,
|
|
3537
3905
|
`SELECT *, 0 as rank FROM events
|
|
3538
3906
|
WHERE content LIKE ?
|
|
3907
|
+
AND ${notActiveQuarantinedSql()}
|
|
3539
3908
|
ORDER BY timestamp DESC
|
|
3540
3909
|
LIMIT ?`,
|
|
3541
3910
|
[likePattern, limit]
|
|
@@ -3742,6 +4111,7 @@ var SQLiteEventStore = class {
|
|
|
3742
4111
|
`SELECT turn_id, MIN(timestamp) as min_ts
|
|
3743
4112
|
FROM events
|
|
3744
4113
|
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4114
|
+
AND ${maybeQuarantinePredicate(options)}
|
|
3745
4115
|
GROUP BY turn_id
|
|
3746
4116
|
ORDER BY min_ts DESC
|
|
3747
4117
|
LIMIT ? OFFSET ?`,
|
|
@@ -3749,7 +4119,7 @@ var SQLiteEventStore = class {
|
|
|
3749
4119
|
);
|
|
3750
4120
|
const turns = [];
|
|
3751
4121
|
for (const turnRow of turnRows) {
|
|
3752
|
-
const events = await this.getEventsByTurn(turnRow.turn_id);
|
|
4122
|
+
const events = await this.getEventsByTurn(turnRow.turn_id, options);
|
|
3753
4123
|
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
3754
4124
|
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
3755
4125
|
const hasResponse = events.some((e) => e.eventType === "agent_response");
|
|
@@ -3768,11 +4138,11 @@ var SQLiteEventStore = class {
|
|
|
3768
4138
|
/**
|
|
3769
4139
|
* Get all events for a specific turn_id
|
|
3770
4140
|
*/
|
|
3771
|
-
async getEventsByTurn(turnId) {
|
|
4141
|
+
async getEventsByTurn(turnId, options) {
|
|
3772
4142
|
await this.initialize();
|
|
3773
4143
|
const rows = sqliteAll(
|
|
3774
4144
|
this.db,
|
|
3775
|
-
`SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
|
|
4145
|
+
`SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3776
4146
|
[turnId]
|
|
3777
4147
|
);
|
|
3778
4148
|
return rows.map(this.rowToEvent);
|
|
@@ -3780,13 +4150,14 @@ var SQLiteEventStore = class {
|
|
|
3780
4150
|
/**
|
|
3781
4151
|
* Count total turns for a session
|
|
3782
4152
|
*/
|
|
3783
|
-
async countSessionTurns(sessionId) {
|
|
4153
|
+
async countSessionTurns(sessionId, options) {
|
|
3784
4154
|
await this.initialize();
|
|
3785
4155
|
const row = sqliteGet(
|
|
3786
4156
|
this.db,
|
|
3787
4157
|
`SELECT COUNT(DISTINCT turn_id) as count
|
|
3788
4158
|
FROM events
|
|
3789
|
-
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4159
|
+
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4160
|
+
AND ${maybeQuarantinePredicate(options)}`,
|
|
3790
4161
|
[sessionId]
|
|
3791
4162
|
);
|
|
3792
4163
|
return row?.count || 0;
|
|
@@ -3878,7 +4249,7 @@ var SQLiteEventStore = class {
|
|
|
3878
4249
|
content: row.content,
|
|
3879
4250
|
canonicalKey: row.canonical_key,
|
|
3880
4251
|
dedupeKey: row.dedupe_key,
|
|
3881
|
-
metadata:
|
|
4252
|
+
metadata: safeParseMetadataValue(row.metadata)
|
|
3882
4253
|
};
|
|
3883
4254
|
if (row.access_count !== void 0) {
|
|
3884
4255
|
event.access_count = row.access_count;
|
|
@@ -4030,6 +4401,7 @@ var VectorStore = class {
|
|
|
4030
4401
|
* Get total count of vectors
|
|
4031
4402
|
*/
|
|
4032
4403
|
async count() {
|
|
4404
|
+
await this.initialize();
|
|
4033
4405
|
if (!this.table)
|
|
4034
4406
|
return 0;
|
|
4035
4407
|
const result = await this.table.countRows();
|
|
@@ -4468,6 +4840,14 @@ var MemoryQueryService = class {
|
|
|
4468
4840
|
await this.initialize();
|
|
4469
4841
|
return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
|
|
4470
4842
|
}
|
|
4843
|
+
async recoverStuckOutboxItems(options) {
|
|
4844
|
+
await this.initialize();
|
|
4845
|
+
return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
|
|
4846
|
+
}
|
|
4847
|
+
async repairLegacyProjectScope(options) {
|
|
4848
|
+
await this.initialize();
|
|
4849
|
+
return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
|
|
4850
|
+
}
|
|
4471
4851
|
async getStats() {
|
|
4472
4852
|
await this.initialize();
|
|
4473
4853
|
const deps = this.getStatsDeps();
|
|
@@ -6090,18 +6470,18 @@ function assertDefaultRetrieverStore(eventStore) {
|
|
|
6090
6470
|
function createMemoryEngineServices(options) {
|
|
6091
6471
|
const factories = options.factories ?? {};
|
|
6092
6472
|
const storagePath = options.storagePath;
|
|
6093
|
-
if (!options.readOnly && !
|
|
6094
|
-
|
|
6473
|
+
if (!options.readOnly && !fs6.existsSync(storagePath)) {
|
|
6474
|
+
fs6.mkdirSync(storagePath, { recursive: true });
|
|
6095
6475
|
}
|
|
6096
6476
|
const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
|
|
6097
|
-
|
|
6477
|
+
path5.join(storagePath, "events.sqlite"),
|
|
6098
6478
|
{
|
|
6099
6479
|
readonly: options.readOnly,
|
|
6100
6480
|
markdownMirrorRoot: storagePath
|
|
6101
6481
|
}
|
|
6102
6482
|
);
|
|
6103
6483
|
const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
|
|
6104
|
-
|
|
6484
|
+
path5.join(storagePath, "vectors")
|
|
6105
6485
|
);
|
|
6106
6486
|
const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
|
|
6107
6487
|
const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
|
|
@@ -6512,8 +6892,8 @@ function createMemoryRuntimeService(deps) {
|
|
|
6512
6892
|
}
|
|
6513
6893
|
|
|
6514
6894
|
// src/extensions/shared-memory/shared-memory-services.ts
|
|
6515
|
-
import * as
|
|
6516
|
-
import * as
|
|
6895
|
+
import * as fs7 from "fs";
|
|
6896
|
+
import * as path6 from "path";
|
|
6517
6897
|
|
|
6518
6898
|
// src/core/shared-event-store.ts
|
|
6519
6899
|
var SharedEventStore = class {
|
|
@@ -7201,7 +7581,7 @@ var SharedMemoryServices = class {
|
|
|
7201
7581
|
this.ensureDirectory(sharedPath, { allowCreate: true });
|
|
7202
7582
|
const store = await this.openStore(sharedPath);
|
|
7203
7583
|
this.sharedVectorStore = this.factories.createSharedVectorStore(
|
|
7204
|
-
|
|
7584
|
+
path6.join(sharedPath, "vectors")
|
|
7205
7585
|
);
|
|
7206
7586
|
await this.sharedVectorStore.initialize();
|
|
7207
7587
|
this.sharedPromoter = this.factories.createSharedPromoter(
|
|
@@ -7274,7 +7654,7 @@ var SharedMemoryServices = class {
|
|
|
7274
7654
|
async createOpenStorePromise(sharedPath) {
|
|
7275
7655
|
if (!this.sharedEventStore) {
|
|
7276
7656
|
const sharedEventStore = this.factories.createSharedEventStore(
|
|
7277
|
-
|
|
7657
|
+
path6.join(sharedPath, "shared.duckdb")
|
|
7278
7658
|
);
|
|
7279
7659
|
await sharedEventStore.initialize();
|
|
7280
7660
|
this.sharedEventStore = sharedEventStore;
|
|
@@ -7294,9 +7674,9 @@ var SharedMemoryServices = class {
|
|
|
7294
7674
|
}
|
|
7295
7675
|
get factories() {
|
|
7296
7676
|
return {
|
|
7297
|
-
existsSync: this.options.factories?.existsSync ??
|
|
7677
|
+
existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
|
|
7298
7678
|
mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
|
|
7299
|
-
|
|
7679
|
+
fs7.mkdirSync(targetPath, { recursive: true });
|
|
7300
7680
|
}),
|
|
7301
7681
|
createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
|
|
7302
7682
|
createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
|
|
@@ -7411,37 +7791,11 @@ function createMemoryServiceComposition(options) {
|
|
|
7411
7791
|
}
|
|
7412
7792
|
function defaultExpandPath(targetPath) {
|
|
7413
7793
|
if (targetPath.startsWith("~")) {
|
|
7414
|
-
return
|
|
7794
|
+
return path7.join(os2.homedir(), targetPath.slice(1));
|
|
7415
7795
|
}
|
|
7416
7796
|
return targetPath;
|
|
7417
7797
|
}
|
|
7418
7798
|
|
|
7419
|
-
// src/core/registry/project-path.ts
|
|
7420
|
-
import * as crypto2 from "crypto";
|
|
7421
|
-
import * as fs7 from "fs";
|
|
7422
|
-
import * as os2 from "os";
|
|
7423
|
-
import * as path7 from "path";
|
|
7424
|
-
function normalizeProjectPath(projectPath) {
|
|
7425
|
-
const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
|
|
7426
|
-
try {
|
|
7427
|
-
return fs7.realpathSync(expanded);
|
|
7428
|
-
} catch {
|
|
7429
|
-
return path7.resolve(expanded);
|
|
7430
|
-
}
|
|
7431
|
-
}
|
|
7432
|
-
function hashProjectPath(projectPath) {
|
|
7433
|
-
const normalizedPath = normalizeProjectPath(projectPath);
|
|
7434
|
-
return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
|
|
7435
|
-
}
|
|
7436
|
-
function getProjectStoragePath(projectPath) {
|
|
7437
|
-
const hash = hashProjectPath(projectPath);
|
|
7438
|
-
return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
|
|
7439
|
-
}
|
|
7440
|
-
function resolveProjectStoragePath(projectOrHash) {
|
|
7441
|
-
const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
|
|
7442
|
-
return isHash ? path7.join(os2.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
|
|
7443
|
-
}
|
|
7444
|
-
|
|
7445
7799
|
// src/core/registry/session-registry.ts
|
|
7446
7800
|
import * as fs8 from "fs";
|
|
7447
7801
|
import * as os3 from "os";
|
|
@@ -7740,6 +8094,12 @@ var MemoryService = class {
|
|
|
7740
8094
|
async getOutboxStats() {
|
|
7741
8095
|
return this.queryService.getOutboxStats();
|
|
7742
8096
|
}
|
|
8097
|
+
async recoverStuckOutboxItems(options) {
|
|
8098
|
+
return this.queryService.recoverStuckOutboxItems(options);
|
|
8099
|
+
}
|
|
8100
|
+
async repairLegacyProjectScope(options) {
|
|
8101
|
+
return this.queryService.repairLegacyProjectScope(options);
|
|
8102
|
+
}
|
|
7743
8103
|
async getRetrievalTraceStats() {
|
|
7744
8104
|
return this.retrievalAnalyticsService.getRetrievalTraceStats();
|
|
7745
8105
|
}
|
|
@@ -8053,6 +8413,26 @@ function getServiceFromQuery(c) {
|
|
|
8053
8413
|
}
|
|
8054
8414
|
return getReadOnlyMemoryService();
|
|
8055
8415
|
}
|
|
8416
|
+
function getWritableServiceFromQuery(c) {
|
|
8417
|
+
const project = c.req.query("project") || c.req.query("projectId");
|
|
8418
|
+
if (project) {
|
|
8419
|
+
const storagePath = resolveProjectStoragePath(project);
|
|
8420
|
+
return new MemoryService({
|
|
8421
|
+
storagePath,
|
|
8422
|
+
readOnly: false,
|
|
8423
|
+
lightweightMode: true,
|
|
8424
|
+
analyticsEnabled: false,
|
|
8425
|
+
sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
|
|
8426
|
+
});
|
|
8427
|
+
}
|
|
8428
|
+
return new MemoryService({
|
|
8429
|
+
storagePath: "~/.claude-code/memory",
|
|
8430
|
+
readOnly: false,
|
|
8431
|
+
lightweightMode: true,
|
|
8432
|
+
analyticsEnabled: false,
|
|
8433
|
+
sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
|
|
8434
|
+
});
|
|
8435
|
+
}
|
|
8056
8436
|
function getLightweightServiceFromQuery(c) {
|
|
8057
8437
|
const project = c.req.query("project") || c.req.query("projectId");
|
|
8058
8438
|
if (project) {
|
|
@@ -9708,6 +10088,13 @@ import { Hono as Hono8 } from "hono";
|
|
|
9708
10088
|
import { streamSSE } from "hono/streaming";
|
|
9709
10089
|
import { spawn } from "child_process";
|
|
9710
10090
|
var chatRouter = new Hono8();
|
|
10091
|
+
var ProviderFailure = class extends Error {
|
|
10092
|
+
constructor(code, message) {
|
|
10093
|
+
super(message);
|
|
10094
|
+
this.code = code;
|
|
10095
|
+
this.name = "ProviderFailure";
|
|
10096
|
+
}
|
|
10097
|
+
};
|
|
9711
10098
|
var CLAUDE_TIMEOUT_MS = 12e4;
|
|
9712
10099
|
chatRouter.post("/", async (c) => {
|
|
9713
10100
|
let body;
|
|
@@ -9719,43 +10106,12 @@ chatRouter.post("/", async (c) => {
|
|
|
9719
10106
|
if (!body.message?.trim()) {
|
|
9720
10107
|
return c.json({ error: "Message is required" }, 400);
|
|
9721
10108
|
}
|
|
9722
|
-
const
|
|
10109
|
+
const memoryOnly = body.mode === "memory-only" || body.memoryOnly === true;
|
|
10110
|
+
const memoryService = memoryOnly ? getLightweightServiceFromQuery(c) : getServiceFromQuery(c);
|
|
9723
10111
|
try {
|
|
9724
10112
|
await memoryService.initialize();
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
try {
|
|
9728
|
-
const result = await memoryService.retrieveMemories(body.message, {
|
|
9729
|
-
topK: 8,
|
|
9730
|
-
minScore: 0.5
|
|
9731
|
-
});
|
|
9732
|
-
if (result.memories.length > 0) {
|
|
9733
|
-
const parts = ["## Relevant Memories\n"];
|
|
9734
|
-
for (const m of result.memories) {
|
|
9735
|
-
const date = new Date(m.event.timestamp).toISOString().split("T")[0];
|
|
9736
|
-
const content = m.event.content.slice(0, 500);
|
|
9737
|
-
parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
|
|
9738
|
-
parts.push(content);
|
|
9739
|
-
if (m.sessionContext) {
|
|
9740
|
-
parts.push(`_Context: ${m.sessionContext}_`);
|
|
9741
|
-
}
|
|
9742
|
-
parts.push("");
|
|
9743
|
-
}
|
|
9744
|
-
memoryContext = parts.join("\n");
|
|
9745
|
-
}
|
|
9746
|
-
} catch {
|
|
9747
|
-
}
|
|
9748
|
-
try {
|
|
9749
|
-
const stats = await memoryService.getStats();
|
|
9750
|
-
const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
|
|
9751
|
-
statsContext = [
|
|
9752
|
-
"## Memory Stats",
|
|
9753
|
-
`- Total events: ${stats.totalEvents}`,
|
|
9754
|
-
`- Vector nodes: ${stats.vectorCount}`,
|
|
9755
|
-
`- By level: ${levels}`
|
|
9756
|
-
].join("\n");
|
|
9757
|
-
} catch {
|
|
9758
|
-
}
|
|
10113
|
+
const { memoryContext, memoryHits } = await collectMemoryContext(memoryService, body.message);
|
|
10114
|
+
const statsContext = await collectStatsContext(memoryService);
|
|
9759
10115
|
const fullPrompt = buildPrompt(
|
|
9760
10116
|
statsContext,
|
|
9761
10117
|
memoryContext,
|
|
@@ -9763,13 +10119,23 @@ chatRouter.post("/", async (c) => {
|
|
|
9763
10119
|
body.message
|
|
9764
10120
|
);
|
|
9765
10121
|
return streamSSE(c, async (stream) => {
|
|
10122
|
+
if (memoryOnly) {
|
|
10123
|
+
await streamMemoryOnlyResponse(stream, {
|
|
10124
|
+
memoryContext,
|
|
10125
|
+
memoryHits,
|
|
10126
|
+
reason: "memory-only-mode"
|
|
10127
|
+
});
|
|
10128
|
+
return;
|
|
10129
|
+
}
|
|
9766
10130
|
try {
|
|
9767
10131
|
await streamClaudeResponse(fullPrompt, stream);
|
|
9768
10132
|
} catch (err) {
|
|
10133
|
+
const diagnostic = providerDiagnostic(err);
|
|
9769
10134
|
await stream.writeSSE({
|
|
9770
|
-
event: "
|
|
9771
|
-
data: JSON.stringify(
|
|
10135
|
+
event: "provider_error",
|
|
10136
|
+
data: JSON.stringify(diagnostic)
|
|
9772
10137
|
});
|
|
10138
|
+
await streamMemoryOnlyFallback(stream, memoryContext, memoryHits);
|
|
9773
10139
|
}
|
|
9774
10140
|
});
|
|
9775
10141
|
} catch (error) {
|
|
@@ -9778,6 +10144,115 @@ chatRouter.post("/", async (c) => {
|
|
|
9778
10144
|
await memoryService.shutdown();
|
|
9779
10145
|
}
|
|
9780
10146
|
});
|
|
10147
|
+
async function collectMemoryContext(memoryService, query) {
|
|
10148
|
+
let memoryHits = [];
|
|
10149
|
+
try {
|
|
10150
|
+
const result = await memoryService.retrieveMemories?.(query, {
|
|
10151
|
+
topK: 8,
|
|
10152
|
+
minScore: 0.5
|
|
10153
|
+
});
|
|
10154
|
+
memoryHits = result?.memories ?? [];
|
|
10155
|
+
} catch {
|
|
10156
|
+
memoryHits = [];
|
|
10157
|
+
}
|
|
10158
|
+
if (memoryHits.length === 0) {
|
|
10159
|
+
try {
|
|
10160
|
+
memoryHits = await memoryService.keywordSearch?.(query, { topK: 8, minScore: 0.05 }) ?? [];
|
|
10161
|
+
} catch {
|
|
10162
|
+
memoryHits = [];
|
|
10163
|
+
}
|
|
10164
|
+
}
|
|
10165
|
+
return {
|
|
10166
|
+
memoryContext: formatMemoryContext(memoryHits),
|
|
10167
|
+
memoryHits
|
|
10168
|
+
};
|
|
10169
|
+
}
|
|
10170
|
+
async function collectStatsContext(memoryService) {
|
|
10171
|
+
try {
|
|
10172
|
+
const stats = await memoryService.getStats?.();
|
|
10173
|
+
if (!stats)
|
|
10174
|
+
return "";
|
|
10175
|
+
const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
|
|
10176
|
+
return [
|
|
10177
|
+
"## Memory Stats",
|
|
10178
|
+
`- Total events: ${stats.totalEvents}`,
|
|
10179
|
+
`- Vector nodes: ${stats.vectorCount}`,
|
|
10180
|
+
`- By level: ${levels}`
|
|
10181
|
+
].join("\n");
|
|
10182
|
+
} catch {
|
|
10183
|
+
return "";
|
|
10184
|
+
}
|
|
10185
|
+
}
|
|
10186
|
+
function formatMemoryContext(memoryHits) {
|
|
10187
|
+
if (memoryHits.length === 0)
|
|
10188
|
+
return "";
|
|
10189
|
+
const parts = ["## Relevant Memories\n"];
|
|
10190
|
+
for (const m of memoryHits) {
|
|
10191
|
+
const date = m.event.timestamp ? new Date(m.event.timestamp).toISOString().split("T")[0] : "unknown-date";
|
|
10192
|
+
const content = (m.event.content ?? "").slice(0, 500);
|
|
10193
|
+
parts.push(`### [${m.event.eventType ?? "memory"}] ${date} (score: ${m.score.toFixed(2)})`);
|
|
10194
|
+
parts.push(content);
|
|
10195
|
+
if (m.sessionContext) {
|
|
10196
|
+
parts.push(`_Context: ${m.sessionContext}_`);
|
|
10197
|
+
}
|
|
10198
|
+
parts.push("");
|
|
10199
|
+
}
|
|
10200
|
+
return parts.join("\n");
|
|
10201
|
+
}
|
|
10202
|
+
async function streamMemoryOnlyResponse(stream, options) {
|
|
10203
|
+
await stream.writeSSE({
|
|
10204
|
+
event: "diagnostic",
|
|
10205
|
+
data: JSON.stringify({
|
|
10206
|
+
provider: "claude-cli",
|
|
10207
|
+
status: "skipped",
|
|
10208
|
+
mode: "memory-only",
|
|
10209
|
+
reason: options.reason,
|
|
10210
|
+
retrievedMemories: options.memoryHits.length
|
|
10211
|
+
})
|
|
10212
|
+
});
|
|
10213
|
+
await streamMemoryOnlyFallback(stream, options.memoryContext, options.memoryHits);
|
|
10214
|
+
}
|
|
10215
|
+
async function streamMemoryOnlyFallback(stream, memoryContext, memoryHits) {
|
|
10216
|
+
const content = memoryHits.length > 0 ? [
|
|
10217
|
+
"Provider unavailable or skipped; showing retrieved memory context directly.",
|
|
10218
|
+
"",
|
|
10219
|
+
memoryContext
|
|
10220
|
+
].join("\n") : "Provider unavailable or skipped, and no directly relevant memories were found for this query.";
|
|
10221
|
+
await stream.writeSSE({
|
|
10222
|
+
event: "message",
|
|
10223
|
+
data: JSON.stringify({ content, mode: "memory-only" })
|
|
10224
|
+
});
|
|
10225
|
+
await stream.writeSSE({ event: "done", data: "{}" });
|
|
10226
|
+
}
|
|
10227
|
+
function providerDiagnostic(err) {
|
|
10228
|
+
if (err instanceof ProviderFailure) {
|
|
10229
|
+
return {
|
|
10230
|
+
provider: "claude-cli",
|
|
10231
|
+
code: err.code,
|
|
10232
|
+
message: err.message,
|
|
10233
|
+
fallback: "memory-only"
|
|
10234
|
+
};
|
|
10235
|
+
}
|
|
10236
|
+
return {
|
|
10237
|
+
provider: "claude-cli",
|
|
10238
|
+
code: "claude-cli-error",
|
|
10239
|
+
message: err instanceof Error ? err.message : "Unknown Claude CLI failure",
|
|
10240
|
+
fallback: "memory-only"
|
|
10241
|
+
};
|
|
10242
|
+
}
|
|
10243
|
+
function classifyProviderFailure(message) {
|
|
10244
|
+
const normalized = message.toLowerCase();
|
|
10245
|
+
if (normalized.includes("401") || normalized.includes("unauthorized") || normalized.includes("auth")) {
|
|
10246
|
+
return new ProviderFailure("claude-cli-auth", "Claude CLI authentication failed; showing memory-only context.");
|
|
10247
|
+
}
|
|
10248
|
+
if (normalized.includes("not found") || normalized.includes("enoent")) {
|
|
10249
|
+
return new ProviderFailure("claude-cli-not-found", "Claude CLI was not found; showing memory-only context.");
|
|
10250
|
+
}
|
|
10251
|
+
if (normalized.includes("timed out")) {
|
|
10252
|
+
return new ProviderFailure("claude-cli-timeout", "Claude CLI timed out; showing memory-only context.");
|
|
10253
|
+
}
|
|
10254
|
+
return new ProviderFailure("claude-cli-error", "Claude CLI failed; showing memory-only context.");
|
|
10255
|
+
}
|
|
9781
10256
|
function buildPrompt(statsContext, memoryContext, history, currentMessage) {
|
|
9782
10257
|
const parts = [];
|
|
9783
10258
|
parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
|
|
@@ -9820,12 +10295,13 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
9820
10295
|
});
|
|
9821
10296
|
const timeout = setTimeout(() => {
|
|
9822
10297
|
proc.kill("SIGTERM");
|
|
9823
|
-
reject(
|
|
10298
|
+
reject(classifyProviderFailure("timed out"));
|
|
9824
10299
|
}, CLAUDE_TIMEOUT_MS);
|
|
9825
10300
|
proc.stdin.write(prompt);
|
|
9826
10301
|
proc.stdin.end();
|
|
9827
10302
|
let buffer = "";
|
|
9828
10303
|
let lastSentText = "";
|
|
10304
|
+
let stderrText = "";
|
|
9829
10305
|
proc.stdout.on("data", async (chunk) => {
|
|
9830
10306
|
buffer += chunk.toString();
|
|
9831
10307
|
const lines = buffer.split("\n");
|
|
@@ -9854,6 +10330,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
9854
10330
|
}
|
|
9855
10331
|
});
|
|
9856
10332
|
proc.stderr.on("data", (chunk) => {
|
|
10333
|
+
stderrText += chunk.toString();
|
|
9857
10334
|
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
9858
10335
|
console.error("[chat] claude stderr:", chunk.toString());
|
|
9859
10336
|
}
|
|
@@ -9861,7 +10338,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
9861
10338
|
proc.on("error", (err) => {
|
|
9862
10339
|
clearTimeout(timeout);
|
|
9863
10340
|
if (err.code === "ENOENT") {
|
|
9864
|
-
reject(
|
|
10341
|
+
reject(classifyProviderFailure("ENOENT not found"));
|
|
9865
10342
|
} else {
|
|
9866
10343
|
reject(err);
|
|
9867
10344
|
}
|
|
@@ -9878,7 +10355,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
9878
10355
|
}
|
|
9879
10356
|
}
|
|
9880
10357
|
if (code !== 0 && code !== null) {
|
|
9881
|
-
reject(
|
|
10358
|
+
reject(classifyProviderFailure(stderrText || `Claude CLI exited with code ${code}`));
|
|
9882
10359
|
} else {
|
|
9883
10360
|
resolve4();
|
|
9884
10361
|
}
|
|
@@ -9927,6 +10404,49 @@ healthRouter.get("/", async (c) => {
|
|
|
9927
10404
|
await memoryService.shutdown();
|
|
9928
10405
|
}
|
|
9929
10406
|
});
|
|
10407
|
+
healthRouter.post("/recover", async (c) => {
|
|
10408
|
+
const memoryService = getWritableServiceFromQuery(c);
|
|
10409
|
+
try {
|
|
10410
|
+
await memoryService.initialize();
|
|
10411
|
+
const body = await c.req.json().catch(() => ({}));
|
|
10412
|
+
const options = {};
|
|
10413
|
+
if (typeof body.stuckThresholdMs === "number" && Number.isFinite(body.stuckThresholdMs)) {
|
|
10414
|
+
options.stuckThresholdMs = body.stuckThresholdMs;
|
|
10415
|
+
}
|
|
10416
|
+
if (typeof body.maxRetries === "number" && Number.isFinite(body.maxRetries)) {
|
|
10417
|
+
options.maxRetries = body.maxRetries;
|
|
10418
|
+
}
|
|
10419
|
+
const before = await memoryService.getOutboxStats();
|
|
10420
|
+
const recovered = await memoryService.recoverStuckOutboxItems(options);
|
|
10421
|
+
const [stats, outbox] = await Promise.all([
|
|
10422
|
+
memoryService.getStats(),
|
|
10423
|
+
memoryService.getOutboxStats()
|
|
10424
|
+
]);
|
|
10425
|
+
return c.json({
|
|
10426
|
+
status: "ok",
|
|
10427
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10428
|
+
recovered,
|
|
10429
|
+
before: {
|
|
10430
|
+
outbox: before
|
|
10431
|
+
},
|
|
10432
|
+
after: {
|
|
10433
|
+
storage: {
|
|
10434
|
+
totalEvents: stats.totalEvents,
|
|
10435
|
+
vectorCount: stats.vectorCount
|
|
10436
|
+
},
|
|
10437
|
+
outbox
|
|
10438
|
+
}
|
|
10439
|
+
});
|
|
10440
|
+
} catch (error) {
|
|
10441
|
+
return c.json({
|
|
10442
|
+
status: "error",
|
|
10443
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10444
|
+
error: error.message
|
|
10445
|
+
}, 500);
|
|
10446
|
+
} finally {
|
|
10447
|
+
await memoryService.shutdown();
|
|
10448
|
+
}
|
|
10449
|
+
});
|
|
9930
10450
|
|
|
9931
10451
|
// src/apps/server/api/index.ts
|
|
9932
10452
|
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);
|