claude-memory-layer 1.0.37 → 1.0.39

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.
@@ -1132,6 +1132,10 @@ var MemoryQueryService = class {
1132
1132
  await this.initialize();
1133
1133
  return this.getMaintenanceStore("recoverStuckOutboxItems").recoverStuckOutboxItems(options);
1134
1134
  }
1135
+ async repairLegacyProjectScope(options) {
1136
+ await this.initialize();
1137
+ return this.getMaintenanceStore("repairLegacyProjectScope").repairLegacyProjectScope(options);
1138
+ }
1135
1139
  async getStats() {
1136
1140
  await this.initialize();
1137
1141
  const deps = this.getStatsDeps();
@@ -1926,6 +1930,9 @@ function parseEntityCanonicalKey(canonicalKey) {
1926
1930
  return null;
1927
1931
  }
1928
1932
 
1933
+ // src/core/sqlite-event-store.ts
1934
+ import * as nodePath2 from "path";
1935
+
1929
1936
  // src/core/sqlite-wrapper.ts
1930
1937
  import Database from "better-sqlite3";
1931
1938
  import * as fs4 from "fs";
@@ -2058,6 +2065,135 @@ function emptyOutboxRecoveryResult() {
2058
2065
  vector: { recoveredProcessing: 0, retriedFailed: 0 }
2059
2066
  };
2060
2067
  }
2068
+ function isRecord(value) {
2069
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2070
+ }
2071
+ function getNestedRecord(root, path8) {
2072
+ let cursor = root;
2073
+ for (const key of path8) {
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, path8) {
2081
+ let cursor = root;
2082
+ for (const key of path8) {
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 basename2 = nodePath2.basename(trimmed);
2167
+ return basename2 ? normalizeRepoName(basename2) : 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
+ }
2061
2197
  var SQLiteEventStore = class {
2062
2198
  db;
2063
2199
  initialized = false;
@@ -2556,11 +2692,11 @@ var SQLiteEventStore = class {
2556
2692
  /**
2557
2693
  * Get events by session ID
2558
2694
  */
2559
- async getSessionEvents(sessionId) {
2695
+ async getSessionEvents(sessionId, options) {
2560
2696
  await this.initialize();
2561
2697
  const rows = sqliteAll(
2562
2698
  this.db,
2563
- `SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
2699
+ `SELECT * FROM events WHERE session_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
2564
2700
  [sessionId]
2565
2701
  );
2566
2702
  return rows.map(this.rowToEvent);
@@ -2568,11 +2704,11 @@ var SQLiteEventStore = class {
2568
2704
  /**
2569
2705
  * Get recent events
2570
2706
  */
2571
- async getRecentEvents(limit = 100) {
2707
+ async getRecentEvents(limit = 100, options) {
2572
2708
  await this.initialize();
2573
2709
  const rows = sqliteAll(
2574
2710
  this.db,
2575
- `SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
2711
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp DESC LIMIT ?`,
2576
2712
  [limit]
2577
2713
  );
2578
2714
  return rows.map(this.rowToEvent);
@@ -2580,11 +2716,11 @@ var SQLiteEventStore = class {
2580
2716
  /**
2581
2717
  * Get event by ID
2582
2718
  */
2583
- async getEvent(id) {
2719
+ async getEvent(id, options) {
2584
2720
  await this.initialize();
2585
2721
  const row = sqliteGet(
2586
2722
  this.db,
2587
- `SELECT * FROM events WHERE id = ?`,
2723
+ `SELECT * FROM events WHERE id = ? AND ${maybeQuarantinePredicate(options)}`,
2588
2724
  [id]
2589
2725
  );
2590
2726
  if (!row)
@@ -2594,11 +2730,11 @@ var SQLiteEventStore = class {
2594
2730
  /**
2595
2731
  * Get events since a timestamp (for sync)
2596
2732
  */
2597
- async getEventsSince(timestamp, limit = 1e3) {
2733
+ async getEventsSince(timestamp, limit = 1e3, options) {
2598
2734
  await this.initialize();
2599
2735
  const rows = sqliteAll(
2600
2736
  this.db,
2601
- `SELECT * FROM events WHERE timestamp > ? ORDER BY timestamp ASC LIMIT ?`,
2737
+ `SELECT * FROM events WHERE timestamp > ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ?`,
2602
2738
  [timestamp, limit]
2603
2739
  );
2604
2740
  return rows.map(this.rowToEvent);
@@ -2607,11 +2743,11 @@ var SQLiteEventStore = class {
2607
2743
  * Get events since a SQLite rowid (for robust incremental replication).
2608
2744
  * Rowid is monotonic for append-only tables, independent of client timestamps.
2609
2745
  */
2610
- async getEventsSinceRowid(lastRowid, limit = 1e3) {
2746
+ async getEventsSinceRowid(lastRowid, limit = 1e3, options) {
2611
2747
  await this.initialize();
2612
2748
  const rows = sqliteAll(
2613
2749
  this.db,
2614
- `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 ?`,
2615
2751
  [lastRowid, limit]
2616
2752
  );
2617
2753
  return rows.map((row) => ({
@@ -2846,19 +2982,19 @@ var SQLiteEventStore = class {
2846
2982
  /**
2847
2983
  * Count total events
2848
2984
  */
2849
- async countEvents() {
2985
+ async countEvents(options) {
2850
2986
  await this.initialize();
2851
- 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)}`);
2852
2988
  return row?.count || 0;
2853
2989
  }
2854
2990
  /**
2855
2991
  * Get events page in timestamp ascending order (stable migration/reindex scans)
2856
2992
  */
2857
- async getEventsPage(limit = 1e3, offset = 0) {
2993
+ async getEventsPage(limit = 1e3, offset = 0, options) {
2858
2994
  await this.initialize();
2859
2995
  const rows = sqliteAll(
2860
2996
  this.db,
2861
- `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
2997
+ `SELECT * FROM events WHERE ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
2862
2998
  [limit, offset]
2863
2999
  );
2864
3000
  return rows.map(this.rowToEvent);
@@ -2932,6 +3068,145 @@ var SQLiteEventStore = class {
2932
3068
  result.vector.retriedFailed = Number(vectorRetried.changes ?? 0);
2933
3069
  return result;
2934
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
+ }
2935
3210
  /**
2936
3211
  * Get embedding/vector outbox health statistics
2937
3212
  */
@@ -2979,7 +3254,11 @@ var SQLiteEventStore = class {
2979
3254
  await this.initialize();
2980
3255
  const rows = sqliteAll(
2981
3256
  this.db,
2982
- `SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
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`
2983
3262
  );
2984
3263
  return rows;
2985
3264
  }
@@ -2995,6 +3274,7 @@ var SQLiteEventStore = class {
2995
3274
  `SELECT e.* FROM events e
2996
3275
  INNER JOIN memory_levels ml ON e.id = ml.event_id
2997
3276
  WHERE ml.level = ?
3277
+ AND ${notActiveQuarantinedSql("e.metadata")}
2998
3278
  ORDER BY e.timestamp DESC
2999
3279
  LIMIT ? OFFSET ?`,
3000
3280
  [level, limit, offset]
@@ -3087,12 +3367,13 @@ var SQLiteEventStore = class {
3087
3367
  /**
3088
3368
  * Get most accessed memories (falls back to recent events if none accessed)
3089
3369
  */
3090
- async getMostAccessed(limit = 10) {
3370
+ async getMostAccessed(limit = 10, options) {
3091
3371
  await this.initialize();
3092
3372
  let rows = sqliteAll(
3093
3373
  this.db,
3094
3374
  `SELECT * FROM events
3095
3375
  WHERE access_count > 0
3376
+ AND ${maybeQuarantinePredicate(options)}
3096
3377
  ORDER BY access_count DESC, last_accessed_at DESC
3097
3378
  LIMIT ?`,
3098
3379
  [limit]
@@ -3101,6 +3382,7 @@ var SQLiteEventStore = class {
3101
3382
  rows = sqliteAll(
3102
3383
  this.db,
3103
3384
  `SELECT * FROM events
3385
+ WHERE ${maybeQuarantinePredicate(options)}
3104
3386
  ORDER BY timestamp DESC
3105
3387
  LIMIT ?`,
3106
3388
  [limit]
@@ -3231,6 +3513,7 @@ var SQLiteEventStore = class {
3231
3513
  FROM memory_helpfulness mh
3232
3514
  JOIN events e ON e.id = mh.event_id
3233
3515
  WHERE mh.measured_at IS NOT NULL
3516
+ AND ${notActiveQuarantinedSql("e.metadata")}
3234
3517
  GROUP BY mh.event_id
3235
3518
  ORDER BY avg_score DESC
3236
3519
  LIMIT ?`,
@@ -3295,6 +3578,7 @@ var SQLiteEventStore = class {
3295
3578
  FROM events_fts fts
3296
3579
  JOIN events e ON e.id = fts.event_id
3297
3580
  WHERE events_fts MATCH ?
3581
+ AND ${notActiveQuarantinedSql("e.metadata")}
3298
3582
  ORDER BY fts.rank
3299
3583
  LIMIT ?`,
3300
3584
  [searchTerms, limit]
@@ -3309,6 +3593,7 @@ var SQLiteEventStore = class {
3309
3593
  this.db,
3310
3594
  `SELECT *, 0 as rank FROM events
3311
3595
  WHERE content LIKE ?
3596
+ AND ${notActiveQuarantinedSql()}
3312
3597
  ORDER BY timestamp DESC
3313
3598
  LIMIT ?`,
3314
3599
  [likePattern, limit]
@@ -3515,6 +3800,7 @@ var SQLiteEventStore = class {
3515
3800
  `SELECT turn_id, MIN(timestamp) as min_ts
3516
3801
  FROM events
3517
3802
  WHERE session_id = ? AND turn_id IS NOT NULL
3803
+ AND ${maybeQuarantinePredicate(options)}
3518
3804
  GROUP BY turn_id
3519
3805
  ORDER BY min_ts DESC
3520
3806
  LIMIT ? OFFSET ?`,
@@ -3522,7 +3808,7 @@ var SQLiteEventStore = class {
3522
3808
  );
3523
3809
  const turns = [];
3524
3810
  for (const turnRow of turnRows) {
3525
- const events = await this.getEventsByTurn(turnRow.turn_id);
3811
+ const events = await this.getEventsByTurn(turnRow.turn_id, options);
3526
3812
  const promptEvent = events.find((e) => e.eventType === "user_prompt");
3527
3813
  const toolEvents = events.filter((e) => e.eventType === "tool_observation");
3528
3814
  const hasResponse = events.some((e) => e.eventType === "agent_response");
@@ -3541,11 +3827,11 @@ var SQLiteEventStore = class {
3541
3827
  /**
3542
3828
  * Get all events for a specific turn_id
3543
3829
  */
3544
- async getEventsByTurn(turnId) {
3830
+ async getEventsByTurn(turnId, options) {
3545
3831
  await this.initialize();
3546
3832
  const rows = sqliteAll(
3547
3833
  this.db,
3548
- `SELECT * FROM events WHERE turn_id = ? ORDER BY timestamp ASC`,
3834
+ `SELECT * FROM events WHERE turn_id = ? AND ${maybeQuarantinePredicate(options)} ORDER BY timestamp ASC`,
3549
3835
  [turnId]
3550
3836
  );
3551
3837
  return rows.map(this.rowToEvent);
@@ -3553,13 +3839,14 @@ var SQLiteEventStore = class {
3553
3839
  /**
3554
3840
  * Count total turns for a session
3555
3841
  */
3556
- async countSessionTurns(sessionId) {
3842
+ async countSessionTurns(sessionId, options) {
3557
3843
  await this.initialize();
3558
3844
  const row = sqliteGet(
3559
3845
  this.db,
3560
3846
  `SELECT COUNT(DISTINCT turn_id) as count
3561
3847
  FROM events
3562
- WHERE session_id = ? AND turn_id IS NOT NULL`,
3848
+ WHERE session_id = ? AND turn_id IS NOT NULL
3849
+ AND ${maybeQuarantinePredicate(options)}`,
3563
3850
  [sessionId]
3564
3851
  );
3565
3852
  return row?.count || 0;
@@ -3651,7 +3938,7 @@ var SQLiteEventStore = class {
3651
3938
  content: row.content,
3652
3939
  canonicalKey: row.canonical_key,
3653
3940
  dedupeKey: row.dedupe_key,
3654
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0
3941
+ metadata: safeParseMetadataValue(row.metadata)
3655
3942
  };
3656
3943
  if (row.access_count !== void 0) {
3657
3944
  event.access_count = row.access_count;