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/index.js
CHANGED
|
@@ -1128,6 +1128,14 @@ var MemoryQueryService = class {
|
|
|
1128
1128
|
await this.initialize();
|
|
1129
1129
|
return this.getMaintenanceStore("getOutboxStats").getOutboxStats();
|
|
1130
1130
|
}
|
|
1131
|
+
async recoverStuckOutboxItems(options) {
|
|
1132
|
+
await this.initialize();
|
|
1133
|
+
return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
|
|
1134
|
+
}
|
|
1135
|
+
async repairLegacyProjectScope(options) {
|
|
1136
|
+
await this.initialize();
|
|
1137
|
+
return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
|
|
1138
|
+
}
|
|
1131
1139
|
async getStats() {
|
|
1132
1140
|
await this.initialize();
|
|
1133
1141
|
const deps = this.getStatsDeps();
|
|
@@ -1922,6 +1930,9 @@ function parseEntityCanonicalKey(canonicalKey) {
|
|
|
1922
1930
|
return null;
|
|
1923
1931
|
}
|
|
1924
1932
|
|
|
1933
|
+
// src/core/sqlite-event-store.ts
|
|
1934
|
+
import * as nodePath2 from "path";
|
|
1935
|
+
|
|
1925
1936
|
// src/core/sqlite-wrapper.ts
|
|
1926
1937
|
import Database from "better-sqlite3";
|
|
1927
1938
|
import * as fs4 from "fs";
|
|
@@ -2046,6 +2057,143 @@ function normalizeQueryRewriteKind(value) {
|
|
|
2046
2057
|
return "none";
|
|
2047
2058
|
}
|
|
2048
2059
|
var REWRITTEN_QUERY_REWRITE_KIND_SQL = `LOWER(TRIM(COALESCE(query_rewrite_kind, 'none'))) IN ('follow-up-context', 'intent-rewrite')`;
|
|
2060
|
+
var DEFAULT_OUTBOX_STUCK_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2061
|
+
var DEFAULT_OUTBOX_MAX_RETRIES = 3;
|
|
2062
|
+
function emptyOutboxRecoveryResult() {
|
|
2063
|
+
return {
|
|
2064
|
+
embedding: { recoveredProcessing: 0, retriedFailed: 0 },
|
|
2065
|
+
vector: { recoveredProcessing: 0, retriedFailed: 0 }
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
function isRecord(value) {
|
|
2069
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2070
|
+
}
|
|
2071
|
+
function getNestedRecord(root, path12) {
|
|
2072
|
+
let cursor = root;
|
|
2073
|
+
for (const key of path12) {
|
|
2074
|
+
if (!isRecord(cursor))
|
|
2075
|
+
return void 0;
|
|
2076
|
+
cursor = cursor[key];
|
|
2077
|
+
}
|
|
2078
|
+
return isRecord(cursor) ? cursor : void 0;
|
|
2079
|
+
}
|
|
2080
|
+
function getNestedString(root, path12) {
|
|
2081
|
+
let cursor = root;
|
|
2082
|
+
for (const key of path12) {
|
|
2083
|
+
if (!isRecord(cursor))
|
|
2084
|
+
return void 0;
|
|
2085
|
+
cursor = cursor[key];
|
|
2086
|
+
}
|
|
2087
|
+
return typeof cursor === "string" && cursor.length > 0 ? cursor : void 0;
|
|
2088
|
+
}
|
|
2089
|
+
function metadataProjectHash(metadata) {
|
|
2090
|
+
return getNestedString(metadata, ["scope", "project", "hash"]);
|
|
2091
|
+
}
|
|
2092
|
+
function metadataProjectPaths(metadata) {
|
|
2093
|
+
const candidates = [
|
|
2094
|
+
getNestedString(metadata, ["projectPath"]),
|
|
2095
|
+
getNestedString(metadata, ["sourceProjectPath"]),
|
|
2096
|
+
getNestedString(metadata, ["scope", "project", "path"])
|
|
2097
|
+
];
|
|
2098
|
+
const paths = [];
|
|
2099
|
+
for (const value of candidates) {
|
|
2100
|
+
if (value && !paths.includes(value))
|
|
2101
|
+
paths.push(value);
|
|
2102
|
+
}
|
|
2103
|
+
return paths;
|
|
2104
|
+
}
|
|
2105
|
+
function metadataProjectPath(metadata) {
|
|
2106
|
+
return metadataProjectPaths(metadata)[0];
|
|
2107
|
+
}
|
|
2108
|
+
function isActiveQuarantinedMetadata(metadata) {
|
|
2109
|
+
const quarantine = getNestedRecord(metadata, ["quarantine"]);
|
|
2110
|
+
return quarantine?.status === "active";
|
|
2111
|
+
}
|
|
2112
|
+
function activeQuarantineStatusExpression(column = "metadata") {
|
|
2113
|
+
return `COALESCE(json_extract(CASE WHEN json_valid(${column}) THEN ${column} ELSE '{}' END, '$.quarantine.status'), '')`;
|
|
2114
|
+
}
|
|
2115
|
+
function notActiveQuarantinedSql(column = "metadata") {
|
|
2116
|
+
return `${activeQuarantineStatusExpression(column)} != 'active'`;
|
|
2117
|
+
}
|
|
2118
|
+
function maybeQuarantinePredicate(options, column = "metadata") {
|
|
2119
|
+
return options?.includeQuarantined ? "1=1" : notActiveQuarantinedSql(column);
|
|
2120
|
+
}
|
|
2121
|
+
function safeParseMetadataValue(value) {
|
|
2122
|
+
if (!value)
|
|
2123
|
+
return void 0;
|
|
2124
|
+
if (typeof value === "object")
|
|
2125
|
+
return isRecord(value) ? value : void 0;
|
|
2126
|
+
if (typeof value !== "string")
|
|
2127
|
+
return void 0;
|
|
2128
|
+
try {
|
|
2129
|
+
const parsed = JSON.parse(value);
|
|
2130
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
2131
|
+
} catch {
|
|
2132
|
+
return void 0;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
function isImportedOrLegacyScopedMetadata(metadata) {
|
|
2136
|
+
if (!metadata)
|
|
2137
|
+
return false;
|
|
2138
|
+
return Boolean(
|
|
2139
|
+
metadata.importedFrom || metadata.sourceSessionId || metadata.sourceSessionHash || metadata.hermesSource || metadata.projectPath || metadata.sourceProjectPath || metadata.source === "hermes" || metadata.source === "claude" || metadata.source === "codex"
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
function addMetadataTag(metadata, tag) {
|
|
2143
|
+
const current = Array.isArray(metadata.tags) ? metadata.tags.filter((value) => typeof value === "string") : [];
|
|
2144
|
+
if (!current.includes(tag))
|
|
2145
|
+
metadata.tags = [...current, tag];
|
|
2146
|
+
}
|
|
2147
|
+
function buildRepairResult(projectHash, dryRun) {
|
|
2148
|
+
return {
|
|
2149
|
+
dryRun,
|
|
2150
|
+
projectHash,
|
|
2151
|
+
scanned: 0,
|
|
2152
|
+
repaired: 0,
|
|
2153
|
+
quarantined: 0,
|
|
2154
|
+
alreadyScoped: 0,
|
|
2155
|
+
skipped: 0,
|
|
2156
|
+
samples: []
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
function normalizeRepoName(value) {
|
|
2160
|
+
return value.replace(/\.git$/i, "").trim().toLowerCase();
|
|
2161
|
+
}
|
|
2162
|
+
function projectBasename(projectPath) {
|
|
2163
|
+
if (!projectPath)
|
|
2164
|
+
return void 0;
|
|
2165
|
+
const trimmed = projectPath.replace(/[\\/]+$/, "");
|
|
2166
|
+
const basename3 = nodePath2.basename(trimmed);
|
|
2167
|
+
return basename3 ? normalizeRepoName(basename3) : void 0;
|
|
2168
|
+
}
|
|
2169
|
+
function isProjectScopeRepairExplanation(content) {
|
|
2170
|
+
const normalized = content.toLowerCase();
|
|
2171
|
+
const hasRepairContext = /project[- ]scope|mis[- ]scoped|quarantine|contamination|legacy|오염|격리|repair/.test(normalized);
|
|
2172
|
+
const hasExplanationContext = /example|detector|trap|not a .*project task|기억|메모리|설명|수정|검증/.test(normalized);
|
|
2173
|
+
return hasRepairContext && hasExplanationContext;
|
|
2174
|
+
}
|
|
2175
|
+
function hasConflictingContentProjectHint(content, projectPath) {
|
|
2176
|
+
const currentName = projectBasename(projectPath);
|
|
2177
|
+
if (!currentName)
|
|
2178
|
+
return false;
|
|
2179
|
+
if (isProjectScopeRepairExplanation(content))
|
|
2180
|
+
return false;
|
|
2181
|
+
const githubRepoPattern = /github\.com[:/]([^/\s`'"#)]+)\/([^/\s`'"#)]+)(?:\.git)?/gi;
|
|
2182
|
+
let githubMatch;
|
|
2183
|
+
while ((githubMatch = githubRepoPattern.exec(content)) !== null) {
|
|
2184
|
+
const repo = normalizeRepoName(githubMatch[2] || "");
|
|
2185
|
+
if (repo && repo !== currentName)
|
|
2186
|
+
return true;
|
|
2187
|
+
}
|
|
2188
|
+
const workspacePathPattern = /\/workspace\/([^/\s`'"#)]+)/gi;
|
|
2189
|
+
let workspaceMatch;
|
|
2190
|
+
while ((workspaceMatch = workspacePathPattern.exec(content)) !== null) {
|
|
2191
|
+
const repo = normalizeRepoName(workspaceMatch[1] || "");
|
|
2192
|
+
if (repo && repo !== currentName)
|
|
2193
|
+
return true;
|
|
2194
|
+
}
|
|
2195
|
+
return false;
|
|
2196
|
+
}
|
|
2049
2197
|
var SQLiteEventStore = class {
|
|
2050
2198
|
db;
|
|
2051
2199
|
initialized = false;
|
|
@@ -2544,11 +2692,11 @@ var SQLiteEventStore = class {
|
|
|
2544
2692
|
/**
|
|
2545
2693
|
* Get events by session ID
|
|
2546
2694
|
*/
|
|
2547
|
-
async getSessionEvents(sessionId) {
|
|
2695
|
+
async getSessionEvents(sessionId, options) {
|
|
2548
2696
|
await this.initialize();
|
|
2549
2697
|
const rows = sqliteAll(
|
|
2550
2698
|
this.db,
|
|
2551
|
-
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
2699
|
+
`SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
2552
2700
|
[sessionId]
|
|
2553
2701
|
);
|
|
2554
2702
|
return rows.map(this.rowToEvent);
|
|
@@ -2556,11 +2704,11 @@ var SQLiteEventStore = class {
|
|
|
2556
2704
|
/**
|
|
2557
2705
|
* Get recent events
|
|
2558
2706
|
*/
|
|
2559
|
-
async getRecentEvents(limit = 100) {
|
|
2707
|
+
async getRecentEvents(limit = 100, options) {
|
|
2560
2708
|
await this.initialize();
|
|
2561
2709
|
const rows = sqliteAll(
|
|
2562
2710
|
this.db,
|
|
2563
|
-
`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
|
|
2711
|
+
`SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
|
|
2564
2712
|
[limit]
|
|
2565
2713
|
);
|
|
2566
2714
|
return rows.map(this.rowToEvent);
|
|
@@ -2568,11 +2716,11 @@ var SQLiteEventStore = class {
|
|
|
2568
2716
|
/**
|
|
2569
2717
|
* Get event by ID
|
|
2570
2718
|
*/
|
|
2571
|
-
async getEvent(id) {
|
|
2719
|
+
async getEvent(id, options) {
|
|
2572
2720
|
await this.initialize();
|
|
2573
2721
|
const row = sqliteGet(
|
|
2574
2722
|
this.db,
|
|
2575
|
-
`SELECT * FROM events WHERE id =
|
|
2723
|
+
`SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
|
|
2576
2724
|
[id]
|
|
2577
2725
|
);
|
|
2578
2726
|
if (!row)
|
|
@@ -2582,11 +2730,11 @@ var SQLiteEventStore = class {
|
|
|
2582
2730
|
/**
|
|
2583
2731
|
* Get events since a timestamp (for sync)
|
|
2584
2732
|
*/
|
|
2585
|
-
async getEventsSince(timestamp, limit = 1e3) {
|
|
2733
|
+
async getEventsSince(timestamp, limit = 1e3, options) {
|
|
2586
2734
|
await this.initialize();
|
|
2587
2735
|
const rows = sqliteAll(
|
|
2588
2736
|
this.db,
|
|
2589
|
-
`SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
|
|
2737
|
+
`SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
|
|
2590
2738
|
[timestamp, limit]
|
|
2591
2739
|
);
|
|
2592
2740
|
return rows.map(this.rowToEvent);
|
|
@@ -2595,11 +2743,11 @@ var SQLiteEventStore = class {
|
|
|
2595
2743
|
* Get events since a SQLite rowid (for robust incremental replication).
|
|
2596
2744
|
* Rowid is monotonic for append-only tables, independent of client timestamps.
|
|
2597
2745
|
*/
|
|
2598
|
-
async getEventsSinceRowid(lastRowid, limit = 1e3) {
|
|
2746
|
+
async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
|
|
2599
2747
|
await this.initialize();
|
|
2600
2748
|
const rows = sqliteAll(
|
|
2601
2749
|
this.db,
|
|
2602
|
-
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`,
|
|
2750
|
+
`SELECT rowid as _rowid, * FROM events WHERE rowid > ? AND ${maybeQuarantinePredicate(options)} ORDER BY rowid ASC LIMIT ?`,
|
|
2603
2751
|
[lastRowid, limit]
|
|
2604
2752
|
);
|
|
2605
2753
|
return rows.map((row) => ({
|
|
@@ -2796,7 +2944,9 @@ var SQLiteEventStore = class {
|
|
|
2796
2944
|
const placeholders = ids.map(() => "?").join(",");
|
|
2797
2945
|
sqliteRun(
|
|
2798
2946
|
this.db,
|
|
2799
|
-
`UPDATE embedding_outbox
|
|
2947
|
+
`UPDATE embedding_outbox
|
|
2948
|
+
SET status = 'processing', processed_at = datetime('now'), error_message = NULL
|
|
2949
|
+
WHERE id IN (${placeholders})`,
|
|
2800
2950
|
ids
|
|
2801
2951
|
);
|
|
2802
2952
|
return pending.map((row) => ({
|
|
@@ -2832,19 +2982,19 @@ var SQLiteEventStore = class {
|
|
|
2832
2982
|
/**
|
|
2833
2983
|
* Count total events
|
|
2834
2984
|
*/
|
|
2835
|
-
async countEvents() {
|
|
2985
|
+
async countEvents(options) {
|
|
2836
2986
|
await this.initialize();
|
|
2837
|
-
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events`);
|
|
2987
|
+
const row = sqliteGet(this.db, `SELECT COUNT(*) as count FROM events WHERE ${maybeQuarantinePredicate(options)}`);
|
|
2838
2988
|
return row?.count || 0;
|
|
2839
2989
|
}
|
|
2840
2990
|
/**
|
|
2841
2991
|
* Get events page in timestamp ascending order (stable migration/reindex scans)
|
|
2842
2992
|
*/
|
|
2843
|
-
async getEventsPage(limit = 1e3, offset = 0) {
|
|
2993
|
+
async getEventsPage(limit = 1e3, offset = 0, options) {
|
|
2844
2994
|
await this.initialize();
|
|
2845
2995
|
const rows = sqliteAll(
|
|
2846
2996
|
this.db,
|
|
2847
|
-
`SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
2997
|
+
`SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
|
|
2848
2998
|
[limit, offset]
|
|
2849
2999
|
);
|
|
2850
3000
|
return rows.map(this.rowToEvent);
|
|
@@ -2866,6 +3016,197 @@ var SQLiteEventStore = class {
|
|
|
2866
3016
|
[error, ...ids]
|
|
2867
3017
|
);
|
|
2868
3018
|
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Recover abandoned outbox work after a worker/process crash.
|
|
3021
|
+
*
|
|
3022
|
+
* Rows in `processing` are claimed work. If the process exits before marking
|
|
3023
|
+
* them done/failed, they otherwise remain invisible to future processing.
|
|
3024
|
+
* Recovery is deliberately age-gated so an active worker is not disturbed.
|
|
3025
|
+
*/
|
|
3026
|
+
async recoverStuckOutboxItems(options = {}) {
|
|
3027
|
+
await this.initialize();
|
|
3028
|
+
const thresholdMs = Number.isFinite(options.stuckThresholdMs) && (options.stuckThresholdMs ?? 0) >= 0 ? options.stuckThresholdMs : DEFAULT_OUTBOX_STUCK_THRESHOLD_MS;
|
|
3029
|
+
const maxRetries = Number.isFinite(options.maxRetries) && (options.maxRetries ?? 0) > 0 ? options.maxRetries : DEFAULT_OUTBOX_MAX_RETRIES;
|
|
3030
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
3031
|
+
const threshold = new Date(now.getTime() - thresholdMs).toISOString();
|
|
3032
|
+
const result = emptyOutboxRecoveryResult();
|
|
3033
|
+
const embeddingRecovered = sqliteRun(
|
|
3034
|
+
this.db,
|
|
3035
|
+
`UPDATE embedding_outbox
|
|
3036
|
+
SET status = 'pending', processed_at = NULL, error_message = NULL
|
|
3037
|
+
WHERE status = 'processing'
|
|
3038
|
+
AND datetime(COALESCE(processed_at, created_at)) < datetime(?)`,
|
|
3039
|
+
[threshold]
|
|
3040
|
+
);
|
|
3041
|
+
result.embedding.recoveredProcessing = Number(embeddingRecovered.changes ?? 0);
|
|
3042
|
+
const embeddingRetried = sqliteRun(
|
|
3043
|
+
this.db,
|
|
3044
|
+
`UPDATE embedding_outbox
|
|
3045
|
+
SET status = 'pending', error_message = NULL
|
|
3046
|
+
WHERE status = 'failed'
|
|
3047
|
+
AND retry_count < ?`,
|
|
3048
|
+
[maxRetries]
|
|
3049
|
+
);
|
|
3050
|
+
result.embedding.retriedFailed = Number(embeddingRetried.changes ?? 0);
|
|
3051
|
+
const vectorRecovered = sqliteRun(
|
|
3052
|
+
this.db,
|
|
3053
|
+
`UPDATE vector_outbox
|
|
3054
|
+
SET status = 'pending', updated_at = ?, error = NULL
|
|
3055
|
+
WHERE status = 'processing'
|
|
3056
|
+
AND datetime(updated_at) < datetime(?)`,
|
|
3057
|
+
[now.toISOString(), threshold]
|
|
3058
|
+
);
|
|
3059
|
+
result.vector.recoveredProcessing = Number(vectorRecovered.changes ?? 0);
|
|
3060
|
+
const vectorRetried = sqliteRun(
|
|
3061
|
+
this.db,
|
|
3062
|
+
`UPDATE vector_outbox
|
|
3063
|
+
SET status = 'pending', updated_at = ?, error = NULL
|
|
3064
|
+
WHERE status = 'failed'
|
|
3065
|
+
AND retry_count < ?`,
|
|
3066
|
+
[now.toISOString(), maxRetries]
|
|
3067
|
+
);
|
|
3068
|
+
result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
|
|
3069
|
+
return result;
|
|
3070
|
+
}
|
|
3071
|
+
/**
|
|
3072
|
+
* Repair legacy imported events that predate canonical project scope metadata.
|
|
3073
|
+
*
|
|
3074
|
+
* Same-project legacy rows are tagged with scope.project.hash. Rows that look
|
|
3075
|
+
* imported but cannot be proven to belong to this project are quarantined so
|
|
3076
|
+
* dashboard default reads/search do not surface cross-project contamination.
|
|
3077
|
+
*/
|
|
3078
|
+
async repairLegacyProjectScope(options = {}) {
|
|
3079
|
+
await this.initialize();
|
|
3080
|
+
const projectHash = options.projectHash || (options.projectPath ? hashProjectPath(options.projectPath) : void 0);
|
|
3081
|
+
if (!projectHash) {
|
|
3082
|
+
throw new Error("repairLegacyProjectScope requires projectPath or projectHash");
|
|
3083
|
+
}
|
|
3084
|
+
if (options.projectPath && options.projectHash && hashProjectPath(options.projectPath) !== options.projectHash) {
|
|
3085
|
+
throw new Error("repairLegacyProjectScope projectPath and projectHash refer to different project stores");
|
|
3086
|
+
}
|
|
3087
|
+
const dryRun = options.dryRun === true;
|
|
3088
|
+
const nowIso = (options.now || /* @__PURE__ */ new Date()).toISOString();
|
|
3089
|
+
const result = buildRepairResult(projectHash, dryRun);
|
|
3090
|
+
const rows = sqliteAll(
|
|
3091
|
+
this.db,
|
|
3092
|
+
`SELECT e.id, e.content, e.metadata, s.project_path as session_project_path
|
|
3093
|
+
FROM events e
|
|
3094
|
+
LEFT JOIN sessions s ON s.id = e.session_id
|
|
3095
|
+
ORDER BY e.timestamp ASC`,
|
|
3096
|
+
[]
|
|
3097
|
+
);
|
|
3098
|
+
const sample = (entry) => {
|
|
3099
|
+
if (result.samples.length < 20)
|
|
3100
|
+
result.samples.push(entry);
|
|
3101
|
+
};
|
|
3102
|
+
for (const row of rows) {
|
|
3103
|
+
result.scanned++;
|
|
3104
|
+
let metadata = {};
|
|
3105
|
+
let metadataParseInvalid = false;
|
|
3106
|
+
if (row.metadata) {
|
|
3107
|
+
const parsed = safeParseMetadataValue(row.metadata);
|
|
3108
|
+
if (parsed) {
|
|
3109
|
+
metadata = parsed;
|
|
3110
|
+
} else {
|
|
3111
|
+
metadataParseInvalid = true;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
if (isActiveQuarantinedMetadata(metadata)) {
|
|
3115
|
+
result.skipped++;
|
|
3116
|
+
continue;
|
|
3117
|
+
}
|
|
3118
|
+
const currentHash = metadataProjectHash(metadata);
|
|
3119
|
+
const explicitPath = metadataProjectPath(metadata);
|
|
3120
|
+
const sessionProjectPath = typeof row.session_project_path === "string" && row.session_project_path.length > 0 ? row.session_project_path : void 0;
|
|
3121
|
+
const candidatePaths = metadataProjectPaths(metadata);
|
|
3122
|
+
if (sessionProjectPath && !candidatePaths.includes(sessionProjectPath)) {
|
|
3123
|
+
candidatePaths.push(sessionProjectPath);
|
|
3124
|
+
}
|
|
3125
|
+
const importedOrLegacy = metadataParseInvalid || isImportedOrLegacyScopedMetadata(metadata) || Boolean(sessionProjectPath);
|
|
3126
|
+
const pathHashes = candidatePaths.map((candidate) => {
|
|
3127
|
+
try {
|
|
3128
|
+
return { path: candidate, hash: hashProjectPath(candidate) };
|
|
3129
|
+
} catch {
|
|
3130
|
+
return { path: candidate, hash: void 0 };
|
|
3131
|
+
}
|
|
3132
|
+
});
|
|
3133
|
+
const matchingPath = pathHashes.find((candidate) => candidate.hash === projectHash);
|
|
3134
|
+
const foreignPath = pathHashes.find((candidate) => candidate.hash && candidate.hash !== projectHash);
|
|
3135
|
+
let action = "skipped";
|
|
3136
|
+
let reason;
|
|
3137
|
+
let observedProjectHash;
|
|
3138
|
+
if (foreignPath) {
|
|
3139
|
+
action = "quarantined";
|
|
3140
|
+
reason = "project-path-mismatch";
|
|
3141
|
+
observedProjectHash = foreignPath.hash;
|
|
3142
|
+
} else if (currentHash === projectHash && importedOrLegacy && hasConflictingContentProjectHint(row.content, options.projectPath)) {
|
|
3143
|
+
action = "quarantined";
|
|
3144
|
+
reason = "content-project-mismatch";
|
|
3145
|
+
} else if (currentHash === projectHash) {
|
|
3146
|
+
result.alreadyScoped++;
|
|
3147
|
+
continue;
|
|
3148
|
+
} else if (currentHash && currentHash !== projectHash) {
|
|
3149
|
+
action = "quarantined";
|
|
3150
|
+
reason = "scope-hash-mismatch";
|
|
3151
|
+
observedProjectHash = currentHash;
|
|
3152
|
+
} else if (matchingPath) {
|
|
3153
|
+
action = "repaired";
|
|
3154
|
+
reason = matchingPath.path === sessionProjectPath && matchingPath.path !== explicitPath ? "session-project-path" : "same-project-path";
|
|
3155
|
+
} else if (candidatePaths.length > 0) {
|
|
3156
|
+
action = "quarantined";
|
|
3157
|
+
reason = "project-path-mismatch";
|
|
3158
|
+
} else if (importedOrLegacy) {
|
|
3159
|
+
action = "quarantined";
|
|
3160
|
+
reason = "missing-project-scope";
|
|
3161
|
+
}
|
|
3162
|
+
if (action === "skipped" || !reason) {
|
|
3163
|
+
result.skipped++;
|
|
3164
|
+
continue;
|
|
3165
|
+
}
|
|
3166
|
+
if (action === "repaired") {
|
|
3167
|
+
const scope = isRecord(metadata.scope) ? { ...metadata.scope } : {};
|
|
3168
|
+
const project = isRecord(scope.project) ? { ...scope.project } : {};
|
|
3169
|
+
project.hash = projectHash;
|
|
3170
|
+
scope.project = project;
|
|
3171
|
+
metadata.scope = scope;
|
|
3172
|
+
metadata.repair = {
|
|
3173
|
+
...isRecord(metadata.repair) ? metadata.repair : {},
|
|
3174
|
+
legacyProjectScope: {
|
|
3175
|
+
action,
|
|
3176
|
+
reason,
|
|
3177
|
+
repairedAt: nowIso
|
|
3178
|
+
}
|
|
3179
|
+
};
|
|
3180
|
+
addMetadataTag(metadata, `proj:${projectHash}`);
|
|
3181
|
+
result.repaired++;
|
|
3182
|
+
} else {
|
|
3183
|
+
metadata.quarantine = {
|
|
3184
|
+
...isRecord(metadata.quarantine) ? metadata.quarantine : {},
|
|
3185
|
+
status: "active",
|
|
3186
|
+
category: "project-scope",
|
|
3187
|
+
reason,
|
|
3188
|
+
detectedAt: nowIso,
|
|
3189
|
+
expectedProjectHash: projectHash,
|
|
3190
|
+
...observedProjectHash ? { observedProjectHash } : {}
|
|
3191
|
+
};
|
|
3192
|
+
metadata.repair = {
|
|
3193
|
+
...isRecord(metadata.repair) ? metadata.repair : {},
|
|
3194
|
+
legacyProjectScope: {
|
|
3195
|
+
action,
|
|
3196
|
+
reason,
|
|
3197
|
+
repairedAt: nowIso
|
|
3198
|
+
}
|
|
3199
|
+
};
|
|
3200
|
+
addMetadataTag(metadata, "quarantine:project-scope");
|
|
3201
|
+
result.quarantined++;
|
|
3202
|
+
}
|
|
3203
|
+
sample({ eventId: row.id, action, reason });
|
|
3204
|
+
if (!dryRun) {
|
|
3205
|
+
sqliteRun(this.db, `UPDATE events SET metadata = ? WHERE id = ?`, [JSON.stringify(metadata), row.id]);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
return result;
|
|
3209
|
+
}
|
|
2869
3210
|
/**
|
|
2870
3211
|
* Get embedding/vector outbox health statistics
|
|
2871
3212
|
*/
|
|
@@ -2913,7 +3254,11 @@ var SQLiteEventStore = class {
|
|
|
2913
3254
|
await this.initialize();
|
|
2914
3255
|
const rows = sqliteAll(
|
|
2915
3256
|
this.db,
|
|
2916
|
-
`SELECT level, COUNT(*) as count
|
|
3257
|
+
`SELECT ml.level, COUNT(*) as count
|
|
3258
|
+
FROM memory_levels ml
|
|
3259
|
+
INNER JOIN events e ON e.id = ml.event_id
|
|
3260
|
+
WHERE ${notActiveQuarantinedSql("e.metadata")}
|
|
3261
|
+
GROUP BY ml.level`
|
|
2917
3262
|
);
|
|
2918
3263
|
return rows;
|
|
2919
3264
|
}
|
|
@@ -2929,6 +3274,7 @@ var SQLiteEventStore = class {
|
|
|
2929
3274
|
`SELECT e.* FROM events e
|
|
2930
3275
|
INNER JOIN memory_levels ml ON e.id = ml.event_id
|
|
2931
3276
|
WHERE ml.level = ?
|
|
3277
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
2932
3278
|
ORDER BY e.timestamp DESC
|
|
2933
3279
|
LIMIT ? OFFSET ?`,
|
|
2934
3280
|
[level, limit, offset]
|
|
@@ -3021,12 +3367,13 @@ var SQLiteEventStore = class {
|
|
|
3021
3367
|
/**
|
|
3022
3368
|
* Get most accessed memories (falls back to recent events if none accessed)
|
|
3023
3369
|
*/
|
|
3024
|
-
async getMostAccessed(limit = 10) {
|
|
3370
|
+
async getMostAccessed(limit = 10, options) {
|
|
3025
3371
|
await this.initialize();
|
|
3026
3372
|
let rows = sqliteAll(
|
|
3027
3373
|
this.db,
|
|
3028
3374
|
`SELECT * FROM events
|
|
3029
3375
|
WHERE access_count > 0
|
|
3376
|
+
AND ${maybeQuarantinePredicate(options)}
|
|
3030
3377
|
ORDER BY access_count DESC, last_accessed_at DESC
|
|
3031
3378
|
LIMIT ?`,
|
|
3032
3379
|
[limit]
|
|
@@ -3035,6 +3382,7 @@ var SQLiteEventStore = class {
|
|
|
3035
3382
|
rows = sqliteAll(
|
|
3036
3383
|
this.db,
|
|
3037
3384
|
`SELECT * FROM events
|
|
3385
|
+
WHERE ${maybeQuarantinePredicate(options)}
|
|
3038
3386
|
ORDER BY timestamp DESC
|
|
3039
3387
|
LIMIT ?`,
|
|
3040
3388
|
[limit]
|
|
@@ -3165,6 +3513,7 @@ var SQLiteEventStore = class {
|
|
|
3165
3513
|
FROM memory_helpfulness mh
|
|
3166
3514
|
JOIN events e ON e.id = mh.event_id
|
|
3167
3515
|
WHERE mh.measured_at IS NOT NULL
|
|
3516
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3168
3517
|
GROUP BY mh.event_id
|
|
3169
3518
|
ORDER BY avg_score DESC
|
|
3170
3519
|
LIMIT ?`,
|
|
@@ -3229,6 +3578,7 @@ var SQLiteEventStore = class {
|
|
|
3229
3578
|
FROM events_fts fts
|
|
3230
3579
|
JOIN events e ON e.id = fts.event_id
|
|
3231
3580
|
WHERE events_fts MATCH ?
|
|
3581
|
+
AND ${notActiveQuarantinedSql("e.metadata")}
|
|
3232
3582
|
ORDER BY fts.rank
|
|
3233
3583
|
LIMIT ?`,
|
|
3234
3584
|
[searchTerms, limit]
|
|
@@ -3243,6 +3593,7 @@ var SQLiteEventStore = class {
|
|
|
3243
3593
|
this.db,
|
|
3244
3594
|
`SELECT *, 0 as rank FROM events
|
|
3245
3595
|
WHERE content LIKE ?
|
|
3596
|
+
AND ${notActiveQuarantinedSql()}
|
|
3246
3597
|
ORDER BY timestamp DESC
|
|
3247
3598
|
LIMIT ?`,
|
|
3248
3599
|
[likePattern, limit]
|
|
@@ -3449,6 +3800,7 @@ var SQLiteEventStore = class {
|
|
|
3449
3800
|
`SELECT turn_id, MIN(timestamp) as min_ts
|
|
3450
3801
|
FROM events
|
|
3451
3802
|
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
3803
|
+
AND ${maybeQuarantinePredicate(options)}
|
|
3452
3804
|
GROUP BY turn_id
|
|
3453
3805
|
ORDER BY min_ts DESC
|
|
3454
3806
|
LIMIT ? OFFSET ?`,
|
|
@@ -3456,7 +3808,7 @@ var SQLiteEventStore = class {
|
|
|
3456
3808
|
);
|
|
3457
3809
|
const turns = [];
|
|
3458
3810
|
for (const turnRow of turnRows) {
|
|
3459
|
-
const events = await this.getEventsByTurn(turnRow.turn_id);
|
|
3811
|
+
const events = await this.getEventsByTurn(turnRow.turn_id, options);
|
|
3460
3812
|
const promptEvent = events.find((e) => e.eventType === "user_prompt");
|
|
3461
3813
|
const toolEvents = events.filter((e) => e.eventType === "tool_observation");
|
|
3462
3814
|
const hasResponse = events.some((e) => e.eventType === "agent_response");
|
|
@@ -3475,11 +3827,11 @@ var SQLiteEventStore = class {
|
|
|
3475
3827
|
/**
|
|
3476
3828
|
* Get all events for a specific turn_id
|
|
3477
3829
|
*/
|
|
3478
|
-
async getEventsByTurn(turnId) {
|
|
3830
|
+
async getEventsByTurn(turnId, options) {
|
|
3479
3831
|
await this.initialize();
|
|
3480
3832
|
const rows = sqliteAll(
|
|
3481
3833
|
this.db,
|
|
3482
|
-
`SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
|
|
3834
|
+
`SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
|
|
3483
3835
|
[turnId]
|
|
3484
3836
|
);
|
|
3485
3837
|
return rows.map(this.rowToEvent);
|
|
@@ -3487,13 +3839,14 @@ var SQLiteEventStore = class {
|
|
|
3487
3839
|
/**
|
|
3488
3840
|
* Count total turns for a session
|
|
3489
3841
|
*/
|
|
3490
|
-
async countSessionTurns(sessionId) {
|
|
3842
|
+
async countSessionTurns(sessionId, options) {
|
|
3491
3843
|
await this.initialize();
|
|
3492
3844
|
const row = sqliteGet(
|
|
3493
3845
|
this.db,
|
|
3494
3846
|
`SELECT COUNT(DISTINCT turn_id) as count
|
|
3495
3847
|
FROM events
|
|
3496
|
-
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
3848
|
+
WHERE session_id = ? AND turn_id IS NOT NULL
|
|
3849
|
+
AND ${maybeQuarantinePredicate(options)}`,
|
|
3497
3850
|
[sessionId]
|
|
3498
3851
|
);
|
|
3499
3852
|
return row?.count || 0;
|
|
@@ -3585,7 +3938,7 @@ var SQLiteEventStore = class {
|
|
|
3585
3938
|
content: row.content,
|
|
3586
3939
|
canonicalKey: row.canonical_key,
|
|
3587
3940
|
dedupeKey: row.dedupe_key,
|
|
3588
|
-
metadata:
|
|
3941
|
+
metadata: safeParseMetadataValue(row.metadata)
|
|
3589
3942
|
};
|
|
3590
3943
|
if (row.access_count !== void 0) {
|
|
3591
3944
|
event.access_count = row.access_count;
|
|
@@ -3737,6 +4090,7 @@ var VectorStore = class {
|
|
|
3737
4090
|
* Get total count of vectors
|
|
3738
4091
|
*/
|
|
3739
4092
|
async count() {
|
|
4093
|
+
await this.initialize();
|
|
3740
4094
|
if (!this.table)
|
|
3741
4095
|
return 0;
|
|
3742
4096
|
const result = await this.table.countRows();
|
|
@@ -12760,6 +13114,12 @@ var MemoryService = class {
|
|
|
12760
13114
|
async getOutboxStats() {
|
|
12761
13115
|
return this.queryService.getOutboxStats();
|
|
12762
13116
|
}
|
|
13117
|
+
async recoverStuckOutboxItems(options) {
|
|
13118
|
+
return this.queryService.recoverStuckOutboxItems(options);
|
|
13119
|
+
}
|
|
13120
|
+
async repairLegacyProjectScope(options) {
|
|
13121
|
+
return this.queryService.repairLegacyProjectScope(options);
|
|
13122
|
+
}
|
|
12763
13123
|
async getRetrievalTraceStats() {
|
|
12764
13124
|
return this.retrievalAnalyticsService.getRetrievalTraceStats();
|
|
12765
13125
|
}
|
|
@@ -13066,7 +13426,7 @@ import * as os7 from "os";
|
|
|
13066
13426
|
import * as readline from "readline";
|
|
13067
13427
|
import { createHash as createHash5, randomUUID as randomUUID16 } from "crypto";
|
|
13068
13428
|
var CODEX_VALIDATION_DEFAULT_MAX_CONTENT_CHARS = 1e4;
|
|
13069
|
-
function
|
|
13429
|
+
function isRecord2(value) {
|
|
13070
13430
|
return typeof value === "object" && value !== null;
|
|
13071
13431
|
}
|
|
13072
13432
|
function normalizeMaybeRealpath(p) {
|
|
@@ -13083,7 +13443,7 @@ function extractCodexContentText(content, maxContentChars = CODEX_VALIDATION_DEF
|
|
|
13083
13443
|
texts.push(content);
|
|
13084
13444
|
} else if (Array.isArray(content)) {
|
|
13085
13445
|
for (const block of content) {
|
|
13086
|
-
if (!
|
|
13446
|
+
if (!isRecord2(block))
|
|
13087
13447
|
continue;
|
|
13088
13448
|
const b = block;
|
|
13089
13449
|
const t = typeof b.type === "string" ? b.type : "";
|
|
@@ -13171,7 +13531,7 @@ async function readCodexSessionMeta(filePath) {
|
|
|
13171
13531
|
const obj = JSON.parse(line);
|
|
13172
13532
|
if (obj.type !== "session_meta")
|
|
13173
13533
|
continue;
|
|
13174
|
-
if (!
|
|
13534
|
+
if (!isRecord2(obj.payload))
|
|
13175
13535
|
break;
|
|
13176
13536
|
const payload = obj.payload;
|
|
13177
13537
|
const sessionId = typeof payload.id === "string" ? payload.id : null;
|
|
@@ -13309,7 +13669,7 @@ async function normalizeCodexSessionFile(filePath, options = {}) {
|
|
|
13309
13669
|
}
|
|
13310
13670
|
if (entry.type === "session_meta")
|
|
13311
13671
|
continue;
|
|
13312
|
-
if (entry.type !== "response_item" || !
|
|
13672
|
+
if (entry.type !== "response_item" || !isRecord2(entry.payload)) {
|
|
13313
13673
|
summary.skippedUnsupportedRecords += 1;
|
|
13314
13674
|
continue;
|
|
13315
13675
|
}
|
|
@@ -13464,7 +13824,7 @@ var CodexSessionHistoryImporter = class {
|
|
|
13464
13824
|
const obj = JSON.parse(line);
|
|
13465
13825
|
if (obj.type !== "session_meta")
|
|
13466
13826
|
continue;
|
|
13467
|
-
if (!
|
|
13827
|
+
if (!isRecord2(obj.payload))
|
|
13468
13828
|
break;
|
|
13469
13829
|
const payload = obj.payload;
|
|
13470
13830
|
const sessionId = typeof payload.id === "string" ? payload.id : null;
|
|
@@ -13680,7 +14040,7 @@ var CodexSessionHistoryImporter = class {
|
|
|
13680
14040
|
try {
|
|
13681
14041
|
const entry = JSON.parse(line);
|
|
13682
14042
|
result.totalMessages++;
|
|
13683
|
-
if (entry.type === "response_item" &&
|
|
14043
|
+
if (entry.type === "response_item" && isRecord2(entry.payload)) {
|
|
13684
14044
|
const payload = entry.payload;
|
|
13685
14045
|
if (payload.type !== "message")
|
|
13686
14046
|
continue;
|