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/cli/index.js
CHANGED
|
@@ -17,8 +17,8 @@ import * as os11 from "os";
|
|
|
17
17
|
import * as os5 from "os";
|
|
18
18
|
|
|
19
19
|
// src/core/engine/memory-service-composition.ts
|
|
20
|
-
import * as
|
|
21
|
-
import * as
|
|
20
|
+
import * as os2 from "os";
|
|
21
|
+
import * as path7 from "path";
|
|
22
22
|
|
|
23
23
|
// src/core/metadata-extractor.ts
|
|
24
24
|
function createToolObservationEmbedding(toolName, metadata, success) {
|
|
@@ -1529,8 +1529,8 @@ function createEndlessMemoryServices(options) {
|
|
|
1529
1529
|
}
|
|
1530
1530
|
|
|
1531
1531
|
// src/core/engine/memory-engine-services.ts
|
|
1532
|
-
import * as
|
|
1533
|
-
import * as
|
|
1532
|
+
import * as fs6 from "fs";
|
|
1533
|
+
import * as path5 from "path";
|
|
1534
1534
|
|
|
1535
1535
|
// src/extensions/vector/embedder.ts
|
|
1536
1536
|
var DEFAULT_EMBEDDING_MODEL = "Xenova/multilingual-e5-small";
|
|
@@ -2208,14 +2208,43 @@ function makeDedupeKey(content, sessionId) {
|
|
|
2208
2208
|
return `${sessionId}:${contentHash}`;
|
|
2209
2209
|
}
|
|
2210
2210
|
|
|
2211
|
+
// src/core/sqlite-event-store.ts
|
|
2212
|
+
import * as nodePath2 from "path";
|
|
2213
|
+
|
|
2214
|
+
// src/core/registry/project-path.ts
|
|
2215
|
+
import * as crypto2 from "crypto";
|
|
2216
|
+
import * as fs3 from "fs";
|
|
2217
|
+
import * as os from "os";
|
|
2218
|
+
import * as path3 from "path";
|
|
2219
|
+
function normalizeProjectPath(projectPath) {
|
|
2220
|
+
const expanded = projectPath.startsWith("~") ? path3.join(os.homedir(), projectPath.slice(1)) : projectPath;
|
|
2221
|
+
try {
|
|
2222
|
+
return fs3.realpathSync(expanded);
|
|
2223
|
+
} catch {
|
|
2224
|
+
return path3.resolve(expanded);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
function hashProjectPath(projectPath) {
|
|
2228
|
+
const normalizedPath = normalizeProjectPath(projectPath);
|
|
2229
|
+
return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
|
|
2230
|
+
}
|
|
2231
|
+
function getProjectStoragePath(projectPath) {
|
|
2232
|
+
const hash = hashProjectPath(projectPath);
|
|
2233
|
+
return path3.join(os.homedir(), ".claude-code", "memory", "projects", hash);
|
|
2234
|
+
}
|
|
2235
|
+
function resolveProjectStoragePath(projectOrHash) {
|
|
2236
|
+
const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
|
|
2237
|
+
return isHash ? path3.join(os.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2211
2240
|
// src/core/sqlite-wrapper.ts
|
|
2212
2241
|
import Database from "better-sqlite3";
|
|
2213
|
-
import * as
|
|
2242
|
+
import * as fs4 from "fs";
|
|
2214
2243
|
import * as nodePath from "path";
|
|
2215
2244
|
function createSQLiteDatabase(path22, options) {
|
|
2216
2245
|
const dir = nodePath.dirname(path22);
|
|
2217
|
-
if (!
|
|
2218
|
-
|
|
2246
|
+
if (!fs4.existsSync(dir)) {
|
|
2247
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
2219
2248
|
}
|
|
2220
2249
|
const db = new Database(path22, {
|
|
2221
2250
|
readonly: options?.readonly ?? false
|
|
@@ -2264,8 +2293,8 @@ function toSQLiteTimestamp(date) {
|
|
|
2264
2293
|
}
|
|
2265
2294
|
|
|
2266
2295
|
// src/core/markdown-mirror.ts
|
|
2267
|
-
import * as
|
|
2268
|
-
import * as
|
|
2296
|
+
import * as fs5 from "fs/promises";
|
|
2297
|
+
import * as path4 from "path";
|
|
2269
2298
|
var DEFAULT_NAMESPACE = "default";
|
|
2270
2299
|
var DEFAULT_CATEGORY = "uncategorized";
|
|
2271
2300
|
function sanitizeSegment2(input, fallback) {
|
|
@@ -2294,7 +2323,7 @@ function buildMirrorPath2(rootDir, event) {
|
|
|
2294
2323
|
const yyyy = d.getFullYear();
|
|
2295
2324
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
2296
2325
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
2297
|
-
return
|
|
2326
|
+
return path4.join(rootDir, "memory", namespace, ...categories, `${yyyy}-${mm}-${dd}.md`);
|
|
2298
2327
|
}
|
|
2299
2328
|
function formatMirrorEntry(event) {
|
|
2300
2329
|
const category = Array.isArray(event.metadata?.categoryPath) ? event.metadata.categoryPath.join("/") : String(event.metadata?.category ?? event.eventType);
|
|
@@ -2315,8 +2344,8 @@ var MarkdownMirror2 = class {
|
|
|
2315
2344
|
}
|
|
2316
2345
|
async append(event) {
|
|
2317
2346
|
const outPath = buildMirrorPath2(this.rootDir, event);
|
|
2318
|
-
await
|
|
2319
|
-
await
|
|
2347
|
+
await fs5.mkdir(path4.dirname(outPath), { recursive: true });
|
|
2348
|
+
await fs5.appendFile(outPath, formatMirrorEntry(event), "utf8");
|
|
2320
2349
|
return outPath;
|
|
2321
2350
|
}
|
|
2322
2351
|
};
|
|
@@ -2329,6 +2358,143 @@ function normalizeQueryRewriteKind(value) {
|
|
|
2329
2358
|
return "none";
|
|
2330
2359
|
}
|
|
2331
2360
|
var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
|
|
2361
|
+
var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2362
|
+
var DEFAULT_OUTBOX_MAX_RETRIES = 3;
|
|
2363
|
+
function emptyOutboxRecoveryResult() {
|
|
2364
|
+
return {
|
|
2365
|
+
embedding: { recoveredProcessing: 0, retriedFailed: 0 },
|
|
2366
|
+
vector: { recoveredProcessing: 0, retriedFailed: 0 }
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
function isRecord(value) {
|
|
2370
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2371
|
+
}
|
|
2372
|
+
function getNestedRecord(root, path22) {
|
|
2373
|
+
let cursor = root;
|
|
2374
|
+
for (const key of path22) {
|
|
2375
|
+
if (!isRecord(cursor))
|
|
2376
|
+
return void 0;
|
|
2377
|
+
cursor = cursor[key];
|
|
2378
|
+
}
|
|
2379
|
+
return isRecord(cursor) ? cursor : void 0;
|
|
2380
|
+
}
|
|
2381
|
+
function getNestedString(root, path22) {
|
|
2382
|
+
let cursor = root;
|
|
2383
|
+
for (const key of path22) {
|
|
2384
|
+
if (!isRecord(cursor))
|
|
2385
|
+
return void 0;
|
|
2386
|
+
cursor = cursor[key];
|
|
2387
|
+
}
|
|
2388
|
+
return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
|
|
2389
|
+
}
|
|
2390
|
+
function metadataProjectHash(metadata) {
|
|
2391
|
+
return getNestedString(metadata, ["scope", "project", "hash"]);
|
|
2392
|
+
}
|
|
2393
|
+
function metadataProjectPaths(metadata) {
|
|
2394
|
+
const candidates = [
|
|
2395
|
+
getNestedString(metadata, ["projectPath"]),
|
|
2396
|
+
getNestedString(metadata, ["sourceProjectPath"]),
|
|
2397
|
+
getNestedString(metadata, ["scope", "project", "path"])
|
|
2398
|
+
];
|
|
2399
|
+
const paths = [];
|
|
2400
|
+
for (const value of candidates) {
|
|
2401
|
+
if (value && !paths.includes(value))
|
|
2402
|
+
paths.push(value);
|
|
2403
|
+
}
|
|
2404
|
+
return paths;
|
|
2405
|
+
}
|
|
2406
|
+
function metadataProjectPath(metadata) {
|
|
2407
|
+
return metadataProjectPaths(metadata)[0];
|
|
2408
|
+
}
|
|
2409
|
+
function isActiveQuarantinedMetadata(metadata) {
|
|
2410
|
+
const quarantine = getNestedRecord(metadata, ["quarantine"]);
|
|
2411
|
+
return quarantine?.status === "active";
|
|
2412
|
+
}
|
|
2413
|
+
function activeQuarantineStatusExpression(column = "metadata") {
|
|
2414
|
+
return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
|
|
2415
|
+
}
|
|
2416
|
+
function notActiveQuarantinedSql(column = "metadata") {
|
|
2417
|
+
return `${activeQuarantineStatusExpression(column)} != 'active'`;
|
|
2418
|
+
}
|
|
2419
|
+
function maybeQuarantinePredicate(options, column = "metadata") {
|
|
2420
|
+
return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
|
|
2421
|
+
}
|
|
2422
|
+
function safeParseMetadataValue(value) {
|
|
2423
|
+
if (!value)
|
|
2424
|
+
return void 0;
|
|
2425
|
+
if (typeof value === "object")
|
|
2426
|
+
return isRecord(value) ? value : void 0;
|
|
2427
|
+
if (typeof value !== "string")
|
|
2428
|
+
return void 0;
|
|
2429
|
+
try {
|
|
2430
|
+
const parsed = JSON.parse(value);
|
|
2431
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
2432
|
+
} catch {
|
|
2433
|
+
return void 0;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
function isImportedOrLegacyScopedMetadata(metadata) {
|
|
2437
|
+
if (!metadata)
|
|
2438
|
+
return false;
|
|
2439
|
+
return Boolean(
|
|
2440
|
+
metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
function addMetadataTag(metadata, tag) {
|
|
2444
|
+
const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
|
|
2445
|
+
if (!current.includes(tag))
|
|
2446
|
+
metadata.tags = [...current, tag];
|
|
2447
|
+
}
|
|
2448
|
+
function buildRepairResult(projectHash, dryRun) {
|
|
2449
|
+
return {
|
|
2450
|
+
dryRun,
|
|
2451
|
+
projectHash,
|
|
2452
|
+
scanned: 0,
|
|
2453
|
+
repaired: 0,
|
|
2454
|
+
quarantined: 0,
|
|
2455
|
+
alreadyScoped: 0,
|
|
2456
|
+
skipped: 0,
|
|
2457
|
+
samples: []
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
function normalizeRepoName(value) {
|
|
2461
|
+
return value.replace(/\.git$/i, "").trim().toLowerCase();
|
|
2462
|
+
}
|
|
2463
|
+
function projectBasename(projectPath) {
|
|
2464
|
+
if (!projectPath)
|
|
2465
|
+
return void 0;
|
|
2466
|
+
const trimmed = projectPath.replace(/[\\/]+$/, "");
|
|
2467
|
+
const basename7 = nodePath2.basename(trimmed);
|
|
2468
|
+
return basename7 ? normalizeRepoName(basename7) : void 0;
|
|
2469
|
+
}
|
|
2470
|
+
function isProjectScopeRepairExplanation(content) {
|
|
2471
|
+
const normalized = content.toLowerCase();
|
|
2472
|
+
const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
|
|
2473
|
+
const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
|
|
2474
|
+
return hasRepairContext && hasExplanationContext;
|
|
2475
|
+
}
|
|
2476
|
+
function hasConflictingContentProjectHint(content, projectPath) {
|
|
2477
|
+
const currentName = projectBasename(projectPath);
|
|
2478
|
+
if (!currentName)
|
|
2479
|
+
return false;
|
|
2480
|
+
if (isProjectScopeRepairExplanation(content))
|
|
2481
|
+
return false;
|
|
2482
|
+
const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
|
|
2483
|
+
let githubMatch;
|
|
2484
|
+
while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
|
|
2485
|
+
const repo = normalizeRepoName(githubMatch[2] || "");
|
|
2486
|
+
if (repo && repo !== currentName)
|
|
2487
|
+
return true;
|
|
2488
|
+
}
|
|
2489
|
+
const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
|
|
2490
|
+
let workspaceMatch;
|
|
2491
|
+
while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
|
|
2492
|
+
const repo = normalizeRepoName(workspaceMatch[1] || "");
|
|
2493
|
+
if (repo && repo !== currentName)
|
|
2494
|
+
return true;
|
|
2495
|
+
}
|
|
2496
|
+
return false;
|
|
2497
|
+
}
|
|
2332
2498
|
var SQLiteEventStore = class {
|
|
2333
2499
|
db;
|
|
2334
2500
|
initialized = false;
|
|
@@ -2827,11 +2993,11 @@ var SQLiteEventStore = class {
|
|
|
2827
2993
|
/**
|
|
2828
2994
|
* Get events by session ID
|
|
2829
2995
|
*/
|
|
2830
|
-
async getSessionEvents(sessionId) {
|
|
2996
|
+
async getSessionEvents(sessionId, options) {
|
|
2831
2997
|
await this.initialize();
|
|
2832
2998
|
const rows = sqliteAll(
|
|
2833
2999
|
this.db,
|
|
2834
|
-
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
3000
|
+
`SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
2835
3001
|
[sessionId]
|
|
2836
3002
|
);
|
|
2837
3003
|
return rows.map(this.rowToEvent);
|
|
@@ -2839,11 +3005,11 @@ var SQLiteEventStore = class {
|
|
|
2839
3005
|
/**
|
|
2840
3006
|
* Get recent events
|
|
2841
3007
|
*/
|
|
2842
|
-
async getRecentEvents(limit = 100) {
|
|
3008
|
+
async getRecentEvents(limit = 100, options) {
|
|
2843
3009
|
await this.initialize();
|
|
2844
3010
|
const rows = sqliteAll(
|
|
2845
3011
|
this.db,
|
|
2846
|
-
`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
|
|
3012
|
+
`SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
|
|
2847
3013
|
[limit]
|
|
2848
3014
|
);
|
|
2849
3015
|
return rows.map(this.rowToEvent);
|
|
@@ -2851,11 +3017,11 @@ var SQLiteEventStore = class {
|
|
|
2851
3017
|
/**
|
|
2852
3018
|
* Get event by ID
|
|
2853
3019
|
*/
|
|
2854
|
-
async getEvent(id) {
|
|
3020
|
+
async getEvent(id, options) {
|
|
2855
3021
|
await this.initialize();
|
|
2856
3022
|
const row = sqliteGet(
|
|
2857
3023
|
this.db,
|
|
2858
|
-
`SELECT * FROM events WHERE id =
|
|
3024
|
+
`SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
|
|
2859
3025
|
[id]
|
|
2860
3026
|
);
|
|
2861
3027
|
if (!row)
|
|
@@ -2865,11 +3031,11 @@ var SQLiteEventStore = class {
|
|
|
2865
3031
|
/**
|
|
2866
3032
|
* Get events since a timestamp (for sync)
|
|
2867
3033
|
*/
|
|
2868
|
-
async getEventsSince(timestamp, limit = 1e3) {
|
|
3034
|
+
async getEventsSince(timestamp, limit = 1e3, options) {
|
|
2869
3035
|
await this.initialize();
|
|
2870
3036
|
const rows = sqliteAll(
|
|
2871
3037
|
this.db,
|
|
2872
|
-
`SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
|
|
3038
|
+
`SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
|
|
2873
3039
|
[timestamp, limit]
|
|
2874
3040
|
);
|
|
2875
3041
|
return rows.map(this.rowToEvent);
|
|
@@ -2878,11 +3044,11 @@ var SQLiteEventStore = class {
|
|
|
2878
3044
|
* Get events since a SQLite rowid (for robust incremental replication).
|
|
2879
3045
|
* Rowid is monotonic for append-only tables, independent of client timestamps.
|
|
2880
3046
|
*/
|
|
2881
|
-
async getEventsSinceRowid(lastRowid, limit = 1e3) {
|
|
3047
|
+
async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
|
|
2882
3048
|
await this.initialize();
|
|
2883
3049
|
const rows = sqliteAll(
|
|
2884
3050
|
this.db,
|
|
2885
|
-
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
|
|
3051
|
+
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
|
|
2886
3052
|
[lastRowid, limit]
|
|
2887
3053
|
);
|
|
2888
3054
|
return rows.map((row) => ({
|
|
@@ -3079,7 +3245,9 @@ var SQLiteEventStore = class {
|
|
|
3079
3245
|
const placeholders = ids.map(() => "?").join(",");
|
|
3080
3246
|
sqliteRun(
|
|
3081
3247
|
this.db,
|
|
3082
|
-
`UPDATE embedding_outbox
|
|
3248
|
+
`UPDATE embedding_outbox
|
|
3249
|
+
SET status = 'processing', processed_at = datetime('now'), error_message = NULL
|
|
3250
|
+
WHERE id IN (${placeholders})`,
|
|
3083
3251
|
ids
|
|
3084
3252
|
);
|
|
3085
3253
|
return pending.map((row) => ({
|
|
@@ -3115,19 +3283,19 @@ var SQLiteEventStore = class {
|
|
|
3115
3283
|
/**
|
|
3116
3284
|
* Count total events
|
|
3117
3285
|
*/
|
|
3118
|
-
async countEvents() {
|
|
3286
|
+
async countEvents(options) {
|
|
3119
3287
|
await this.initialize();
|
|
3120
|
-
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
|
|
3288
|
+
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
|
|
3121
3289
|
return row?.count || 0;
|
|
3122
3290
|
}
|
|
3123
3291
|
/**
|
|
3124
3292
|
* Get events page in timestamp ascending order (stable migration/reindex scans)
|
|
3125
3293
|
*/
|
|
3126
|
-
async getEventsPage(limit = 1e3, offset = 0) {
|
|
3294
|
+
async getEventsPage(limit = 1e3, offset = 0, options) {
|
|
3127
3295
|
await this.initialize();
|
|
3128
3296
|
const rows = sqliteAll(
|
|
3129
3297
|
this.db,
|
|
3130
|
-
`SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
3298
|
+
`SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
3131
3299
|
[limit, offset]
|
|
3132
3300
|
);
|
|
3133
3301
|
return rows.map(this.rowToEvent);
|
|
@@ -3149,6 +3317,197 @@ var SQLiteEventStore = class {
|
|
|
3149
3317
|
[error, ...ids]
|
|
3150
3318
|
);
|
|
3151
3319
|
}
|
|
3320
|
+
/**
|
|
3321
|
+
* Recover abandoned outbox work after a worker/process crash.
|
|
3322
|
+
*
|
|
3323
|
+
* Rows in `processing` are claimed work. If the process exits before marking
|
|
3324
|
+
* them done/failed, they otherwise remain invisible to future processing.
|
|
3325
|
+
* Recovery is deliberately age-gated so an active worker is not disturbed.
|
|
3326
|
+
*/
|
|
3327
|
+
async recoverStuckOutboxItems(options = {}) {
|
|
3328
|
+
await this.initialize();
|
|
3329
|
+
const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
|
|
3330
|
+
const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
|
|
3331
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
3332
|
+
const threshold = new Date(now.getTime() - thresholdMs).toISOString();
|
|
3333
|
+
const result = emptyOutboxRecoveryResult();
|
|
3334
|
+
const embeddingRecovered = sqliteRun(
|
|
3335
|
+
this.db,
|
|
3336
|
+
`UPDATE embedding_outbox
|
|
3337
|
+
SET status = 'pending', processed_at = NULL, error_message = NULL
|
|
3338
|
+
WHERE status = 'processing'
|
|
3339
|
+
AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
|
|
3340
|
+
[threshold]
|
|
3341
|
+
);
|
|
3342
|
+
result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
|
|
3343
|
+
const embeddingRetried = sqliteRun(
|
|
3344
|
+
this.db,
|
|
3345
|
+
`UPDATE embedding_outbox
|
|
3346
|
+
SET status = 'pending', error_message = NULL
|
|
3347
|
+
WHERE status = 'failed'
|
|
3348
|
+
AND retry_count < ?`,
|
|
3349
|
+
[maxRetries]
|
|
3350
|
+
);
|
|
3351
|
+
result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
|
|
3352
|
+
const vectorRecovered = sqliteRun(
|
|
3353
|
+
this.db,
|
|
3354
|
+
`UPDATE vector_outbox
|
|
3355
|
+
SET status = 'pending', updated_at = ?, error = NULL
|
|
3356
|
+
WHERE status = 'processing'
|
|
3357
|
+
AND datetime(updated_at) < datetime(?)`,
|
|
3358
|
+
[now.toISOString(), threshold]
|
|
3359
|
+
);
|
|
3360
|
+
result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
|
|
3361
|
+
const vectorRetried = sqliteRun(
|
|
3362
|
+
this.db,
|
|
3363
|
+
`UPDATE vector_outbox
|
|
3364
|
+
SET status = 'pending', updated_at = ?, error = NULL
|
|
3365
|
+
WHERE status = 'failed'
|
|
3366
|
+
AND retry_count < ?`,
|
|
3367
|
+
[now.toISOString(), maxRetries]
|
|
3368
|
+
);
|
|
3369
|
+
result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
|
|
3370
|
+
return result;
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Repair legacy imported events that predate canonical project scope metadata.
|
|
3374
|
+
*
|
|
3375
|
+
* Same-project legacy rows are tagged with scope.project.hash. Rows that look
|
|
3376
|
+
* imported but cannot be proven to belong to this project are quarantined so
|
|
3377
|
+
* dashboard default reads/search do not surface cross-project contamination.
|
|
3378
|
+
*/
|
|
3379
|
+
async repairLegacyProjectScope(options = {}) {
|
|
3380
|
+
await this.initialize();
|
|
3381
|
+
const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
|
|
3382
|
+
if (!projectHash) {
|
|
3383
|
+
throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
|
|
3384
|
+
}
|
|
3385
|
+
if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
|
|
3386
|
+
throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
|
|
3387
|
+
}
|
|
3388
|
+
const dryRun = options.dryRun === true;
|
|
3389
|
+
const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
|
|
3390
|
+
const result = buildRepairResult(projectHash, dryRun);
|
|
3391
|
+
const rows = sqliteAll(
|
|
3392
|
+
this.db,
|
|
3393
|
+
`SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
|
|
3394
|
+
FROM events e
|
|
3395
|
+
LEFT JOIN sessions s ON s.id = e.session_id
|
|
3396
|
+
ORDER BY e.timestamp ASC`,
|
|
3397
|
+
[]
|
|
3398
|
+
);
|
|
3399
|
+
const sample = (entry) => {
|
|
3400
|
+
if (result.samples.length < 20)
|
|
3401
|
+
result.samples.push(entry);
|
|
3402
|
+
};
|
|
3403
|
+
for (const row of rows) {
|
|
3404
|
+
result.scanned++;
|
|
3405
|
+
let metadata = {};
|
|
3406
|
+
let metadataParseInvalid = false;
|
|
3407
|
+
if (row.metadata) {
|
|
3408
|
+
const parsed = safeParseMetadataValue(row.metadata);
|
|
3409
|
+
if (parsed) {
|
|
3410
|
+
metadata = parsed;
|
|
3411
|
+
} else {
|
|
3412
|
+
metadataParseInvalid = true;
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
if (isActiveQuarantinedMetadata(metadata)) {
|
|
3416
|
+
result.skipped++;
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
const currentHash = metadataProjectHash(metadata);
|
|
3420
|
+
const explicitPath = metadataProjectPath(metadata);
|
|
3421
|
+
const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
|
|
3422
|
+
const candidatePaths = metadataProjectPaths(metadata);
|
|
3423
|
+
if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
|
|
3424
|
+
candidatePaths.push(sessionProjectPath);
|
|
3425
|
+
}
|
|
3426
|
+
const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
|
|
3427
|
+
const pathHashes = candidatePaths.map((candidate) => {
|
|
3428
|
+
try {
|
|
3429
|
+
return { path: candidate, hash: hashProjectPath(candidate) };
|
|
3430
|
+
} catch {
|
|
3431
|
+
return { path: candidate, hash: void 0 };
|
|
3432
|
+
}
|
|
3433
|
+
});
|
|
3434
|
+
const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
|
|
3435
|
+
const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
|
|
3436
|
+
let action = "skipped";
|
|
3437
|
+
let reason;
|
|
3438
|
+
let observedProjectHash;
|
|
3439
|
+
if (foreignPath) {
|
|
3440
|
+
action = "quarantined";
|
|
3441
|
+
reason = "project-path-mismatch";
|
|
3442
|
+
observedProjectHash = foreignPath.hash;
|
|
3443
|
+
} else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
|
|
3444
|
+
action = "quarantined";
|
|
3445
|
+
reason = "content-project-mismatch";
|
|
3446
|
+
} else if (currentHash === projectHash) {
|
|
3447
|
+
result.alreadyScoped++;
|
|
3448
|
+
continue;
|
|
3449
|
+
} else if (currentHash && currentHash !== projectHash) {
|
|
3450
|
+
action = "quarantined";
|
|
3451
|
+
reason = "scope-hash-mismatch";
|
|
3452
|
+
observedProjectHash = currentHash;
|
|
3453
|
+
} else if (matchingPath) {
|
|
3454
|
+
action = "repaired";
|
|
3455
|
+
reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
|
|
3456
|
+
} else if (candidatePaths.length > 0) {
|
|
3457
|
+
action = "quarantined";
|
|
3458
|
+
reason = "project-path-mismatch";
|
|
3459
|
+
} else if (importedOrLegacy) {
|
|
3460
|
+
action = "quarantined";
|
|
3461
|
+
reason = "missing-project-scope";
|
|
3462
|
+
}
|
|
3463
|
+
if (action === "skipped" || !reason) {
|
|
3464
|
+
result.skipped++;
|
|
3465
|
+
continue;
|
|
3466
|
+
}
|
|
3467
|
+
if (action === "repaired") {
|
|
3468
|
+
const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
|
|
3469
|
+
const project = isRecord(scope.project) ? { ...scope.project } : {};
|
|
3470
|
+
project.hash = projectHash;
|
|
3471
|
+
scope.project = project;
|
|
3472
|
+
metadata.scope = scope;
|
|
3473
|
+
metadata.repair = {
|
|
3474
|
+
...isRecord(metadata.repair) ? metadata.repair : {},
|
|
3475
|
+
legacyProjectScope: {
|
|
3476
|
+
action,
|
|
3477
|
+
reason,
|
|
3478
|
+
repairedAt: nowIso
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
addMetadataTag(metadata, `proj:${projectHash}`);
|
|
3482
|
+
result.repaired++;
|
|
3483
|
+
} else {
|
|
3484
|
+
metadata.quarantine = {
|
|
3485
|
+
...isRecord(metadata.quarantine) ? metadata.quarantine : {},
|
|
3486
|
+
status: "active",
|
|
3487
|
+
category: "project-scope",
|
|
3488
|
+
reason,
|
|
3489
|
+
detectedAt: nowIso,
|
|
3490
|
+
expectedProjectHash: projectHash,
|
|
3491
|
+
...observedProjectHash ? { observedProjectHash } : {}
|
|
3492
|
+
};
|
|
3493
|
+
metadata.repair = {
|
|
3494
|
+
...isRecord(metadata.repair) ? metadata.repair : {},
|
|
3495
|
+
legacyProjectScope: {
|
|
3496
|
+
action,
|
|
3497
|
+
reason,
|
|
3498
|
+
repairedAt: nowIso
|
|
3499
|
+
}
|
|
3500
|
+
};
|
|
3501
|
+
addMetadataTag(metadata, "quarantine:project-scope");
|
|
3502
|
+
result.quarantined++;
|
|
3503
|
+
}
|
|
3504
|
+
sample({ eventId: row.id, action, reason });
|
|
3505
|
+
if (!dryRun) {
|
|
3506
|
+
sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
return result;
|
|
3510
|
+
}
|
|
3152
3511
|
/**
|
|
3153
3512
|
* Get embedding/vector outbox health statistics
|
|
3154
3513
|
*/
|
|
@@ -3196,7 +3555,11 @@ var SQLiteEventStore = class {
|
|
|
3196
3555
|
await this.initialize();
|
|
3197
3556
|
const rows = sqliteAll(
|
|
3198
3557
|
this.db,
|
|
3199
|
-
`SELECT level, COUNT(*) as count
|
|
3558
|
+
`SELECT ml.level, COUNT(*) as count
|
|
3559
|
+
FROM memory_levels ml
|
|
3560
|
+
INNER JOIN events e ON e.id = ml.event_id
|
|
3561
|
+
WHERE ${notActiveQuarantinedSql("e.metadata")}
|
|
3562
|
+
GROUP BY ml.level`
|
|
3200
3563
|
);
|
|
3201
3564
|
return rows;
|
|
3202
3565
|
}
|
|
@@ -3212,6 +3575,7 @@ var SQLiteEventStore = class {
|
|
|
3212
3575
|
`SELECT e.* FROM events e
|
|
3213
3576
|
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
3214
3577
|
WHERE ml.level = ?
|
|
3578
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3215
3579
|
ORDER BY e.timestamp DESC
|
|
3216
3580
|
LIMIT ? OFFSET ?`,
|
|
3217
3581
|
[level, limit, offset]
|
|
@@ -3304,12 +3668,13 @@ var SQLiteEventStore = class {
|
|
|
3304
3668
|
/**
|
|
3305
3669
|
* Get most accessed memories (falls back to recent events if none accessed)
|
|
3306
3670
|
*/
|
|
3307
|
-
async getMostAccessed(limit = 10) {
|
|
3671
|
+
async getMostAccessed(limit = 10, options) {
|
|
3308
3672
|
await this.initialize();
|
|
3309
3673
|
let rows = sqliteAll(
|
|
3310
3674
|
this.db,
|
|
3311
3675
|
`SELECT * FROM events
|
|
3312
3676
|
WHERE access_count > 0
|
|
3677
|
+
AND ${maybeQuarantinePredicate(options)}
|
|
3313
3678
|
ORDER BY access_count DESC, last_accessed_at DESC
|
|
3314
3679
|
LIMIT ?`,
|
|
3315
3680
|
[limit]
|
|
@@ -3318,6 +3683,7 @@ var SQLiteEventStore = class {
|
|
|
3318
3683
|
rows = sqliteAll(
|
|
3319
3684
|
this.db,
|
|
3320
3685
|
`SELECT * FROM events
|
|
3686
|
+
WHERE ${maybeQuarantinePredicate(options)}
|
|
3321
3687
|
ORDER BY timestamp DESC
|
|
3322
3688
|
LIMIT ?`,
|
|
3323
3689
|
[limit]
|
|
@@ -3448,6 +3814,7 @@ var SQLiteEventStore = class {
|
|
|
3448
3814
|
FROM memory_helpfulness mh
|
|
3449
3815
|
JOIN events e ON e.id = mh.event_id
|
|
3450
3816
|
WHERE mh.measured_at IS NOT NULL
|
|
3817
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3451
3818
|
GROUP BY mh.event_id
|
|
3452
3819
|
ORDER BY avg_score DESC
|
|
3453
3820
|
LIMIT ?`,
|
|
@@ -3512,6 +3879,7 @@ var SQLiteEventStore = class {
|
|
|
3512
3879
|
FROM events_fts fts
|
|
3513
3880
|
JOIN events e ON e.id = fts.event_id
|
|
3514
3881
|
WHERE events_fts MATCH ?
|
|
3882
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3515
3883
|
ORDER BY fts.rank
|
|
3516
3884
|
LIMIT ?`,
|
|
3517
3885
|
[searchTerms, limit]
|
|
@@ -3526,6 +3894,7 @@ var SQLiteEventStore = class {
|
|
|
3526
3894
|
this.db,
|
|
3527
3895
|
`SELECT *, 0 as rank FROM events
|
|
3528
3896
|
WHERE content LIKE ?
|
|
3897
|
+
AND ${notActiveQuarantinedSql()}
|
|
3529
3898
|
ORDER BY timestamp DESC
|
|
3530
3899
|
LIMIT ?`,
|
|
3531
3900
|
[likePattern, limit]
|
|
@@ -3732,6 +4101,7 @@ var SQLiteEventStore = class {
|
|
|
3732
4101
|
`SELECT turn_id, MIN(timestamp) as min_ts
|
|
3733
4102
|
FROM events
|
|
3734
4103
|
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4104
|
+
AND ${maybeQuarantinePredicate(options)}
|
|
3735
4105
|
GROUP BY turn_id
|
|
3736
4106
|
ORDER BY min_ts DESC
|
|
3737
4107
|
LIMIT ? OFFSET ?`,
|
|
@@ -3739,7 +4109,7 @@ var SQLiteEventStore = class {
|
|
|
3739
4109
|
);
|
|
3740
4110
|
const turns = [];
|
|
3741
4111
|
for (const turnRow of turnRows) {
|
|
3742
|
-
const events = await this.getEventsByTurn(turnRow.turn_id);
|
|
4112
|
+
const events = await this.getEventsByTurn(turnRow.turn_id, options);
|
|
3743
4113
|
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
3744
4114
|
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
3745
4115
|
const hasResponse = events.some((e) => e.eventType === "agent_response");
|
|
@@ -3758,11 +4128,11 @@ var SQLiteEventStore = class {
|
|
|
3758
4128
|
/**
|
|
3759
4129
|
* Get all events for a specific turn_id
|
|
3760
4130
|
*/
|
|
3761
|
-
async getEventsByTurn(turnId) {
|
|
4131
|
+
async getEventsByTurn(turnId, options) {
|
|
3762
4132
|
await this.initialize();
|
|
3763
4133
|
const rows = sqliteAll(
|
|
3764
4134
|
this.db,
|
|
3765
|
-
`SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
|
|
4135
|
+
`SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3766
4136
|
[turnId]
|
|
3767
4137
|
);
|
|
3768
4138
|
return rows.map(this.rowToEvent);
|
|
@@ -3770,13 +4140,14 @@ var SQLiteEventStore = class {
|
|
|
3770
4140
|
/**
|
|
3771
4141
|
* Count total turns for a session
|
|
3772
4142
|
*/
|
|
3773
|
-
async countSessionTurns(sessionId) {
|
|
4143
|
+
async countSessionTurns(sessionId, options) {
|
|
3774
4144
|
await this.initialize();
|
|
3775
4145
|
const row = sqliteGet(
|
|
3776
4146
|
this.db,
|
|
3777
4147
|
`SELECT COUNT(DISTINCT turn_id) as count
|
|
3778
4148
|
FROM events
|
|
3779
|
-
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4149
|
+
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
4150
|
+
AND ${maybeQuarantinePredicate(options)}`,
|
|
3780
4151
|
[sessionId]
|
|
3781
4152
|
);
|
|
3782
4153
|
return row?.count || 0;
|
|
@@ -3868,7 +4239,7 @@ var SQLiteEventStore = class {
|
|
|
3868
4239
|
content: row.content,
|
|
3869
4240
|
canonicalKey: row.canonical_key,
|
|
3870
4241
|
dedupeKey: row.dedupe_key,
|
|
3871
|
-
metadata:
|
|
4242
|
+
metadata: safeParseMetadataValue(row.metadata)
|
|
3872
4243
|
};
|
|
3873
4244
|
if (row.access_count !== void 0) {
|
|
3874
4245
|
event.access_count = row.access_count;
|
|
@@ -4020,6 +4391,7 @@ var VectorStore = class {
|
|
|
4020
4391
|
* Get total count of vectors
|
|
4021
4392
|
*/
|
|
4022
4393
|
async count() {
|
|
4394
|
+
await this.initialize();
|
|
4023
4395
|
if (!this.table)
|
|
4024
4396
|
return 0;
|
|
4025
4397
|
const result = await this.table.countRows();
|
|
@@ -4458,6 +4830,14 @@ var MemoryQueryService = class {
|
|
|
4458
4830
|
await this.initialize();
|
|
4459
4831
|
return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
|
|
4460
4832
|
}
|
|
4833
|
+
async recoverStuckOutboxItems(options) {
|
|
4834
|
+
await this.initialize();
|
|
4835
|
+
return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
|
|
4836
|
+
}
|
|
4837
|
+
async repairLegacyProjectScope(options) {
|
|
4838
|
+
await this.initialize();
|
|
4839
|
+
return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
|
|
4840
|
+
}
|
|
4461
4841
|
async getStats() {
|
|
4462
4842
|
await this.initialize();
|
|
4463
4843
|
const deps = this.getStatsDeps();
|
|
@@ -6080,18 +6460,18 @@ function assertDefaultRetrieverStore(eventStore) {
|
|
|
6080
6460
|
function createMemoryEngineServices(options) {
|
|
6081
6461
|
const factories = options.factories ?? {};
|
|
6082
6462
|
const storagePath = options.storagePath;
|
|
6083
|
-
if (!options.readOnly && !
|
|
6084
|
-
|
|
6463
|
+
if (!options.readOnly && !fs6.existsSync(storagePath)) {
|
|
6464
|
+
fs6.mkdirSync(storagePath, { recursive: true });
|
|
6085
6465
|
}
|
|
6086
6466
|
const sqliteStore = (factories.createSQLiteEventStore ?? defaultCreateSQLiteEventStore)(
|
|
6087
|
-
|
|
6467
|
+
path5.join(storagePath, "events.sqlite"),
|
|
6088
6468
|
{
|
|
6089
6469
|
readonly: options.readOnly,
|
|
6090
6470
|
markdownMirrorRoot: storagePath
|
|
6091
6471
|
}
|
|
6092
6472
|
);
|
|
6093
6473
|
const vectorStore = (factories.createVectorStore ?? defaultCreateVectorStore)(
|
|
6094
|
-
|
|
6474
|
+
path5.join(storagePath, "vectors")
|
|
6095
6475
|
);
|
|
6096
6476
|
const embeddingModel = options.embeddingModel || process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
|
|
6097
6477
|
const embedder = embeddingModel ? (factories.createEmbedder ?? defaultCreateEmbedder)(embeddingModel) : (factories.getDefaultEmbedder ?? getDefaultEmbedder)();
|
|
@@ -6502,8 +6882,8 @@ function createMemoryRuntimeService(deps) {
|
|
|
6502
6882
|
}
|
|
6503
6883
|
|
|
6504
6884
|
// src/extensions/shared-memory/shared-memory-services.ts
|
|
6505
|
-
import * as
|
|
6506
|
-
import * as
|
|
6885
|
+
import * as fs7 from "fs";
|
|
6886
|
+
import * as path6 from "path";
|
|
6507
6887
|
|
|
6508
6888
|
// src/core/shared-event-store.ts
|
|
6509
6889
|
var SharedEventStore = class {
|
|
@@ -7191,7 +7571,7 @@ var SharedMemoryServices = class {
|
|
|
7191
7571
|
this.ensureDirectory(sharedPath, { allowCreate: true });
|
|
7192
7572
|
const store = await this.openStore(sharedPath);
|
|
7193
7573
|
this.sharedVectorStore = this.factories.createSharedVectorStore(
|
|
7194
|
-
|
|
7574
|
+
path6.join(sharedPath, "vectors")
|
|
7195
7575
|
);
|
|
7196
7576
|
await this.sharedVectorStore.initialize();
|
|
7197
7577
|
this.sharedPromoter = this.factories.createSharedPromoter(
|
|
@@ -7264,7 +7644,7 @@ var SharedMemoryServices = class {
|
|
|
7264
7644
|
async createOpenStorePromise(sharedPath) {
|
|
7265
7645
|
if (!this.sharedEventStore) {
|
|
7266
7646
|
const sharedEventStore = this.factories.createSharedEventStore(
|
|
7267
|
-
|
|
7647
|
+
path6.join(sharedPath, "shared.duckdb")
|
|
7268
7648
|
);
|
|
7269
7649
|
await sharedEventStore.initialize();
|
|
7270
7650
|
this.sharedEventStore = sharedEventStore;
|
|
@@ -7284,9 +7664,9 @@ var SharedMemoryServices = class {
|
|
|
7284
7664
|
}
|
|
7285
7665
|
get factories() {
|
|
7286
7666
|
return {
|
|
7287
|
-
existsSync: this.options.factories?.existsSync ??
|
|
7667
|
+
existsSync: this.options.factories?.existsSync ?? fs7.existsSync,
|
|
7288
7668
|
mkdirSync: this.options.factories?.mkdirSync ?? ((targetPath) => {
|
|
7289
|
-
|
|
7669
|
+
fs7.mkdirSync(targetPath, { recursive: true });
|
|
7290
7670
|
}),
|
|
7291
7671
|
createSharedEventStore: this.options.factories?.createSharedEventStore ?? createSharedEventStore,
|
|
7292
7672
|
createSharedStore: this.options.factories?.createSharedStore ?? createSharedStore,
|
|
@@ -7401,37 +7781,11 @@ function createMemoryServiceComposition(options) {
|
|
|
7401
7781
|
}
|
|
7402
7782
|
function defaultExpandPath(targetPath) {
|
|
7403
7783
|
if (targetPath.startsWith("~")) {
|
|
7404
|
-
return
|
|
7784
|
+
return path7.join(os2.homedir(), targetPath.slice(1));
|
|
7405
7785
|
}
|
|
7406
7786
|
return targetPath;
|
|
7407
7787
|
}
|
|
7408
7788
|
|
|
7409
|
-
// src/core/registry/project-path.ts
|
|
7410
|
-
import * as crypto2 from "crypto";
|
|
7411
|
-
import * as fs7 from "fs";
|
|
7412
|
-
import * as os2 from "os";
|
|
7413
|
-
import * as path7 from "path";
|
|
7414
|
-
function normalizeProjectPath(projectPath) {
|
|
7415
|
-
const expanded = projectPath.startsWith("~") ? path7.join(os2.homedir(), projectPath.slice(1)) : projectPath;
|
|
7416
|
-
try {
|
|
7417
|
-
return fs7.realpathSync(expanded);
|
|
7418
|
-
} catch {
|
|
7419
|
-
return path7.resolve(expanded);
|
|
7420
|
-
}
|
|
7421
|
-
}
|
|
7422
|
-
function hashProjectPath(projectPath) {
|
|
7423
|
-
const normalizedPath = normalizeProjectPath(projectPath);
|
|
7424
|
-
return crypto2.createHash("sha256").update(normalizedPath).digest("hex").slice(0, 8);
|
|
7425
|
-
}
|
|
7426
|
-
function getProjectStoragePath(projectPath) {
|
|
7427
|
-
const hash = hashProjectPath(projectPath);
|
|
7428
|
-
return path7.join(os2.homedir(), ".claude-code", "memory", "projects", hash);
|
|
7429
|
-
}
|
|
7430
|
-
function resolveProjectStoragePath(projectOrHash) {
|
|
7431
|
-
const isHash = /^[a-f0-9]{8}$/.test(projectOrHash);
|
|
7432
|
-
return isHash ? path7.join(os2.homedir(), ".claude-code", "memory", "projects", projectOrHash) : getProjectStoragePath(projectOrHash);
|
|
7433
|
-
}
|
|
7434
|
-
|
|
7435
7789
|
// src/core/registry/session-registry.ts
|
|
7436
7790
|
import * as fs8 from "fs";
|
|
7437
7791
|
import * as os3 from "os";
|
|
@@ -7755,6 +8109,12 @@ var MemoryService = class {
|
|
|
7755
8109
|
async getOutboxStats() {
|
|
7756
8110
|
return this.queryService.getOutboxStats();
|
|
7757
8111
|
}
|
|
8112
|
+
async recoverStuckOutboxItems(options) {
|
|
8113
|
+
return this.queryService.recoverStuckOutboxItems(options);
|
|
8114
|
+
}
|
|
8115
|
+
async repairLegacyProjectScope(options) {
|
|
8116
|
+
return this.queryService.repairLegacyProjectScope(options);
|
|
8117
|
+
}
|
|
7758
8118
|
async getRetrievalTraceStats() {
|
|
7759
8119
|
return this.retrievalAnalyticsService.getRetrievalTraceStats();
|
|
7760
8120
|
}
|
|
@@ -8484,7 +8844,7 @@ import * as os7 from "os";
|
|
|
8484
8844
|
import * as readline2 from "readline";
|
|
8485
8845
|
import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
|
|
8486
8846
|
var CODEX_VALIDATION_DEFAULT_MAX_CONTENT_CHARS = 1e4;
|
|
8487
|
-
function
|
|
8847
|
+
function isRecord2(value) {
|
|
8488
8848
|
return typeof value === "object" && value !== null;
|
|
8489
8849
|
}
|
|
8490
8850
|
function normalizeMaybeRealpath(p) {
|
|
@@ -8501,7 +8861,7 @@ function extractCodexContentText(content, maxContentChars = CODEX_VALIDATION_DEF
|
|
|
8501
8861
|
texts.push(content);
|
|
8502
8862
|
} else if (Array.isArray(content)) {
|
|
8503
8863
|
for (const block of content) {
|
|
8504
|
-
if (!
|
|
8864
|
+
if (!isRecord2(block))
|
|
8505
8865
|
continue;
|
|
8506
8866
|
const b = block;
|
|
8507
8867
|
const t = typeof b.type === "string" ? b.type : "";
|
|
@@ -8589,7 +8949,7 @@ async function readCodexSessionMeta(filePath) {
|
|
|
8589
8949
|
const obj = JSON.parse(line);
|
|
8590
8950
|
if (obj.type !== "session_meta")
|
|
8591
8951
|
continue;
|
|
8592
|
-
if (!
|
|
8952
|
+
if (!isRecord2(obj.payload))
|
|
8593
8953
|
break;
|
|
8594
8954
|
const payload = obj.payload;
|
|
8595
8955
|
const sessionId = typeof payload.id === "string" ? payload.id : null;
|
|
@@ -8727,7 +9087,7 @@ async function normalizeCodexSessionFile(filePath, options = {}) {
|
|
|
8727
9087
|
}
|
|
8728
9088
|
if (entry.type === "session_meta")
|
|
8729
9089
|
continue;
|
|
8730
|
-
if (entry.type !== "response_item" || !
|
|
9090
|
+
if (entry.type !== "response_item" || !isRecord2(entry.payload)) {
|
|
8731
9091
|
summary.skippedUnsupportedRecords += 1;
|
|
8732
9092
|
continue;
|
|
8733
9093
|
}
|
|
@@ -8882,7 +9242,7 @@ var CodexSessionHistoryImporter = class {
|
|
|
8882
9242
|
const obj = JSON.parse(line);
|
|
8883
9243
|
if (obj.type !== "session_meta")
|
|
8884
9244
|
continue;
|
|
8885
|
-
if (!
|
|
9245
|
+
if (!isRecord2(obj.payload))
|
|
8886
9246
|
break;
|
|
8887
9247
|
const payload = obj.payload;
|
|
8888
9248
|
const sessionId = typeof payload.id === "string" ? payload.id : null;
|
|
@@ -9098,7 +9458,7 @@ var CodexSessionHistoryImporter = class {
|
|
|
9098
9458
|
try {
|
|
9099
9459
|
const entry = JSON.parse(line);
|
|
9100
9460
|
result.totalMessages++;
|
|
9101
|
-
if (entry.type === "response_item" &&
|
|
9461
|
+
if (entry.type === "response_item" && isRecord2(entry.payload)) {
|
|
9102
9462
|
const payload = entry.payload;
|
|
9103
9463
|
if (payload.type !== "message")
|
|
9104
9464
|
continue;
|
|
@@ -9290,10 +9650,10 @@ var SENSITIVE_PATTERNS = [
|
|
|
9290
9650
|
// Redact the whole URI so usernames, credentials, hosts, paths, and query
|
|
9291
9651
|
// params do not leak either.
|
|
9292
9652
|
/\b[a-z][a-z0-9+.-]*:\/\/[^\s'"`<>/@]+@[^\s'"`<>]+/gi,
|
|
9293
|
-
/password\s*[:=]\s*['"]?[^\s'"]+/gi,
|
|
9294
|
-
/api[_
|
|
9295
|
-
/secret\s*[:=]\s*['"]?[^\s'"]+/gi,
|
|
9296
|
-
/token\s*[:=]\s*['"]?[^\s'"]+/gi,
|
|
9653
|
+
/(?:[\w.-]+[-_])?password\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
|
|
9654
|
+
/(?:[\w.-]+[-_])?api[-_]?key\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
|
|
9655
|
+
/(?:[\w.-]+[-_])?secret\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
|
|
9656
|
+
/(?:[\w.-]+[-_])?token\s*[:=]\s*(?!\[REDACTED\])['"]?[^\s'"]+/gi,
|
|
9297
9657
|
/bearer\s+[a-zA-Z0-9\-_.]+/gi,
|
|
9298
9658
|
/AWS[_-]?ACCESS[_-]?KEY[_-]?ID\s*[:=]\s*['"]?[A-Z0-9]+/gi,
|
|
9299
9659
|
/AWS[_-]?SECRET[_-]?ACCESS[_-]?KEY\s*[:=]\s*['"]?[^\s'"]+/gi,
|
|
@@ -9303,6 +9663,35 @@ var SENSITIVE_PATTERNS = [
|
|
|
9303
9663
|
/sk-[a-zA-Z0-9]{48}/g
|
|
9304
9664
|
// OpenAI API Key
|
|
9305
9665
|
];
|
|
9666
|
+
var CLI_SECRET_OPTION_PATTERNS = [
|
|
9667
|
+
/(--(?:[a-z0-9]+[-_])*(?:password|secret|api[-_]?key|token|bearer)(?:[-_][a-z0-9]+)*(?:\s+|=))(?:"[^"]*"|'[^']*'|[^\s'"`<>]+)/gi
|
|
9668
|
+
];
|
|
9669
|
+
var URL_FOLLOWING_SECRET_PATTERN = /((?:https?:\/\/[^\s'"`<>]+)\s*\r?\n\s*)([A-Za-z0-9!@#$%^&*._+\-~:=/]{6,})(?=\s*(?:\r?\n|$))/gi;
|
|
9670
|
+
function looksLikePastedSecret(value) {
|
|
9671
|
+
return value.length >= 8 && /(?:\d|[^A-Za-z0-9])/.test(value);
|
|
9672
|
+
}
|
|
9673
|
+
function maskUrlFollowingSecret(value) {
|
|
9674
|
+
let count = 0;
|
|
9675
|
+
const content = value.replace(URL_FOLLOWING_SECRET_PATTERN, (_match, prefix, secret) => {
|
|
9676
|
+
if (!looksLikePastedSecret(secret))
|
|
9677
|
+
return `${prefix}${secret}`;
|
|
9678
|
+
count++;
|
|
9679
|
+
return `${prefix}[REDACTED]`;
|
|
9680
|
+
});
|
|
9681
|
+
return { content, count };
|
|
9682
|
+
}
|
|
9683
|
+
function maskCliSecretOptions(value) {
|
|
9684
|
+
let count = 0;
|
|
9685
|
+
let filtered = value;
|
|
9686
|
+
for (const pattern of CLI_SECRET_OPTION_PATTERNS) {
|
|
9687
|
+
pattern.lastIndex = 0;
|
|
9688
|
+
filtered = filtered.replace(pattern, (_match, prefix) => {
|
|
9689
|
+
count++;
|
|
9690
|
+
return `${prefix}[REDACTED]`;
|
|
9691
|
+
});
|
|
9692
|
+
}
|
|
9693
|
+
return { content: filtered, count };
|
|
9694
|
+
}
|
|
9306
9695
|
function applyPrivacyFilter(content, config) {
|
|
9307
9696
|
let filtered = content;
|
|
9308
9697
|
let privateTagCount = 0;
|
|
@@ -9316,6 +9705,12 @@ function applyPrivacyFilter(content, config) {
|
|
|
9316
9705
|
filtered = tagResult.filtered;
|
|
9317
9706
|
privateTagCount = tagResult.stats.count;
|
|
9318
9707
|
}
|
|
9708
|
+
const cliResult = maskCliSecretOptions(filtered);
|
|
9709
|
+
filtered = cliResult.content;
|
|
9710
|
+
patternMatchCount += cliResult.count;
|
|
9711
|
+
const urlSecretResult = maskUrlFollowingSecret(filtered);
|
|
9712
|
+
filtered = urlSecretResult.content;
|
|
9713
|
+
patternMatchCount += urlSecretResult.count;
|
|
9319
9714
|
for (const pattern of SENSITIVE_PATTERNS) {
|
|
9320
9715
|
pattern.lastIndex = 0;
|
|
9321
9716
|
const matches = filtered.match(pattern);
|
|
@@ -9327,13 +9722,13 @@ function applyPrivacyFilter(content, config) {
|
|
|
9327
9722
|
for (const patternStr of config.excludePatterns || []) {
|
|
9328
9723
|
try {
|
|
9329
9724
|
const regex = new RegExp(
|
|
9330
|
-
`(${patternStr})\\s*[:=]\\s*['"]?[^\\s'"]+`,
|
|
9725
|
+
`(^|[^\\w-])(${patternStr})\\s*[:=]\\s*['"]?[^\\s'"]+`,
|
|
9331
9726
|
"gi"
|
|
9332
9727
|
);
|
|
9333
9728
|
const matches = filtered.match(regex);
|
|
9334
9729
|
if (matches) {
|
|
9335
9730
|
patternMatchCount += matches.length;
|
|
9336
|
-
filtered = filtered.replace(regex, "[REDACTED]");
|
|
9731
|
+
filtered = filtered.replace(regex, "$1[REDACTED]");
|
|
9337
9732
|
}
|
|
9338
9733
|
} catch {
|
|
9339
9734
|
}
|
|
@@ -10345,6 +10740,26 @@ function getServiceFromQuery(c) {
|
|
|
10345
10740
|
}
|
|
10346
10741
|
return getReadOnlyMemoryService();
|
|
10347
10742
|
}
|
|
10743
|
+
function getWritableServiceFromQuery(c) {
|
|
10744
|
+
const project = c.req.query("project") || c.req.query("projectId");
|
|
10745
|
+
if (project) {
|
|
10746
|
+
const storagePath = resolveProjectStoragePath(project);
|
|
10747
|
+
return new MemoryService({
|
|
10748
|
+
storagePath,
|
|
10749
|
+
readOnly: false,
|
|
10750
|
+
lightweightMode: true,
|
|
10751
|
+
analyticsEnabled: false,
|
|
10752
|
+
sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
|
|
10753
|
+
});
|
|
10754
|
+
}
|
|
10755
|
+
return new MemoryService({
|
|
10756
|
+
storagePath: "~/.claude-code/memory",
|
|
10757
|
+
readOnly: false,
|
|
10758
|
+
lightweightMode: true,
|
|
10759
|
+
analyticsEnabled: false,
|
|
10760
|
+
sharedStoreConfig: DISABLED_SHARED_STORE_CONFIG
|
|
10761
|
+
});
|
|
10762
|
+
}
|
|
10348
10763
|
function getLightweightServiceFromQuery(c) {
|
|
10349
10764
|
const project = c.req.query("project") || c.req.query("projectId");
|
|
10350
10765
|
if (project) {
|
|
@@ -12000,6 +12415,13 @@ import { Hono as Hono8 } from "hono";
|
|
|
12000
12415
|
import { streamSSE } from "hono/streaming";
|
|
12001
12416
|
import { spawn } from "child_process";
|
|
12002
12417
|
var chatRouter = new Hono8();
|
|
12418
|
+
var ProviderFailure = class extends Error {
|
|
12419
|
+
constructor(code, message) {
|
|
12420
|
+
super(message);
|
|
12421
|
+
this.code = code;
|
|
12422
|
+
this.name = "ProviderFailure";
|
|
12423
|
+
}
|
|
12424
|
+
};
|
|
12003
12425
|
var CLAUDE_TIMEOUT_MS = 12e4;
|
|
12004
12426
|
chatRouter.post("/", async (c) => {
|
|
12005
12427
|
let body;
|
|
@@ -12011,43 +12433,12 @@ chatRouter.post("/", async (c) => {
|
|
|
12011
12433
|
if (!body.message?.trim()) {
|
|
12012
12434
|
return c.json({ error: "Message is required" }, 400);
|
|
12013
12435
|
}
|
|
12014
|
-
const
|
|
12436
|
+
const memoryOnly = body.mode === "memory-only" || body.memoryOnly === true;
|
|
12437
|
+
const memoryService = memoryOnly ? getLightweightServiceFromQuery(c) : getServiceFromQuery(c);
|
|
12015
12438
|
try {
|
|
12016
12439
|
await memoryService.initialize();
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
try {
|
|
12020
|
-
const result = await memoryService.retrieveMemories(body.message, {
|
|
12021
|
-
topK: 8,
|
|
12022
|
-
minScore: 0.5
|
|
12023
|
-
});
|
|
12024
|
-
if (result.memories.length > 0) {
|
|
12025
|
-
const parts = ["## Relevant Memories\n"];
|
|
12026
|
-
for (const m of result.memories) {
|
|
12027
|
-
const date = new Date(m.event.timestamp).toISOString().split("T")[0];
|
|
12028
|
-
const content = m.event.content.slice(0, 500);
|
|
12029
|
-
parts.push(`### [${m.event.eventType}] ${date} (score: ${m.score.toFixed(2)})`);
|
|
12030
|
-
parts.push(content);
|
|
12031
|
-
if (m.sessionContext) {
|
|
12032
|
-
parts.push(`_Context: ${m.sessionContext}_`);
|
|
12033
|
-
}
|
|
12034
|
-
parts.push("");
|
|
12035
|
-
}
|
|
12036
|
-
memoryContext = parts.join("\n");
|
|
12037
|
-
}
|
|
12038
|
-
} catch {
|
|
12039
|
-
}
|
|
12040
|
-
try {
|
|
12041
|
-
const stats = await memoryService.getStats();
|
|
12042
|
-
const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
|
|
12043
|
-
statsContext = [
|
|
12044
|
-
"## Memory Stats",
|
|
12045
|
-
`- Total events: ${stats.totalEvents}`,
|
|
12046
|
-
`- Vector nodes: ${stats.vectorCount}`,
|
|
12047
|
-
`- By level: ${levels}`
|
|
12048
|
-
].join("\n");
|
|
12049
|
-
} catch {
|
|
12050
|
-
}
|
|
12440
|
+
const { memoryContext, memoryHits } = await collectMemoryContext(memoryService, body.message);
|
|
12441
|
+
const statsContext = await collectStatsContext(memoryService);
|
|
12051
12442
|
const fullPrompt = buildPrompt(
|
|
12052
12443
|
statsContext,
|
|
12053
12444
|
memoryContext,
|
|
@@ -12055,13 +12446,23 @@ chatRouter.post("/", async (c) => {
|
|
|
12055
12446
|
body.message
|
|
12056
12447
|
);
|
|
12057
12448
|
return streamSSE(c, async (stream) => {
|
|
12449
|
+
if (memoryOnly) {
|
|
12450
|
+
await streamMemoryOnlyResponse(stream, {
|
|
12451
|
+
memoryContext,
|
|
12452
|
+
memoryHits,
|
|
12453
|
+
reason: "memory-only-mode"
|
|
12454
|
+
});
|
|
12455
|
+
return;
|
|
12456
|
+
}
|
|
12058
12457
|
try {
|
|
12059
12458
|
await streamClaudeResponse(fullPrompt, stream);
|
|
12060
12459
|
} catch (err) {
|
|
12460
|
+
const diagnostic = providerDiagnostic(err);
|
|
12061
12461
|
await stream.writeSSE({
|
|
12062
|
-
event: "
|
|
12063
|
-
data: JSON.stringify(
|
|
12462
|
+
event: "provider_error",
|
|
12463
|
+
data: JSON.stringify(diagnostic)
|
|
12064
12464
|
});
|
|
12465
|
+
await streamMemoryOnlyFallback(stream, memoryContext, memoryHits);
|
|
12065
12466
|
}
|
|
12066
12467
|
});
|
|
12067
12468
|
} catch (error) {
|
|
@@ -12070,6 +12471,115 @@ chatRouter.post("/", async (c) => {
|
|
|
12070
12471
|
await memoryService.shutdown();
|
|
12071
12472
|
}
|
|
12072
12473
|
});
|
|
12474
|
+
async function collectMemoryContext(memoryService, query) {
|
|
12475
|
+
let memoryHits = [];
|
|
12476
|
+
try {
|
|
12477
|
+
const result = await memoryService.retrieveMemories?.(query, {
|
|
12478
|
+
topK: 8,
|
|
12479
|
+
minScore: 0.5
|
|
12480
|
+
});
|
|
12481
|
+
memoryHits = result?.memories ?? [];
|
|
12482
|
+
} catch {
|
|
12483
|
+
memoryHits = [];
|
|
12484
|
+
}
|
|
12485
|
+
if (memoryHits.length === 0) {
|
|
12486
|
+
try {
|
|
12487
|
+
memoryHits = await memoryService.keywordSearch?.(query, { topK: 8, minScore: 0.05 }) ?? [];
|
|
12488
|
+
} catch {
|
|
12489
|
+
memoryHits = [];
|
|
12490
|
+
}
|
|
12491
|
+
}
|
|
12492
|
+
return {
|
|
12493
|
+
memoryContext: formatMemoryContext(memoryHits),
|
|
12494
|
+
memoryHits
|
|
12495
|
+
};
|
|
12496
|
+
}
|
|
12497
|
+
async function collectStatsContext(memoryService) {
|
|
12498
|
+
try {
|
|
12499
|
+
const stats = await memoryService.getStats?.();
|
|
12500
|
+
if (!stats)
|
|
12501
|
+
return "";
|
|
12502
|
+
const levels = stats.levelStats.map((l) => `${l.level}: ${l.count}`).join(", ");
|
|
12503
|
+
return [
|
|
12504
|
+
"## Memory Stats",
|
|
12505
|
+
`- Total events: ${stats.totalEvents}`,
|
|
12506
|
+
`- Vector nodes: ${stats.vectorCount}`,
|
|
12507
|
+
`- By level: ${levels}`
|
|
12508
|
+
].join("\n");
|
|
12509
|
+
} catch {
|
|
12510
|
+
return "";
|
|
12511
|
+
}
|
|
12512
|
+
}
|
|
12513
|
+
function formatMemoryContext(memoryHits) {
|
|
12514
|
+
if (memoryHits.length === 0)
|
|
12515
|
+
return "";
|
|
12516
|
+
const parts = ["## Relevant Memories\n"];
|
|
12517
|
+
for (const m of memoryHits) {
|
|
12518
|
+
const date = m.event.timestamp ? new Date(m.event.timestamp).toISOString().split("T")[0] : "unknown-date";
|
|
12519
|
+
const content = (m.event.content ?? "").slice(0, 500);
|
|
12520
|
+
parts.push(`### [${m.event.eventType ?? "memory"}] ${date} (score: ${m.score.toFixed(2)})`);
|
|
12521
|
+
parts.push(content);
|
|
12522
|
+
if (m.sessionContext) {
|
|
12523
|
+
parts.push(`_Context: ${m.sessionContext}_`);
|
|
12524
|
+
}
|
|
12525
|
+
parts.push("");
|
|
12526
|
+
}
|
|
12527
|
+
return parts.join("\n");
|
|
12528
|
+
}
|
|
12529
|
+
async function streamMemoryOnlyResponse(stream, options) {
|
|
12530
|
+
await stream.writeSSE({
|
|
12531
|
+
event: "diagnostic",
|
|
12532
|
+
data: JSON.stringify({
|
|
12533
|
+
provider: "claude-cli",
|
|
12534
|
+
status: "skipped",
|
|
12535
|
+
mode: "memory-only",
|
|
12536
|
+
reason: options.reason,
|
|
12537
|
+
retrievedMemories: options.memoryHits.length
|
|
12538
|
+
})
|
|
12539
|
+
});
|
|
12540
|
+
await streamMemoryOnlyFallback(stream, options.memoryContext, options.memoryHits);
|
|
12541
|
+
}
|
|
12542
|
+
async function streamMemoryOnlyFallback(stream, memoryContext, memoryHits) {
|
|
12543
|
+
const content = memoryHits.length > 0 ? [
|
|
12544
|
+
"Provider unavailable or skipped; showing retrieved memory context directly.",
|
|
12545
|
+
"",
|
|
12546
|
+
memoryContext
|
|
12547
|
+
].join("\n") : "Provider unavailable or skipped, and no directly relevant memories were found for this query.";
|
|
12548
|
+
await stream.writeSSE({
|
|
12549
|
+
event: "message",
|
|
12550
|
+
data: JSON.stringify({ content, mode: "memory-only" })
|
|
12551
|
+
});
|
|
12552
|
+
await stream.writeSSE({ event: "done", data: "{}" });
|
|
12553
|
+
}
|
|
12554
|
+
function providerDiagnostic(err) {
|
|
12555
|
+
if (err instanceof ProviderFailure) {
|
|
12556
|
+
return {
|
|
12557
|
+
provider: "claude-cli",
|
|
12558
|
+
code: err.code,
|
|
12559
|
+
message: err.message,
|
|
12560
|
+
fallback: "memory-only"
|
|
12561
|
+
};
|
|
12562
|
+
}
|
|
12563
|
+
return {
|
|
12564
|
+
provider: "claude-cli",
|
|
12565
|
+
code: "claude-cli-error",
|
|
12566
|
+
message: err instanceof Error ? err.message : "Unknown Claude CLI failure",
|
|
12567
|
+
fallback: "memory-only"
|
|
12568
|
+
};
|
|
12569
|
+
}
|
|
12570
|
+
function classifyProviderFailure(message) {
|
|
12571
|
+
const normalized = message.toLowerCase();
|
|
12572
|
+
if (normalized.includes("401") || normalized.includes("unauthorized") || normalized.includes("auth")) {
|
|
12573
|
+
return new ProviderFailure("claude-cli-auth", "Claude CLI authentication failed; showing memory-only context.");
|
|
12574
|
+
}
|
|
12575
|
+
if (normalized.includes("not found") || normalized.includes("enoent")) {
|
|
12576
|
+
return new ProviderFailure("claude-cli-not-found", "Claude CLI was not found; showing memory-only context.");
|
|
12577
|
+
}
|
|
12578
|
+
if (normalized.includes("timed out")) {
|
|
12579
|
+
return new ProviderFailure("claude-cli-timeout", "Claude CLI timed out; showing memory-only context.");
|
|
12580
|
+
}
|
|
12581
|
+
return new ProviderFailure("claude-cli-error", "Claude CLI failed; showing memory-only context.");
|
|
12582
|
+
}
|
|
12073
12583
|
function buildPrompt(statsContext, memoryContext, history, currentMessage) {
|
|
12074
12584
|
const parts = [];
|
|
12075
12585
|
parts.push("You are a helpful assistant that answers questions about the user's code memory data.");
|
|
@@ -12112,12 +12622,13 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
12112
12622
|
});
|
|
12113
12623
|
const timeout = setTimeout(() => {
|
|
12114
12624
|
proc.kill("SIGTERM");
|
|
12115
|
-
reject(
|
|
12625
|
+
reject(classifyProviderFailure("timed out"));
|
|
12116
12626
|
}, CLAUDE_TIMEOUT_MS);
|
|
12117
12627
|
proc.stdin.write(prompt);
|
|
12118
12628
|
proc.stdin.end();
|
|
12119
12629
|
let buffer = "";
|
|
12120
12630
|
let lastSentText = "";
|
|
12631
|
+
let stderrText = "";
|
|
12121
12632
|
proc.stdout.on("data", async (chunk) => {
|
|
12122
12633
|
buffer += chunk.toString();
|
|
12123
12634
|
const lines = buffer.split("\n");
|
|
@@ -12146,6 +12657,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
12146
12657
|
}
|
|
12147
12658
|
});
|
|
12148
12659
|
proc.stderr.on("data", (chunk) => {
|
|
12660
|
+
stderrText += chunk.toString();
|
|
12149
12661
|
if (process.env.CLAUDE_MEMORY_DEBUG) {
|
|
12150
12662
|
console.error("[chat] claude stderr:", chunk.toString());
|
|
12151
12663
|
}
|
|
@@ -12153,7 +12665,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
12153
12665
|
proc.on("error", (err) => {
|
|
12154
12666
|
clearTimeout(timeout);
|
|
12155
12667
|
if (err.code === "ENOENT") {
|
|
12156
|
-
reject(
|
|
12668
|
+
reject(classifyProviderFailure("ENOENT not found"));
|
|
12157
12669
|
} else {
|
|
12158
12670
|
reject(err);
|
|
12159
12671
|
}
|
|
@@ -12170,7 +12682,7 @@ function streamClaudeResponse(prompt, stream) {
|
|
|
12170
12682
|
}
|
|
12171
12683
|
}
|
|
12172
12684
|
if (code !== 0 && code !== null) {
|
|
12173
|
-
reject(
|
|
12685
|
+
reject(classifyProviderFailure(stderrText || `Claude CLI exited with code ${code}`));
|
|
12174
12686
|
} else {
|
|
12175
12687
|
resolve8();
|
|
12176
12688
|
}
|
|
@@ -12219,6 +12731,49 @@ healthRouter.get("/", async (c) => {
|
|
|
12219
12731
|
await memoryService.shutdown();
|
|
12220
12732
|
}
|
|
12221
12733
|
});
|
|
12734
|
+
healthRouter.post("/recover", async (c) => {
|
|
12735
|
+
const memoryService = getWritableServiceFromQuery(c);
|
|
12736
|
+
try {
|
|
12737
|
+
await memoryService.initialize();
|
|
12738
|
+
const body = await c.req.json().catch(() => ({}));
|
|
12739
|
+
const options = {};
|
|
12740
|
+
if (typeof body.stuckThresholdMs === "number" && Number.isFinite(body.stuckThresholdMs)) {
|
|
12741
|
+
options.stuckThresholdMs = body.stuckThresholdMs;
|
|
12742
|
+
}
|
|
12743
|
+
if (typeof body.maxRetries === "number" && Number.isFinite(body.maxRetries)) {
|
|
12744
|
+
options.maxRetries = body.maxRetries;
|
|
12745
|
+
}
|
|
12746
|
+
const before = await memoryService.getOutboxStats();
|
|
12747
|
+
const recovered = await memoryService.recoverStuckOutboxItems(options);
|
|
12748
|
+
const [stats, outbox] = await Promise.all([
|
|
12749
|
+
memoryService.getStats(),
|
|
12750
|
+
memoryService.getOutboxStats()
|
|
12751
|
+
]);
|
|
12752
|
+
return c.json({
|
|
12753
|
+
status: "ok",
|
|
12754
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12755
|
+
recovered,
|
|
12756
|
+
before: {
|
|
12757
|
+
outbox: before
|
|
12758
|
+
},
|
|
12759
|
+
after: {
|
|
12760
|
+
storage: {
|
|
12761
|
+
totalEvents: stats.totalEvents,
|
|
12762
|
+
vectorCount: stats.vectorCount
|
|
12763
|
+
},
|
|
12764
|
+
outbox
|
|
12765
|
+
}
|
|
12766
|
+
});
|
|
12767
|
+
} catch (error) {
|
|
12768
|
+
return c.json({
|
|
12769
|
+
status: "error",
|
|
12770
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12771
|
+
error: error.message
|
|
12772
|
+
}, 500);
|
|
12773
|
+
} finally {
|
|
12774
|
+
await memoryService.shutdown();
|
|
12775
|
+
}
|
|
12776
|
+
});
|
|
12222
12777
|
|
|
12223
12778
|
// src/apps/server/api/index.ts
|
|
12224
12779
|
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);
|
|
@@ -13402,6 +13957,54 @@ function resolveDashboardCommandOptions(options) {
|
|
|
13402
13957
|
};
|
|
13403
13958
|
}
|
|
13404
13959
|
|
|
13960
|
+
// src/apps/cli/repair-command.ts
|
|
13961
|
+
function resolveLegacyProjectScopeRepairOptions(options) {
|
|
13962
|
+
if (options.project !== void 0 && options.project.trim().length === 0) {
|
|
13963
|
+
throw new Error("legacy project-scope repair --project must not be empty");
|
|
13964
|
+
}
|
|
13965
|
+
if (options.projectHash !== void 0 && options.projectHash.trim().length === 0) {
|
|
13966
|
+
throw new Error("legacy project-scope repair --project-hash must not be empty");
|
|
13967
|
+
}
|
|
13968
|
+
const projectPath = typeof options.project === "string" && options.project.length > 0 ? options.project : void 0;
|
|
13969
|
+
const projectHash = typeof options.projectHash === "string" && options.projectHash.length > 0 ? options.projectHash : void 0;
|
|
13970
|
+
if (!projectPath && !projectHash) {
|
|
13971
|
+
throw new Error("legacy project-scope repair requires --project or --project-hash");
|
|
13972
|
+
}
|
|
13973
|
+
if (projectHash && !/^[a-f0-9]{8}$/.test(projectHash)) {
|
|
13974
|
+
throw new Error("legacy project-scope repair --project-hash must be an 8-character lowercase hex hash");
|
|
13975
|
+
}
|
|
13976
|
+
if (projectPath && projectHash && hashProjectPath(projectPath) !== projectHash) {
|
|
13977
|
+
throw new Error("legacy project-scope repair --project and --project-hash refer to different project stores");
|
|
13978
|
+
}
|
|
13979
|
+
return {
|
|
13980
|
+
projectPath,
|
|
13981
|
+
projectHash,
|
|
13982
|
+
dryRun: options.apply !== true
|
|
13983
|
+
};
|
|
13984
|
+
}
|
|
13985
|
+
function formatLegacyProjectScopeRepairResult(result) {
|
|
13986
|
+
const lines = [
|
|
13987
|
+
"Legacy project-scope repair",
|
|
13988
|
+
`Mode: ${result.dryRun ? "dry-run" : "apply"}`,
|
|
13989
|
+
`Project: ${result.projectHash}`,
|
|
13990
|
+
`Scanned: ${result.scanned}`,
|
|
13991
|
+
`Already scoped: ${result.alreadyScoped}`,
|
|
13992
|
+
`Repaired: ${result.repaired}`,
|
|
13993
|
+
`Quarantined: ${result.quarantined}`,
|
|
13994
|
+
`Skipped: ${result.skipped}`
|
|
13995
|
+
];
|
|
13996
|
+
if (result.samples.length > 0) {
|
|
13997
|
+
lines.push("Samples:");
|
|
13998
|
+
for (const sample of result.samples) {
|
|
13999
|
+
lines.push(`- ${sample.eventId} ${sample.action} ${sample.reason}`);
|
|
14000
|
+
}
|
|
14001
|
+
}
|
|
14002
|
+
if (result.dryRun) {
|
|
14003
|
+
lines.push("Dry-run only. Re-run with --apply to mutate event metadata.");
|
|
14004
|
+
}
|
|
14005
|
+
return lines.join("\n");
|
|
14006
|
+
}
|
|
14007
|
+
|
|
13405
14008
|
// src/core/external-market-context.ts
|
|
13406
14009
|
var MAX_RENDERED_ITEMS = 8;
|
|
13407
14010
|
var MAX_FRED_SERIES = 10;
|
|
@@ -14012,7 +14615,7 @@ async function runMarketContextCommand(options) {
|
|
|
14012
14615
|
}
|
|
14013
14616
|
}
|
|
14014
14617
|
var program = new Command();
|
|
14015
|
-
program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.
|
|
14618
|
+
program.name("claude-memory-layer").description("Claude Code Memory Plugin CLI").version("1.0.38");
|
|
14016
14619
|
program.command("market-context").description("Fetch read-only DART/FRED/Finnhub context with structured MarketContextSnapshot bull/bear/risk/catalyst analysis").option("--company <name>", "Company name for DART fallback search and report subject").option("--dart-corp-code <code>", "Exact DART corp_code for issuer-specific filings").option("--symbol <ticker>", "Listed ticker for Finnhub company profile").option("--providers <list>", "Comma-separated providers: dart,fred,finnhub").option("--fred-series <list>", "Comma-separated FRED series IDs").option("--json", "Print structured JSON including analysis.marketSnapshot").option("--no-snapshot", "Disable MarketContextSnapshot and DART company snapshot analysis").action(async (options) => {
|
|
14017
14620
|
try {
|
|
14018
14621
|
await runMarketContextCommand(options);
|
|
@@ -14247,11 +14850,18 @@ program.command("forget [eventId]").description("Remove memories from storage").
|
|
|
14247
14850
|
process.exit(1);
|
|
14248
14851
|
}
|
|
14249
14852
|
});
|
|
14250
|
-
program.command("process").description("Process pending embeddings").option("-p, --project <path>", "Project path (defaults to cwd)").action(async (options) => {
|
|
14853
|
+
program.command("process").description("Process pending embeddings").option("-p, --project <path>", "Project path (defaults to cwd)").option("--no-recover-stuck", "Skip stale processing outbox recovery before processing").action(async (options) => {
|
|
14251
14854
|
const projectPath = options.project || process.cwd();
|
|
14252
14855
|
const service = getMemoryServiceForProject(projectPath);
|
|
14253
14856
|
try {
|
|
14254
14857
|
await service.initialize();
|
|
14858
|
+
if (options.recoverStuck !== false) {
|
|
14859
|
+
const recovered = await service.recoverStuckOutboxItems();
|
|
14860
|
+
const recoveredCount = recovered.embedding.recoveredProcessing + recovered.embedding.retriedFailed + recovered.vector.recoveredProcessing + recovered.vector.retriedFailed;
|
|
14861
|
+
if (recoveredCount > 0) {
|
|
14862
|
+
console.log(`\u267B\uFE0F Recovered stuck outbox work: embedding=${recovered.embedding.recoveredProcessing}/${recovered.embedding.retriedFailed}, vector=${recovered.vector.recoveredProcessing}/${recovered.vector.retriedFailed}`);
|
|
14863
|
+
}
|
|
14864
|
+
}
|
|
14255
14865
|
console.log("\u23F3 Processing pending embeddings...");
|
|
14256
14866
|
const count = await service.processPendingEmbeddings();
|
|
14257
14867
|
console.log(`\u2705 Processed ${count} embeddings`);
|
|
@@ -14261,6 +14871,46 @@ program.command("process").description("Process pending embeddings").option("-p,
|
|
|
14261
14871
|
process.exit(1);
|
|
14262
14872
|
}
|
|
14263
14873
|
});
|
|
14874
|
+
var repairCommand = program.command("repair").description("Repair or quarantine legacy memory metadata");
|
|
14875
|
+
repairCommand.command("legacy-project-scope").description("Dry-run or apply project-scope repair/quarantine for legacy imported events").option("-p, --project <path>", "Project path (defaults to cwd unless --project-hash is used)").option("--project-hash <hash>", "Project storage hash for hash-only repair flows").option("--apply", "Apply metadata changes (default is dry-run)").action(async (options) => {
|
|
14876
|
+
try {
|
|
14877
|
+
const projectPath = options.project !== void 0 ? options.project : !options.projectHash ? process.cwd() : void 0;
|
|
14878
|
+
const repairOptions = resolveLegacyProjectScopeRepairOptions({
|
|
14879
|
+
project: projectPath,
|
|
14880
|
+
projectHash: options.projectHash,
|
|
14881
|
+
apply: options.apply
|
|
14882
|
+
});
|
|
14883
|
+
const storagePath = projectPath ? getProjectStoragePath(projectPath) : resolveProjectStoragePath(repairOptions.projectHash);
|
|
14884
|
+
const dbPath = path21.join(storagePath, "events.sqlite");
|
|
14885
|
+
if (repairOptions.dryRun && !fs18.existsSync(dbPath)) {
|
|
14886
|
+
const projectHash = repairOptions.projectHash || hashProjectPath(repairOptions.projectPath);
|
|
14887
|
+
console.log(formatLegacyProjectScopeRepairResult({
|
|
14888
|
+
dryRun: true,
|
|
14889
|
+
projectHash,
|
|
14890
|
+
scanned: 0,
|
|
14891
|
+
repaired: 0,
|
|
14892
|
+
quarantined: 0,
|
|
14893
|
+
alreadyScoped: 0,
|
|
14894
|
+
skipped: 0,
|
|
14895
|
+
samples: []
|
|
14896
|
+
}));
|
|
14897
|
+
return;
|
|
14898
|
+
}
|
|
14899
|
+
const store = new SQLiteEventStore(dbPath, {
|
|
14900
|
+
readonly: repairOptions.dryRun
|
|
14901
|
+
});
|
|
14902
|
+
try {
|
|
14903
|
+
const result = await store.repairLegacyProjectScope(repairOptions);
|
|
14904
|
+
console.log(formatLegacyProjectScopeRepairResult(result));
|
|
14905
|
+
} finally {
|
|
14906
|
+
await store.close().catch(() => void 0);
|
|
14907
|
+
}
|
|
14908
|
+
} catch (error) {
|
|
14909
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14910
|
+
console.error(`Repair failed: ${message}`);
|
|
14911
|
+
process.exit(1);
|
|
14912
|
+
}
|
|
14913
|
+
});
|
|
14264
14914
|
program.command("mongo-sync").description("Sync events with MongoDB for multi-server collaboration (optional)").option("-p, --project <path>", "Project path (defaults to cwd)").option("--mongo-uri <uri>", "MongoDB connection URI (env: CLAUDE_MEMORY_MONGO_URI)").option("--mongo-db <name>", "MongoDB database name (env: CLAUDE_MEMORY_MONGO_DB)").option("--mongo-project <key>", "Remote project key (env: CLAUDE_MEMORY_MONGO_PROJECT, default: basename(projectPath))").option("--direction <dir>", "push|pull|both", "both").option("--batch-size <n>", "Batch size", "500").option("--interval <ms>", "Watch interval ms", "30000").option("--watch", "Run continuously").action(async (options) => {
|
|
14265
14915
|
const projectPath = options.project || process.cwd();
|
|
14266
14916
|
const mongoUri = options.mongoUri || process.env.CLAUDE_MEMORY_MONGO_URI;
|