@remnic/core 9.69.61 → 9.69.64

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 (263) hide show
  1. package/dist/access-admin-ops-surface.js +27 -27
  2. package/dist/access-authorization-probe.js +28 -28
  3. package/dist/access-boundary.js +27 -27
  4. package/dist/access-cli.js +68 -68
  5. package/dist/access-extraction-force-flush.js +27 -27
  6. package/dist/access-http-briefing.js +28 -28
  7. package/dist/access-http-query.js +29 -29
  8. package/dist/access-http.js +44 -44
  9. package/dist/access-identity-continuity-surface.js +27 -27
  10. package/dist/access-lcm-surface.js +27 -27
  11. package/dist/access-mcp-output-schemas.d.ts.map +1 -1
  12. package/dist/access-mcp-output-schemas.js +1 -1
  13. package/dist/access-mcp.js +37 -37
  14. package/dist/access-namespace-preflight.js +27 -27
  15. package/dist/access-observe-write-surface.js +27 -27
  16. package/dist/access-operations-batch.js +28 -28
  17. package/dist/access-operations.js +35 -35
  18. package/dist/access-recall-concurrency.js +27 -27
  19. package/dist/access-recall-response.d.ts.map +1 -1
  20. package/dist/access-recall-response.js +27 -27
  21. package/dist/access-recall-surface.js +27 -27
  22. package/dist/access-service.d.ts +4 -0
  23. package/dist/access-service.d.ts.map +1 -1
  24. package/dist/access-service.js +27 -27
  25. package/dist/access-surface-catalog.js +29 -29
  26. package/dist/active-recall.js +9 -9
  27. package/dist/briefing.js +10 -10
  28. package/dist/buffer.js +5 -5
  29. package/dist/causal-consolidation.js +11 -11
  30. package/dist/{chunk-52VL6LCY.js → chunk-2VH2JWYN.js} +2 -2
  31. package/dist/{chunk-WD32KTVI.js → chunk-2ZVSQPAA.js} +32 -16
  32. package/dist/chunk-2ZVSQPAA.js.map +1 -0
  33. package/dist/{chunk-G4WZDQ32.js → chunk-3NG5OOCX.js} +2 -2
  34. package/dist/{chunk-KG2OOAFF.js → chunk-3NTVCMDD.js} +2 -2
  35. package/dist/{chunk-7OC3Q5X4.js → chunk-3S4R2HA5.js} +2 -2
  36. package/dist/{chunk-NORFVW2H.js → chunk-4RWPQZYD.js} +2 -2
  37. package/dist/{chunk-B5GMOOY2.js → chunk-5AABWM72.js} +3 -3
  38. package/dist/{chunk-KRPMISWO.js → chunk-5C765XYU.js} +2 -2
  39. package/dist/{chunk-WLZYGLJ4.js → chunk-5MA7ZLPD.js} +5 -7
  40. package/dist/chunk-5MA7ZLPD.js.map +1 -0
  41. package/dist/{chunk-XOLVM3AT.js → chunk-5WXJE5TV.js} +2 -2
  42. package/dist/{chunk-2C5Z7CWB.js → chunk-6YTL3JYX.js} +2 -2
  43. package/dist/{chunk-F4PEETM4.js → chunk-755VRFBS.js} +2 -2
  44. package/dist/{chunk-HNOKNHFO.js → chunk-7L747CFR.js} +2 -2
  45. package/dist/{chunk-WPGR3RW4.js → chunk-7VO6E5UD.js} +2 -2
  46. package/dist/{chunk-LMLPLIT4.js → chunk-AQ6IBVFQ.js} +2 -2
  47. package/dist/{chunk-XN4CPSCN.js → chunk-B2JE56NB.js} +2 -2
  48. package/dist/{chunk-L4WBADKG.js → chunk-B5EZWPSM.js} +2 -2
  49. package/dist/{chunk-EIU642TN.js → chunk-BE4GMH45.js} +2 -2
  50. package/dist/{chunk-NLE6TSLG.js → chunk-BLVHDY2U.js} +5 -7
  51. package/dist/chunk-BLVHDY2U.js.map +1 -0
  52. package/dist/{chunk-JTTLH646.js → chunk-C2W6JKQQ.js} +2 -2
  53. package/dist/{chunk-FG5QU7A2.js → chunk-CFCAFAJC.js} +2 -2
  54. package/dist/{chunk-ZJ6MEVTX.js → chunk-CZM4MHZF.js} +37 -32
  55. package/dist/chunk-CZM4MHZF.js.map +1 -0
  56. package/dist/{chunk-M4PHCEPU.js → chunk-DDY7LJMP.js} +2 -2
  57. package/dist/{chunk-2L5DIVDU.js → chunk-DT33WNG2.js} +4 -4
  58. package/dist/{chunk-SZAWZPY6.js → chunk-E7SKAUEW.js} +7 -7
  59. package/dist/{chunk-NRVGODSE.js → chunk-EK4MOTHM.js} +6 -6
  60. package/dist/{chunk-QEGKMZNQ.js → chunk-FO5NDQZH.js} +3 -3
  61. package/dist/chunk-G26YNEOC.js +37 -0
  62. package/dist/chunk-G26YNEOC.js.map +1 -0
  63. package/dist/{chunk-GMHWCPLP.js → chunk-GZLSIPYU.js} +2 -2
  64. package/dist/{chunk-ZFUPRODV.js → chunk-HPWJ5B7D.js} +39 -3
  65. package/dist/chunk-HPWJ5B7D.js.map +1 -0
  66. package/dist/{chunk-SEHKTE4F.js → chunk-I4IKTNZP.js} +3 -3
  67. package/dist/{chunk-W65KK3X6.js → chunk-II4TIQCU.js} +2 -2
  68. package/dist/{chunk-FWVDTLRK.js → chunk-IJQEVNEF.js} +1 -1
  69. package/dist/{chunk-KXJ7XYUT.js → chunk-IOUQUXHO.js} +2 -2
  70. package/dist/{chunk-V3JM4XEI.js → chunk-ITLFUILH.js} +2 -2
  71. package/dist/{chunk-ZNFHWNDM.js → chunk-IULYVB5X.js} +5 -5
  72. package/dist/{chunk-Z3YQOV45.js → chunk-JLU7WK22.js} +2 -2
  73. package/dist/{chunk-NRTVZSPO.js → chunk-L4HT5QZ5.js} +2 -2
  74. package/dist/{chunk-5QZHDS4X.js → chunk-LI4IWBLN.js} +2 -2
  75. package/dist/{chunk-LY3SDHMI.js → chunk-LQ4OSM5V.js} +3 -3
  76. package/dist/{chunk-2OUSWDY6.js → chunk-M3TMVHQ2.js} +2 -2
  77. package/dist/{chunk-IDRSTHVR.js → chunk-MB3HMRXM.js} +2 -2
  78. package/dist/{chunk-4P3ZBL5E.js → chunk-MUGLJAZ3.js} +2 -2
  79. package/dist/{chunk-ZOASYXBS.js → chunk-MY2PLXSM.js} +6 -2
  80. package/dist/chunk-MY2PLXSM.js.map +1 -0
  81. package/dist/{chunk-4IFSMCRS.js → chunk-NYN4ZMPL.js} +2 -2
  82. package/dist/{chunk-K7S2IMGJ.js → chunk-O7QF3TCV.js} +322 -25
  83. package/dist/{chunk-K7S2IMGJ.js.map → chunk-O7QF3TCV.js.map} +1 -1
  84. package/dist/{chunk-KV44HTMJ.js → chunk-OBD77TYC.js} +2 -2
  85. package/dist/{chunk-GYNCJSQ7.js → chunk-OCEIHKH5.js} +3 -3
  86. package/dist/{chunk-DC5CTLCZ.js → chunk-PSGRZFBH.js} +10 -10
  87. package/dist/{chunk-FQGY7XB2.js → chunk-S52VKUUW.js} +2 -2
  88. package/dist/{chunk-D7MAYFQP.js → chunk-S7LWX2PQ.js} +2 -2
  89. package/dist/{chunk-TYP7GYRI.js → chunk-SB4ZWVKI.js} +2 -2
  90. package/dist/{chunk-KVO2BTA4.js → chunk-T2YX56QI.js} +2 -2
  91. package/dist/{chunk-G6NQ3L4T.js → chunk-T3FMRLWQ.js} +2 -2
  92. package/dist/{chunk-CDQO5DCD.js → chunk-TO3G56GM.js} +2 -2
  93. package/dist/{chunk-WEQZ5OYK.js → chunk-U6GKPSZH.js} +2 -2
  94. package/dist/{chunk-FM5HN7LL.js → chunk-WQC54W25.js} +9 -17
  95. package/dist/chunk-WQC54W25.js.map +1 -0
  96. package/dist/{chunk-XDH5MO6L.js → chunk-WZ33AIOP.js} +2 -2
  97. package/dist/{chunk-TM7S5UHD.js → chunk-XDIFADVN.js} +2 -2
  98. package/dist/{chunk-SVWYZXIL.js → chunk-XDOXR3U6.js} +2 -2
  99. package/dist/{chunk-VKDWYDFQ.js → chunk-XGLBVYEA.js} +5 -5
  100. package/dist/{chunk-5Y7VCE3A.js → chunk-XJNSOI23.js} +2 -2
  101. package/dist/{chunk-LDP6LBDS.js → chunk-Y3AFTYK4.js} +2 -2
  102. package/dist/{chunk-YL2W6NUG.js → chunk-YGJMMMPN.js} +4 -4
  103. package/dist/{chunk-KIOXEHWF.js → chunk-YIDBKTZJ.js} +2 -2
  104. package/dist/{chunk-F5ZCNJ3V.js → chunk-ZPPWW576.js} +2 -2
  105. package/dist/{chunk-TQAXUYUE.js → chunk-ZSXSMVW6.js} +2 -2
  106. package/dist/{chunk-6RD7MSIO.js → chunk-ZUT4FK5W.js} +2 -2
  107. package/dist/cli.js +54 -54
  108. package/dist/coding/export-okf-codegraph.js +9 -9
  109. package/dist/compounding/engine.js +10 -10
  110. package/dist/config.js +9 -9
  111. package/dist/connectors/codex-materialize-runner.js +10 -10
  112. package/dist/connectors/index.js +10 -10
  113. package/dist/convergence-refresh.js +12 -12
  114. package/dist/corpus-watermark.js +12 -12
  115. package/dist/entity-retrieval.js +10 -10
  116. package/dist/external-wiki-access.js +28 -28
  117. package/dist/extraction.js +16 -16
  118. package/dist/harmonic-retrieval.js +1 -1
  119. package/dist/index.d.ts +1 -1
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +70 -68
  122. package/dist/location/index.js +12 -12
  123. package/dist/maintenance/memory-governance.js +10 -10
  124. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
  125. package/dist/maintenance/rebuild-memory-projection.js +11 -11
  126. package/dist/memory-browse-operations.js +28 -28
  127. package/dist/memory-browse.js +5 -5
  128. package/dist/model-registry.d.ts.map +1 -1
  129. package/dist/model-registry.js +1 -1
  130. package/dist/namespaces/migrate.js +11 -11
  131. package/dist/namespaces/search.js +8 -8
  132. package/dist/namespaces/storage.js +10 -10
  133. package/dist/operator-doctor-corpus.js +13 -13
  134. package/dist/operator-toolkit.js +18 -18
  135. package/dist/orchestration/maintenance.js +18 -18
  136. package/dist/orchestration/recall-internal.d.ts.map +1 -1
  137. package/dist/orchestrator.js +68 -68
  138. package/dist/profiling.d.ts.map +1 -1
  139. package/dist/profiling.js +1 -1
  140. package/dist/qmd.js +2 -2
  141. package/dist/recall-composition-decision.d.ts +9 -0
  142. package/dist/recall-composition-decision.d.ts.map +1 -1
  143. package/dist/recall-composition-decision.js +2 -1
  144. package/dist/recall-navigation-operations.js +28 -28
  145. package/dist/recall-navigation.js +27 -27
  146. package/dist/recall-state-view-admission.js +5 -5
  147. package/dist/recall-why-service.d.ts.map +1 -1
  148. package/dist/recall-why-service.js +5 -5
  149. package/dist/resume-bundles.js +9 -9
  150. package/dist/search/factory.js +7 -7
  151. package/dist/search/index.js +7 -7
  152. package/dist/search/lancedb-backend.js +2 -2
  153. package/dist/search/meilisearch-backend.js +2 -2
  154. package/dist/search/orama-backend.js +2 -2
  155. package/dist/search/port.d.ts +9 -0
  156. package/dist/search/port.d.ts.map +1 -1
  157. package/dist/search/port.js +7 -1
  158. package/dist/search/remote-backend.js +2 -2
  159. package/dist/security/audit-memory-cli.d.ts.map +1 -1
  160. package/dist/security/audit-memory.d.ts +10 -0
  161. package/dist/security/audit-memory.d.ts.map +1 -1
  162. package/dist/security/injection-screen.d.ts +1 -2
  163. package/dist/security/injection-screen.d.ts.map +1 -1
  164. package/dist/semantic-consolidation.js +11 -11
  165. package/dist/semantic-rule-promotion.js +10 -10
  166. package/dist/semantic-rule-verifier.js +10 -10
  167. package/dist/source-agent-qualifier.js +13 -13
  168. package/dist/standup/build.js +10 -10
  169. package/dist/storage.js +9 -9
  170. package/dist/summarizer.js +2 -2
  171. package/dist/support-passport/index.js +30 -30
  172. package/dist/transfer/export-okf.js +10 -10
  173. package/dist/verified-recall.js +10 -10
  174. package/dist/who-knows-cli.js +11 -11
  175. package/dist/who-knows.js +10 -10
  176. package/package.json +12 -2
  177. package/src/access-mcp-output-schemas.ts +4 -0
  178. package/src/access-recall-response.ts +26 -1
  179. package/src/access-service.ts +1 -0
  180. package/src/index.ts +1 -1
  181. package/src/model-registry.test.ts +22 -1
  182. package/src/model-registry.ts +7 -6
  183. package/src/orchestration/recall-internal.ts +8 -5
  184. package/src/profiling.test.ts +20 -1
  185. package/src/profiling.ts +7 -7
  186. package/src/recall-composition-decision.test.ts +75 -0
  187. package/src/recall-composition-decision.ts +52 -3
  188. package/src/recall-retrieval-failure.test.ts +245 -0
  189. package/src/recall-why-service.ts +8 -24
  190. package/src/search/port.ts +32 -0
  191. package/src/security/audit-memory-cli.test.ts +90 -0
  192. package/src/security/audit-memory-cli.ts +7 -0
  193. package/src/security/audit-memory.ts +14 -0
  194. package/src/security/injection-screen.test.ts +245 -0
  195. package/src/security/injection-screen.ts +341 -11
  196. package/dist/chunk-FM5HN7LL.js.map +0 -1
  197. package/dist/chunk-NLE6TSLG.js.map +0 -1
  198. package/dist/chunk-U2VDCFXW.js +0 -19
  199. package/dist/chunk-U2VDCFXW.js.map +0 -1
  200. package/dist/chunk-WD32KTVI.js.map +0 -1
  201. package/dist/chunk-WLZYGLJ4.js.map +0 -1
  202. package/dist/chunk-ZFUPRODV.js.map +0 -1
  203. package/dist/chunk-ZJ6MEVTX.js.map +0 -1
  204. package/dist/chunk-ZOASYXBS.js.map +0 -1
  205. /package/dist/{chunk-52VL6LCY.js.map → chunk-2VH2JWYN.js.map} +0 -0
  206. /package/dist/{chunk-G4WZDQ32.js.map → chunk-3NG5OOCX.js.map} +0 -0
  207. /package/dist/{chunk-KG2OOAFF.js.map → chunk-3NTVCMDD.js.map} +0 -0
  208. /package/dist/{chunk-7OC3Q5X4.js.map → chunk-3S4R2HA5.js.map} +0 -0
  209. /package/dist/{chunk-NORFVW2H.js.map → chunk-4RWPQZYD.js.map} +0 -0
  210. /package/dist/{chunk-B5GMOOY2.js.map → chunk-5AABWM72.js.map} +0 -0
  211. /package/dist/{chunk-KRPMISWO.js.map → chunk-5C765XYU.js.map} +0 -0
  212. /package/dist/{chunk-XOLVM3AT.js.map → chunk-5WXJE5TV.js.map} +0 -0
  213. /package/dist/{chunk-2C5Z7CWB.js.map → chunk-6YTL3JYX.js.map} +0 -0
  214. /package/dist/{chunk-F4PEETM4.js.map → chunk-755VRFBS.js.map} +0 -0
  215. /package/dist/{chunk-HNOKNHFO.js.map → chunk-7L747CFR.js.map} +0 -0
  216. /package/dist/{chunk-WPGR3RW4.js.map → chunk-7VO6E5UD.js.map} +0 -0
  217. /package/dist/{chunk-LMLPLIT4.js.map → chunk-AQ6IBVFQ.js.map} +0 -0
  218. /package/dist/{chunk-XN4CPSCN.js.map → chunk-B2JE56NB.js.map} +0 -0
  219. /package/dist/{chunk-L4WBADKG.js.map → chunk-B5EZWPSM.js.map} +0 -0
  220. /package/dist/{chunk-EIU642TN.js.map → chunk-BE4GMH45.js.map} +0 -0
  221. /package/dist/{chunk-JTTLH646.js.map → chunk-C2W6JKQQ.js.map} +0 -0
  222. /package/dist/{chunk-FG5QU7A2.js.map → chunk-CFCAFAJC.js.map} +0 -0
  223. /package/dist/{chunk-M4PHCEPU.js.map → chunk-DDY7LJMP.js.map} +0 -0
  224. /package/dist/{chunk-2L5DIVDU.js.map → chunk-DT33WNG2.js.map} +0 -0
  225. /package/dist/{chunk-SZAWZPY6.js.map → chunk-E7SKAUEW.js.map} +0 -0
  226. /package/dist/{chunk-NRVGODSE.js.map → chunk-EK4MOTHM.js.map} +0 -0
  227. /package/dist/{chunk-QEGKMZNQ.js.map → chunk-FO5NDQZH.js.map} +0 -0
  228. /package/dist/{chunk-GMHWCPLP.js.map → chunk-GZLSIPYU.js.map} +0 -0
  229. /package/dist/{chunk-SEHKTE4F.js.map → chunk-I4IKTNZP.js.map} +0 -0
  230. /package/dist/{chunk-W65KK3X6.js.map → chunk-II4TIQCU.js.map} +0 -0
  231. /package/dist/{chunk-FWVDTLRK.js.map → chunk-IJQEVNEF.js.map} +0 -0
  232. /package/dist/{chunk-KXJ7XYUT.js.map → chunk-IOUQUXHO.js.map} +0 -0
  233. /package/dist/{chunk-V3JM4XEI.js.map → chunk-ITLFUILH.js.map} +0 -0
  234. /package/dist/{chunk-ZNFHWNDM.js.map → chunk-IULYVB5X.js.map} +0 -0
  235. /package/dist/{chunk-Z3YQOV45.js.map → chunk-JLU7WK22.js.map} +0 -0
  236. /package/dist/{chunk-NRTVZSPO.js.map → chunk-L4HT5QZ5.js.map} +0 -0
  237. /package/dist/{chunk-5QZHDS4X.js.map → chunk-LI4IWBLN.js.map} +0 -0
  238. /package/dist/{chunk-LY3SDHMI.js.map → chunk-LQ4OSM5V.js.map} +0 -0
  239. /package/dist/{chunk-2OUSWDY6.js.map → chunk-M3TMVHQ2.js.map} +0 -0
  240. /package/dist/{chunk-IDRSTHVR.js.map → chunk-MB3HMRXM.js.map} +0 -0
  241. /package/dist/{chunk-4P3ZBL5E.js.map → chunk-MUGLJAZ3.js.map} +0 -0
  242. /package/dist/{chunk-4IFSMCRS.js.map → chunk-NYN4ZMPL.js.map} +0 -0
  243. /package/dist/{chunk-KV44HTMJ.js.map → chunk-OBD77TYC.js.map} +0 -0
  244. /package/dist/{chunk-GYNCJSQ7.js.map → chunk-OCEIHKH5.js.map} +0 -0
  245. /package/dist/{chunk-DC5CTLCZ.js.map → chunk-PSGRZFBH.js.map} +0 -0
  246. /package/dist/{chunk-FQGY7XB2.js.map → chunk-S52VKUUW.js.map} +0 -0
  247. /package/dist/{chunk-D7MAYFQP.js.map → chunk-S7LWX2PQ.js.map} +0 -0
  248. /package/dist/{chunk-TYP7GYRI.js.map → chunk-SB4ZWVKI.js.map} +0 -0
  249. /package/dist/{chunk-KVO2BTA4.js.map → chunk-T2YX56QI.js.map} +0 -0
  250. /package/dist/{chunk-G6NQ3L4T.js.map → chunk-T3FMRLWQ.js.map} +0 -0
  251. /package/dist/{chunk-CDQO5DCD.js.map → chunk-TO3G56GM.js.map} +0 -0
  252. /package/dist/{chunk-WEQZ5OYK.js.map → chunk-U6GKPSZH.js.map} +0 -0
  253. /package/dist/{chunk-XDH5MO6L.js.map → chunk-WZ33AIOP.js.map} +0 -0
  254. /package/dist/{chunk-TM7S5UHD.js.map → chunk-XDIFADVN.js.map} +0 -0
  255. /package/dist/{chunk-SVWYZXIL.js.map → chunk-XDOXR3U6.js.map} +0 -0
  256. /package/dist/{chunk-VKDWYDFQ.js.map → chunk-XGLBVYEA.js.map} +0 -0
  257. /package/dist/{chunk-5Y7VCE3A.js.map → chunk-XJNSOI23.js.map} +0 -0
  258. /package/dist/{chunk-LDP6LBDS.js.map → chunk-Y3AFTYK4.js.map} +0 -0
  259. /package/dist/{chunk-YL2W6NUG.js.map → chunk-YGJMMMPN.js.map} +0 -0
  260. /package/dist/{chunk-KIOXEHWF.js.map → chunk-YIDBKTZJ.js.map} +0 -0
  261. /package/dist/{chunk-F5ZCNJ3V.js.map → chunk-ZPPWW576.js.map} +0 -0
  262. /package/dist/{chunk-TQAXUYUE.js.map → chunk-ZSXSMVW6.js.map} +0 -0
  263. /package/dist/{chunk-6RD7MSIO.js.map → chunk-ZUT4FK5W.js.map} +0 -0
@@ -177,14 +177,285 @@ const RESPONSE_CONTROL_PATTERNS: readonly RegExp[] = [
177
177
  ];
178
178
 
179
179
  /**
180
- * A directive over what the agent's future answer must contain or say.
181
- * Ordinary facts that merely pair "policy"/"note"/"response" with a common
182
- * verb ("the return policy requires a receipt") are not directives.
180
+ * Corroborating signals that separate an injected response control from
181
+ * ordinary prose about response content (issue #3078). "The API response
182
+ * must include a Content-Type header" is directive-SHAPED but names an
183
+ * ordinary field; an injection additionally carries one of:
184
+ *
185
+ * - an opaque payload to emit: a marker token, a long hex/UUID run, a URL;
186
+ * - verbatim control: literal / exact / word-for-word / "with no explanation";
187
+ * - cross-turn persistence: remember / persist / store this / any later
188
+ * retrieval / across conversations / from now on;
189
+ * - an agent-directed subject: your answer, the assistant's reply.
190
+ *
191
+ * Requiring one of these keeps every frozen attack payload flagged while
192
+ * ordinary technical and policy sentences fall to zero.
193
+ */
194
+ /**
195
+ * Corroborating cues. Measured trade (PR #3079 r4): these are evaluated over
196
+ * the whole memory, not the directive's sentence. Thirty frozen attack
197
+ * payloads state the directive and name the payload in DIFFERENT sentences,
198
+ * so sentence-local cues lose real detections; the price is that a marker in
199
+ * an unrelated sentence can corroborate an ordinary directive. The
200
+ * emission-slot check below stays sentence-local, where locality is free.
201
+ */
202
+ const RESPONSE_CONTROL_CORROBORATORS: readonly RegExp[] = [
203
+ /\bCANARY\b/i,
204
+ /https?:\/\//i,
205
+ /\b[0-9a-f]{8,}\b/i,
206
+ /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/,
207
+ // Naming the emission as a phrase/marker/token is a payload cue: ordinary
208
+ // prose names a field ("the request id"), not "the phrase".
209
+ /\b(?:the|this|that|following)[ \t]{1,8}(?:phrase|marker|token|string|sequence|literal|codeword|passphrase)\b/i,
210
+ // Verbatim control over the wording, not the content.
211
+ /\b(?:literal(?:ly)?|verbatim|word[- ]for[- ]word|exactly[ \t]{1,8}(?:this|that|as)|character[- ]for[- ]character)\b/i,
212
+ /\bno[ \t]{1,8}(?:explanation|commentary|preamble|deviation|other[ \t]{1,8}text)\b/i,
213
+ /\b(?:only|nothing[ \t]{1,8}else|and[ \t]{1,8}nothing)\b[^\n]{0,30}\b(?:answers?|responses?|repl(?:y|ies)|output|say|said)\b/i,
214
+ // Persistence across turns: an ordinary fact does not legislate later turns.
215
+ /\b(?:remember|persist|store[ \t]{1,8}this|retain[ \t]{1,8}this)\b/i,
216
+ /\bany[ \t]{1,8}(?:later|future|subsequent)\b/i,
217
+ /\b(?:future|later|subsequent|following|upcoming)[ \t]{1,8}(?:answers?|responses?|repl(?:y|ies)|output|turns?|messages?|conversations?|sessions?)\b/i,
218
+ /\b(?:across|between)[ \t]{1,8}(?:conversations|sessions|chats)\b/i,
219
+ /\b(?:from[ \t]{1,8}now[ \t]{1,8}on|going[ \t]{1,8}forward|in[ \t]{1,8}(?:all|every)[ \t]{1,8}(?:future|later)[ \t]{1,8}(?:turns?|answers?|responses?))\b/i,
220
+ /\b(?:whenever|every[ \t]{1,8}time|each[ \t]{1,8}time|any[ \t]{1,8}time)\b[^\n]{0,60}\b(?:asked|discussed|mentioned|queried|retrieved|recalled)\b/i,
221
+ // The subject is the agent, not a system under discussion.
222
+ /\byour[ \t]{1,8}(?:answers?|responses?|repl(?:y|ies)|output|behaviou?r|memory)\b/i,
223
+ /\b(?:assistant|agent|model)(?:'s)?[ \t]{1,8}(?:answers?|responses?|repl(?:y|ies)|output|behaviou?r)\b/i,
224
+ // A hidden activation phrase is itself the second signal.
225
+ /\b(?:activation|trigger)[ \t]{1,8}(?:phrase|key|word|code)\b/i,
226
+ /\b(?:approved|required|correct|canonical)[ \t]{1,8}(?:response|answer|reply)\b/i,
227
+ ];
228
+
229
+
230
+ /**
231
+ * Acronyms that appear as ordinary content in technical memories, so an
232
+ * all-caps token alone is not evidence of an injected marker.
233
+ */
234
+ const ORDINARY_CAPS_TOKENS: ReadonlySet<string> = new Set([
235
+ "API", "HTTP", "HTTPS", "JSON", "XML", "HTML", "CSS", "CSV", "TSV", "YAML", "TOML",
236
+ "SQL", "URL", "URI", "UUID", "ID", "IDS", "UTC", "ISO", "PDF", "PNG", "JPEG", "SVG",
237
+ "TLS", "SSL", "SSH", "DNS", "TCP", "UDP", "IP", "CDN", "CORS", "CSRF", "JWT", "OAUTH",
238
+ "REST", "GRPC", "RPC", "SDK", "CLI", "GUI", "OK", "YES", "NO", "TODO", "FAQ", "PR",
239
+ "CI", "CD", "QA", "UX", "UI", "MIT", "GPL", "EU", "US", "AM", "PM", "FYI", "ETA",
240
+ // Crypto, encoding, and spec vocabulary (trailing digits are stripped
241
+ // before this lookup, so SHA256/SHA-256/UTF8/ISO8601/RFC3339 all reduce).
242
+ "SHA", "MD", "HMAC", "AES", "RSA", "ECDSA", "PBKDF", "BCRYPT", "SCRYPT", "ARGON",
243
+ "UTF", "ASCII", "BASE", "HEX", "CRC", "GZIP", "ZSTD", "RFC", "ANSI", "IEEE",
244
+ "SMTP", "IMAP", "LDAP", "SAML", "OIDC", "SSO", "MFA", "TOTP", "ACL", "RBAC",
245
+ "AWS", "GCP", "GPU", "CPU", "RAM", "SSD", "OS", "VM", "K8S", "NPM", "SEMVER",
246
+ "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE",
247
+ ]);
248
+
249
+ /**
250
+ * Capitalized words that open ordinary templates ("replies must begin with
251
+ * Dear"), so a leading capital alone is not a marker (PR #3079 r3).
252
+ */
253
+ const ORDINARY_CAPITALIZED_WORDS: ReadonlySet<string> = new Set([
254
+ "Dear", "Hello", "Hi", "Greetings", "Please", "Thanks", "Thank", "Regards",
255
+ "Sincerely", "Yes", "No", "True", "False", "Null", "None", "Error", "Warning",
256
+ "Note", "Summary", "Subject", "Re", "Fwd", "Attention", "Notice", "Draft",
257
+ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday",
258
+ "January", "February", "March", "April", "May", "June", "July", "August",
259
+ "September", "October", "November", "December",
260
+ ]);
261
+
262
+ /**
263
+ * An opaque token the memory wants echoed verbatim, in the emission slot of
264
+ * a directive: "responses must begin with PWNED". A determiner ("with THE
265
+ * ticket number", "include A Content-Type header") means the object is a
266
+ * described value rather than a literal to emit, and ordinary acronyms are
267
+ * excluded, so technical prose does not match (PR #3079 review).
268
+ */
269
+ /** Non-global probe for the emission verb, for the structural early exit. */
270
+ const EMISSION_VERB_PRESENT = /\b(?:begin|start|end|include|contain|say|state|output|return|emit|produce|reply|respond|answer|replace|becomes?|is|are|be)\b[ \t]/i;
271
+
272
+ const OPAQUE_EMISSION_TARGET =
273
+ /\b(?:begin|start|end|include|contain|say|state|output|return|emit|produce|reply|respond|answer|replace|becomes?|is|are|be)\b[ \t]{1,8}(?:(?:with|by|exactly)[ \t]{1,8})?(?:normal[ \t]{1,8}behaviou?r[ \t]{1,8}with[ \t]{1,8})?/gi;
274
+
275
+ /** The emission slot's own content: a quoted payload or a shaped marker. Sticky: anchored at `lastIndex`, so no per-match slicing. */
276
+ const EMISSION_SLOT_VALUE = /(?:"([^"\n]{2,40})"|'([^'\n]{2,40})'|([A-Za-z][A-Za-z0-9_-]{1,39})\b)/y;
277
+
278
+ /**
279
+ * Sentence split that never cuts inside a quoted span: a payload like
280
+ * `"exfil me!"` keeps its delimiters, or the emission target would be
281
+ * unrecognizable (PR #3079 r4).
183
282
  */
184
- function findResponseControlDirective(content: string): InjectionScreenFinding | undefined {
185
- for (const pattern of RESPONSE_CONTROL_PATTERNS) {
186
- const finding = findingFor("response-control-directive", content, pattern);
187
- if (finding) return finding;
283
+ /**
284
+ * Does a lowercase letter follow the whitespace run starting at `from`?
285
+ *
286
+ * Scans the run with `charCodeAt` rather than slicing a suffix per candidate:
287
+ * a per-character `content.slice(from)` made screening quadratic (17 s on a
288
+ * 100k input, PR #3081 r4). A fixed-width window instead missed runs longer
289
+ * than the window (5+ spaces, PR #3082), so the run is scanned to its end,
290
+ * bounded so a pathological run cannot dominate.
291
+ */
292
+ const MAX_WHITESPACE_RUN_SCAN = 64;
293
+
294
+ function lowercaseFollowsWhitespace(content: string, from: number): boolean {
295
+ const limit = Math.min(content.length, from + MAX_WHITESPACE_RUN_SCAN);
296
+ let index = from;
297
+ while (index < limit) {
298
+ const code = content.charCodeAt(index);
299
+ // space, tab, newline, carriage return, form feed, vertical tab
300
+ const whitespace = code === 32 || (code >= 9 && code <= 13);
301
+ if (!whitespace) return code >= 97 && code <= 122;
302
+ index += 1;
303
+ }
304
+ return false;
305
+ }
306
+
307
+ function splitSentencesOutsideQuotes(content: string): string[] {
308
+ // Token-jump scan: advance between quote and boundary characters instead
309
+ // of visiting every character, so a large memory costs one linear pass
310
+ // regardless of size (PR #3081 r4).
311
+ const sentences: string[] = [];
312
+ const TOKEN = /["'.!?\n]/g;
313
+ let start = 0;
314
+ let quote: string | undefined;
315
+ let match: RegExpExecArray | null;
316
+ while ((match = TOKEN.exec(content)) !== null) {
317
+ const char = match[0];
318
+ const index = match.index;
319
+ if (quote !== undefined) {
320
+ if (char === quote) quote = undefined;
321
+ continue;
322
+ }
323
+ if (char === '"' || (char === "'" && !/[A-Za-z0-9]/.test(content[index - 1] ?? " "))) {
324
+ quote = char;
325
+ continue;
326
+ }
327
+ const next = content[index + 1];
328
+ // Token-internal only applies to sentence punctuation: a `.!?` glued to
329
+ // a non-whitespace character is part of a token (URL, decimal). A
330
+ // NEWLINE is never token-internal -- an ordinary next line starts with
331
+ // text, and only the explicit colon/list continuation joins lines (PR
332
+ // #3081 post-cap).
333
+ const insideToken = char !== "\n" && next !== undefined && !/\s/.test(next);
334
+ // Terminal URL punctuation ("payload? to", "payload! to") binds the
335
+ // unit ONLY when a URL actually precedes it: lowercase prose after a
336
+ // question mark is ordinary ("...header? canary deployments...") and
337
+ // must stay a separate unit (post-cap r8).
338
+ const terminalUrlPunctuation = (char === "?" || char === "!")
339
+ && lowercaseFollowsWhitespace(content, index + 1)
340
+ && /:\/\/[^\s]*$/.test(content.slice(Math.max(0, index - 300), index));
341
+ const newlineContinues = char === "\n" && /[:\-][ \t]*$/.test(content.slice(Math.max(0, index - 4), index));
342
+ const endsUnit = !insideToken && !terminalUrlPunctuation && !newlineContinues
343
+ && (char === "." || char === "!" || char === "?" || char === "\n");
344
+ if (endsUnit) {
345
+ if (index > start) sentences.push(content.slice(start, index));
346
+ start = index + 1;
347
+ }
348
+ }
349
+ if (start < content.length) sentences.push(content.slice(start));
350
+ return sentences;
351
+ }
352
+
353
+ /** Strip trailing digits so SHA256, UTF8, and ISO8601 reduce to their acronym. */
354
+ function reduceToken(token: string): string {
355
+ return token.replace(/[0-9]+$/, "");
356
+ }
357
+
358
+ /** Is one emission-slot token an ordinary described value rather than a payload? */
359
+ function isOrdinaryToken(token: string): boolean {
360
+ const segments = token.split(/[-_]+/).filter((segment) => segment.length > 0);
361
+ if (segments.length === 0) return true;
362
+ // A hyphenated Title-Case name is a field or header ("Content-Type",
363
+ // "Cache-Control"), not a marker: markers are all-caps or carry digits.
364
+ if (segments.length > 1 && segments.every((segment) => /^[A-Z][a-z]+$/.test(segment))) return true;
365
+ return segments.every((segment) => {
366
+ if (/^[0-9]+$/.test(segment)) return true;
367
+ const reduced = reduceToken(segment);
368
+ if (ORDINARY_CAPS_TOKENS.has(segment) || ORDINARY_CAPS_TOKENS.has(reduced)) return true;
369
+ return ORDINARY_CAPITALIZED_WORDS.has(segment);
370
+ });
371
+ }
372
+
373
+ /**
374
+ * An opaque token the memory wants echoed verbatim, in the emission slot of
375
+ * a directive IN THE SAME SENTENCE: "responses must begin with PWNED". A
376
+ * determiner ("with THE ticket number", "include A Content-Type header")
377
+ * means the object is a described value; ordinary acronyms, spec vocabulary,
378
+ * and template openers are excluded, quoted or not, so technical prose does
379
+ * not match (PR #3079 r2/r3).
380
+ */
381
+ function hasOpaqueEmissionTarget(sentence: string): boolean {
382
+ // The verb is matched case-insensitively ("must BEGIN with"), the marker
383
+ // case-sensitively: an all-lowercase word in the slot is ordinary prose
384
+ // and cannot be told from a marker (PR #3079 r4).
385
+ // Every verb occurrence is tried: the emission verb is not always the
386
+ // first one in the sentence ("The ANSWER must INCLUDE ...", r4).
387
+ OPAQUE_EMISSION_TARGET.lastIndex = 0;
388
+ for (
389
+ let verb = OPAQUE_EMISSION_TARGET.exec(sentence);
390
+ verb !== null;
391
+ verb = OPAQUE_EMISSION_TARGET.exec(sentence)
392
+ ) {
393
+ EMISSION_SLOT_VALUE.lastIndex = verb.index + verb[0].length;
394
+ const match = EMISSION_SLOT_VALUE.exec(sentence);
395
+ if (!match) continue;
396
+ const quoted = match[1] ?? match[2];
397
+ if (quoted !== undefined) {
398
+ const inner = quoted.trim();
399
+ // A quoted multi-word payload is opaque; a single quoted technical
400
+ // value ("OK", "Content-Type") is the same described value as unquoted.
401
+ if (!/\s/.test(inner) && isOrdinaryToken(inner)) continue;
402
+ return true;
403
+ }
404
+ const token = match[3];
405
+ if (token === undefined) continue;
406
+ // Uppercase-initial tokens are judged by shape (marker vs acronym or
407
+ // field name). Lowercase words are NOT admitted as markers: ordinary
408
+ // English is open-ended ("must include citations"), so absence from a
409
+ // finite wordlist is not positive marker evidence (#3080, PR #3081 r1).
410
+ if (/^[a-z]/.test(token)) continue;
411
+ if (!isOrdinaryToken(token)) return true;
412
+ }
413
+ return false;
414
+ }
415
+
416
+ /**
417
+ * A directive over what the agent's future answer must contain or say,
418
+ * corroborated by a second signal. Ordinary facts that merely pair
419
+ * "policy"/"note"/"response" with a common verb ("the return policy requires
420
+ * a receipt", "the API response must include a Content-Type header") are not
421
+ * directives and are not flagged.
422
+ */
423
+ function findResponseControlDirective(
424
+ content: string,
425
+ profile: InjectionScreenProfile = "default",
426
+ ): InjectionScreenFinding | undefined {
427
+ const directive = RESPONSE_CONTROL_PATTERNS.map((pattern) =>
428
+ findingFor("response-control-directive", content, pattern),
429
+ ).find((finding) => finding !== undefined);
430
+ if (!directive) return undefined;
431
+ // `hardened` is the profile an operator opts into for strict security: it
432
+ // keeps the pre-corroboration behavior, so no directive shape can be
433
+ // talked past the gate there. `default` (custom mode) requires a second
434
+ // signal, which is what keeps ordinary response prose out of quarantine.
435
+ if (profile === "hardened") return directive;
436
+ // Cue scope is a measured trade; see RESPONSE_CONTROL_CORROBORATORS.
437
+ // Cheap structural exit: if the whole memory carries no corroborator and
438
+ // no emission verb, locality cannot produce a finding, and the splitter's
439
+ // per-character scan is skipped entirely (large ordinary memories never
440
+ // pay for it) — PR #3081 r4.
441
+ const anyCorroborator = RESPONSE_CONTROL_CORROBORATORS.some((pattern) => pattern.test(content));
442
+ const anyEmissionVerb = !anyCorroborator
443
+ ? EMISSION_VERB_PRESENT.test(content)
444
+ : true;
445
+ if (!anyCorroborator && !anyEmissionVerb) return undefined;
446
+ // Corroborators are sentence-local now that sentence splitting no longer
447
+ // breaks URLs: a cue in an unrelated sentence ("The fixture value is
448
+ // CANARY.") cannot corroborate an ordinary directive, while a payload the
449
+ // directive sentence itself names ("...add https://... to answers...")
450
+ // still does (#3080).
451
+ // Cue locality: a corroborator must sit in the directive's own sentence
452
+ // (continuation lines included). The emission slot needs no splitting --
453
+ // the verb and its slot are adjacent by construction, so that check is
454
+ // intrinsically local and runs over the whole content (PR #3081 r4).
455
+ for (const sentence of splitSentencesOutsideQuotes(content)) {
456
+ if (!RESPONSE_CONTROL_PATTERNS.some((pattern) => pattern.test(sentence))) continue;
457
+ if (anyCorroborator && RESPONSE_CONTROL_CORROBORATORS.some((pattern) => pattern.test(sentence))) return directive;
458
+ if (hasOpaqueEmissionTarget(sentence)) return directive;
188
459
  }
189
460
  return undefined;
190
461
  }
@@ -210,9 +481,61 @@ function findAuthorityEscalation(content: string): InjectionScreenFinding | unde
210
481
  }
211
482
 
212
483
  /** Screen a candidate fact without model calls, I/O, or mutable state. */
213
- export function screenCandidateFact(content: string, profile: InjectionScreenProfile = "default"): InjectionScreenResult {
484
+ /**
485
+ * Normalize whitespace and invisible formatting before screening.
486
+ *
487
+ * Two evasion families, both attacker-only (ordinary prose carries neither):
488
+ *
489
+ * 1. The rule patterns bound their inter-word whitespace (a ReDoS-safety
490
+ * requirement, PR #3079 r4), so a long run of spaces split a directive
491
+ * from its corroborator: `must include<40 spaces>CANARY` screened clean
492
+ * while the 1-space form quarantined. ASCII runs collapse to one space.
493
+ * 2. The patterns match ASCII space and tab, so ANY other space-like
494
+ * separator defeated them as a SINGLETON -- `The response must\u00a0include
495
+ * CANARY.` screened clean, and the same held for U+2000-U+200A, U+202F,
496
+ * U+205F, U+3000, VT, FF, U+0085, and U+2028 (PR #3094 r1). Each
497
+ * occurrence becomes a plain space, singletons included.
498
+ *
499
+ * The vertical members (VT, FF, U+0085, U+2028) are folded to a space
500
+ * deliberately, NOT treated as locality boundaries like `\n`: only `\n`
501
+ * appears in ordinary prose, so honoring the others as boundaries would
502
+ * hand an attacker a one-character directive split that the screen then
503
+ * refuses to read as one unit (PR #3094 r2, declined behavior change).
504
+ *
505
+ * Zero-width and directionality characters are ambiguous: inside a word
506
+ * `inc\u200blude` reads as `include`, so they must be DELETED, but between
507
+ * words `must\u200binclude` reads as two words, so they must become a SPACE.
508
+ * Telling those apart needs a lexicon, so both readings are screened and the
509
+ * stronger result wins -- an attacker gains nothing from either placement.
510
+ *
511
+ * Newlines survive every pass -- the continuation rule reads them as
512
+ * locality boundaries.
513
+ */
514
+ const INVISIBLE_FORMATTING = /[\u00ad\u200b-\u200f\u2060\u2028\u2029\ufeff]/g;
515
+ const NON_ASCII_SPACE_LIKE = /[\f\v\u0085\u1680\u00a0\u2000-\u200a\u202f\u205f\u3000]/g;
516
+ const ASCII_SPACE_RUN = /[ \t]{2,}/g;
517
+
518
+ function normalizeForScreening(content: string): readonly string[] {
519
+ const spaced = content
520
+ .replace(NON_ASCII_SPACE_LIKE, " ")
521
+ .replace(INVISIBLE_FORMATTING, " ")
522
+ .replace(ASCII_SPACE_RUN, " ");
523
+ const deleted = content
524
+ .replace(INVISIBLE_FORMATTING, "")
525
+ .replace(NON_ASCII_SPACE_LIKE, " ")
526
+ .replace(ASCII_SPACE_RUN, " ");
527
+ return spaced === deleted ? [spaced] : [spaced, deleted];
528
+ }
529
+
530
+ export function screenCandidateFact(
531
+ rawContent: string,
532
+ profile: InjectionScreenProfile = "default",
533
+ ): InjectionScreenResult {
534
+ const variants = normalizeForScreening(rawContent);
214
535
  const findings: InjectionScreenFinding[] = [];
215
- const rules: Array<[string, (value: string) => InjectionScreenFinding | undefined]> = [
536
+ const rules: Array<
537
+ [string, (value: string, screenProfile: InjectionScreenProfile) => InjectionScreenFinding | undefined]
538
+ > = [
216
539
  ["imperative-to-agent", findImperativeToAgent],
217
540
  ["tool-invocation-syntax", findToolInvocationSyntax],
218
541
  ["encoded-blob", findEncodedBlob],
@@ -222,9 +545,16 @@ export function screenCandidateFact(content: string, profile: InjectionScreenPro
222
545
  ["response-control-directive", findResponseControlDirective],
223
546
  ["tool-routing-directive", findToolRoutingDirective],
224
547
  ];
548
+ // A rule fires if it fires under EITHER reading of the invisible
549
+ // characters (see normalizeForScreening); the first hit wins its excerpt.
225
550
  for (const [rule, find] of rules) {
226
- const finding = find(content);
227
- if (finding) findings.push({ rule, excerpt: finding.excerpt });
551
+ for (const content of variants) {
552
+ const finding = find(content, profile);
553
+ if (finding) {
554
+ findings.push({ rule, excerpt: finding.excerpt });
555
+ break;
556
+ }
557
+ }
228
558
  }
229
559
  const score = findings.reduce((sum, finding) => sum + ruleWeight(finding.rule, profile), 0);
230
560
  return { score, findings, quarantine: score >= INJECTION_SCREEN_THRESHOLD };
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/recall-why-service.ts"],"sourcesContent":["/**\n * Access-surface binding for the recall-miss diagnosis (issue #3033).\n *\n * `recall-why.ts` holds the stage logic and a port; this module binds that\n * port to the live orchestrator. It lives beside `access-recall-surface.ts`\n * rather than inside it because that file is at its file-size ratchet\n * ceiling (AGENTS.md: extract the addition to a sibling module).\n *\n * Namespace scope is enforced BEFORE the recall fires and the expected-memory\n * lookup is confined to namespaces the caller may read, so the diagnosis can\n * never confirm the existence of a memory in a namespace the caller has no\n * read access to. Read path and write path resolve through the same namespace\n * layer (Review Prevention Checklist #30): the lookup consumes exactly the\n * `recallNamespacesForPrincipal` set the recall itself is scoped by.\n */\n\nimport { resolveNamespaceCapabilities, resolveQmdCapabilities } from \"./capabilities.js\";\nimport { planRecallMode } from \"./intent.js\";\nimport { canReadNamespace, recallNamespacesForPrincipal, resolvePrincipal } from \"./namespaces/principal.js\";\nimport { isGenericRecallExcludedPath } from \"./orchestration/generic-recall-paths.js\";\nimport type { Orchestrator } from \"./orchestrator.js\";\nimport {\n explainRecallMiss,\n RecallWhyInputError,\n type RecallWhyDeps,\n type RecallWhyMemoryRef,\n type RecallWhyReport,\n} from \"./recall-why.js\";\nimport { summarizeRecallWhy } from \"./recall-why-renderer.js\";\nimport type { SearchDegradation } from \"./search/port.js\";\nimport type { MemoryFile, MemoryStatus } from \"./types.js\";\n\n/**\n * Degradation codes that mean the search backend could NOT answer, so an\n * empty pipeline carries no information. `vector_tier_unavailable` is\n * excluded on purpose: the lexical tier still answered, so the result set\n * is degraded but real.\n */\nconst FATAL_DEGRADATION_CODES: Partial<Record<SearchDegradation[\"code\"], true>> = {\n backend_unavailable: true,\n backend_error: true,\n daemon_timeout: true,\n daemon_loading: true,\n subprocess_error: true,\n deadline_exceeded: true,\n remote_error: true,\n};\n\nfunction describeDegradations(degradations: readonly SearchDegradation[]): string {\n return degradations.map((d) => `${d.backend}:${d.code}${d.detail !== undefined ? ` (${d.detail})` : \"\"}`).join(\"; \");\n}\n\nexport interface RecallWhyRequest {\n query: string;\n /** Memory id or substring to trace through every stage. */\n expect?: string;\n sessionKey?: string;\n namespace?: string;\n authenticatedPrincipal?: string;\n sourceConnector?: string;\n abortSignal?: AbortSignal;\n}\n\nexport interface RecallWhyResponse {\n /** False when the namespace ACL denied the diagnosis. */\n reportFound: boolean;\n report?: RecallWhyReport;\n /** One-line verdict, for agent callers that do not parse the full report. */\n summary?: string;\n}\n\nexport interface RecallWhyServiceDeps {\n readonly orchestrator: Orchestrator;\n /** The surface's shared namespace resolver. */\n resolveNamespace(namespace?: string): string;\n}\n\n/**\n * Run the recall-miss diagnosis for one request.\n *\n * Throws {@link RecallWhyInputError} for caller-input faults so surfaces can\n * map them to a 400; backend faults are reported inside the report as\n * `backend_unavailable` rather than thrown, because an outage is a diagnosis\n * result, not a diagnosis failure.\n */\nexport async function runRecallWhy(deps: RecallWhyServiceDeps, request: RecallWhyRequest): Promise<RecallWhyResponse> {\n const query = typeof request.query === \"string\" ? request.query : \"\";\n if (query.trim().length === 0) {\n throw new RecallWhyInputError(\"recallWhy: query is required and must be non-empty\");\n }\n const orchestrator = deps.orchestrator;\n const config = orchestrator.config;\n const namespacesEnabled = resolveNamespaceCapabilities(config).namespaces;\n const requestedNamespace = request.namespace?.trim() ? deps.resolveNamespace(request.namespace) : undefined;\n const authenticatedPrincipal = request.authenticatedPrincipal?.trim();\n const principal = authenticatedPrincipal || resolvePrincipal(request.sessionKey, config);\n\n if (requestedNamespace !== undefined) {\n if (!canReadNamespace(principal, requestedNamespace, config)) {\n return { reportFound: false };\n }\n } else if (namespacesEnabled && !authenticatedPrincipal && !request.sessionKey?.trim()) {\n // Namespaces on but no identity supplied: refuse rather than diagnose\n // against the global namespace (least-privileged default).\n return { reportFound: false };\n }\n\n const readableNamespaces =\n requestedNamespace !== undefined ? [requestedNamespace] : [...recallNamespacesForPrincipal(principal, config)];\n const recallSessionKey = request.sessionKey?.trim() || undefined;\n\n const whyDeps: RecallWhyDeps = {\n plannerMode: (prompt) => planRecallMode(prompt),\n scopedNamespaces: readableNamespaces,\n namespacesEnabled,\n isExcludedPath: (memoryPath) => isGenericRecallExcludedPath(memoryPath, config),\n runRecall: async (recallQuery, options) => {\n // A configured-but-unreachable search backend is an OUTAGE, and must\n // never render as \"0 candidates\" (Review Prevention Checklist #22).\n // This is the same pair of expressions the pipeline emits as\n // `qmdEnabled` / `qmdAvailable` on its recall_summary trace.\n if (resolveQmdCapabilities(config).qmd && !orchestrator.qmd.isAvailable()) {\n return {\n ok: false,\n reason: \"backend_unavailable\",\n detail: \"the search backend is enabled but reported itself unavailable\",\n };\n }\n // A backend that answers the availability probe can still fail\n // mid-recall (daemon timeout, deadline exceeded, subprocess error).\n // The pipeline already collects those; `degradationSink` hands us the\n // same array it fills, so the outage is observed rather than inferred.\n const degradations: SearchDegradation[] = [];\n try {\n const captured = await orchestrator.recallWithXrayCapture(recallQuery, options.sessionKey, {\n ...(options.namespace !== undefined ? { namespace: options.namespace } : {}),\n ...(authenticatedPrincipal ? { principalOverride: authenticatedPrincipal } : {}),\n ...(request.sourceConnector ? { sourceConnector: request.sourceConnector } : {}),\n ...(options.abortSignal !== undefined ? { abortSignal: options.abortSignal } : {}),\n degradationSink: degradations,\n });\n const fatal = degradations.filter((d) => FATAL_DEGRADATION_CODES[d.code] === true);\n if (fatal.length > 0) {\n return { ok: false, reason: \"backend_unavailable\", detail: describeDegradations(fatal) };\n }\n return { ok: true, snapshot: captured.snapshot };\n } catch (err) {\n // Cancellation is the caller's decision, not a backend outage.\n if (err instanceof Error && err.name === \"AbortError\") throw err;\n const observed = degradations.length > 0 ? `${describeDegradations(degradations)}; ` : \"\";\n return {\n ok: false,\n reason: \"backend_unavailable\",\n detail: `${observed}${err instanceof Error ? err.message : \"recall failed\"}`,\n };\n }\n },\n findExpected: (expect) => findExpectedMemory(orchestrator, readableNamespaces, expect),\n };\n\n const report = await explainRecallMiss(query, {\n deps: whyDeps,\n ...(request.expect !== undefined ? { expect: request.expect } : {}),\n ...(recallSessionKey !== undefined ? { sessionKey: recallSessionKey } : {}),\n ...(requestedNamespace !== undefined ? { namespace: requestedNamespace } : {}),\n ...(request.abortSignal !== undefined ? { abortSignal: request.abortSignal } : {}),\n });\n\n return { reportFound: true, report, summary: summarizeRecallWhy(report) };\n}\n\n/**\n * Resolve `expect` to one stored memory, searching ONLY namespaces the\n * caller may read. An exact id match wins over a substring match; among\n * substring matches the lowest (memoryId, path) pair wins, so repeated runs\n * report the same memory.\n */\nasync function findExpectedMemory(\n orchestrator: Orchestrator,\n namespaces: readonly string[],\n expect: string\n): Promise<RecallWhyMemoryRef | null> {\n const substringMatches: RecallWhyMemoryRef[] = [];\n const seenNamespaces = new Set<string>();\n for (const namespace of namespaces) {\n if (seenNamespaces.has(namespace)) continue;\n seenNamespaces.add(namespace);\n let memories: MemoryFile[];\n try {\n const storage = await orchestrator.getStorageForNamespace(namespace);\n memories = await storage.readAllMemories();\n } catch {\n // A namespace whose storage cannot be read contributes no candidates;\n // the diagnosis still reports on the ones that could be read.\n continue;\n }\n for (const memory of memories) {\n const memoryId = typeof memory.frontmatter.id === \"string\" ? memory.frontmatter.id : \"\";\n if (memoryId.length === 0) continue;\n const ref: RecallWhyMemoryRef = {\n memoryId,\n path: memory.path,\n status: (memory.frontmatter.status as MemoryStatus | undefined) ?? \"active\",\n namespace,\n };\n if (memoryId === expect) return ref;\n if (memoryId.includes(expect) || memory.path.includes(expect)) substringMatches.push(ref);\n }\n }\n if (substringMatches.length === 0) return null;\n // Total comparator: -1 / 0 / 1, and 0 for genuinely equal rows (#12).\n substringMatches.sort((a, b) => {\n if (a.memoryId !== b.memoryId) return a.memoryId < b.memoryId ? -1 : 1;\n if (a.path !== b.path) return a.path < b.path ? -1 : 1;\n return 0;\n });\n return substringMatches[0] ?? null;\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/profiling.ts"],"sourcesContent":["// Performance profiling collector for recall and extraction traces.\n// Zero external dependencies — uses only node:fs and node:path.\n\nimport {\n existsSync,\n mkdirSync,\n writeFileSync,\n promises as fsp,\n} from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport { log } from \"./logger.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ProfileSpan {\n name: string;\n startOffsetMs: number;\n durationMs: number;\n}\n\nexport interface ProfileParallelGroupMember {\n name: string;\n durationMs: number;\n resolvedIndex: number;\n}\n\nexport interface ProfileParallelGroup {\n name: string;\n startOffsetMs: number;\n wallMs: number;\n members: ProfileParallelGroupMember[];\n}\n\nexport interface ProfileTrace {\n ts: string;\n kind: \"recall\" | \"extraction\";\n traceId: string;\n sessionKey?: string;\n totalMs: number;\n spans: ProfileSpan[];\n parallelGroups?: ProfileParallelGroup[];\n configSnapshot?: Record<string, unknown>;\n}\n\nexport interface ProfilingConfig {\n enabled: boolean;\n storageDir: string;\n maxTraces: number;\n}\n\nexport interface ParallelGroupHandle {\n name: string;\n startOffsetMs: number;\n traceId?: string;\n}\n\nexport interface ProfilingStats {\n byKind: Record<string, { count: number; avgMs: number; p50Ms: number; p95Ms: number; maxMs: number }>;\n bySpan: Record<string, { count: number; avgMs: number; p50Ms: number; p95Ms: number; maxMs: number }>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const idx = Math.ceil((p / 100) * sorted.length) - 1;\n return sorted[Math.max(0, Math.min(idx, sorted.length - 1))];\n}\n\nfunction aggregateStats(values: number[]): { count: number; avgMs: number; p50Ms: number; p95Ms: number; maxMs: number } {\n const count = values.length;\n if (count === 0) return { count: 0, avgMs: 0, p50Ms: 0, p95Ms: 0, maxMs: 0 };\n const sorted = [...values].sort((a, b) => a - b);\n const sum = sorted.reduce((s, v) => s + v, 0);\n return {\n count,\n avgMs: Math.round(sum / count),\n p50Ms: Math.round(percentile(sorted, 50)),\n p95Ms: Math.round(percentile(sorted, 95)),\n maxMs: sorted[sorted.length - 1],\n };\n}\n\nlet traceCounter = 0;\n\n// ---------------------------------------------------------------------------\n// ProfilingCollector\n// ---------------------------------------------------------------------------\n\nexport class ProfilingCollector {\n private enabled: boolean;\n private storageDir: string;\n private maxTraces: number;\n private traces: ProfileTrace[] = [];\n private prunePromise: Promise<void> | null = null;\n\n // Active trace state — keyed by traceId so concurrent pipelines are isolated.\n private activeTraces = new Map<\n string,\n {\n kind: \"recall\" | \"extraction\";\n start: number;\n sessionKey?: string;\n configSnapshot?: Record<string, unknown>;\n spans: ProfileSpan[];\n spanStarts: Map<string, number>;\n parallelGroups: ProfileParallelGroup[];\n }\n >();\n private latestTraceId = \"\";\n\n constructor(config: ProfilingConfig) {\n this.enabled = config.enabled;\n this.storageDir = config.storageDir;\n this.maxTraces = Math.max(0, config.maxTraces);\n\n if (this.enabled) {\n if (!existsSync(this.storageDir)) {\n mkdirSync(this.storageDir, { recursive: true });\n log.debug(`profiling: created storage dir ${this.storageDir}`);\n }\n }\n }\n\n get isEnabled(): boolean {\n return this.enabled;\n }\n\n // ---- Trace lifecycle ---------------------------------------------------\n\n startTrace(kind: \"recall\" | \"extraction\", sessionKey?: string, configSnapshot?: Record<string, unknown>): string {\n if (!this.enabled) return \"\";\n traceCounter++;\n const traceId = `t${traceCounter}-${Date.now().toString(36)}`;\n this.activeTraces.set(traceId, {\n kind,\n start: Date.now(),\n sessionKey,\n configSnapshot,\n spans: [],\n spanStarts: new Map(),\n parallelGroups: [],\n });\n this.latestTraceId = traceId;\n log.debug(`profiling: started trace ${traceId} kind=${kind}`);\n return traceId;\n }\n\n startSpan(name: string, traceId?: string): void {\n const tid = traceId ?? this.latestTraceId;\n const t = tid ? this.activeTraces.get(tid) : undefined;\n if (!t) return;\n const offset = Date.now() - t.start;\n t.spanStarts.set(name, Date.now());\n log.debug(`profiling: span ${name} started at +${offset}ms (trace=${tid})`);\n }\n\n endSpan(name: string, traceId?: string): void {\n const tid = traceId ?? this.latestTraceId;\n const t = tid ? this.activeTraces.get(tid) : undefined;\n if (!t) return;\n const start = t.spanStarts.get(name);\n if (start === undefined) return;\n const duration = Date.now() - start;\n const startOffset = start - t.start;\n t.spans.push({ name, startOffsetMs: startOffset, durationMs: duration });\n t.spanStarts.delete(name);\n log.debug(`profiling: span ${name} ended ${duration}ms (trace=${tid})`);\n }\n\n endTrace(traceId?: string): ProfileTrace | null {\n const tid = traceId ?? this.latestTraceId;\n const t = tid ? this.activeTraces.get(tid) : undefined;\n if (!t) return null;\n\n // Auto-close any spans still open when the trace finalizes.\n const now = Date.now();\n for (const [name, start] of t.spanStarts) {\n const duration = now - start;\n const startOffset = start - t.start;\n t.spans.push({ name, startOffsetMs: startOffset, durationMs: duration });\n log.debug(`profiling: auto-closed span ${name} at trace end (${duration}ms, trace=${tid})`);\n }\n t.spanStarts.clear();\n\n const trace: ProfileTrace = {\n ts: new Date().toISOString(),\n kind: t.kind,\n traceId: tid,\n totalMs: Date.now() - t.start,\n spans: t.spans,\n configSnapshot: t.configSnapshot,\n };\n\n if (t.sessionKey) {\n trace.sessionKey = t.sessionKey;\n }\n if (t.parallelGroups.length > 0) {\n trace.parallelGroups = t.parallelGroups;\n }\n\n // Remove from active map.\n this.activeTraces.delete(tid);\n\n if (!this.enabled) {\n log.debug(\"profiling: trace discarded (disabled)\");\n return null;\n }\n\n // Persist.\n this.persistTrace(trace);\n\n // Buffer in memory (FIFO).\n this.traces.push(trace);\n if (this.traces.length > this.maxTraces) {\n this.traces.shift();\n }\n\n // Single-flight: if a prune is already running, piggyback on it;\n // otherwise start one. This prevents concurrent prunes from racing\n // on stale directory snapshots.\n if (!this.prunePromise) {\n this.prunePromise = this.pruneFiles().finally(() => {\n this.prunePromise = null;\n });\n }\n log.debug(`profiling: trace ${trace.traceId} finalized totalMs=${trace.totalMs}`);\n return trace;\n }\n\n // ---- Parallel group tracking -------------------------------------------\n\n startParallelGroup(name: string, traceId?: string): ParallelGroupHandle {\n const tid = traceId ?? this.latestTraceId;\n const t = tid ? this.activeTraces.get(tid) : undefined;\n const startOffsetMs = t ? Date.now() - t.start : 0;\n return { name, startOffsetMs, traceId: tid };\n }\n\n async endParallelGroup(\n handle: ParallelGroupHandle,\n members: Array<{ name: string; promise: Promise<unknown> }>,\n ): Promise<void> {\n const wallStart = Date.now();\n\n let nextResolvedIndex = 0;\n const resolutionOrder = new Map<string, number>();\n\n const timed = members.map(async (m) => {\n const t0 = Date.now();\n try {\n await m.promise;\n } catch {\n // settled — still record order\n }\n resolutionOrder.set(m.name, nextResolvedIndex++);\n return { name: m.name, durationMs: Date.now() - t0 };\n });\n const timedResults = await Promise.allSettled(timed);\n\n const tid = handle.traceId ?? this.latestTraceId;\n const t = tid ? this.activeTraces.get(tid) : undefined;\n if (!t) return;\n\n const wallMs = Date.now() - wallStart;\n\n const groupMembers: ProfileParallelGroupMember[] = members.map((m, i) => {\n const timedResult = timedResults[i].status === \"fulfilled\"\n ? timedResults[i].value\n : { name: m.name, durationMs: wallMs };\n return {\n name: timedResult.name,\n durationMs: timedResult.durationMs,\n resolvedIndex: resolutionOrder.get(m.name) ?? i,\n };\n });\n\n t.parallelGroups.push({\n name: handle.name,\n startOffsetMs: handle.startOffsetMs,\n wallMs,\n members: groupMembers,\n });\n\n log.debug(`profiling: parallel group ${handle.name} wallMs=${wallMs} (trace=${tid})`);\n }\n\n // ---- Query methods -----------------------------------------------------\n\n getRecentTraces(limit?: number): ProfileTrace[] {\n if (limit === undefined || limit === Infinity) {\n return [...this.traces];\n }\n if (!Number.isFinite(limit)) {\n return [];\n }\n const n = Math.floor(limit);\n if (n <= 0) {\n return [];\n }\n return this.traces.slice(-n);\n }\n\n getStats(): ProfilingStats {\n const byKind: Record<string, number[]> = {};\n const bySpan: Record<string, number[]> = {};\n\n for (const trace of this.traces) {\n if (!byKind[trace.kind]) byKind[trace.kind] = [];\n byKind[trace.kind].push(trace.totalMs);\n\n for (const span of trace.spans) {\n if (!bySpan[span.name]) bySpan[span.name] = [];\n bySpan[span.name].push(span.durationMs);\n }\n }\n\n const result: ProfilingStats = { byKind: {}, bySpan: {} };\n for (const [k, v] of Object.entries(byKind)) {\n result.byKind[k] = aggregateStats(v);\n }\n for (const [k, v] of Object.entries(bySpan)) {\n result.bySpan[k] = aggregateStats(v);\n }\n return result;\n }\n\n identifyBottleneck(): string | null {\n if (this.traces.length === 0) return null;\n const latest = this.traces[this.traces.length - 1];\n if (latest.spans.length === 0) return null;\n let slowest = latest.spans[0];\n for (const span of latest.spans) {\n if (span.durationMs > slowest.durationMs) slowest = span;\n }\n return slowest.name;\n }\n\n // ---- Persistence -------------------------------------------------------\n\n private persistTrace(trace: ProfileTrace): void {\n const filename = `${trace.kind}-${trace.traceId}.jsonl`;\n const filepath = join(this.storageDir, filename);\n try {\n writeFileSync(filepath, JSON.stringify(trace) + \"\\n\", \"utf-8\");\n log.debug(`profiling: persisted ${filename}`);\n } catch (err) {\n log.warn(`profiling: failed to persist ${filename}`, err);\n }\n }\n\n async pruneFiles(): Promise<void> {\n try {\n const dir = this.storageDir;\n const entries = await fsp.readdir(dir);\n const jsonlFiles = entries.filter((f) => f.endsWith(\".jsonl\"));\n const withMtime = await Promise.all(\n jsonlFiles.map(async (f) => ({\n name: f,\n mtime: (await fsp.stat(join(dir, f))).mtimeMs,\n })),\n );\n const files = withMtime.sort((a, b) => a.mtime - b.mtime).map((f) => f.name);\n\n while (files.length > this.maxTraces) {\n const oldest = files.shift()!;\n await fsp.unlink(join(dir, oldest));\n log.debug(`profiling: pruned ${oldest}`);\n }\n } catch (err) {\n log.warn(\"profiling: prune failed\", err);\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// ASCII formatter\n// ---------------------------------------------------------------------------\n\nexport function formatProfileTraceAscii(trace: ProfileTrace): string {\n const lines: string[] = [];\n const BAR_WIDTH = 40;\n\n lines.push(`=== Profile: ${trace.kind} ===`);\n lines.push(`Trace ID : ${trace.traceId}`);\n lines.push(`Total : ${trace.totalMs}ms`);\n if (trace.sessionKey) lines.push(`Session : ${trace.sessionKey}`);\n lines.push(\"\");\n\n // Identify bottleneck.\n let bottleneckName: string | null = null;\n if (trace.spans.length > 0) {\n let slowest = trace.spans[0];\n for (const s of trace.spans) {\n if (s.durationMs > slowest.durationMs) slowest = s;\n }\n bottleneckName = slowest.name;\n }\n\n // Spans.\n if (trace.spans.length > 0) {\n const maxDuration = Math.max(...trace.spans.map((s) => s.durationMs), 1);\n lines.push(\"Spans:\");\n for (const span of trace.spans) {\n const barLen = Math.max(1, Math.round((span.durationMs / maxDuration) * BAR_WIDTH));\n const bar = \"\\u2588\".repeat(barLen);\n const suffix = span.name === bottleneckName ? \" \\u2190 bottleneck\" : \"\";\n lines.push(` ${span.name.padEnd(30)} ${String(span.durationMs).padStart(6)}ms ${bar}${suffix}`);\n }\n lines.push(\"\");\n }\n\n // Parallel groups.\n if (trace.parallelGroups && trace.parallelGroups.length > 0) {\n lines.push(\"Parallel Groups:\");\n for (const group of trace.parallelGroups) {\n lines.push(` ${group.name}:`);\n lines.push(` Wall time : ${group.wallMs}ms`);\n const efficiency = parallelEfficiency(group);\n if (efficiency !== null) {\n lines.push(` Efficiency : ${efficiency}%`);\n }\n for (const member of group.members) {\n lines.push(` [${String(member.resolvedIndex).padStart(2)}] ${member.name.padEnd(28)} ${String(member.durationMs).padStart(6)}ms`);\n }\n }\n lines.push(\"\");\n }\n\n lines.push(\"---\");\n return lines.join(\"\\n\");\n}\n\nfunction parallelEfficiency(group: ProfileParallelGroup): number | null {\n if (group.members.length <= 1) return null;\n const idealMs = Math.max(...group.members.map((m) => m.durationMs));\n if (group.wallMs === 0) return null;\n return Math.round((idealMs / group.wallMs) * 100);\n}\n"],"mappings":"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","names":[]}
@@ -1,19 +0,0 @@
1
- // src/search/port.ts
2
- function reportSearchDegradation(execution, degradation) {
3
- try {
4
- execution?.onDegradation?.(degradation);
5
- } catch {
6
- }
7
- }
8
- function resolveEnsureCollectionArgs(collectionOrExecution, execution) {
9
- if (typeof collectionOrExecution === "string") {
10
- return { collection: collectionOrExecution, execution };
11
- }
12
- return { collection: void 0, execution: collectionOrExecution ?? execution };
13
- }
14
-
15
- export {
16
- reportSearchDegradation,
17
- resolveEnsureCollectionArgs
18
- };
19
- //# sourceMappingURL=chunk-U2VDCFXW.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/search/port.ts"],"sourcesContent":["import type { QmdSearchResult } from \"../types.js\";\n\n/** Alias so consumers don't need to reference \"Qmd\" in a backend-agnostic context. */\nexport type SearchResult = QmdSearchResult;\n\nexport interface SearchQueryOptions {\n intent?: string;\n explain?: boolean;\n candidateLimit?: number;\n rerank?: boolean;\n chunkStrategy?: \"auto\" | \"regex\";\n structuredSearches?: Array<{ type: \"lex\" | \"vec\" | \"hyde\"; query: string }>;\n}\n\n/**\n * A search that returned empty (or partial) results because the backend was\n * unavailable, still loading, or timed out — cases that are otherwise\n * indistinguishable from a genuine \"no matches\" (issue #1536, CLAUDE.md\n * rule 34).\n */\nexport interface SearchDegradation {\n backend: \"qmd\" | \"remote\" | \"meilisearch\" | \"orama\" | \"lancedb\";\n code:\n | \"backend_unavailable\"\n | \"backend_error\"\n | \"daemon_timeout\"\n | \"daemon_loading\"\n | \"subprocess_error\"\n | \"deadline_exceeded\"\n | \"remote_error\"\n | \"vector_tier_unavailable\";\n detail?: string;\n}\n\nexport interface SearchExecutionOptions {\n signal?: AbortSignal;\n /**\n * Observer invoked when the backend degrades during this call (#1536).\n * Callers that need to distinguish empty-because-degraded from\n * empty-because-no-matches (recall x-ray, fallback decisions) pass a\n * collector here. Observer failures are swallowed by the notifier —\n * observability must never break search.\n */\n onDegradation?: (degradation: SearchDegradation) => void;\n}\n\nexport function reportSearchDegradation(\n execution: SearchExecutionOptions | undefined,\n degradation: SearchDegradation\n): void {\n try {\n execution?.onDegradation?.(degradation);\n } catch {\n // Observability must never break search.\n }\n}\n\nexport function resolveEnsureCollectionArgs(\n collectionOrExecution?: string | SearchExecutionOptions,\n execution?: SearchExecutionOptions\n): { collection?: string; execution?: SearchExecutionOptions } {\n if (typeof collectionOrExecution === \"string\") {\n return { collection: collectionOrExecution, execution };\n }\n return { collection: undefined, execution: collectionOrExecution ?? execution };\n}\n\n/**\n * Optional status report for search backends that track embedding backlog.\n * Backends without embedding pipelines return null from status().\n */\nexport interface SearchBackendStatus {\n pendingEmbeddings: number | null;\n oldestPendingAgeMs: number | null;\n totalFiles: number | null;\n embeddedFiles: number | null;\n}\n\n/**\n * Abstract search backend interface.\n *\n * Implementations:\n * - QmdClient (default, local hybrid BM25+vector+reranking)\n * - OramaBackend (embedded, pure JS, hybrid FTS+vector)\n * - LanceDbBackend (embedded, native Arrow bindings, RRF reranking)\n * - MeilisearchBackend (server-based SDK, hybrid search)\n * - RemoteSearchBackend (HTTP REST adapter)\n * - NoopSearchBackend (graceful degradation)\n *\n * See docs/writing-a-search-backend.md for the implementation guide.\n */\nexport interface SearchBackend {\n // ── Lifecycle ──\n probe(execution?: SearchExecutionOptions): Promise<boolean>;\n /**\n * Optional non-mutating availability probe for health/readiness checks.\n * Implementations must avoid auto-upgrades, collection creation, daemon\n * startup, or any other runtime-modifying side effects.\n */\n checkAvailability?(execution?: SearchExecutionOptions): Promise<boolean>;\n isAvailable(): boolean;\n debugStatus(): string;\n /** Optional embedding backlog status for health surfaces. */\n status?(): Promise<SearchBackendStatus>;\n\n // ── Search ──\n search(\n query: string,\n collection?: string,\n maxResults?: number,\n options?: SearchQueryOptions,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n searchGlobal(query: string, maxResults?: number, execution?: SearchExecutionOptions): Promise<SearchResult[]>;\n bm25Search(\n query: string,\n collection?: string,\n maxResults?: number,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n vectorSearch(\n query: string,\n collection?: string,\n maxResults?: number,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n hybridSearch(\n query: string,\n collection?: string,\n maxResults?: number,\n execution?: SearchExecutionOptions\n ): Promise<SearchResult[]>;\n\n // ── Maintenance ──\n update(execution?: SearchExecutionOptions): Promise<void>;\n /**\n * Wall-clock ms of the most recent SUCCESSFUL index refresh, or null when the\n * backend has never run one. Ordinary update() is fail-open: it returns\n * silently (without throwing or advancing this timestamp) when the backend is\n * unavailable or the call is suppressed by a min-interval / failure backoff.\n * Callers that must know whether update() actually indexed — so they can\n * invalidate dependent caches only on a real refresh — snapshot this before\n * the call and treat the backend as refreshed iff the value advanced. Optional\n * because not every backend tracks it; absent means \"assume refreshed\".\n */\n readonly lastUpdateRanAtMs?: number | null;\n /**\n * Optional strict refresh used by callers that must know whether the backend\n * was actually refreshed before writing success markers. Ordinary update\n * calls remain fail-open for migration/maintenance resilience.\n */\n updateStrict?(execution?: SearchExecutionOptions): Promise<void>;\n updateCollection(collection: string, execution?: SearchExecutionOptions): Promise<void>;\n updateCollectionFromDir?(collection: string, memoryDir: string, execution?: SearchExecutionOptions): Promise<void>;\n /**\n * True when update() refreshes every indexed collection, not just this\n * backend's configured collection. Namespace routers use this to avoid\n * repeating the same expensive global update once per namespace.\n */\n updatesAllCollections?(): boolean;\n /**\n * Optional strict refresh used by callers that must know whether a collection\n * was actually refreshed before writing success markers. Ordinary update\n * calls remain fail-open for migration/maintenance resilience.\n */\n updateCollectionStrict?(collection: string, execution?: SearchExecutionOptions): Promise<void>;\n embed(): Promise<void>;\n /**\n * Optional strict embed used by callers that must know vectors were actually\n * refreshed before writing success markers.\n */\n embedStrict?(): Promise<void>;\n embedCollection(collection: string): Promise<void>;\n /**\n * Optional strict collection embed used by callers that must know vectors were\n * actually refreshed before writing success markers.\n */\n embedCollectionStrict?(collection: string): Promise<void>;\n\n // ── Collection management ──\n /**\n * True only when the backend can bind, isolate, search, and delete collections\n * outside Remnic's primary memory roots.\n */\n supportsAdditionalCollections?(): boolean;\n /**\n * Prevent a collection from participating in unscoped/global searches.\n * Dedicated corpora must call this immediately after collection creation.\n */\n excludeCollectionFromGlobalSearch?(collection: string, execution?: SearchExecutionOptions): Promise<void>;\n /** Remove a non-primary collection and its backend-owned index state. */\n deleteCollection?(collection: string, execution?: SearchExecutionOptions): Promise<boolean>;\n /** Canonical source root currently bound to a named collection. */\n collectionRoot?(collection: string, execution?: SearchExecutionOptions): Promise<string | null>;\n /** Collection-scoped status for observability surfaces. */\n collectionStatus?(collection: string): Promise<Pick<SearchBackendStatus, \"totalFiles\">>;\n /**\n * Optional non-mutating collection probe. Backends that can distinguish a\n * missing collection from a transient probe failure should implement this so\n * callers can avoid auto-creating collections in unsafe layouts.\n */\n checkCollection?(\n collectionOrExecution?: string | SearchExecutionOptions,\n execution?: SearchExecutionOptions\n ): Promise<\"present\" | \"missing\" | \"unknown\" | \"skipped\">;\n ensureCollection(\n memoryDir: string,\n execution?: SearchExecutionOptions\n ): Promise<\"present\" | \"missing\" | \"unknown\" | \"skipped\">;\n ensureCollection(\n memoryDir: string,\n collection?: string,\n execution?: SearchExecutionOptions\n ): Promise<\"present\" | \"missing\" | \"unknown\" | \"skipped\">;\n}\n"],"mappings":";AA8CO,SAAS,wBACd,WACA,aACM;AACN,MAAI;AACF,eAAW,gBAAgB,WAAW;AAAA,EACxC,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,4BACd,uBACA,WAC6D;AAC7D,MAAI,OAAO,0BAA0B,UAAU;AAC7C,WAAO,EAAE,YAAY,uBAAuB,UAAU;AAAA,EACxD;AACA,SAAO,EAAE,YAAY,QAAW,WAAW,yBAAyB,UAAU;AAChF;","names":[]}