@remnic/core 9.38.2 → 9.39.0

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 (189) hide show
  1. package/dist/access-admin-ops-surface.d.ts +5 -5
  2. package/dist/access-admin-ops-surface.js +13 -13
  3. package/dist/access-authorization-probe.d.ts +5 -5
  4. package/dist/access-authorization-probe.js +14 -14
  5. package/dist/access-boundary.d.ts +5 -5
  6. package/dist/access-boundary.js +13 -13
  7. package/dist/access-cli.js +35 -35
  8. package/dist/access-extraction-force-flush.d.ts +5 -5
  9. package/dist/access-extraction-force-flush.js +13 -13
  10. package/dist/access-http-lcm-compaction.d.ts +5 -5
  11. package/dist/access-http-lifecycle-flush.d.ts +5 -5
  12. package/dist/access-http.d.ts +5 -5
  13. package/dist/access-http.js +18 -18
  14. package/dist/access-identity-continuity-surface.d.ts +4 -4
  15. package/dist/access-identity-continuity-surface.js +13 -13
  16. package/dist/access-lcm-surface.d.ts +5 -5
  17. package/dist/access-lcm-surface.js +13 -13
  18. package/dist/access-mcp.d.ts +5 -5
  19. package/dist/access-mcp.js +16 -16
  20. package/dist/access-namespace-preflight.js +13 -13
  21. package/dist/access-observe-write-surface.d.ts +5 -5
  22. package/dist/access-observe-write-surface.js +13 -13
  23. package/dist/access-operations-batch.js +14 -14
  24. package/dist/access-operations.d.ts +5 -5
  25. package/dist/access-operations.js +15 -15
  26. package/dist/access-recall-concurrency.d.ts +5 -5
  27. package/dist/access-recall-concurrency.js +13 -13
  28. package/dist/access-recall-response.d.ts +5 -5
  29. package/dist/access-recall-response.js +13 -13
  30. package/dist/access-recall-surface.d.ts +5 -5
  31. package/dist/access-recall-surface.js +13 -13
  32. package/dist/{access-service-C2WYT1pO.d.ts → access-service-C6QuKBCG.d.ts} +3 -3
  33. package/dist/access-service-helpers.d.ts +5 -5
  34. package/dist/access-service.d.ts +5 -5
  35. package/dist/access-service.js +13 -13
  36. package/dist/access-surface-catalog.d.ts +5 -5
  37. package/dist/access-wearables-meetings-surface.d.ts +2 -2
  38. package/dist/bootstrap.d.ts +4 -4
  39. package/dist/briefing.d.ts +1 -1
  40. package/dist/briefing.js +5 -5
  41. package/dist/buffer.d.ts +1 -1
  42. package/dist/buffer.js +2 -2
  43. package/dist/causal-consolidation.js +6 -6
  44. package/dist/{chunk-2YJPCRUM.js → chunk-4CSTEPFA.js} +2 -2
  45. package/dist/{chunk-T3FVAHM7.js → chunk-4VXGRIH6.js} +286 -7
  46. package/dist/chunk-4VXGRIH6.js.map +1 -0
  47. package/dist/{chunk-A4P6H25D.js → chunk-5JUYBWJD.js} +19 -19
  48. package/dist/{chunk-JYLN6KHH.js → chunk-5TRIAPT4.js} +7 -9
  49. package/dist/{chunk-JYLN6KHH.js.map → chunk-5TRIAPT4.js.map} +1 -1
  50. package/dist/{chunk-6D5D5A4M.js → chunk-7ERHAARG.js} +2 -2
  51. package/dist/{chunk-GORHSN5C.js → chunk-7OCTH7AJ.js} +2 -2
  52. package/dist/{chunk-ILGC6JH6.js → chunk-7RJUVTVF.js} +16 -1
  53. package/dist/chunk-7RJUVTVF.js.map +1 -0
  54. package/dist/{chunk-FE7MUX7M.js → chunk-AL73FWSS.js} +2 -2
  55. package/dist/{chunk-6TF4M2LB.js → chunk-AVGTOOYP.js} +2 -2
  56. package/dist/{chunk-B6XHIBIB.js → chunk-AX2J2LC4.js} +2 -2
  57. package/dist/{chunk-ZB3YODID.js → chunk-BVJIAIB3.js} +8 -8
  58. package/dist/{chunk-XN3EW6P2.js → chunk-CMENJQ6I.js} +2 -2
  59. package/dist/{chunk-PRP3M5Y3.js → chunk-CMFUYOYB.js} +2 -2
  60. package/dist/{chunk-M3DBB2ND.js → chunk-CNZLKQEL.js} +2 -2
  61. package/dist/{chunk-YVMVVE4B.js → chunk-CPD6C6K6.js} +2 -2
  62. package/dist/{chunk-VU7732DU.js → chunk-CYM2PHLB.js} +5 -5
  63. package/dist/{chunk-RY2XZTFN.js → chunk-DJH35IVS.js} +2 -2
  64. package/dist/{chunk-Q3CVY7LV.js → chunk-ENLZOWYK.js} +201 -231
  65. package/dist/chunk-ENLZOWYK.js.map +1 -0
  66. package/dist/{chunk-IJAQCBGV.js → chunk-F5EVTFQK.js} +2 -2
  67. package/dist/{chunk-RY7CHVGE.js → chunk-G57BGHQ2.js} +2 -2
  68. package/dist/{chunk-F3BNEYX2.js → chunk-GUQYMOPU.js} +2 -2
  69. package/dist/{chunk-IMEXE4XS.js → chunk-GYGXKATH.js} +6 -6
  70. package/dist/{chunk-CKBKHZKZ.js → chunk-JKTHT4KX.js} +3 -3
  71. package/dist/{chunk-HQYYJRU7.js → chunk-KJP2R5MJ.js} +15 -15
  72. package/dist/{chunk-UBWOCQ3I.js → chunk-MHCRASVK.js} +2 -2
  73. package/dist/{chunk-RYMPLSIT.js → chunk-NF3U3MPG.js} +41 -8
  74. package/dist/chunk-NF3U3MPG.js.map +1 -0
  75. package/dist/{chunk-DKKZ6SO2.js → chunk-SHTPVI47.js} +22 -45
  76. package/dist/chunk-SHTPVI47.js.map +1 -0
  77. package/dist/{chunk-PYTUBWAS.js → chunk-UGAZILZG.js} +2 -2
  78. package/dist/{chunk-4TAD3B56.js → chunk-VGK33D3K.js} +2 -2
  79. package/dist/{chunk-BGLDKAYY.js → chunk-VK33JSBR.js} +2 -2
  80. package/dist/{chunk-NBAEGFXT.js → chunk-VQTRSX4R.js} +11 -14
  81. package/dist/chunk-VQTRSX4R.js.map +1 -0
  82. package/dist/{chunk-WET4M3R6.js → chunk-XSPD6KQL.js} +2 -2
  83. package/dist/{chunk-HUS2XN3T.js → chunk-YFHMW37V.js} +2 -2
  84. package/dist/{chunk-HKF5VASQ.js → chunk-ZA53AZND.js} +17 -6
  85. package/dist/chunk-ZA53AZND.js.map +1 -0
  86. package/dist/{chunk-5ZGHUWTT.js → chunk-ZBDORYWM.js} +2 -2
  87. package/dist/{cli-jkmzdT8P.d.ts → cli-ClXRCu08.d.ts} +3 -3
  88. package/dist/cli.d.ts +6 -6
  89. package/dist/cli.js +27 -27
  90. package/dist/compounding/engine.d.ts +1 -1
  91. package/dist/compounding/engine.js +5 -5
  92. package/dist/connectors/codex-materialize-runner.js +5 -5
  93. package/dist/connectors/index.js +5 -5
  94. package/dist/consolidation-provenance-check.d.ts +1 -1
  95. package/dist/consolidation-undo.d.ts +1 -1
  96. package/dist/contradiction/index.d.ts +1 -1
  97. package/dist/corpus-watermark.js +7 -7
  98. package/dist/entity-retrieval.d.ts +1 -1
  99. package/dist/entity-retrieval.js +5 -5
  100. package/dist/explicit-capture.d.ts +4 -4
  101. package/dist/extraction.js +8 -8
  102. package/dist/index.d.ts +10 -10
  103. package/dist/index.js +35 -35
  104. package/dist/maintenance/memory-governance.js +5 -5
  105. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
  106. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
  107. package/dist/maintenance/rebuild-memory-projection.d.ts +17 -10
  108. package/dist/maintenance/rebuild-memory-projection.js +6 -6
  109. package/dist/{maintenance-Ha9Dg93M.d.ts → maintenance-Dsvprbod.d.ts} +1 -1
  110. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  111. package/dist/memory-lifecycle-ledger-utils.d.ts +8 -1
  112. package/dist/memory-lifecycle-ledger-utils.js +5 -1
  113. package/dist/memory-worth-outcomes.d.ts +1 -1
  114. package/dist/namespaces/migrate.d.ts +1 -1
  115. package/dist/namespaces/migrate.js +6 -6
  116. package/dist/namespaces/storage.d.ts +1 -1
  117. package/dist/namespaces/storage.js +5 -5
  118. package/dist/offline-sync-impression-drain.js +5 -3
  119. package/dist/operator-doctor-corpus.js +8 -8
  120. package/dist/operator-toolkit.d.ts +1 -1
  121. package/dist/operator-toolkit.js +11 -11
  122. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  123. package/dist/orchestration/maintenance.d.ts +2 -2
  124. package/dist/orchestration/maintenance.js +10 -10
  125. package/dist/{orchestrator-DV_u9bfx.d.ts → orchestrator-CEhod0MJ.d.ts} +3 -3
  126. package/dist/orchestrator.d.ts +4 -4
  127. package/dist/orchestrator.js +35 -35
  128. package/dist/semantic-consolidation.js +6 -6
  129. package/dist/semantic-rule-promotion.js +5 -5
  130. package/dist/semantic-rule-verifier.js +5 -5
  131. package/dist/{service-JxXXY-Eh.d.ts → service-BlOfGid8.d.ts} +1 -1
  132. package/dist/source-agent-qualifier.js +7 -7
  133. package/dist/{storage-BbH4HDUS.d.ts → storage-CNKZocJF.d.ts} +1 -1
  134. package/dist/storage-guards.d.ts +9 -2
  135. package/dist/storage-guards.js +3 -1
  136. package/dist/storage.d.ts +1 -1
  137. package/dist/storage.js +4 -4
  138. package/dist/temporal-supersession.d.ts +1 -1
  139. package/dist/tier-migration.d.ts +1 -1
  140. package/dist/verified-recall.js +5 -5
  141. package/package.json +2 -2
  142. package/src/maintenance/projection-support.ts +192 -1
  143. package/src/maintenance/rebuild-memory-lifecycle-ledger.ts +5 -16
  144. package/src/maintenance/rebuild-memory-projection.ts +28 -60
  145. package/src/memory-lifecycle-ledger-utils.ts +21 -0
  146. package/src/orchestration/extraction-run-helpers.ts +47 -0
  147. package/src/orchestration/extraction-run.test.ts +133 -0
  148. package/src/orchestration/extraction-run.ts +11 -7
  149. package/src/orchestration/maintenance.test.ts +30 -0
  150. package/src/orchestrator-flush.test.ts +51 -0
  151. package/src/storage/memory-lifecycle-ledger-access.ts +198 -1
  152. package/src/storage/secure-line-reader.ts +3 -1
  153. package/src/storage-guards.ts +28 -7
  154. package/src/storage-memory-lifecycle-ledger.test.ts +291 -1
  155. package/src/storage.ts +11 -21
  156. package/dist/chunk-DKKZ6SO2.js.map +0 -1
  157. package/dist/chunk-HKF5VASQ.js.map +0 -1
  158. package/dist/chunk-ILGC6JH6.js.map +0 -1
  159. package/dist/chunk-NBAEGFXT.js.map +0 -1
  160. package/dist/chunk-Q3CVY7LV.js.map +0 -1
  161. package/dist/chunk-RYMPLSIT.js.map +0 -1
  162. package/dist/chunk-T3FVAHM7.js.map +0 -1
  163. /package/dist/{chunk-2YJPCRUM.js.map → chunk-4CSTEPFA.js.map} +0 -0
  164. /package/dist/{chunk-A4P6H25D.js.map → chunk-5JUYBWJD.js.map} +0 -0
  165. /package/dist/{chunk-6D5D5A4M.js.map → chunk-7ERHAARG.js.map} +0 -0
  166. /package/dist/{chunk-GORHSN5C.js.map → chunk-7OCTH7AJ.js.map} +0 -0
  167. /package/dist/{chunk-FE7MUX7M.js.map → chunk-AL73FWSS.js.map} +0 -0
  168. /package/dist/{chunk-6TF4M2LB.js.map → chunk-AVGTOOYP.js.map} +0 -0
  169. /package/dist/{chunk-B6XHIBIB.js.map → chunk-AX2J2LC4.js.map} +0 -0
  170. /package/dist/{chunk-ZB3YODID.js.map → chunk-BVJIAIB3.js.map} +0 -0
  171. /package/dist/{chunk-XN3EW6P2.js.map → chunk-CMENJQ6I.js.map} +0 -0
  172. /package/dist/{chunk-PRP3M5Y3.js.map → chunk-CMFUYOYB.js.map} +0 -0
  173. /package/dist/{chunk-M3DBB2ND.js.map → chunk-CNZLKQEL.js.map} +0 -0
  174. /package/dist/{chunk-YVMVVE4B.js.map → chunk-CPD6C6K6.js.map} +0 -0
  175. /package/dist/{chunk-VU7732DU.js.map → chunk-CYM2PHLB.js.map} +0 -0
  176. /package/dist/{chunk-RY2XZTFN.js.map → chunk-DJH35IVS.js.map} +0 -0
  177. /package/dist/{chunk-IJAQCBGV.js.map → chunk-F5EVTFQK.js.map} +0 -0
  178. /package/dist/{chunk-RY7CHVGE.js.map → chunk-G57BGHQ2.js.map} +0 -0
  179. /package/dist/{chunk-F3BNEYX2.js.map → chunk-GUQYMOPU.js.map} +0 -0
  180. /package/dist/{chunk-IMEXE4XS.js.map → chunk-GYGXKATH.js.map} +0 -0
  181. /package/dist/{chunk-CKBKHZKZ.js.map → chunk-JKTHT4KX.js.map} +0 -0
  182. /package/dist/{chunk-HQYYJRU7.js.map → chunk-KJP2R5MJ.js.map} +0 -0
  183. /package/dist/{chunk-UBWOCQ3I.js.map → chunk-MHCRASVK.js.map} +0 -0
  184. /package/dist/{chunk-PYTUBWAS.js.map → chunk-UGAZILZG.js.map} +0 -0
  185. /package/dist/{chunk-4TAD3B56.js.map → chunk-VGK33D3K.js.map} +0 -0
  186. /package/dist/{chunk-BGLDKAYY.js.map → chunk-VK33JSBR.js.map} +0 -0
  187. /package/dist/{chunk-WET4M3R6.js.map → chunk-XSPD6KQL.js.map} +0 -0
  188. /package/dist/{chunk-HUS2XN3T.js.map → chunk-YFHMW37V.js.map} +0 -0
  189. /package/dist/{chunk-5ZGHUWTT.js.map → chunk-ZBDORYWM.js.map} +0 -0
@@ -21,6 +21,7 @@ import path from "node:path";
21
21
  import { readProjectionRebuiltAt } from "../maintenance/projection-support.js";
22
22
  import { lstat, mkdir, mkdtemp, readdir, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
23
23
  import { execFileSync } from "node:child_process";
24
+ import { setTimeout as delay } from "node:timers/promises";
24
25
 
25
26
  import { MaintenanceScheduler } from "./maintenance.js";
26
27
  import {
@@ -2952,3 +2953,32 @@ test("scheduled projection rebuild skips when the projection was rebuilt within
2952
2953
  await rm(memoryDir, { recursive: true, force: true });
2953
2954
  }
2954
2955
  });
2956
+
2957
+ test("maintenance tool request invokes the scheduled projection rebuild wiring (#2119)", async () => {
2958
+ const memoryDir = await seedMemoryDirWithOneFact();
2959
+ const scheduler = new MaintenanceScheduler({
2960
+ config: fixtureConfig({
2961
+ memoryDir,
2962
+ projectionRebuildEnabled: true,
2963
+ projectionRebuildIntervalMs: 6 * 60 * 60 * 1000,
2964
+ }),
2965
+ getQmd: () => ({ isAvailable: () => false }) as unknown as SearchBackend,
2966
+ namespaceSearchRouter: {} as unknown as NamespaceSearchRouter,
2967
+ namespaceCatalog: {} as unknown as NamespaceCatalog,
2968
+ });
2969
+ try {
2970
+ scheduler.requestQmdMaintenanceForTool("test");
2971
+ for (let attempt = 0; attempt < 100 && probeProjectionHealth(memoryDir).state === "absent"; attempt += 1) {
2972
+ await delay(10);
2973
+ }
2974
+ assert.equal(
2975
+ probeProjectionHealth(memoryDir).state,
2976
+ "openable",
2977
+ "the public maintenance request entrypoint must reach the projection rebuild",
2978
+ );
2979
+ assert.equal(readProjectedMemoryBrowse(memoryDir, { limit: 5, offset: 0 })?.total, 1);
2980
+ } finally {
2981
+ await scheduler.dispose();
2982
+ await rm(memoryDir, { recursive: true, force: true });
2983
+ }
2984
+ });
@@ -1781,6 +1781,57 @@ test("runExtraction preserves empty-result buffers when fingerprint persistence
1781
1781
  assert.equal(clearCalls, 0);
1782
1782
  });
1783
1783
 
1784
+ test("runExtraction keeps the empty live path fail-open when liveness metadata write fails (#2223)", async () => {
1785
+ const config = parseConfig({});
1786
+ config.extractionMinChars = 0;
1787
+ config.extractionMinUserTurns = 1;
1788
+
1789
+ let clearCalls = 0;
1790
+ let saveMetaCalls = 0;
1791
+ const meta = {
1792
+ extractionCount: 0,
1793
+ lastExtractionAt: null,
1794
+ lastConsolidationAt: null,
1795
+ totalMemories: 0,
1796
+ totalEntities: 0,
1797
+ processedExtractionFingerprints: [] as Array<{ fingerprint: string; observedAt: string }>,
1798
+ };
1799
+
1800
+ const orchestrator = Object.create(Orchestrator.prototype) as any;
1801
+ orchestrator.config = config;
1802
+ orchestrator.buffer = {
1803
+ clearAfterExtraction: async () => {
1804
+ clearCalls += 1;
1805
+ },
1806
+ };
1807
+ orchestrator.storageRouter = {
1808
+ storageFor: async () => ({
1809
+ listEntityNames: async () => [],
1810
+ loadMeta: async () => meta,
1811
+ saveMeta: async () => {
1812
+ saveMetaCalls += 1;
1813
+ throw new Error("meta save failed");
1814
+ },
1815
+ }),
1816
+ };
1817
+ orchestrator.extraction = {
1818
+ extract: async () => ({ facts: [], entities: [], questions: [], profileUpdates: [] }),
1819
+ };
1820
+
1821
+ // Normal live turn (no persistProcessedFingerprint): the liveness stamp is
1822
+ // diagnostic only, so a metadata write failure must neither reject the task
1823
+ // nor retain the buffer — extraction succeeded and produced nothing durable.
1824
+ const result = await orchestrator.runExtraction(
1825
+ [makeTurn("session-live-empty", "a transient note not worth remembering")],
1826
+ { bufferKey: "session-live-empty" },
1827
+ );
1828
+
1829
+ assert.equal(result.status, "skipped");
1830
+ assert.equal(result.reason, "empty_extraction_result");
1831
+ assert.equal(saveMetaCalls, 1, "the liveness stamp was attempted");
1832
+ assert.equal(clearCalls, 1, "fail-open: the live buffer clears despite the metadata write failure");
1833
+ });
1834
+
1784
1835
  test("runExtraction preserves deduped buffers when the caller aborts during meta load", async () => {
1785
1836
  const config = parseConfig({});
1786
1837
  config.extractionMinChars = 0;
@@ -1,7 +1,7 @@
1
1
  import { SecureStoreLockedError } from "../secure-store/secure-fs.js";
2
2
  import { isErrnoCode } from "../utils/errno.js";
3
3
  import { displayErrorDetail } from "../runtime/better-sqlite.js";
4
- import { rename, stat, unlink } from "node:fs/promises";
4
+ import { lstat, rename, stat, unlink } from "node:fs/promises";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import path from "node:path";
7
7
  import type { MemoryLifecycleEvent } from "../types.js";
@@ -19,6 +19,15 @@ import {
19
19
  readMemoryLifecycleEventsFromLines,
20
20
  STATE_FILE_MAX_DECRYPT_BYTES,
21
21
  } from "./secure-line-reader.js";
22
+ import { getMemoryProjectionPath } from "../memory-projection-store.js";
23
+ import {
24
+ openProjectionLifecycleLedgerLagTracker,
25
+ type ProjectionLifecycleLedgerLagTracker,
26
+ } from "../maintenance/projection-support.js";
27
+ import {
28
+ type ProjectionLedgerLagTelemetry,
29
+ warnProjectionFallback,
30
+ } from "../storage-guards.js";
22
31
 
23
32
  /** Reads a lifecycle-ledger file through the secure whole-file/plaintext line source. */
24
33
  export type LedgerSecureReader = (filePath: string) => Promise<string>;
@@ -190,6 +199,194 @@ export async function readBoundedLifecycleEventsFromLedger(
190
199
  }
191
200
  }
192
201
 
202
+ export interface LifecycleLedgerProjectionLagRead {
203
+ events: MemoryLifecycleEvent[];
204
+ currentEventCount: number;
205
+ deltaEvents: number;
206
+ telemetryAvailable: boolean;
207
+ }
208
+
209
+ export async function readBoundedLifecycleEventsWithProjectionLag(
210
+ ledgerPath: string,
211
+ readSecureFile: LedgerSecureReader,
212
+ limit: number,
213
+ memoryId: string,
214
+ tracker: ProjectionLifecycleLedgerLagTracker,
215
+ ): Promise<LifecycleLedgerProjectionLagRead> {
216
+ let currentEventCount = 0;
217
+ let deltaEvents = 0;
218
+ let telemetryAvailable = true;
219
+ try {
220
+ const events = await readMemoryLifecycleEventsFromLines(
221
+ readMaybeEncryptedLines(
222
+ ledgerPath,
223
+ () => readSecureFile(ledgerPath),
224
+ STATE_FILE_MAX_DECRYPT_BYTES,
225
+ ),
226
+ Math.max(0, Math.floor(limit)),
227
+ memoryId,
228
+ compareMemoryLifecycleEvents,
229
+ (event) => {
230
+ if (!telemetryAvailable) return;
231
+ try {
232
+ const tracked = tracker.record(event);
233
+ if (!tracked.unique) return;
234
+ currentEventCount += 1;
235
+ if (!tracked.projected) deltaEvents += 1;
236
+ } catch {
237
+ telemetryAvailable = false;
238
+ currentEventCount = 0;
239
+ deltaEvents = 0;
240
+ }
241
+ },
242
+ );
243
+ return { events, currentEventCount, deltaEvents, telemetryAvailable };
244
+ } catch (err) {
245
+ if (err instanceof SecureStoreLockedError) throw err;
246
+ if (!isErrnoCode(err, "ENOENT")) throw err;
247
+ return { events: [], currentEventCount: 0, deltaEvents: 0, telemetryAvailable: false };
248
+ }
249
+ }
250
+ /**
251
+ * Probe filesystem identity stamps (dev:ino:size:mtimeMs:ctimeMs) for the
252
+ * projection database and lifecycle-ledger file. Returns `null` when either
253
+ * file is missing or unstatable so caller can disable the lag cache without
254
+ * throwing on transient I/O or permission errors (#2119 / review).
255
+ */
256
+ export async function probeProjectionLedgerIdentities(
257
+ projectionPath: string,
258
+ ledgerPath: string,
259
+ ): Promise<{ projectionIdentity: string; ledgerIdentity: string } | null> {
260
+ try {
261
+ const [projectionIdentity, ledgerIdentity] = await Promise.all(
262
+ [projectionPath, ledgerPath].map(async (filePath) => {
263
+ try {
264
+ const file = await lstat(filePath);
265
+ return `${file.dev}:${file.ino}:${file.size}:${file.mtimeMs}:${file.ctimeMs}`;
266
+ } catch (err) {
267
+ if (isErrnoCode(err, "ENOENT")) return "absent";
268
+ // Probe failure: disable lag cache, do not block the primary timeline read (#2119 review).
269
+ return null;
270
+ }
271
+ }),
272
+ );
273
+ if (!projectionIdentity || !ledgerIdentity) return null;
274
+ return { projectionIdentity, ledgerIdentity };
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+
280
+ export interface ProjectionLedgerLagScan {
281
+ telemetry: ProjectionLedgerLagTelemetry | null;
282
+ events: MemoryLifecycleEvent[];
283
+ memoryId: string;
284
+ limit: number;
285
+ }
286
+
287
+ export class ProjectionLedgerLagManager {
288
+ private lagCache: {
289
+ generation: string;
290
+ telemetry: ProjectionLedgerLagTelemetry;
291
+ } | null = null;
292
+ private lagInFlight: {
293
+ generation: string;
294
+ scan: Promise<ProjectionLedgerLagScan>;
295
+ } | null = null;
296
+
297
+ async resolveTimelineWithLag(options: {
298
+ baseDir: string;
299
+ ledgerPath: string;
300
+ memoryId: string;
301
+ cappedLimit: number;
302
+ readSecureFile: LedgerSecureReader;
303
+ projectionAge: () => string;
304
+ }): Promise<MemoryLifecycleEvent[]> {
305
+ const { baseDir, ledgerPath, memoryId, cappedLimit, readSecureFile, projectionAge } = options;
306
+ const projectionPath = getMemoryProjectionPath(baseDir);
307
+ const identities = await probeProjectionLedgerIdentities(projectionPath, ledgerPath);
308
+
309
+ if (!identities) {
310
+ warnProjectionFallback(baseDir, "getMemoryTimeline", projectionAge);
311
+ return readBoundedLifecycleEventsFromLedger(ledgerPath, readSecureFile, cappedLimit, memoryId);
312
+ }
313
+
314
+ const { projectionIdentity, ledgerIdentity } = identities;
315
+ const generation = `${projectionIdentity}\0${ledgerIdentity}`;
316
+
317
+ const cachedLag = this.lagCache?.generation === generation
318
+ ? this.lagCache.telemetry
319
+ : null;
320
+ if (cachedLag) {
321
+ warnProjectionFallback(baseDir, "getMemoryTimeline", projectionAge, cachedLag);
322
+ return readBoundedLifecycleEventsFromLedger(ledgerPath, readSecureFile, cappedLimit, memoryId);
323
+ }
324
+
325
+ const inFlight = this.lagInFlight?.generation === generation
326
+ ? this.lagInFlight.scan
327
+ : null;
328
+ if (inFlight) {
329
+ const result = await inFlight;
330
+ if (result.telemetry) {
331
+ this.lagCache = { generation, telemetry: result.telemetry };
332
+ }
333
+ warnProjectionFallback(baseDir, "getMemoryTimeline", projectionAge, result.telemetry ?? undefined);
334
+ if (result.memoryId === memoryId && result.limit === cappedLimit) return result.events;
335
+ return readBoundedLifecycleEventsFromLedger(ledgerPath, readSecureFile, cappedLimit, memoryId);
336
+ }
337
+
338
+ const tracker = openProjectionLifecycleLedgerLagTracker(baseDir);
339
+ if (tracker) {
340
+ const highWater = tracker.highWater;
341
+ const scan = (async (): Promise<ProjectionLedgerLagScan> => {
342
+ try {
343
+ const fallback = await readBoundedLifecycleEventsWithProjectionLag(
344
+ ledgerPath,
345
+ readSecureFile,
346
+ cappedLimit,
347
+ memoryId,
348
+ tracker,
349
+ );
350
+ return {
351
+ telemetry: fallback.telemetryAvailable
352
+ ? {
353
+ projectedEvents: highWater.eventCount,
354
+ currentLedgerEvents: fallback.currentEventCount,
355
+ deltaEvents: fallback.deltaEvents,
356
+ }
357
+ : null,
358
+ events: fallback.events,
359
+ memoryId,
360
+ limit: cappedLimit,
361
+ };
362
+ } finally {
363
+ try {
364
+ tracker.close();
365
+ } catch {
366
+ // Lag telemetry cleanup must not replace a successful ledger fallback.
367
+ }
368
+ }
369
+ })();
370
+ this.lagInFlight = { generation, scan };
371
+ try {
372
+ const result = await scan;
373
+ if (result.telemetry) {
374
+ this.lagCache = { generation, telemetry: result.telemetry };
375
+ }
376
+ warnProjectionFallback(baseDir, "getMemoryTimeline", projectionAge, result.telemetry ?? undefined);
377
+ return result.events;
378
+ } finally {
379
+ if (this.lagInFlight?.scan === scan) {
380
+ this.lagInFlight = null;
381
+ }
382
+ }
383
+ }
384
+
385
+ warnProjectionFallback(baseDir, "getMemoryTimeline", projectionAge);
386
+ return readBoundedLifecycleEventsFromLedger(ledgerPath, readSecureFile, cappedLimit, memoryId);
387
+ }
388
+ }
389
+
193
390
  /**
194
391
  * Directory holding the durable pending-append spill for a lifecycle ledger
195
392
  * (#2033). When an append cannot acquire the ledger lock within its budget —
@@ -227,6 +227,7 @@ export async function readMemoryLifecycleEventsFromLines(
227
227
  limit: number,
228
228
  memoryId?: string,
229
229
  compare?: (a: MemoryLifecycleEvent, b: MemoryLifecycleEvent) => number,
230
+ onEvent?: (event: MemoryLifecycleEvent) => void,
230
231
  ): Promise<MemoryLifecycleEvent[]> {
231
232
  if (limit <= 0) return [];
232
233
  const top = compare ? new BoundedLifecycleTopN(limit, compare) : null;
@@ -238,8 +239,9 @@ export async function readMemoryLifecycleEventsFromLines(
238
239
  if (!line) continue;
239
240
  try {
240
241
  const parsed = JSON.parse(line) as Partial<MemoryLifecycleEvent>;
241
- if (memoryId !== undefined && parsed.memoryId !== memoryId) continue;
242
242
  if (!isValidLifecycleEventRow(parsed)) continue;
243
+ onEvent?.(parsed);
244
+ if (memoryId !== undefined && parsed.memoryId !== memoryId) continue;
243
245
  if (top) {
244
246
  top.add(parsed, seq++);
245
247
  } else if (ring.length < limit) {
@@ -1,6 +1,7 @@
1
1
  import { log } from "./logger.js";
2
2
 
3
3
  const PROJECTION_FALLBACK_WARN_INTERVAL_MS = 5 * 60_000;
4
+ export const PROJECTION_LEDGER_LAG_WARN_THRESHOLD_EVENTS = 100;
4
5
  const projectionFallbackWarnedAt = new Map<string, number>();
5
6
 
6
7
  export function assertMemoryFrontmatterId(
@@ -18,23 +19,43 @@ export function assertMemoryFrontmatterId(
18
19
  }
19
20
  }
20
21
 
22
+ export interface ProjectionLedgerLagTelemetry {
23
+ projectedEvents: number;
24
+ currentLedgerEvents: number;
25
+ deltaEvents: number;
26
+ warnThresholdEvents?: number;
27
+ }
28
+
21
29
  export function warnProjectionFallback(
22
30
  memoryDir: string,
23
31
  consumer: string,
24
- // Lazy so the (potentially I/O-bound) lag computation runs only when this
25
- // call actually logs, not on every rate-limited-suppressed fallback (#2119).
26
32
  detail?: () => string | undefined,
33
+ ledgerLag?: ProjectionLedgerLagTelemetry,
27
34
  ): null {
35
+ const fallback = `storage.${consumer}: memory projection absent or empty; falling back to full corpus`;
36
+ const thresholdEvents = Math.max(
37
+ 0,
38
+ Math.floor(ledgerLag?.warnThresholdEvents ?? PROJECTION_LEDGER_LAG_WARN_THRESHOLD_EVENTS),
39
+ );
40
+ const lag = ledgerLag
41
+ ? `projected_events=${ledgerLag.projectedEvents} current_ledger_events=${ledgerLag.currentLedgerEvents} `
42
+ + `delta_events=${ledgerLag.deltaEvents} threshold_events=${thresholdEvents} `
43
+ + "fallback_action=full-ledger-scan"
44
+ : undefined;
45
+
46
+ if (ledgerLag && ledgerLag.deltaEvents <= thresholdEvents) {
47
+ const suffix = [detail?.(), lag].filter(Boolean).join("; ");
48
+ log.debug(`${fallback}${suffix ? ` (${suffix})` : ""}`);
49
+ return null;
50
+ }
51
+
28
52
  const key = `${memoryDir}\0${consumer}`;
29
53
  const now = Date.now();
30
54
  const warnedAt = projectionFallbackWarnedAt.get(key) ?? 0;
31
55
  if (now - warnedAt >= PROJECTION_FALLBACK_WARN_INTERVAL_MS) {
32
56
  projectionFallbackWarnedAt.set(key, now);
33
- const suffix = detail?.();
34
- log.warn(
35
- `storage.${consumer}: memory projection absent or empty; falling back to full corpus`
36
- + (suffix ? ` (${suffix})` : ""),
37
- );
57
+ const suffix = [detail?.(), lag].filter(Boolean).join("; ");
58
+ log.warn(`${fallback}${suffix ? ` (${suffix})` : ""}`);
38
59
  }
39
60
  return null;
40
61
  }