@remnic/core 10.0.39 → 10.0.41

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 (199) hide show
  1. package/dist/access-admin-ops-surface.js +14 -14
  2. package/dist/access-authorization-probe.js +15 -15
  3. package/dist/access-boundary.js +14 -14
  4. package/dist/access-cli.js +53 -53
  5. package/dist/access-extraction-force-flush.js +14 -14
  6. package/dist/access-http-briefing.js +15 -15
  7. package/dist/access-http-query.js +16 -16
  8. package/dist/access-http.js +30 -30
  9. package/dist/access-identity-continuity-surface.js +14 -14
  10. package/dist/access-lcm-surface.js +14 -14
  11. package/dist/access-mcp.js +23 -23
  12. package/dist/access-namespace-preflight.js +14 -14
  13. package/dist/access-observe-write-surface.js +14 -14
  14. package/dist/access-operations-batch.js +15 -15
  15. package/dist/access-operations.js +22 -22
  16. package/dist/access-recall-concurrency.js +14 -14
  17. package/dist/access-recall-response.js +14 -14
  18. package/dist/access-recall-surface.js +14 -14
  19. package/dist/access-service.d.ts +2 -2
  20. package/dist/access-service.d.ts.map +1 -1
  21. package/dist/access-service.js +14 -14
  22. package/dist/access-surface-catalog.js +16 -16
  23. package/dist/active-recall.js +2 -2
  24. package/dist/briefing.js +3 -3
  25. package/dist/causal-consolidation.js +4 -4
  26. package/dist/{chunk-M7KGIW5F.js → chunk-4KS4IHKH.js} +2 -2
  27. package/dist/{chunk-JORSMFL6.js → chunk-574N53FL.js} +5 -5
  28. package/dist/{chunk-BUKZ7MTN.js → chunk-5TRU6QQC.js} +2 -2
  29. package/dist/{chunk-SRPGA6WN.js → chunk-7BCYAJKP.js} +2 -2
  30. package/dist/{chunk-MD3W2H3G.js → chunk-7G4O5EQT.js} +31 -31
  31. package/dist/{chunk-MD3W2H3G.js.map → chunk-7G4O5EQT.js.map} +1 -1
  32. package/dist/{chunk-F2IU6A2L.js → chunk-7XGWW3LQ.js} +16 -16
  33. package/dist/{chunk-ZXFKG7VZ.js → chunk-AOBDA3VC.js} +2 -2
  34. package/dist/{chunk-JRTKYFFY.js → chunk-AY6LO7GD.js} +2 -2
  35. package/dist/{chunk-5KZNWERU.js → chunk-BIYVUPC6.js} +19 -3
  36. package/dist/{chunk-5KZNWERU.js.map → chunk-BIYVUPC6.js.map} +1 -1
  37. package/dist/{chunk-V2XMG7GM.js → chunk-CHNZPOWX.js} +3 -2
  38. package/dist/{chunk-V2XMG7GM.js.map → chunk-CHNZPOWX.js.map} +1 -1
  39. package/dist/{chunk-GPVGAHXL.js → chunk-CNAL35D5.js} +2 -2
  40. package/dist/{chunk-NJ2DZYEK.js → chunk-DBM2NQBG.js} +2 -2
  41. package/dist/{chunk-U66CTIF5.js → chunk-F2RDRE7M.js} +2 -2
  42. package/dist/{chunk-VIS23B6O.js → chunk-F7MP76CB.js} +15 -13
  43. package/dist/chunk-F7MP76CB.js.map +1 -0
  44. package/dist/{chunk-B7VKRCAG.js → chunk-FNEYSTB6.js} +2 -2
  45. package/dist/{chunk-GSBSGT43.js → chunk-FPHU6KVI.js} +2 -2
  46. package/dist/{chunk-RO2NNWR5.js → chunk-GAUS4XGN.js} +3 -3
  47. package/dist/{chunk-4UT2NUAC.js → chunk-GCDIHTWF.js} +2 -2
  48. package/dist/{chunk-4HJ2QK32.js → chunk-GDOI4U4D.js} +2 -2
  49. package/dist/{chunk-VCOCOBUS.js → chunk-GZMHBRIR.js} +3 -3
  50. package/dist/{chunk-LJXBFVI2.js → chunk-ICTJOOWZ.js} +2 -2
  51. package/dist/{chunk-6Z2ZWBOV.js → chunk-IWDFHVPJ.js} +2 -2
  52. package/dist/{chunk-TTQYX7VU.js → chunk-J3FPMHQG.js} +2 -2
  53. package/dist/{chunk-GCW7UT5I.js → chunk-JJSMSS45.js} +2 -2
  54. package/dist/{chunk-O2O4FDAH.js → chunk-JXQTECMU.js} +2 -2
  55. package/dist/{chunk-ANV373S5.js → chunk-KYYN2FK4.js} +2 -2
  56. package/dist/{chunk-PJRKLB4Z.js → chunk-L5PM7B3I.js} +16 -2
  57. package/dist/{chunk-PJRKLB4Z.js.map → chunk-L5PM7B3I.js.map} +1 -1
  58. package/dist/{chunk-5TJEOU6Y.js → chunk-LKRXGOQW.js} +18 -8
  59. package/dist/{chunk-5TJEOU6Y.js.map → chunk-LKRXGOQW.js.map} +1 -1
  60. package/dist/{chunk-B3I2QQU7.js → chunk-LWIU5BMA.js} +3 -3
  61. package/dist/{chunk-WQYS47G4.js → chunk-N3PUGD7E.js} +2 -2
  62. package/dist/{chunk-EKR6OO4P.js → chunk-NTD3VWJ2.js} +2 -2
  63. package/dist/{chunk-JQCP5QW3.js → chunk-O67H7OXJ.js} +2 -2
  64. package/dist/{chunk-4LLV2N3R.js → chunk-ONIVKGNC.js} +2 -2
  65. package/dist/{chunk-GLB6VV3J.js → chunk-OUYYEPXD.js} +3 -3
  66. package/dist/{chunk-RWW5KPIY.js → chunk-PCZZMMBL.js} +12 -12
  67. package/dist/chunk-PCZZMMBL.js.map +1 -0
  68. package/dist/{chunk-WT2VVYBM.js → chunk-PJXSBXEC.js} +2 -2
  69. package/dist/{chunk-P3YUEPSQ.js → chunk-PUMP554C.js} +2 -2
  70. package/dist/{chunk-SJHFU52W.js → chunk-Q47WGGKI.js} +5 -5
  71. package/dist/chunk-Q47WGGKI.js.map +1 -0
  72. package/dist/{chunk-RIAKNXZB.js → chunk-Q77SLBWT.js} +3 -3
  73. package/dist/{chunk-22YJQTPN.js → chunk-QGNZ5QPV.js} +2 -2
  74. package/dist/{chunk-GE5ZNDLE.js → chunk-R42VUN5V.js} +3 -3
  75. package/dist/{chunk-7IANFJ24.js → chunk-RWMLDC2F.js} +17 -17
  76. package/dist/chunk-RWMLDC2F.js.map +1 -0
  77. package/dist/{chunk-LPD6I2U6.js → chunk-SLVR3U4E.js} +2 -2
  78. package/dist/{chunk-IAOW2L6K.js → chunk-TNTYAH7T.js} +2 -2
  79. package/dist/{chunk-CR72WZ3M.js → chunk-UBGFPD62.js} +4 -4
  80. package/dist/{chunk-CR72WZ3M.js.map → chunk-UBGFPD62.js.map} +1 -1
  81. package/dist/{chunk-JZFJZLJY.js → chunk-UGV256H3.js} +2 -2
  82. package/dist/{chunk-L74NWAD4.js → chunk-UKIIAEJF.js} +2 -2
  83. package/dist/{chunk-5Q6CR65T.js → chunk-VTZSIG6O.js} +2 -2
  84. package/dist/{chunk-IZRNMXZC.js → chunk-WCUYCTTG.js} +2 -2
  85. package/dist/{chunk-JT3IXJIL.js → chunk-XBHUMPIQ.js} +7 -7
  86. package/dist/{chunk-JM6U3SAE.js → chunk-XJ77IOAD.js} +2 -2
  87. package/dist/{chunk-ODJ2HRFW.js → chunk-XXBCUIYQ.js} +7 -7
  88. package/dist/{chunk-JOOGPZJE.js → chunk-XXW3P3RX.js} +2 -2
  89. package/dist/cli.js +40 -40
  90. package/dist/coding/export-okf-codegraph.js +2 -2
  91. package/dist/compounding/engine.js +4 -4
  92. package/dist/config.js +2 -2
  93. package/dist/connectors/codex-materialize-runner.js +3 -3
  94. package/dist/connectors/index.js +4 -4
  95. package/dist/convergence-refresh.js +5 -5
  96. package/dist/corpus-watermark.js +5 -5
  97. package/dist/entity-retrieval.js +3 -3
  98. package/dist/external-wiki-access.js +15 -15
  99. package/dist/extraction-error-classification.d.ts +15 -0
  100. package/dist/extraction-error-classification.d.ts.map +1 -1
  101. package/dist/extraction-error-classification.js +7 -3
  102. package/dist/extraction.js +8 -8
  103. package/dist/harmonic-retrieval.js +3 -3
  104. package/dist/index.js +53 -53
  105. package/dist/location/index.js +5 -5
  106. package/dist/maintenance/memory-governance.js +3 -3
  107. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  108. package/dist/maintenance/rebuild-memory-projection-runner.js +5 -5
  109. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  110. package/dist/memory-browse-operations.js +15 -15
  111. package/dist/namespaces/migrate.js +4 -4
  112. package/dist/namespaces/search.d.ts +1 -0
  113. package/dist/namespaces/search.d.ts.map +1 -1
  114. package/dist/namespaces/search.js +5 -3
  115. package/dist/namespaces/storage.js +3 -3
  116. package/dist/operator-doctor-corpus.js +6 -6
  117. package/dist/operator-toolkit.js +11 -11
  118. package/dist/orchestration/maintenance.js +13 -13
  119. package/dist/orchestration/qmd-result-resolver.d.ts +5 -0
  120. package/dist/orchestration/qmd-result-resolver.d.ts.map +1 -1
  121. package/dist/orchestration/recall-search-pipeline.d.ts.map +1 -1
  122. package/dist/orchestrator.js +53 -53
  123. package/dist/recall-navigation-operations.js +15 -15
  124. package/dist/recall-navigation.js +14 -14
  125. package/dist/resume-bundles.js +2 -2
  126. package/dist/runtime/daemon-health.d.ts.map +1 -1
  127. package/dist/semantic-consolidation.js +4 -4
  128. package/dist/semantic-rule-promotion.js +3 -3
  129. package/dist/semantic-rule-verifier.js +3 -3
  130. package/dist/source-agent-qualifier.js +6 -6
  131. package/dist/standup/build.js +3 -3
  132. package/dist/storage.js +2 -2
  133. package/dist/support-passport/index.js +17 -17
  134. package/dist/transfer/export-okf.js +3 -3
  135. package/dist/verified-recall.js +3 -3
  136. package/dist/who-knows-cli.js +4 -4
  137. package/dist/who-knows.js +3 -3
  138. package/package.json +3 -3
  139. package/src/access-entity-principal.test.ts +190 -0
  140. package/src/access-http.ts +2 -2
  141. package/src/access-operations-batch.ts +2 -2
  142. package/src/access-service.ts +7 -6
  143. package/src/chat/chat-executor.ts +1 -1
  144. package/src/extraction-error-classification.ts +23 -0
  145. package/src/extraction-local-failure-class.test.ts +55 -0
  146. package/src/extraction.ts +9 -9
  147. package/src/namespaces/search.ts +1 -1
  148. package/src/orchestration/extraction-run.test.ts +83 -2
  149. package/src/orchestration/qmd-result-resolver.ts +26 -1
  150. package/src/orchestration/recall-search-pipeline.ts +11 -12
  151. package/src/runtime/daemon-health.test.ts +85 -0
  152. package/src/runtime/daemon-health.ts +20 -7
  153. package/dist/chunk-7IANFJ24.js.map +0 -1
  154. package/dist/chunk-RWW5KPIY.js.map +0 -1
  155. package/dist/chunk-SJHFU52W.js.map +0 -1
  156. package/dist/chunk-VIS23B6O.js.map +0 -1
  157. /package/dist/{chunk-M7KGIW5F.js.map → chunk-4KS4IHKH.js.map} +0 -0
  158. /package/dist/{chunk-JORSMFL6.js.map → chunk-574N53FL.js.map} +0 -0
  159. /package/dist/{chunk-BUKZ7MTN.js.map → chunk-5TRU6QQC.js.map} +0 -0
  160. /package/dist/{chunk-SRPGA6WN.js.map → chunk-7BCYAJKP.js.map} +0 -0
  161. /package/dist/{chunk-F2IU6A2L.js.map → chunk-7XGWW3LQ.js.map} +0 -0
  162. /package/dist/{chunk-ZXFKG7VZ.js.map → chunk-AOBDA3VC.js.map} +0 -0
  163. /package/dist/{chunk-JRTKYFFY.js.map → chunk-AY6LO7GD.js.map} +0 -0
  164. /package/dist/{chunk-GPVGAHXL.js.map → chunk-CNAL35D5.js.map} +0 -0
  165. /package/dist/{chunk-NJ2DZYEK.js.map → chunk-DBM2NQBG.js.map} +0 -0
  166. /package/dist/{chunk-U66CTIF5.js.map → chunk-F2RDRE7M.js.map} +0 -0
  167. /package/dist/{chunk-B7VKRCAG.js.map → chunk-FNEYSTB6.js.map} +0 -0
  168. /package/dist/{chunk-GSBSGT43.js.map → chunk-FPHU6KVI.js.map} +0 -0
  169. /package/dist/{chunk-RO2NNWR5.js.map → chunk-GAUS4XGN.js.map} +0 -0
  170. /package/dist/{chunk-4UT2NUAC.js.map → chunk-GCDIHTWF.js.map} +0 -0
  171. /package/dist/{chunk-4HJ2QK32.js.map → chunk-GDOI4U4D.js.map} +0 -0
  172. /package/dist/{chunk-VCOCOBUS.js.map → chunk-GZMHBRIR.js.map} +0 -0
  173. /package/dist/{chunk-LJXBFVI2.js.map → chunk-ICTJOOWZ.js.map} +0 -0
  174. /package/dist/{chunk-6Z2ZWBOV.js.map → chunk-IWDFHVPJ.js.map} +0 -0
  175. /package/dist/{chunk-TTQYX7VU.js.map → chunk-J3FPMHQG.js.map} +0 -0
  176. /package/dist/{chunk-GCW7UT5I.js.map → chunk-JJSMSS45.js.map} +0 -0
  177. /package/dist/{chunk-O2O4FDAH.js.map → chunk-JXQTECMU.js.map} +0 -0
  178. /package/dist/{chunk-ANV373S5.js.map → chunk-KYYN2FK4.js.map} +0 -0
  179. /package/dist/{chunk-B3I2QQU7.js.map → chunk-LWIU5BMA.js.map} +0 -0
  180. /package/dist/{chunk-WQYS47G4.js.map → chunk-N3PUGD7E.js.map} +0 -0
  181. /package/dist/{chunk-EKR6OO4P.js.map → chunk-NTD3VWJ2.js.map} +0 -0
  182. /package/dist/{chunk-JQCP5QW3.js.map → chunk-O67H7OXJ.js.map} +0 -0
  183. /package/dist/{chunk-4LLV2N3R.js.map → chunk-ONIVKGNC.js.map} +0 -0
  184. /package/dist/{chunk-GLB6VV3J.js.map → chunk-OUYYEPXD.js.map} +0 -0
  185. /package/dist/{chunk-WT2VVYBM.js.map → chunk-PJXSBXEC.js.map} +0 -0
  186. /package/dist/{chunk-P3YUEPSQ.js.map → chunk-PUMP554C.js.map} +0 -0
  187. /package/dist/{chunk-RIAKNXZB.js.map → chunk-Q77SLBWT.js.map} +0 -0
  188. /package/dist/{chunk-22YJQTPN.js.map → chunk-QGNZ5QPV.js.map} +0 -0
  189. /package/dist/{chunk-GE5ZNDLE.js.map → chunk-R42VUN5V.js.map} +0 -0
  190. /package/dist/{chunk-LPD6I2U6.js.map → chunk-SLVR3U4E.js.map} +0 -0
  191. /package/dist/{chunk-IAOW2L6K.js.map → chunk-TNTYAH7T.js.map} +0 -0
  192. /package/dist/{chunk-JZFJZLJY.js.map → chunk-UGV256H3.js.map} +0 -0
  193. /package/dist/{chunk-L74NWAD4.js.map → chunk-UKIIAEJF.js.map} +0 -0
  194. /package/dist/{chunk-5Q6CR65T.js.map → chunk-VTZSIG6O.js.map} +0 -0
  195. /package/dist/{chunk-IZRNMXZC.js.map → chunk-WCUYCTTG.js.map} +0 -0
  196. /package/dist/{chunk-JT3IXJIL.js.map → chunk-XBHUMPIQ.js.map} +0 -0
  197. /package/dist/{chunk-JM6U3SAE.js.map → chunk-XJ77IOAD.js.map} +0 -0
  198. /package/dist/{chunk-ODJ2HRFW.js.map → chunk-XXBCUIYQ.js.map} +0 -0
  199. /package/dist/{chunk-JOOGPZJE.js.map → chunk-XXW3P3RX.js.map} +0 -0
@@ -0,0 +1,190 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test, { type TestContext } from "node:test";
6
+ import { EngramAccessHttpServer } from "./access-http.js";
7
+ import { getOperation } from "./access-boundary.js";
8
+ import { EngramAccessService } from "./access-service.js";
9
+ import { createChatExecutor } from "./chat/chat-executor.js";
10
+ import { parseConfig } from "./config.js";
11
+ import { NamespaceStorageRouter } from "./namespaces/storage.js";
12
+ import type { Orchestrator } from "./orchestrator.js";
13
+
14
+ interface EntityHttpBody {
15
+ error?: string;
16
+ found?: boolean;
17
+ namespace?: string;
18
+ entities?: Array<{ name?: string }>;
19
+ entity?: { name?: string };
20
+ }
21
+
22
+ interface McpToolBody {
23
+ result: { isError: boolean; content: Array<{ text: string }>; structuredContent: { entity: { name: string } } };
24
+ }
25
+
26
+ async function readMcp(response: Response): Promise<McpToolBody> {
27
+ // SAFETY: /mcp answers tools/call with the JSON-RPC result shape named above; each test asserts the fields it reads.
28
+ return (await response.json()) as McpToolBody;
29
+ }
30
+
31
+ async function readBody(response: Response): Promise<EntityHttpBody> {
32
+ // SAFETY: the access server answers these routes with the JSON shape named above; each test asserts the fields it reads.
33
+ return (await response.json()) as EntityHttpBody;
34
+ }
35
+
36
+ /** Entity reads need only config and real storage routing from the orchestrator. */
37
+ async function createFixture(t: TestContext, namespacesEnabled = true) {
38
+ const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-entity-acl-"));
39
+ t.after(() => rm(memoryDir, { recursive: true, force: true }));
40
+ const config = parseConfig({
41
+ memoryDir,
42
+ namespacesEnabled,
43
+ defaultNamespace: "default",
44
+ sharedNamespace: "shared",
45
+ namespacePolicies: [
46
+ { name: "default", readPrincipals: ["owner"], writePrincipals: ["owner"] },
47
+ { name: "restricted", readPrincipals: ["owner", "reader"], writePrincipals: ["owner"] },
48
+ ],
49
+ });
50
+ const router = new NamespaceStorageRouter(config);
51
+ for (const namespace of namespacesEnabled ? ["default", "restricted"] : ["default"]) {
52
+ const storage = await router.storageFor(namespace);
53
+ const entitiesDir = path.join(storage.dir, "entities");
54
+ await mkdir(entitiesDir, { recursive: true });
55
+ await writeFile(path.join(entitiesDir, "project-widget.md"), [
56
+ `# Widget-${namespace}`,
57
+ "",
58
+ "**Type:** project",
59
+ "",
60
+ "## Summary",
61
+ "",
62
+ `Synthetic entity in ${namespace}.`,
63
+ "",
64
+ ].join("\n"));
65
+ }
66
+ const dependencies: Pick<Orchestrator, "config" | "getStorage"> = {
67
+ config,
68
+ getStorage: (namespace) => router.storageFor(namespace ?? config.defaultNamespace),
69
+ };
70
+ // SAFETY: the entity read paths touch only `config` and `getStorage` (the Pick above); the rest of the Orchestrator is unused here.
71
+ return new EngramAccessService(dependencies as Orchestrator);
72
+ }
73
+
74
+ test("entity service enforces read ACLs and requires a principal before list/get", async (t) => {
75
+ const service = await createFixture(t);
76
+ await assert.rejects(() => service.entityList({ namespace: "restricted" }, "intruder"), /not readable/);
77
+ await assert.rejects(() => service.entityGet("project-widget", "restricted", "intruder"), /not readable/);
78
+ await assert.rejects(() => service.entityList({ namespace: "restricted" }), /authentication required/);
79
+ await assert.rejects(() => service.entityGet("project-widget", "restricted"), /authentication required/);
80
+ await assert.rejects(() => service.entityList({}, "reader"), /not readable/);
81
+ await assert.rejects(() => service.entityGet("project-widget", undefined, "reader"), /not readable/);
82
+
83
+ const list = await service.entityList({ namespace: " restricted ", query: "widget", limit: 1 }, "reader");
84
+ assert.equal(list.namespace, "restricted");
85
+ assert.equal(list.total, 1);
86
+ assert.equal(list.entities[0]?.name, "Widget-restricted");
87
+ const entity = await service.entityGet("project-widget", " restricted ", "reader");
88
+ assert.equal(entity.namespace, "restricted");
89
+ assert.equal(entity.entity?.name, "Widget-restricted");
90
+ assert.deepEqual(await service.entityGet("missing", "restricted", "reader"), {
91
+ found: false, namespace: "restricted",
92
+ });
93
+ });
94
+
95
+ test("entity service retains principal-free reads when namespaces are disabled", async (t) => {
96
+ const service = await createFixture(t, false);
97
+ assert.equal((await service.entityList()).entities[0]?.name, "Widget-default");
98
+ assert.equal((await service.entityGet("project-widget")).entity?.name, "Widget-default");
99
+ });
100
+
101
+ test("entity_list registry dispatch preserves the trusted principal", async (t) => {
102
+ const service = await createFixture(t);
103
+ const operation = getOperation("entity_list");
104
+ assert.ok(operation);
105
+ await assert.rejects(
106
+ () => operation.run({ principal: "owner" }, { service, authenticatedPrincipal: "reader" }),
107
+ /not readable/,
108
+ );
109
+ const output = await operation.run({}, { service, authenticatedPrincipal: "owner" });
110
+ const result = JSON.parse(JSON.stringify(output)).result;
111
+ assert.equal(result.namespace, "default");
112
+ assert.equal(result.entities[0]?.name, "Widget-default");
113
+ });
114
+
115
+ test("HTTP and MCP entity reads bind ACLs to the transport principal across aliases", async (t) => {
116
+ const service = await createFixture(t);
117
+ const token = "synthetic-entity-token";
118
+ const server = new EngramAccessHttpServer({
119
+ service,
120
+ host: "127.0.0.1",
121
+ port: 0,
122
+ principal: "reader",
123
+ enableAdapters: false,
124
+ trustPrincipalHeader: false,
125
+ authTokenEntriesGetter: () => [{
126
+ token,
127
+ capabilities: { version: 1, ops: ["entity_get", "entity_list"], namespaces: ["default", "restricted"] },
128
+ }],
129
+ });
130
+ const started = await server.start();
131
+ t.after(() => server.stop());
132
+ const base = `http://${started.host}:${started.port}`;
133
+ const headers = {
134
+ authorization: `Bearer ${token}`,
135
+ "content-type": "application/json",
136
+ "x-engram-principal": "owner",
137
+ };
138
+ for (const prefix of ["engram", "remnic"]) {
139
+ // The entity HTTP routes are served under /engram/v1 only; the /remnic prefix is an alias for the MCP tool names below.
140
+ for (const suffix of prefix === "engram" ? ["entities", "entities/project-widget"] : []) {
141
+ const denied = await fetch(`${base}/${prefix}/v1/${suffix}?namespace=default&principal=owner`, { headers });
142
+ assert.equal(denied.status, 400);
143
+ assert.match((await readBody(denied)).error ?? "", /not readable/);
144
+ const allowed = await fetch(`${base}/${prefix}/v1/${suffix}?namespace=restricted`, { headers });
145
+ assert.equal(allowed.status, 200);
146
+ const body = await readBody(allowed);
147
+ assert.equal(body.namespace, "restricted");
148
+ assert.equal(suffix === "entities" ? body.entities?.[0]?.name : body.entity?.name, "Widget-restricted");
149
+ }
150
+ if (prefix === "engram") {
151
+ const missing = await fetch(`${base}/${prefix}/v1/entities/missing?namespace=restricted`, { headers });
152
+ assert.equal(missing.status, 404);
153
+ assert.equal((await readBody(missing)).found, false);
154
+ }
155
+
156
+ for (const namespace of ["default", "restricted"]) {
157
+ const response = await fetch(`${base}/mcp`, {
158
+ method: "POST",
159
+ headers,
160
+ body: JSON.stringify({
161
+ jsonrpc: "2.0",
162
+ id: `${prefix}-${namespace}`,
163
+ method: "tools/call",
164
+ params: {
165
+ name: `${prefix}.entity_get`,
166
+ arguments: {
167
+ name: "project-widget", namespace, principal: "owner", authenticatedPrincipal: "owner",
168
+ },
169
+ },
170
+ }),
171
+ });
172
+ assert.equal(response.status, 200);
173
+ const body = await readMcp(response);
174
+ assert.equal(body.result.isError, namespace === "default");
175
+ if (namespace === "default") {
176
+ assert.match(body.result.content[0].text, /not readable/);
177
+ } else {
178
+ assert.equal(body.result.structuredContent.entity.name, "Widget-restricted");
179
+ }
180
+ }
181
+ }
182
+ });
183
+
184
+ test("chat entity reads preserve the bound principal instead of losing identity", async (t) => {
185
+ const service = await createFixture(t);
186
+ const allowed = createChatExecutor({ service, namespace: "restricted", principal: "reader" });
187
+ assert.equal(JSON.parse(await allowed.entityGet("project-widget")).entity.name, "Widget-restricted");
188
+ const denied = createChatExecutor({ service, namespace: "default", principal: "reader" });
189
+ await assert.rejects(() => denied.entityGet("project-widget"), /not readable/);
190
+ });
@@ -2168,7 +2168,7 @@ export class EngramAccessHttpServer extends ReviewDeckAccessHttpBase {
2168
2168
  query: parsed.searchParams.get("q") ?? undefined,
2169
2169
  limit,
2170
2170
  offset,
2171
- });
2171
+ }, this.resolveRequestPrincipal(req));
2172
2172
  this.respondJson(res, 200, response);
2173
2173
  return;
2174
2174
  }
@@ -2178,7 +2178,7 @@ export class EngramAccessHttpServer extends ReviewDeckAccessHttpBase {
2178
2178
  this.enforceTokenOp("entity_get"); // boundary dispatch (issue #1525)
2179
2179
  const entityName = decodeURIComponent(entityMatch[1] ?? "");
2180
2180
  const namespace = this.resolveNamespace(req, parsed.searchParams.get("namespace") ?? undefined);
2181
- const response = await this.service.entityGet(entityName, namespace);
2181
+ const response = await this.service.entityGet(entityName, namespace, this.resolveRequestPrincipal(req));
2182
2182
  this.respondJson(res, response.found ? 200 : 404, response);
2183
2183
  return;
2184
2184
  }
@@ -279,7 +279,7 @@ defineOperation({
279
279
  };
280
280
  },
281
281
  });
282
- defineOperation({ name: "entity_get", description: "Get entity.", schema: strictSchema({ name: S.str, namespace: S.str }), handler: async (input, ctx) => ({ result: await ctx.service.entityGet(optStr(input.name) ?? "", optStr(input.namespace)) }) });
282
+ defineOperation({ name: "entity_get", description: "Get entity.", schema: strictSchema({ name: S.str, namespace: S.str }), handler: async (input, ctx) => ({ result: await ctx.service.entityGet(optStr(input.name) ?? "", optStr(input.namespace), ctx.authenticatedPrincipal) }) });
283
283
  defineOperation({ name: "review_queue_list", description: "List review queue.", schema: strictSchema({ runId: S.str, namespace: S.str }), handler: async (input, ctx) => ({ result: await ctx.service.reviewQueue(optStr(input.runId) ?? "", optStr(input.namespace), ctx.authenticatedPrincipal) }) });
284
284
  // Issue #2482: observe validates through the canonical observeRequestSchema
285
285
  // (same as HTTP/MCP wire validation). Nullable wire fields (parts /
@@ -472,7 +472,7 @@ defineOperation({
472
472
  return { result: getRecallTimingStatus(ctx.service.configRef) };
473
473
  },
474
474
  });
475
- defineOperation({ name: "entity_list", description: "List entities.", schema: strictSchema({}), handler: async (_i, ctx) => ({ result: await ctx.service.entityList() }) });
475
+ defineOperation({ name: "entity_list", description: "List entities.", schema: strictSchema({}), handler: async (_i, ctx) => ({ result: await ctx.service.entityList({}, ctx.authenticatedPrincipal) }) });
476
476
  defineOperation({ name: "maintenance_status", description: "Maintenance status.", schema: strictSchema({}), handler: async (_i, ctx) => ({ result: await ctx.service.maintenance() }) });
477
477
  defineOperation({ name: "quality_status", description: "Quality status.", schema: strictSchema({}), handler: async (_i, ctx) => ({ result: await ctx.service.quality() }) });
478
478
  defineOperation({ name: "trust_zones_status", description: "Trust-zone status.", schema: strictSchema({}), handler: async (_i, ctx) => ({ result: await ctx.service.trustZoneStatus() }) });
@@ -3506,10 +3506,11 @@ export class EngramAccessService extends SupportPassportAccessServiceBase {
3506
3506
  query?: string;
3507
3507
  limit?: number;
3508
3508
  offset?: number;
3509
- } = {}
3509
+ } = {},
3510
+ principal?: string
3510
3511
  ): Promise<EngramAccessEntityListResponse> {
3511
- const storage = await this.orchestrator.getStorage(options.namespace);
3512
- const resolvedNamespace = options.namespace?.trim() || this.orchestrator.config.defaultNamespace;
3512
+ const resolvedNamespace = this.resolveReadableNamespace(options.namespace, principal);
3513
+ const storage = await this.orchestrator.getStorage(resolvedNamespace);
3513
3514
  const { limit, offset } = normalizePagination(options.limit, options.offset);
3514
3515
  const query = options.query?.trim().toLowerCase() ?? "";
3515
3516
 
@@ -3553,9 +3554,9 @@ export class EngramAccessService extends SupportPassportAccessServiceBase {
3553
3554
  };
3554
3555
  }
3555
3556
 
3556
- async entityGet(name: string, namespace?: string): Promise<EngramAccessEntityResponse> {
3557
- const storage = await this.orchestrator.getStorage(namespace);
3558
- const resolvedNamespace = namespace?.trim() || this.orchestrator.config.defaultNamespace;
3557
+ async entityGet(name: string, namespace?: string, principal?: string): Promise<EngramAccessEntityResponse> {
3558
+ const resolvedNamespace = this.resolveReadableNamespace(namespace, principal);
3559
+ const storage = await this.orchestrator.getStorage(resolvedNamespace);
3559
3560
  const raw = await storage.readEntity(name);
3560
3561
  if (!raw) return { found: false, namespace: resolvedNamespace };
3561
3562
  return {
@@ -60,7 +60,7 @@ export function createChatExecutor(bindings: ChatExecutorBindings): ChatToolExec
60
60
  },
61
61
 
62
62
  async entityGet(name: string): Promise<string> {
63
- const result = await service.entityGet(name, namespace);
63
+ const result = await service.entityGet(name, namespace, principal);
64
64
  return JSON.stringify(result);
65
65
  },
66
66
 
@@ -85,3 +85,26 @@ export function formatExtractionParseFailureLog(input: {
85
85
  `traceId=${input.traceId}`,
86
86
  ].join(" ");
87
87
  }
88
+
89
+ /**
90
+ * The local endpoint answered, and what it said rejects this input (unusable content, or a client error that is not
91
+ * auth, timeout or rate limiting): the chunk's problem, not an outage. Retried like an empty parse, and a trigger-drain
92
+ * chunk that keeps drawing it is dropped after its recovery copy is saved.
93
+ */
94
+ export class LocalLlmRejectedError extends Error {
95
+ constructor(readonly httpStatus?: number) {
96
+ super(httpStatus === undefined ? "local LLM answered with unusable content" : `local LLM rejected the input (HTTP ${httpStatus})`);
97
+ this.name = "LocalLlmRejectedError";
98
+ }
99
+ }
100
+
101
+ /** What the local client recorded about a failed call (`failureDiag`). */
102
+ export interface LocalFailureDiag {
103
+ lastError?: unknown;
104
+ }
105
+
106
+ /** The HTTP status when the local endpoint answered and refuses this input (400, 413, 422); else undefined. A wrong model name or route (404, 405, 410), auth, timeout and rate limiting are the setup's problem, not the chunk's. */
107
+ export function localRejectionStatus(diag: LocalFailureDiag): number | undefined {
108
+ const status = httpStatusFromError(diag.lastError);
109
+ return status !== undefined && [400, 413, 422].includes(status) ? status : undefined;
110
+ }
@@ -0,0 +1,55 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseConfig } from "./config.js";
5
+ import { ExtractionEngine } from "./extraction.js";
6
+ import type { BufferTurn } from "./types.js";
7
+
8
+ // Follow-up to #3266: the trigger drain drops a chunk after N failures, but not for an outage. Whether a local-model failure
9
+ // is the chunk's fault is decided here, by what the local endpoint did.
10
+ const TURNS: BufferTurn[] = [
11
+ { role: "user", content: "I keep my backups on the NAS in the garage.", timestamp: "2026-10-30T01:00:00.000Z" },
12
+ { role: "assistant", content: "Noted.", timestamp: "2026-10-30T01:00:05.000Z" },
13
+ ];
14
+
15
+ type LocalMessages = Array<{ role: string; content: string }>;
16
+ type LocalStub = (messages: LocalMessages, options: { failureDiag?: { lastError?: Error } }) => Promise<{ content: string } | null>;
17
+
18
+ async function extractWith(local: LocalStub) {
19
+ const engine = new ExtractionEngine(parseConfig({ localLlmEnabled: true, localLlmModel: "fixture-local", localLlmFallback: false }));
20
+ const modelRegistry = { calculateContextSizes: () => ({ maxInputChars: 8_000, maxOutputTokens: 1_000, description: "fixture" }) };
21
+ assert.equal(Reflect.set(engine, "localLlm", { chatCompletion: local }), true);
22
+ assert.equal(Reflect.set(engine, "modelRegistry", modelRegistry), true);
23
+ return await engine.extract(TURNS);
24
+ }
25
+
26
+ const withStatus = (status: number) => Object.assign(new Error("local LLM http failure"), { status });
27
+
28
+ test("a local endpoint that answers with something unusable rejects the chunk: parse_empty", async () => {
29
+ const result = await extractWith(async () => ({ content: "I cannot help with that request." }));
30
+ assert.equal(result.extractionFailure, "local_llm_error");
31
+ assert.equal(result.extractionFailureClass, "parse_empty");
32
+ });
33
+
34
+ test("a local endpoint that answers with a client error rejects the chunk: parse_empty", async () => {
35
+ for (const status of [400, 413, 422]) {
36
+ const result = await extractWith(async (_messages, options) => {
37
+ if (options.failureDiag) options.failureDiag.lastError = withStatus(status);
38
+ return null;
39
+ });
40
+ assert.equal(result.extractionFailureClass, "parse_empty", `HTTP ${status}`);
41
+ }
42
+ });
43
+
44
+ test("not the chunk's fault: unreachable, timeout, 5xx, 429, auth and a wrong model or route (404) stay provider_retryable", async () => {
45
+ const outage = await extractWith(async () => null);
46
+ assert.equal(outage.extractionFailure, "local_llm_unavailable");
47
+ assert.equal(outage.extractionFailureClass, "provider_retryable");
48
+ for (const status of [401, 403, 404, 405, 408, 410, 429, 500, 503]) {
49
+ const result = await extractWith(async (_messages, options) => {
50
+ if (options.failureDiag) options.failureDiag.lastError = withStatus(status);
51
+ return null;
52
+ });
53
+ assert.equal(result.extractionFailureClass, "provider_retryable", `HTTP ${status}`);
54
+ }
55
+ });
package/src/extraction.ts CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  import { applyGroundingWithConnector, headerConnector, renderExtractionConversation, resolveSourceConnector, type ExtractionGroundingContext } from "./source-agent-qualifier.js";
39
39
  import { isMemoryCategory } from "./write-envelope.js";
40
40
  export { classifyExtractionThrownError, classifyFallbackParseFailure, formatExtractionParseFailureLog } from "./extraction-error-classification.js";
41
- import { classifyExtractionThrownError, classifyFallbackParseFailure, formatExtractionParseFailureLog } from "./extraction-error-classification.js";
41
+ import { LocalLlmRejectedError, classifyExtractionThrownError, classifyFallbackParseFailure, formatExtractionParseFailureLog, localRejectionStatus, type LocalFailureDiag } from "./extraction-error-classification.js";
42
42
  import { AMBIENT_CAPTURE_PROMPT_SECTION_COMPACT, clampAmbientCaptureConfidence } from "./ambient-provenance.js";
43
43
  import {
44
44
  CONSOLIDATION_RESPONSE_SCHEMA,
@@ -1244,7 +1244,7 @@ export class ExtractionEngine {
1244
1244
  entities: [],
1245
1245
  questions: [],
1246
1246
  extractionFailure: "local_llm_error",
1247
- extractionFailureClass: "provider_retryable",
1247
+ extractionFailureClass: err instanceof LocalLlmRejectedError ? "parse_empty" : classifyExtractionThrownError(err),
1248
1248
  };
1249
1249
  }
1250
1250
  log.info("extraction: local LLM error, falling back to gateway default AI:", err);
@@ -1494,9 +1494,8 @@ ${extractionResponseShape(this.config.extraction?.spanMode ?? "off")}
1494
1494
  Conversation:
1495
1495
  ${truncatedConversation}`;
1496
1496
 
1497
- log.debug(
1498
- `extractWithLocalLlm: calling localLlm.chatCompletion with prompt length ${localPrompt.length}...`,
1499
- );
1497
+ log.debug(`extractWithLocalLlm: calling localLlm.chatCompletion with prompt length ${localPrompt.length}...`);
1498
+ const failureDiag: LocalFailureDiag = {};
1500
1499
  const response = await this.localLlm.chatCompletion(
1501
1500
  [
1502
1501
  { role: "system", content: "You are a memory extraction system. Output valid JSON only." },
@@ -1512,9 +1511,12 @@ ${truncatedConversation}`;
1512
1511
  : undefined,
1513
1512
  priority: "background",
1514
1513
  signal,
1514
+ failureDiag,
1515
1515
  },
1516
1516
  );
1517
1517
  if (!response?.content) {
1518
+ const rejected = localRejectionStatus(failureDiag);
1519
+ if (rejected !== undefined) throw new LocalLlmRejectedError(rejected); // the endpoint is up and refuses this input
1518
1520
  log.debug("extractWithLocalLlm: chatCompletion returned null or empty content");
1519
1521
  return null;
1520
1522
  }
@@ -1547,12 +1549,10 @@ ${truncatedConversation}`;
1547
1549
  log.debug("extractWithLocalLlm: JSON parse failed, attempting partial extraction...");
1548
1550
  const partial = this.extractPartialFacts(content);
1549
1551
  if (partial.facts.length > 0 || partial.entities.length > 0) {
1550
- log.debug(
1551
- `extractWithLocalLlm: extracted ${partial.facts.length} partial facts from truncated JSON`,
1552
- );
1552
+ log.debug(`extractWithLocalLlm: extracted ${partial.facts.length} partial facts from truncated JSON`);
1553
1553
  return partial;
1554
1554
  }
1555
- return null;
1555
+ throw new LocalLlmRejectedError(); // the endpoint answered and nothing in the answer is usable
1556
1556
  }
1557
1557
 
1558
1558
  /**
@@ -700,7 +700,7 @@ export function normalizeQmdResultPath(resultPath: string, collection: string):
700
700
  }
701
701
  return value;
702
702
  }
703
- function resolveNamespaceResultPath(
703
+ export function resolveNamespaceResultPath(
704
704
  memoryDir: string,
705
705
  collection: string,
706
706
  resultPath: string,
@@ -88,6 +88,7 @@ const migrationSummary: TierMigrationCycleSummary = {
88
88
 
89
89
  interface Harness {
90
90
  config: PluginConfig;
91
+ buffer: SmartBuffer;
91
92
  storageForNs: (ns: string) => Promise<StorageManager>;
92
93
  setStorageForDelay: (delayMs: number) => void;
93
94
  newCoordinator: () => ExtractionRunCoordinator;
@@ -105,7 +106,7 @@ interface Harness {
105
106
  coord: ExtractionRunCoordinator,
106
107
  content: string,
107
108
  opts?: { force?: boolean; failClosed?: boolean; writeNamespaceOverride?: string },
108
- ) => Promise<{ status: string; reason?: string }>;
109
+ ) => Promise<{ status: string; reason?: string; failureClass?: string; retainedBuffer?: boolean }>;
109
110
  cleanup: () => Promise<void>;
110
111
  passiveCapture: () => { principal?: string; namespace?: string } | null;
111
112
  setThreadingBlock: (blocked: boolean) => void;
@@ -201,6 +202,7 @@ async function makeHarness(overrides: Record<string, unknown> = {}): Promise<Har
201
202
 
202
203
  return {
203
204
  config,
205
+ buffer,
204
206
  storageForNs,
205
207
  setStorageForDelay: (delayMs) => {
206
208
  storageForDelayMs = delayMs;
@@ -243,7 +245,7 @@ async function makeHarness(overrides: Record<string, unknown> = {}): Promise<Har
243
245
  forceExtractionAttempt: opts.force === true,
244
246
  writeNamespaceOverride: opts.writeNamespaceOverride,
245
247
  });
246
- return { status: result.status, reason: result.reason };
248
+ return { status: result.status, reason: result.reason, failureClass: result.failureClass, retainedBuffer: result.retainedBuffer };
247
249
  },
248
250
  cleanup: async () => {
249
251
  StorageManager.clearAllStaticCaches();
@@ -1336,3 +1338,82 @@ test("#3140: a successful flush heals the recorded LLM health", async () => {
1336
1338
  await h.cleanup();
1337
1339
  }
1338
1340
  });
1341
+
1342
+ // ---------------------------------------------------------------------------
1343
+ // Wiring the trigger drain depends on (#3266): what a real run hands back, and what it asks of the buffer
1344
+ // ---------------------------------------------------------------------------
1345
+
1346
+ test("a retained failure carries the extraction's failure class, so the drain can tell an outage from a bad chunk", async () => {
1347
+ for (const cls of ["provider_retryable", "auth_config", "parse_empty"] as const) {
1348
+ const h = await makeHarness({ extractionRetryScheduleMs: [3_600_000], extractionBreakerFailureThreshold: 100 });
1349
+ try {
1350
+ h.setRespond(() => failureResult(cls));
1351
+ const result = await h.run(h.newCoordinator(), `class-${cls}`);
1352
+ assert.equal(result.retainedBuffer, true, `${cls}: the window is kept for a retry`);
1353
+ assert.equal(result.failureClass, cls, `${cls}: the class reaches the run result`);
1354
+ } finally {
1355
+ await h.cleanup();
1356
+ }
1357
+ }
1358
+ });
1359
+
1360
+ test("a failure with no class defaults to provider_retryable on the run result, like the retry bookkeeping", async () => {
1361
+ const h = await makeHarness({ extractionRetryScheduleMs: [3_600_000], extractionBreakerFailureThreshold: 100 });
1362
+ try {
1363
+ h.setRespond(() => ({ ...failureResult("parse_empty"), extractionFailureClass: undefined }));
1364
+ const result = await h.run(h.newCoordinator(), "class-default");
1365
+ assert.equal(result.failureClass, "provider_retryable");
1366
+ } finally {
1367
+ await h.cleanup();
1368
+ }
1369
+ });
1370
+
1371
+ test("a run without a failure reports no failure class", async () => {
1372
+ const h = await makeHarness();
1373
+ try {
1374
+ h.setRespond(() => ({ ...successResult(), facts: [] }));
1375
+ const result = await h.run(h.newCoordinator(), "class-none");
1376
+ assert.equal(result.failureClass, undefined);
1377
+ } finally {
1378
+ await h.cleanup();
1379
+ }
1380
+ });
1381
+
1382
+ test("the retention window a chunk run is given reaches the buffer as the processed snapshot (#3266)", async () => {
1383
+ const h = await makeHarness();
1384
+ try {
1385
+ const seen: Array<readonly BufferTurn[] | undefined> = [];
1386
+ const original = h.buffer.retainDeferredTurns.bind(h.buffer);
1387
+ h.buffer.retainDeferredTurns = async (key, turns, max, processed) => {
1388
+ seen.push(processed);
1389
+ await original(key, turns, max, processed);
1390
+ };
1391
+ const chunk = makeTurns("chunk-turns");
1392
+ const window = [...makeTurns("earlier-turns"), ...chunk];
1393
+ h.setRespond(() => successResult());
1394
+ await h.newCoordinator().runExtraction(chunk, {
1395
+ skipCharThreshold: true,
1396
+ skipUserTurnThreshold: true,
1397
+ clearBufferAfterExtraction: true,
1398
+ bufferKey: "window",
1399
+ failOnExtractionFailure: false,
1400
+ forceExtractionAttempt: false,
1401
+ retentionWindow: window,
1402
+ });
1403
+ assert.equal(seen.length, 1, "the run asked the buffer to update its retention slot once");
1404
+ assert.deepEqual(seen[0], window, "with the window it was given, not just its own chunk");
1405
+
1406
+ seen.length = 0;
1407
+ await h.newCoordinator().runExtraction(chunk, {
1408
+ skipCharThreshold: true,
1409
+ skipUserTurnThreshold: true,
1410
+ clearBufferAfterExtraction: true,
1411
+ bufferKey: "window",
1412
+ failOnExtractionFailure: false,
1413
+ forceExtractionAttempt: false,
1414
+ });
1415
+ assert.deepEqual(seen[0], chunk, "without a window the run keeps naming its own turns");
1416
+ } finally {
1417
+ await h.cleanup();
1418
+ }
1419
+ });
@@ -26,12 +26,13 @@
26
26
  * RecallRerankCoordinator / ConversationIndexCoordinator accessor pattern.
27
27
  */
28
28
 
29
+ import { existsSync } from "node:fs";
29
30
  import path from "node:path";
30
31
  import { resolveNamespaceCapabilities } from "../capabilities.js";
31
32
  import type { StorageManager } from "../index.js";
32
33
  import { log } from "../logger.js";
33
34
  import { namespaceIdentityFromToken } from "../namespaces/identity.js";
34
- import { normalizeQmdResultPath } from "../namespaces/search.js";
35
+ import { normalizeQmdResultPath, resolveNamespaceResultPath } from "../namespaces/search.js";
35
36
  import { SecureStoreLockedError } from "../secure-store/index.js";
36
37
  import { isPathInsideStorageRoot } from "../storage-paths.js";
37
38
  import { isSupportPassportPrivateMemory } from "../support-passport/card-projection.js";
@@ -136,6 +137,30 @@ export function qmdResultPathCandidates(storageDir: string, resultPath: string):
136
137
  return [...candidates];
137
138
  }
138
139
 
140
+ /** Canonicalize flat hot-memory identities before merging with native hits (#3194).
141
+ * This is a path-only map: no candidates, collections or namespace scopes are removed.
142
+ * Cold/explicit foreign collections keep their existing owner-resolution path.
143
+ */
144
+ export function normalizeFlatQmdResults(
145
+ results: QmdSearchResult[],
146
+ config: PluginConfig,
147
+ collection = config.qmdCollection,
148
+ ): QmdSearchResult[] {
149
+ if (!collection || resolveNamespaceCapabilities(config).namespaces || collection !== config.qmdCollection) return results;
150
+ return results.map((result) => ({
151
+ ...result,
152
+ path: withHotFactsPath(config.memoryDir, resolveNamespaceResultPath(config.memoryDir, collection, result.path)),
153
+ }));
154
+ }
155
+
156
+ /** A hot-facts collection registered at the facts/ subtree returns date-relative paths (#2111); meet the loaded memory's path. */
157
+ function withHotFactsPath(memoryDir: string, resolved: string): string {
158
+ const relative = path.relative(path.resolve(memoryDir), resolved).split(path.sep).join("/");
159
+ if (!/^\d{4}-\d{2}-\d{2}\//.test(relative) || existsSync(resolved)) return resolved;
160
+ const hot = path.join(path.resolve(memoryDir), "facts", relative);
161
+ return existsSync(hot) ? hot : resolved;
162
+ }
163
+
139
164
  /**
140
165
  * Extract a non-empty `dir` from a storage, returning `null` when the
141
166
  * property is absent or not a non-empty string at runtime. Proxy-backed