@remnic/core 9.3.755 → 9.3.756

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.
Files changed (181) hide show
  1. package/dist/access-admin-ops-surface.d.ts +1 -1
  2. package/dist/access-admin-ops-surface.js +34 -34
  3. package/dist/access-boundary.d.ts +1 -1
  4. package/dist/access-boundary.js +35 -35
  5. package/dist/access-cli.js +83 -83
  6. package/dist/access-http.d.ts +1 -1
  7. package/dist/access-http.js +44 -44
  8. package/dist/access-lcm-surface.d.ts +1 -1
  9. package/dist/access-lcm-surface.js +34 -34
  10. package/dist/access-mcp.d.ts +1 -1
  11. package/dist/access-mcp.js +41 -41
  12. package/dist/access-observe-write-surface.d.ts +225 -0
  13. package/dist/access-observe-write-surface.js +128 -0
  14. package/dist/access-operations-batch.js +38 -38
  15. package/dist/access-operations.d.ts +1 -1
  16. package/dist/access-operations.js +40 -40
  17. package/dist/access-recall-surface.d.ts +1 -1
  18. package/dist/access-recall-surface.js +34 -34
  19. package/dist/access-schema.d.ts +2 -2
  20. package/dist/access-schema.js +3 -3
  21. package/dist/{access-service-C3jkLi2U.d.ts → access-service-CJwcDoQp.d.ts} +4 -5
  22. package/dist/access-service.d.ts +1 -1
  23. package/dist/access-service.js +34 -34
  24. package/dist/access-surface-catalog.d.ts +1 -1
  25. package/dist/active-recall.js +1 -1
  26. package/dist/adapters/index.js +4 -4
  27. package/dist/adapters/registry.js +2 -2
  28. package/dist/{auto-sync-G4YKBPOU.js → auto-sync-RZO5ZPHR.js} +3 -3
  29. package/dist/briefing.js +7 -7
  30. package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-FFWJ4X4A.js} +2 -2
  31. package/dist/causal-behavior.js +2 -2
  32. package/dist/causal-chain.js +2 -2
  33. package/dist/causal-consolidation.js +15 -15
  34. package/dist/causal-retrieval.js +2 -2
  35. package/dist/causal-trajectory.js +1 -1
  36. package/dist/{chunk-M5QKGHCR.js → chunk-3I2IASNK.js} +2 -2
  37. package/dist/{chunk-OCFITZRV.js → chunk-4AC57RCM.js} +4 -4
  38. package/dist/{chunk-3DOD6ICJ.js → chunk-4I45UZ4V.js} +5 -5
  39. package/dist/{chunk-TAPSNIMA.js → chunk-5CEJH5ZN.js} +4 -4
  40. package/dist/{chunk-I6IAJCJS.js → chunk-5VWRYFQE.js} +7 -7
  41. package/dist/{chunk-4LQUYBZM.js → chunk-5ZKT7WTR.js} +7 -7
  42. package/dist/{chunk-VL5QHTJI.js → chunk-6DA5BBLS.js} +117 -117
  43. package/dist/{chunk-LFKEFIJA.js → chunk-6DXXIXED.js} +4 -4
  44. package/dist/{chunk-XBM5DG4O.js → chunk-7APVFQ34.js} +680 -595
  45. package/dist/chunk-7APVFQ34.js.map +1 -0
  46. package/dist/{chunk-NCNOYM6F.js → chunk-7CFGXWYX.js} +2 -2
  47. package/dist/{chunk-TJATYB3F.js → chunk-A6XCHI53.js} +5 -5
  48. package/dist/{chunk-UQJ6YTKR.js → chunk-BTH6MQJT.js} +7 -7
  49. package/dist/{chunk-G7LCG56O.js → chunk-CXW6Y7SI.js} +4 -4
  50. package/dist/{chunk-GCEV6SXP.js → chunk-DWDDQLSZ.js} +2298 -2218
  51. package/dist/chunk-DWDDQLSZ.js.map +1 -0
  52. package/dist/{chunk-DS4QM6I7.js → chunk-EEKDRAWX.js} +4 -4
  53. package/dist/{chunk-I3UWEZTK.js → chunk-EF3VFQC4.js} +2 -2
  54. package/dist/{chunk-437S3G37.js → chunk-EJJQDP3H.js} +1 -1
  55. package/dist/{chunk-FQLM6ZUP.js → chunk-EVVLGYHR.js} +3 -3
  56. package/dist/{chunk-GGYUR4M2.js → chunk-GBXP62CL.js} +10 -10
  57. package/dist/{chunk-N4AGSRVA.js → chunk-GG6G2RYR.js} +9 -9
  58. package/dist/{chunk-JITAVROY.js → chunk-H2T4OJNI.js} +4 -4
  59. package/dist/{chunk-2MR3MFQB.js → chunk-JTYM4HPA.js} +2 -2
  60. package/dist/{chunk-LD53WPMU.js → chunk-LQ6JI4VH.js} +4 -4
  61. package/dist/{chunk-GPFXPC2V.js → chunk-NG2NHB4C.js} +7 -7
  62. package/dist/{chunk-CIDGUFAL.js → chunk-NLXKSM46.js} +2 -2
  63. package/dist/{chunk-T6WHI3JW.js → chunk-OLAVQS6S.js} +6 -6
  64. package/dist/{chunk-ER3CCWK2.js → chunk-ORAK3LNZ.js} +2 -2
  65. package/dist/{chunk-PJ7DIEFC.js → chunk-PBZQVVLG.js} +5 -5
  66. package/dist/{chunk-PJQMISPD.js → chunk-PPAEJXVF.js} +5 -5
  67. package/dist/{chunk-GMWKWA6N.js → chunk-PRX77NPC.js} +5 -5
  68. package/dist/{chunk-24463PIV.js → chunk-PS572XYO.js} +2 -2
  69. package/dist/{chunk-KKOCFUI5.js → chunk-REZ4TUD2.js} +10 -10
  70. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  71. package/dist/{chunk-V5UV7BOA.js → chunk-SQNED75R.js} +1 -1
  72. package/dist/{chunk-FLVL4L3L.js → chunk-TCKP4EHY.js} +2 -2
  73. package/dist/{chunk-SP42M3XP.js → chunk-TXV3I2O6.js} +7 -7
  74. package/dist/{chunk-S6UCPBPL.js → chunk-UNV6ZGTS.js} +2 -2
  75. package/dist/{chunk-2BFLDH5F.js → chunk-WEPWY4HB.js} +2 -2
  76. package/dist/{chunk-YQGHW5ML.js → chunk-WRFKZEO6.js} +4 -4
  77. package/dist/{chunk-XCXN3D2U.js → chunk-Y4TAFMUE.js} +6 -6
  78. package/dist/{chunk-H6S3DJVE.js → chunk-YMGQXBPS.js} +2 -2
  79. package/dist/{chunk-T422SYM6.js → chunk-ZUDM75KG.js} +5 -5
  80. package/dist/{chunk-XD6JVK5P.js → chunk-ZUHD6XZZ.js} +43 -43
  81. package/dist/{chunk-FOJY75BL.js → chunk-ZW5SZ3TH.js} +12 -12
  82. package/dist/{cli-BJQ2ahrh.d.ts → cli-cZyANcmY.d.ts} +1 -1
  83. package/dist/cli.d.ts +2 -2
  84. package/dist/cli.js +59 -59
  85. package/dist/compounding/engine.js +8 -8
  86. package/dist/config.js +1 -1
  87. package/dist/connectors/codex-materialize-runner.js +8 -8
  88. package/dist/connectors/index.js +9 -9
  89. package/dist/contradiction/index.js +2 -2
  90. package/dist/{contradiction-scan-2SODRNUF.js → contradiction-scan-3FJYWS3G.js} +3 -3
  91. package/dist/contradiction-scan-3FJYWS3G.js.map +1 -0
  92. package/dist/conversation-index/backend.js +2 -2
  93. package/dist/entity-retrieval.js +8 -8
  94. package/dist/extraction.js +2 -2
  95. package/dist/index.d.ts +2 -2
  96. package/dist/index.js +126 -126
  97. package/dist/lcm/engine.js +2 -2
  98. package/dist/lcm/index.js +2 -2
  99. package/dist/maintenance/memory-governance.js +8 -8
  100. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
  101. package/dist/maintenance/rebuild-memory-projection.js +9 -9
  102. package/dist/mcp-memory-inspector-app.d.ts +1 -1
  103. package/dist/namespaces/migrate.js +18 -18
  104. package/dist/namespaces/search.js +9 -9
  105. package/dist/namespaces/storage.js +9 -9
  106. package/dist/operator-toolkit.js +24 -24
  107. package/dist/orchestration/maintenance.js +12 -12
  108. package/dist/orchestrator.js +82 -82
  109. package/dist/resume-bundles.js +2 -2
  110. package/dist/schemas.d.ts +10 -10
  111. package/dist/search/factory.js +8 -8
  112. package/dist/search/index.js +8 -8
  113. package/dist/search/lancedb-backend.js +2 -2
  114. package/dist/search/meilisearch-backend.js +2 -2
  115. package/dist/search/orama-backend.js +2 -2
  116. package/dist/semantic-consolidation.js +11 -11
  117. package/dist/semantic-rule-promotion.js +7 -7
  118. package/dist/semantic-rule-verifier.js +8 -8
  119. package/dist/shared-context/manager.d.ts +2 -2
  120. package/dist/storage.d.ts +60 -78
  121. package/dist/storage.js +14 -6
  122. package/dist/summarizer.js +2 -2
  123. package/dist/transfer/backup.js +2 -2
  124. package/dist/transfer/capsule-export.js +2 -2
  125. package/dist/transfer/capsule-import.js +2 -2
  126. package/dist/transfer/types.d.ts +12 -12
  127. package/dist/verified-recall.js +8 -8
  128. package/package.json +2 -2
  129. package/src/access-observe-write-surface.ts +1007 -0
  130. package/src/access-service.ts +48 -794
  131. package/src/orchestration/self-deps.ts +9 -1
  132. package/src/storage/entity-store.ts +4 -2
  133. package/src/storage/memory-read-store.ts +756 -0
  134. package/src/storage.ts +67 -627
  135. package/dist/chunk-GCEV6SXP.js.map +0 -1
  136. package/dist/chunk-XBM5DG4O.js.map +0 -1
  137. /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-observe-write-surface.js.map} +0 -0
  138. /package/dist/{auto-sync-G4YKBPOU.js.map → auto-sync-RZO5ZPHR.js.map} +0 -0
  139. /package/dist/{contradiction-scan-2SODRNUF.js.map → capsule-crypto-FFWJ4X4A.js.map} +0 -0
  140. /package/dist/{chunk-M5QKGHCR.js.map → chunk-3I2IASNK.js.map} +0 -0
  141. /package/dist/{chunk-OCFITZRV.js.map → chunk-4AC57RCM.js.map} +0 -0
  142. /package/dist/{chunk-3DOD6ICJ.js.map → chunk-4I45UZ4V.js.map} +0 -0
  143. /package/dist/{chunk-TAPSNIMA.js.map → chunk-5CEJH5ZN.js.map} +0 -0
  144. /package/dist/{chunk-I6IAJCJS.js.map → chunk-5VWRYFQE.js.map} +0 -0
  145. /package/dist/{chunk-4LQUYBZM.js.map → chunk-5ZKT7WTR.js.map} +0 -0
  146. /package/dist/{chunk-VL5QHTJI.js.map → chunk-6DA5BBLS.js.map} +0 -0
  147. /package/dist/{chunk-LFKEFIJA.js.map → chunk-6DXXIXED.js.map} +0 -0
  148. /package/dist/{chunk-NCNOYM6F.js.map → chunk-7CFGXWYX.js.map} +0 -0
  149. /package/dist/{chunk-TJATYB3F.js.map → chunk-A6XCHI53.js.map} +0 -0
  150. /package/dist/{chunk-UQJ6YTKR.js.map → chunk-BTH6MQJT.js.map} +0 -0
  151. /package/dist/{chunk-G7LCG56O.js.map → chunk-CXW6Y7SI.js.map} +0 -0
  152. /package/dist/{chunk-DS4QM6I7.js.map → chunk-EEKDRAWX.js.map} +0 -0
  153. /package/dist/{chunk-I3UWEZTK.js.map → chunk-EF3VFQC4.js.map} +0 -0
  154. /package/dist/{chunk-437S3G37.js.map → chunk-EJJQDP3H.js.map} +0 -0
  155. /package/dist/{chunk-FQLM6ZUP.js.map → chunk-EVVLGYHR.js.map} +0 -0
  156. /package/dist/{chunk-GGYUR4M2.js.map → chunk-GBXP62CL.js.map} +0 -0
  157. /package/dist/{chunk-N4AGSRVA.js.map → chunk-GG6G2RYR.js.map} +0 -0
  158. /package/dist/{chunk-JITAVROY.js.map → chunk-H2T4OJNI.js.map} +0 -0
  159. /package/dist/{chunk-2MR3MFQB.js.map → chunk-JTYM4HPA.js.map} +0 -0
  160. /package/dist/{chunk-LD53WPMU.js.map → chunk-LQ6JI4VH.js.map} +0 -0
  161. /package/dist/{chunk-GPFXPC2V.js.map → chunk-NG2NHB4C.js.map} +0 -0
  162. /package/dist/{chunk-CIDGUFAL.js.map → chunk-NLXKSM46.js.map} +0 -0
  163. /package/dist/{chunk-T6WHI3JW.js.map → chunk-OLAVQS6S.js.map} +0 -0
  164. /package/dist/{chunk-ER3CCWK2.js.map → chunk-ORAK3LNZ.js.map} +0 -0
  165. /package/dist/{chunk-PJ7DIEFC.js.map → chunk-PBZQVVLG.js.map} +0 -0
  166. /package/dist/{chunk-PJQMISPD.js.map → chunk-PPAEJXVF.js.map} +0 -0
  167. /package/dist/{chunk-GMWKWA6N.js.map → chunk-PRX77NPC.js.map} +0 -0
  168. /package/dist/{chunk-24463PIV.js.map → chunk-PS572XYO.js.map} +0 -0
  169. /package/dist/{chunk-KKOCFUI5.js.map → chunk-REZ4TUD2.js.map} +0 -0
  170. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  171. /package/dist/{chunk-V5UV7BOA.js.map → chunk-SQNED75R.js.map} +0 -0
  172. /package/dist/{chunk-FLVL4L3L.js.map → chunk-TCKP4EHY.js.map} +0 -0
  173. /package/dist/{chunk-SP42M3XP.js.map → chunk-TXV3I2O6.js.map} +0 -0
  174. /package/dist/{chunk-S6UCPBPL.js.map → chunk-UNV6ZGTS.js.map} +0 -0
  175. /package/dist/{chunk-2BFLDH5F.js.map → chunk-WEPWY4HB.js.map} +0 -0
  176. /package/dist/{chunk-YQGHW5ML.js.map → chunk-WRFKZEO6.js.map} +0 -0
  177. /package/dist/{chunk-XCXN3D2U.js.map → chunk-Y4TAFMUE.js.map} +0 -0
  178. /package/dist/{chunk-H6S3DJVE.js.map → chunk-YMGQXBPS.js.map} +0 -0
  179. /package/dist/{chunk-T422SYM6.js.map → chunk-ZUDM75KG.js.map} +0 -0
  180. /package/dist/{chunk-XD6JVK5P.js.map → chunk-ZUHD6XZZ.js.map} +0 -0
  181. /package/dist/{chunk-FOJY75BL.js.map → chunk-ZW5SZ3TH.js.map} +0 -0
package/src/storage.ts CHANGED
@@ -4,8 +4,9 @@ import { createHash } from "node:crypto"
4
4
  import { normalizeContent, computeContentHash } from "./content-hash.js";;
5
5
  import path from "node:path";
6
6
  import { log } from "./logger.js";
7
- import { EntityStore } from "./storage/entity-store.js";
7
+ import { MemoryReadStore } from "./storage/memory-read-store.js";
8
8
  import { selfDeps } from "./orchestration/self-deps.js";
9
+ import { EntityStore } from "./storage/entity-store.js";
9
10
  import { isErrnoCode } from "./utils/errno.js";
10
11
  import { RECALL_FALLBACK_DIRS, getCategoryDir, categoryDirName } from "./utils/category-dir.js";
11
12
  import { assertPathInsideRoot } from "./utils/path-containment.js";
@@ -521,7 +522,7 @@ function parseLinkReasonValue(rawValue: string): string {
521
522
  }
522
523
  }
523
524
 
524
- function parseFrontmatterStringValue(rawValue: string | undefined): string | undefined {
525
+ export function parseFrontmatterStringValue(rawValue: string | undefined): string | undefined {
525
526
  if (rawValue === undefined) return undefined;
526
527
  const trimmed = rawValue.trim();
527
528
  if (trimmed.length === 0) return undefined;
@@ -567,7 +568,7 @@ function parseReinforcementCountField(raw: string | undefined): number | undefin
567
568
  return n;
568
569
  }
569
570
 
570
- function parseFrontmatter(
571
+ export function parseFrontmatter(
571
572
  raw: string,
572
573
  ): { frontmatter: MemoryFrontmatter; content: string } | null {
573
574
  const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
@@ -954,7 +955,7 @@ function inferEntityTypeFromFilename(pathRel: string): string | undefined {
954
955
  return KNOWN_ENTITY_FILENAME_PREFIXES.has(candidate) ? candidate : undefined;
955
956
  }
956
957
 
957
- function normalizeFrontmatterForPath(frontmatter: MemoryFrontmatter, pathRel: string, content: string = ""): MemoryFrontmatter {
958
+ export function normalizeFrontmatterForPath(frontmatter: MemoryFrontmatter, pathRel: string, content: string = ""): MemoryFrontmatter {
958
959
  const normalizedPath = pathRel.split(path.sep).join("/");
959
960
  let normalizedFrontmatter = frontmatter;
960
961
 
@@ -2222,7 +2223,7 @@ export function buildEntitySchemaCacheKey(entitySchemas?: PluginConfig["entitySc
2222
2223
  * downstream report requires at read time so the limit semantics and
2223
2224
  * the distribution semantics stay consistent.
2224
2225
  */
2225
- function isValidBufferSurpriseEvent(value: unknown): value is BufferSurpriseEvent {
2226
+ export function isValidBufferSurpriseEvent(value: unknown): value is BufferSurpriseEvent {
2226
2227
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2227
2228
  const v = value as Record<string, unknown>;
2228
2229
  if (v.event !== "BUFFER_SURPRISE") return false;
@@ -2264,7 +2265,8 @@ export class StorageManager {
2264
2265
  private artifactIndexCache: { memories: MemoryFile[]; loadedAtMs: number; writeVersion: number } | null = null;
2265
2266
  /** Read by storage/entity-store.ts (decomposition), hence not `private`. */
2266
2267
  static readonly KNOWLEDGE_INDEX_CACHE_TTL_MS = 600_000; // 10 minutes (entity mutations invalidate)
2267
- private static readonly ARTIFACT_INDEX_CACHE_TTL_MS = 60_000; // 1 minute
2268
+ /** Read by storage/memory-read-store.ts (decomposition). */
2269
+ static readonly ARTIFACT_INDEX_CACHE_TTL_MS = 60_000; // 1 minute
2268
2270
  private static readonly artifactWriteVersionByDir = new Map<string, number>();
2269
2271
  private static readonly memoryStatusVersionByDir = new Map<string, number>();
2270
2272
  private static readonly secureStoreEntityCacheKeyIds = new WeakMap<Buffer, number>();
@@ -2293,14 +2295,18 @@ export class StorageManager {
2293
2295
  // on every cold-tier write, making the cache correct across process boundaries
2294
2296
  // (gateway + CLI). After Finding UTsP broadened the scan to the entire cold/
2295
2297
  // subtree, amortizing across back-to-back writes is even more important.
2296
- private static readonly COLD_SCAN_CACHE_TTL_MS = 30_000; // 30 seconds
2297
- private static readonly coldMemoriesCache = new Map<string, { memories: MemoryFile[]; loadedAt: number; coldVersion: number }>();
2298
+ /** Read by storage/memory-read-store.ts (decomposition). */
2299
+ static readonly COLD_SCAN_CACHE_TTL_MS = 30_000; // 30 seconds
2300
+ /** Read by storage/memory-read-store.ts (decomposition). */
2301
+ static readonly coldMemoriesCache = new Map<string, { memories: MemoryFile[]; loadedAt: number; coldVersion: number }>();
2298
2302
 
2299
2303
  // Cache for readQuestions() — avoids serially re-reading tens of thousands of
2300
2304
  // question files on every recall. 60-second TTL is intentionally short so that
2301
2305
  // newly written questions surface quickly.
2302
- private static readonly QUESTIONS_CACHE_TTL_MS = 60_000; // 1 minute
2303
- private static readonly questionsCache = new Map<
2306
+ /** Read by storage/memory-read-store.ts (decomposition). */
2307
+ static readonly QUESTIONS_CACHE_TTL_MS = 60_000; // 1 minute
2308
+ /** Read by storage/memory-read-store.ts (decomposition). */
2309
+ static readonly questionsCache = new Map<
2304
2310
  string,
2305
2311
  {
2306
2312
  questions: Array<{
@@ -2516,7 +2522,32 @@ export class StorageManager {
2516
2522
  this.loadAliasesSync();
2517
2523
  }
2518
2524
 
2525
+ /**
2526
+ * The LIVE class object of this instance (issue #1809 review of the
2527
+ * storage decomposition): extracted store modules must read/write the
2528
+ * shared static caches (questionsCache, coldMemoriesCache, ...) through
2529
+ * the SAME class binding the host methods use. Referencing the imported
2530
+ * `StorageManager` symbol from a split bundle chunk can yield a second
2531
+ * class copy with its own statics, split-braining the caches
2532
+ * (resolveQuestion clears copy A while readQuestions reads copy B).
2533
+ */
2534
+ get storageManagerClass(): typeof StorageManager {
2535
+ return this.constructor as typeof StorageManager;
2536
+ }
2537
+
2519
2538
  /** The root directory of this storage instance. */
2539
+ /** MemoryReadStore (storage.ts decomposition). Lazy; selfDeps live wiring. */
2540
+ private _memoryReadStore: MemoryReadStore | undefined;
2541
+
2542
+ private get memoryReadStore(): MemoryReadStore {
2543
+ if (!this._memoryReadStore) {
2544
+ this._memoryReadStore = new MemoryReadStore(
2545
+ selfDeps<ConstructorParameters<typeof MemoryReadStore>[0]>(this),
2546
+ );
2547
+ }
2548
+ return this._memoryReadStore;
2549
+ }
2550
+
2520
2551
  /** EntityStore (storage.ts decomposition). Lazy; selfDeps live wiring. */
2521
2552
  private _entityStore: EntityStore | undefined;
2522
2553
 
@@ -2660,52 +2691,12 @@ export class StorageManager {
2660
2691
  }
2661
2692
  }
2662
2693
 
2663
- /**
2664
- * List stored transcript days, newest first, optionally scoped to one
2665
- * source. Non-transcript files in the tree are ignored.
2666
- */
2667
2694
  async listWearableTranscriptDays(
2668
2695
  sourceId?: string,
2669
2696
  ): Promise<Array<{ source: string; date: string }>> {
2670
- const days: Array<{ source: string; date: string }> = [];
2671
- let sources: string[];
2672
- if (sourceId !== undefined) {
2673
- sources = [sourceId];
2674
- } else {
2675
- try {
2676
- const entries = await readdir(this.wearablesDir, { withFileTypes: true });
2677
- sources = entries
2678
- .filter((entry) => entry.isDirectory())
2679
- .map((entry) => entry.name);
2680
- } catch (err) {
2681
- if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
2682
- throw err;
2683
- }
2684
- }
2685
- for (const source of sources) {
2686
- if (!/^[a-z][a-z0-9-]{0,63}$/.test(source)) continue;
2687
- let entries: string[];
2688
- try {
2689
- entries = await readdir(path.join(this.wearablesDir, source));
2690
- } catch (err) {
2691
- if ((err as NodeJS.ErrnoException).code === "ENOENT") continue;
2692
- throw err;
2693
- }
2694
- for (const entry of entries) {
2695
- if (!entry.endsWith(".md")) continue;
2696
- const date = entry.slice(0, -3);
2697
- if (!isValidTranscriptDate(date)) continue;
2698
- days.push({ source, date });
2699
- }
2700
- }
2701
- days.sort((a, b) => {
2702
- if (a.date > b.date) return -1;
2703
- if (a.date < b.date) return 1;
2704
- if (a.source < b.source) return -1;
2705
- if (a.source > b.source) return 1;
2706
- return 0;
2707
- });
2708
- return days;
2697
+ return this.memoryReadStore.listWearableTranscriptDays(
2698
+ sourceId,
2699
+ );
2709
2700
  }
2710
2701
 
2711
2702
  /**
@@ -4044,55 +4035,8 @@ export class StorageManager {
4044
4035
  }
4045
4036
 
4046
4037
  private async readAllArtifactsCached(): Promise<MemoryFile[]> {
4047
- if (
4048
- this.artifactIndexCache &&
4049
- Date.now() - this.artifactIndexCache.loadedAtMs <= StorageManager.ARTIFACT_INDEX_CACHE_TTL_MS &&
4050
- this.artifactIndexCache.writeVersion === this.getArtifactWriteVersion()
4051
- ) {
4052
- return this.artifactIndexCache.memories;
4053
- }
4054
-
4055
- const scanArtifacts = async (): Promise<MemoryFile[]> => {
4056
- const artifacts: MemoryFile[] = [];
4057
- const readDir = async (dir: string) => {
4058
- try {
4059
- const entries = await readdir(dir, { withFileTypes: true });
4060
- for (const entry of entries) {
4061
- const fullPath = path.join(dir, entry.name);
4062
- if (entry.isDirectory()) {
4063
- await readDir(fullPath);
4064
- continue;
4065
- }
4066
- if (!entry.name.endsWith(".md")) continue;
4067
- const memory = await this.readMemoryByPath(fullPath);
4068
- if (!memory) continue;
4069
- artifacts.push(memory);
4070
- }
4071
- } catch {
4072
- // Directory doesn't exist yet
4073
- }
4074
- };
4075
- await readDir(this.artifactsDir);
4076
- return artifacts;
4077
- };
4078
-
4079
- const MAX_REBUILD_RETRIES = 2;
4080
- let latestArtifacts: MemoryFile[] = [];
4081
- for (let attempt = 0; attempt <= MAX_REBUILD_RETRIES; attempt += 1) {
4082
- const versionBefore = this.getArtifactWriteVersion();
4083
- const artifacts = await scanArtifacts();
4084
- const versionAfter = this.getArtifactWriteVersion();
4085
- latestArtifacts = artifacts;
4086
- if (versionAfter === versionBefore) {
4087
- this.artifactIndexCache = { memories: artifacts, loadedAtMs: Date.now(), writeVersion: versionAfter };
4088
- return artifacts;
4089
- }
4090
- }
4091
-
4092
- // Highly concurrent writer churn; keep cache invalid so next read retries a clean rebuild.
4093
- // Return best-effort latest scan instead of an empty set to avoid dropping recall entirely.
4094
- this.artifactIndexCache = null;
4095
- return latestArtifacts;
4038
+ return this.memoryReadStore.readAllArtifactsCached(
4039
+ );
4096
4040
  }
4097
4041
 
4098
4042
  async searchArtifacts(query: string, maxResults: number): Promise<MemoryFile[]> {
@@ -4363,98 +4307,8 @@ export class StorageManager {
4363
4307
  }
4364
4308
 
4365
4309
  private async collectActiveMemoryPaths(): Promise<string[]> {
4366
- const filePaths: string[] = [];
4367
-
4368
- // Resolve the memory root once for containment checks below. A category dir
4369
- // symlinked outside memoryDir (e.g. decisions/ -> an external dir) must NOT
4370
- // pull out-of-store files into the QMD-unavailable recall fallback (info
4371
- // leak). Same walker-hardening pattern as document-scanner.ts / cli.ts /
4372
- // consolidation-provenance-check.ts; reuses the shared containment helper.
4373
- let memoryRootReal: string;
4374
- try {
4375
- memoryRootReal = await realpath(this.baseDir);
4376
- } catch {
4377
- return filePaths;
4378
- }
4379
-
4380
- const collectPaths = async (dir: string) => {
4381
- // Directory-level guard, isolated from per-entry handling: skip symlinked
4382
- // or non-directory category dirs and assert the resolved dir stays inside
4383
- // the memory root before reading. A failure here means the whole subtree
4384
- // does not exist or escaped the store — fail closed by skipping it.
4385
- let entries: Dirent[];
4386
- try {
4387
- const dirStat = await lstat(dir);
4388
- if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) return;
4389
- assertPathInsideRoot(memoryRootReal, await realpath(dir), dir);
4390
- entries = await readdir(dir, { withFileTypes: true });
4391
- } catch {
4392
- return;
4393
- }
4394
-
4395
- const subdirs: string[] = [];
4396
- for (const entry of entries) {
4397
- // Never follow symlinked entries out of the store.
4398
- if (entry.isSymbolicLink()) continue;
4399
- const fullPath = path.join(dir, entry.name);
4400
- if (entry.isDirectory()) {
4401
- subdirs.push(fullPath);
4402
- } else if (entry.name.endsWith(".md")) {
4403
- // Isolate per-entry failures in their own try/catch: a containment or
4404
- // realpath failure on ONE .md entry must not drop sibling files or,
4405
- // crucially, the deferred subdir recursion below (Cursor Bugbot:
4406
- // "Poisoned md skips sibling subdirs"). Mirrors the per-file try/catch
4407
- // in search/document-scanner.ts scanDir and
4408
- // consolidation-provenance-check.ts walkMarkdownFiles.
4409
- try {
4410
- assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);
4411
- filePaths.push(fullPath);
4412
- } catch {
4413
- // Skip just this entry (symlink/containment/realpath failure).
4414
- }
4415
- }
4416
- }
4417
- // Recurse into real subdirectories regardless of any single poisoned entry
4418
- // above, so valid nested in-store memories are never dropped.
4419
- for (const subdir of subdirs) {
4420
- await collectPaths(subdir);
4421
- }
4422
- };
4423
-
4424
- // Scan EVERY supported memory category directory, not just the legacy four
4425
- // (facts/procedures/reasoning-traces/corrections). Issue #1497: the QMD
4426
- // filesystem-fallback recall path (orchestrator `recent_scan` ->
4427
- // readAllMemoriesForNamespaces -> readAllMemories -> here) must read every
4428
- // recall category dir so on-disk memories in preferences/decisions/moments/
4429
- // commitments/principles/rules/skills/relationships are not missed when QMD
4430
- // is disabled, missing, or unhealthy. RECALL_FALLBACK_DIRS is the single
4431
- // source of truth derived from ALL_CATEGORY_DIRS (shared with
4432
- // ensureDirectories() and the write routing in utils/category-dir.ts).
4433
- // These paths resolve identically to the legacy this.factsDir /
4434
- // this.correctionsDir / this.proceduresDir / this.reasoningTracesDir
4435
- // getters (all `path.join(this.baseDir, <dir>)`), so the scan stays
4436
- // namespace-aware: this.baseDir is per-namespace, set by the storage router.
4437
- // Deliberately EXCLUDED (issue #1497 + PR #1503 review): the non-category
4438
- // content dirs that ensureDirectories() also creates — entities/, state/,
4439
- // artifacts/, identity/, config/ — plus the root profile.md, AND the
4440
- // questions/ queue dir. questions/ holds operational question-QUEUE items
4441
- // written by writeQuestion() (frontmatter `{ id, created, priority,
4442
- // resolved }`), read only via readQuestions() and surfaced through the
4443
- // dedicated, disabled-by-default `injectQuestions` recall-pipeline stage —
4444
- // never as standard recall memories. The QMD primary recall corpus does not
4445
- // include them, so the fallback must not either (corpus parity; CLAUDE.md
4446
- // rule #39). Were questions/ scanned here, parseFrontmatter() would accept
4447
- // those files (they have a `---` frontmatter block) and leak queue items
4448
- // into recall. None of these excluded dirs are in RECALL_FALLBACK_DIRS; the
4449
- // exclusion is asserted by tests in storage-fallback-category-dirs.test.ts.
4450
- //
4451
- // collectPaths() already ignores missing dirs (try/catch) and the parser
4452
- // returns null for non-memory markdown, so unrelated files never crash the
4453
- // scan.
4454
- for (const dir of RECALL_FALLBACK_DIRS) {
4455
- await collectPaths(path.join(this.baseDir, dir));
4456
- }
4457
- return filePaths;
4310
+ return this.memoryReadStore.collectActiveMemoryPaths(
4311
+ );
4458
4312
  }
4459
4313
 
4460
4314
  private async readParsedMemoriesFromPaths(
@@ -4598,52 +4452,9 @@ export class StorageManager {
4598
4452
  batchSize?: number;
4599
4453
  updatedAfter?: Date;
4600
4454
  } = {}): Promise<{ memories: MemoryFile[]; filePaths: string[] }> {
4601
- const allPaths = await this.collectActiveMemoryPaths();
4602
- const sortedPaths = this.orderWindowPaths(allPaths);
4603
- const maxMemories =
4604
- typeof options.maxMemories === "number" && Number.isFinite(options.maxMemories)
4605
- ? Math.max(1, Math.floor(options.maxMemories))
4606
- : undefined;
4607
- const maxCandidatePaths = maxMemories === undefined ? undefined : maxMemories * 2;
4608
- const updatedAfterMs = options.updatedAfter?.getTime();
4609
- const normalizedBatchSize = this.normalizeMemoryReadBatchSize(options.batchSize);
4610
- const memories: MemoryFile[] = [];
4611
- const selectedPaths: string[] = [];
4612
-
4613
- for (let i = 0; i < sortedPaths.length; i += normalizedBatchSize) {
4614
- if (
4615
- maxMemories !== undefined
4616
- && (memories.length >= maxMemories || (maxCandidatePaths !== undefined && selectedPaths.length >= maxCandidatePaths))
4617
- ) {
4618
- return { memories, filePaths: selectedPaths };
4619
- }
4620
- const batchPaths = sortedPaths.slice(i, i + normalizedBatchSize);
4621
- const candidateBatchPaths = updatedAfterMs === undefined
4622
- ? batchPaths
4623
- : await this.filterWindowPathsByUpdatedAfter(batchPaths, updatedAfterMs);
4624
- const remainingSlots = maxMemories === undefined ? undefined : Math.max(0, maxMemories - memories.length);
4625
- const remainingInspectionBudget = maxCandidatePaths === undefined ? undefined : Math.max(0, maxCandidatePaths - selectedPaths.length);
4626
- const { memories: batchMemories, filePaths: parsedCandidatePaths } = remainingSlots === undefined
4627
- ? {
4628
- memories: await this.readParsedMemoriesFromPaths(candidateBatchPaths, normalizedBatchSize),
4629
- filePaths: candidateBatchPaths,
4630
- }
4631
- : await this.readWindowBoundedBatch(
4632
- candidateBatchPaths,
4633
- remainingSlots,
4634
- remainingInspectionBudget ?? remainingSlots,
4635
- normalizedBatchSize,
4636
- );
4637
- selectedPaths.push(...parsedCandidatePaths);
4638
- for (const memory of batchMemories) {
4639
- memories.push(memory);
4640
- if (maxMemories !== undefined && memories.length >= maxMemories) {
4641
- return { memories, filePaths: selectedPaths };
4642
- }
4643
- }
4644
- }
4645
-
4646
- return { memories, filePaths: selectedPaths };
4455
+ return this.memoryReadStore.readMemoriesWindow(
4456
+ options,
4457
+ );
4647
4458
  }
4648
4459
 
4649
4460
  private async _readAllMemoriesFromDisk(): Promise<MemoryFile[]> {
@@ -4651,85 +4462,9 @@ export class StorageManager {
4651
4462
  return this.readParsedMemoriesFromPaths(filePaths, 50);
4652
4463
  }
4653
4464
 
4654
- /**
4655
- * Read all memories from the cold tier by scanning the entire cold/ root
4656
- * tree. Previously this only scanned cold/facts/ and cold/corrections/, but
4657
- * structuredAttributes can appear on any MemoryCategory (preference, decision,
4658
- * entity, etc.). buildTierMemoryPath now routes each category to its own
4659
- * cold/<dir>/ subtree via the shared categoryDirName() chokepoint (issue
4660
- * #1546), so cold decisions/preferences/... live outside cold/facts/.
4661
- * Scanning the full coldRoot covers every category dir and guards against
4662
- * files placed in unexpected subdirectories during manual operations or future
4663
- * refactors.
4664
- *
4665
- * Broadened in PR #402 round-6 (Finding UTsP): scanning only facts/ and
4666
- * corrections/ was a narrower-than-necessary subset of the cold directory
4667
- * tree. Correctness trumps the minor performance difference — cold scans
4668
- * already happen at most once per supersession write.
4669
- *
4670
- * Used by applyTemporalSupersession so that memories already demoted to
4671
- * cold/ can still be marked superseded when a newer hot fact arrives.
4672
- *
4673
- * Cached with a TTL (Finding UOGi, PR #402 round-6): back-to-back
4674
- * structured-attribute writes in the same burst reuse the cached result
4675
- * instead of re-scanning the cold tree on every call. The cache is
4676
- * invalidated whenever a write calls invalidateAllMemoriesCache() (which
4677
- * covers any hot→cold demotion that changes cold-tier contents) and
4678
- * expires after COLD_SCAN_CACHE_TTL_MS as a safety net.
4679
- */
4680
4465
  async readAllColdMemories(): Promise<MemoryFile[]> {
4681
- const coldRoot = this.resolveTierRootDir("cold");
4682
-
4683
- // Read the on-disk cold-version sentinel BEFORE checking the cache so that
4684
- // writes made by other processes (gateway + CLI) are detected immediately.
4685
- // Finding UvUy (PR #402 round-11): without this check the cache served
4686
- // stale data for up to 30s when another process wrote a new cold memory.
4687
- const currentColdVersion = this.readColdWriteVersion();
4688
-
4689
- // Return cached result if still valid by both TTL and sentinel version.
4690
- const cached = StorageManager.coldMemoriesCache.get(coldRoot);
4691
- if (
4692
- cached &&
4693
- Date.now() - cached.loadedAt < StorageManager.COLD_SCAN_CACHE_TTL_MS &&
4694
- cached.coldVersion === currentColdVersion
4695
- ) {
4696
- return cached.memories;
4697
- }
4698
-
4699
- const filePaths: string[] = [];
4700
-
4701
- const collectPaths = async (dir: string) => {
4702
- try {
4703
- const entries = await readdir(dir, { withFileTypes: true });
4704
- const subdirs: string[] = [];
4705
- for (const entry of entries) {
4706
- const fullPath = path.join(dir, entry.name);
4707
- if (entry.isDirectory()) {
4708
- subdirs.push(fullPath);
4709
- } else if (entry.name.endsWith(".md")) {
4710
- filePaths.push(fullPath);
4711
- }
4712
- }
4713
- for (const subdir of subdirs) {
4714
- await collectPaths(subdir);
4715
- }
4716
- } catch {
4717
- // Directory does not exist yet — cold tier may be empty.
4718
- }
4719
- };
4720
-
4721
- // Scan the entire cold root so that memories in any subdirectory (facts/,
4722
- // corrections/, artifacts/, or any future category-specific subdirectory)
4723
- // are included. This is broader than the previous facts/+corrections/ scan
4724
- // and ensures that any memory with structuredAttributes is found regardless
4725
- // of which category it was written with.
4726
- await collectPaths(coldRoot);
4727
- const memories = await this.readParsedMemoriesFromPaths(filePaths, 50);
4728
-
4729
- // Store in cache with the sentinel version captured above so that any
4730
- // subsequent cold-version bump (by this or another process) invalidates it.
4731
- StorageManager.coldMemoriesCache.set(coldRoot, { memories, loadedAt: Date.now(), coldVersion: currentColdVersion });
4732
- return memories;
4466
+ return this.memoryReadStore.readAllColdMemories(
4467
+ );
4733
4468
  }
4734
4469
 
4735
4470
  /**
@@ -4779,67 +4514,10 @@ export class StorageManager {
4779
4514
  return memories;
4780
4515
  }
4781
4516
 
4782
- /** Read a single memory file by its absolute path. Returns null if unreadable. */
4783
4517
  async readMemoryByPath(filePath: string): Promise<MemoryFile | null> {
4784
- try {
4785
- const raw = await readMaybeEncryptedFile(filePath, this._secureStoreKey, this.baseDir);
4786
- // Note: the outer catch intentionally swallows most errors (ENOENT etc.)
4787
- // but SecureStoreLockedError must propagate — see re-throw below.
4788
- const parsed = parseFrontmatter(raw);
4789
- if (parsed) {
4790
- return {
4791
- path: filePath,
4792
- frontmatter: normalizeFrontmatterForPath(
4793
- parsed.frontmatter,
4794
- toMemoryPathRel(this.baseDir, filePath),
4795
- parsed.content,
4796
- ),
4797
- content: parsed.content,
4798
- };
4799
- }
4800
-
4801
- // Entity files use a `# Name` + `**Type:** ...` markdown format rather than
4802
- // YAML frontmatter. Build a synthetic MemoryFile so entity files returned by
4803
- // the direct retrieval agent participate in boostSearchResults and last-recall
4804
- // tracking rather than being silently dropped.
4805
- const normalizedPath = filePath.split(path.sep).join("/");
4806
- if (normalizedPath.includes("/entities/") && filePath.endsWith(".md")) {
4807
- const entity = parseEntityFile(raw, this.entitySchemas);
4808
- if (!entity.name) return null;
4809
- const nameWithoutExt = path.basename(filePath, ".md");
4810
- // Fall back to file mtime rather than new Date() so that entities without
4811
- // an explicit Updated: timestamp are not treated as freshly created on every
4812
- // read. Using new Date() would inflate boostSearchResults recency scores for
4813
- // every entity that lacks a timestamp.
4814
- // Use epoch as the last-resort fallback so that entities without a
4815
- // parseable timestamp don't appear as "freshly created" and inflate scores.
4816
- const fileMtime = entity.updated
4817
- || await stat(filePath).then((s) => s.mtime.toISOString()).catch(() => new Date(0).toISOString());
4818
- return {
4819
- path: filePath,
4820
- frontmatter: {
4821
- id: nameWithoutExt,
4822
- category: "entity",
4823
- created: fileMtime,
4824
- updated: fileMtime,
4825
- source: "entity_extraction",
4826
- confidence: 0.9,
4827
- confidenceTier: confidenceTier(0.9),
4828
- tags: entity.type ? [entity.type] : [],
4829
- },
4830
- content: raw,
4831
- };
4832
- }
4833
-
4834
- return null;
4835
- } catch (err) {
4836
- // Re-throw store-locked errors — callers need to distinguish "locked"
4837
- // from "file not found / parse error". Swallowing a locked error here
4838
- // would silently return null and leave the daemon appearing to work
4839
- // while returning no memories (subtle data loss).
4840
- if (err instanceof SecureStoreLockedError) throw err;
4841
- return null;
4842
- }
4518
+ return this.memoryReadStore.readMemoryByPath(
4519
+ filePath,
4520
+ );
4843
4521
  }
4844
4522
 
4845
4523
  private resolveTierRootDir(tier: "hot" | "cold"): string {
@@ -5340,130 +5018,20 @@ export class StorageManager {
5340
5018
  return events.length;
5341
5019
  }
5342
5020
 
5343
- /**
5344
- * Append a batch of `BUFFER_SURPRISE` telemetry events (issue #563 PR 3).
5345
- *
5346
- * Each event records a single buffer flush decision driven by the
5347
- * surprise gate. The ledger is consumed by
5348
- * `reportBufferSurpriseDistribution` (Doctor report) and by downstream
5349
- * benchmark analysis. This method is fire-and-forget by contract:
5350
- * callers log but do not fail the hot path if the append throws.
5351
- */
5352
5021
  async appendBufferSurpriseEvents(
5353
5022
  events: BufferSurpriseEvent[],
5354
5023
  ): Promise<number> {
5355
- if (events.length === 0) return 0;
5356
- await this.ensureDirectories();
5357
-
5358
- const nowIso = new Date().toISOString();
5359
- const payload = events
5360
- .map((event) => {
5361
- const normalized: BufferSurpriseEvent = {
5362
- ...event,
5363
- event: "BUFFER_SURPRISE",
5364
- timestamp:
5365
- event.timestamp && event.timestamp.length > 0
5366
- ? event.timestamp
5367
- : nowIso,
5368
- };
5369
- return `${JSON.stringify(normalized)}\n`;
5370
- })
5371
- .join("");
5372
-
5373
- await this.appendStorageSecureFile(this.bufferSurpriseLedgerPath, payload);
5374
- return events.length;
5024
+ return this.memoryReadStore.appendBufferSurpriseEvents(
5025
+ events,
5026
+ );
5375
5027
  }
5376
5028
 
5377
- /**
5378
- * Read the buffer-surprise ledger, most recent rows last.
5379
- *
5380
- * `limit` bounds the number of **valid rows** returned (not the
5381
- * number of raw lines parsed). We parse every row, discard malformed
5382
- * ones, then take the tail — so a partial/truncated trailing line
5383
- * (the common failure mode after an interrupted append) cannot hide
5384
- * otherwise-valid recent data above it.
5385
- *
5386
- * Non-positive / non-integer / non-finite limits return `[]` rather
5387
- * than the entire file, matching the other ledger readers in this
5388
- * class and protecting against `slice(-0.5)` → `slice(-0)` silently
5389
- * devolving into an unbounded parse.
5390
- *
5391
- * # Performance note
5392
- *
5393
- * For very large ledgers (issue #563 follow-up), a tail-first reader
5394
- * would avoid parsing the full file when only a recent window is
5395
- * needed. We keep the full-scan implementation here because:
5396
- *
5397
- * - the ledger is opt-in (flag off by default), so early deployments
5398
- * accumulate rows slowly;
5399
- * - telemetry rows are small (~200 bytes), so even 100k rows parse
5400
- * in well under a second;
5401
- * - the governance archive/cleanup flow can trim the ledger when
5402
- * size becomes a concern, reusing the existing maintenance hooks.
5403
- *
5404
- * Swap to a chunked tail-reader if production logs show this is a
5405
- * hot path — leaving that work for a follow-up keeps this PR scoped
5406
- * to correctness, not optimization.
5407
- */
5408
5029
  async readBufferSurpriseEvents(
5409
5030
  options: { limit?: number } = {},
5410
5031
  ): Promise<BufferSurpriseEvent[]> {
5411
- let raw: string;
5412
- try {
5413
- raw = await this.readStorageSecureFile(this.bufferSurpriseLedgerPath);
5414
- } catch (err) {
5415
- if (err instanceof SecureStoreLockedError) throw err;
5416
- const code = (err as NodeJS.ErrnoException).code;
5417
- if (code === "ENOENT") return [];
5418
- throw err;
5419
- }
5420
-
5421
- // Resolve the effective limit up front. Any non-finite / non-positive
5422
- // value returns no rows — callers who want "everything" should OMIT
5423
- // the `limit` key (treated as "no bound" below). We intentionally
5424
- // reject `Infinity` too, because the slice math `events.slice(-Inf)`
5425
- // is surprising and ambiguous; omit the key instead. Fractional
5426
- // values <1 floor to 0, which would make `slice(-0)` return the
5427
- // entire file — guard against that too.
5428
- let effectiveLimit: number | null = null;
5429
- if (options.limit !== undefined) {
5430
- if (
5431
- typeof options.limit !== "number" ||
5432
- !Number.isFinite(options.limit) ||
5433
- options.limit <= 0
5434
- ) {
5435
- return [];
5436
- }
5437
- const floored = Math.floor(options.limit);
5438
- if (floored <= 0) return [];
5439
- effectiveLimit = floored;
5440
- }
5441
-
5442
- const lines = raw.split("\n");
5443
- const events: BufferSurpriseEvent[] = [];
5444
- for (const line of lines) {
5445
- const trimmed = line.trim();
5446
- if (trimmed.length === 0) continue;
5447
- try {
5448
- const parsed = JSON.parse(trimmed);
5449
- if (isValidBufferSurpriseEvent(parsed)) {
5450
- events.push(parsed);
5451
- }
5452
- } catch {
5453
- // Malformed row — fail open, skip.
5454
- }
5455
- }
5456
-
5457
- events.sort(
5458
- (left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp),
5032
+ return this.memoryReadStore.readBufferSurpriseEvents(
5033
+ options,
5459
5034
  );
5460
-
5461
- if (effectiveLimit === null) return events;
5462
- // Slice over VALID rows, not raw lines, so malformed tails cannot
5463
- // mask good data above them. Sort by event timestamp before slicing
5464
- // so concurrent probe completion order cannot make an older scored
5465
- // turn look newer than a later scored turn.
5466
- return events.slice(-effectiveLimit);
5467
5035
  }
5468
5036
 
5469
5037
  async appendBehaviorSignals(events: BehaviorSignalEvent[]): Promise<number> {
@@ -5781,106 +5349,9 @@ export class StorageManager {
5781
5349
  private async readCompressionGuidelineStateFile(
5782
5350
  filePath: string,
5783
5351
  ): Promise<CompressionGuidelineOptimizerState | null> {
5784
- const isFiniteNonNegativeInteger = (value: unknown): value is number =>
5785
- typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
5786
- const isValidActionSummary = (
5787
- value: unknown,
5788
- ): value is NonNullable<CompressionGuidelineOptimizerState["actionSummaries"]>[number] => {
5789
- if (!value || typeof value !== "object") return false;
5790
- const summary = value as NonNullable<CompressionGuidelineOptimizerState["actionSummaries"]>[number];
5791
- return (
5792
- typeof summary.action === "string" &&
5793
- isFiniteNonNegativeInteger(summary.total) &&
5794
- summary.outcomes !== null &&
5795
- typeof summary.outcomes === "object" &&
5796
- isFiniteNonNegativeInteger(summary.outcomes.applied) &&
5797
- isFiniteNonNegativeInteger(summary.outcomes.skipped) &&
5798
- isFiniteNonNegativeInteger(summary.outcomes.failed) &&
5799
- summary.quality !== null &&
5800
- typeof summary.quality === "object" &&
5801
- isFiniteNonNegativeInteger(summary.quality.good) &&
5802
- isFiniteNonNegativeInteger(summary.quality.poor) &&
5803
- isFiniteNonNegativeInteger(summary.quality.unknown)
5804
- );
5805
- };
5806
- const isValidRuleUpdate = (
5807
- value: unknown,
5808
- ): value is NonNullable<CompressionGuidelineOptimizerState["ruleUpdates"]>[number] => {
5809
- if (!value || typeof value !== "object") return false;
5810
- const rule = value as NonNullable<CompressionGuidelineOptimizerState["ruleUpdates"]>[number];
5811
- return (
5812
- typeof rule.action === "string" &&
5813
- typeof rule.delta === "number" &&
5814
- Number.isFinite(rule.delta) &&
5815
- (rule.direction === "increase" || rule.direction === "decrease" || rule.direction === "hold") &&
5816
- (rule.confidence === "low" || rule.confidence === "medium" || rule.confidence === "high") &&
5817
- Array.isArray(rule.notes) &&
5818
- rule.notes.every((note) => typeof note === "string")
5819
- );
5820
- };
5821
-
5822
- try {
5823
- const raw = await this.readStorageSecureFile(filePath);
5824
- const parsed = JSON.parse(raw) as Partial<CompressionGuidelineOptimizerState>;
5825
- const sourceWindow = parsed?.sourceWindow as Partial<CompressionGuidelineOptimizerState["sourceWindow"]>;
5826
- const eventCounts = parsed?.eventCounts as Partial<CompressionGuidelineOptimizerState["eventCounts"]>;
5827
- const activationState =
5828
- parsed?.activationState === "draft" || parsed?.activationState === "active"
5829
- ? parsed.activationState
5830
- : undefined;
5831
- const contentHash =
5832
- typeof parsed?.contentHash === "string" && parsed.contentHash.length > 0
5833
- ? parsed.contentHash
5834
- : undefined;
5835
- const actionSummaries = Array.isArray(parsed?.actionSummaries)
5836
- ? parsed.actionSummaries.filter(isValidActionSummary)
5837
- : undefined;
5838
- const ruleUpdates = Array.isArray(parsed?.ruleUpdates)
5839
- ? parsed.ruleUpdates.filter(isValidRuleUpdate)
5840
- : undefined;
5841
- if (
5842
- !isFiniteNonNegativeInteger(parsed?.version) ||
5843
- typeof parsed?.updatedAt !== "string" ||
5844
- parsed.updatedAt.length === 0 ||
5845
- !sourceWindow ||
5846
- typeof sourceWindow.from !== "string" ||
5847
- sourceWindow.from.length === 0 ||
5848
- typeof sourceWindow.to !== "string" ||
5849
- sourceWindow.to.length === 0 ||
5850
- !eventCounts ||
5851
- !isFiniteNonNegativeInteger(eventCounts.total) ||
5852
- !isFiniteNonNegativeInteger(eventCounts.applied) ||
5853
- !isFiniteNonNegativeInteger(eventCounts.skipped) ||
5854
- !isFiniteNonNegativeInteger(eventCounts.failed) ||
5855
- !isFiniteNonNegativeInteger(parsed?.guidelineVersion)
5856
- ) {
5857
- return null;
5858
- }
5859
-
5860
- return {
5861
- version: parsed.version,
5862
- updatedAt: parsed.updatedAt,
5863
- sourceWindow: {
5864
- from: sourceWindow.from,
5865
- to: sourceWindow.to,
5866
- },
5867
- eventCounts: {
5868
- total: eventCounts.total,
5869
- applied: eventCounts.applied,
5870
- skipped: eventCounts.skipped,
5871
- failed: eventCounts.failed,
5872
- },
5873
- guidelineVersion: parsed.guidelineVersion,
5874
- ...(contentHash ? { contentHash } : {}),
5875
- ...(activationState ? { activationState } : {}),
5876
- ...(actionSummaries ? { actionSummaries } : {}),
5877
- ...(ruleUpdates ? { ruleUpdates } : {}),
5878
- };
5879
- } catch (err) {
5880
- if (err instanceof SecureStoreLockedError) throw err;
5881
- if (!isErrnoCode(err, "ENOENT")) throw err;
5882
- return null;
5883
- }
5352
+ return this.memoryReadStore.readCompressionGuidelineStateFile(
5353
+ filePath,
5354
+ );
5884
5355
  }
5885
5356
 
5886
5357
  async writeIdentityAnchor(content: string): Promise<void> {
@@ -6109,40 +5580,9 @@ export class StorageManager {
6109
5580
  filePath: string;
6110
5581
  }>
6111
5582
  > {
6112
- const cacheKey = this.questionsDir;
6113
- const cached = StorageManager.questionsCache.get(cacheKey);
6114
- if (cached && Date.now() - cached.loadedAt < StorageManager.QUESTIONS_CACHE_TTL_MS) {
6115
- // Check dir mtime for cross-process invalidation — if another process
6116
- // wrote/resolved a question, the directory mtime will be newer than loadedAt.
6117
- try {
6118
- const dirStat = await stat(this.questionsDir);
6119
- if (dirStat.mtimeMs <= cached.loadedAt) {
6120
- const all = cached.questions;
6121
- return opts?.unresolvedOnly ? all.filter((q) => !q.resolved) : all;
6122
- }
6123
- } catch {
6124
- // Dir doesn't exist — fall through to re-read
6125
- }
6126
- }
6127
-
6128
- try {
6129
- const files = await readdir(this.questionsDir);
6130
- const questions = [];
6131
- for (const file of files) {
6132
- if (!file.endsWith(".md")) continue;
6133
- const filePath = path.join(this.questionsDir, file);
6134
- const raw = await readMaybeEncryptedFile(filePath, this._secureStoreKey, this.baseDir);
6135
- const parsed = this.parseQuestionFile(raw, filePath);
6136
- if (parsed) {
6137
- questions.push(parsed);
6138
- }
6139
- }
6140
- const sorted = questions.sort((a, b) => b.priority - a.priority);
6141
- StorageManager.questionsCache.set(cacheKey, { questions: sorted, loadedAt: Date.now() });
6142
- return opts?.unresolvedOnly ? sorted.filter((q) => !q.resolved) : sorted;
6143
- } catch {
6144
- return [];
6145
- }
5583
+ return this.memoryReadStore.readQuestions(
5584
+ opts,
5585
+ );
6146
5586
  }
6147
5587
 
6148
5588
  /** Invalidate the questions cache (call after writing a question). */