@remnic/core 9.7.13 → 9.7.15

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 (358) hide show
  1. package/dist/access-admin-ops-surface.d.ts +6 -6
  2. package/dist/access-admin-ops-surface.js +22 -22
  3. package/dist/access-boundary.d.ts +6 -6
  4. package/dist/access-boundary.js +23 -23
  5. package/dist/access-cli.js +51 -51
  6. package/dist/access-http.d.ts +6 -6
  7. package/dist/access-http.js +28 -28
  8. package/dist/access-identity-continuity-surface.d.ts +5 -5
  9. package/dist/access-identity-continuity-surface.js +22 -22
  10. package/dist/access-lcm-surface.d.ts +6 -6
  11. package/dist/access-lcm-surface.js +22 -22
  12. package/dist/access-mcp.d.ts +6 -6
  13. package/dist/access-mcp.js +27 -27
  14. package/dist/access-observe-write-surface.d.ts +6 -6
  15. package/dist/access-observe-write-surface.js +22 -22
  16. package/dist/access-operations-batch.js +25 -25
  17. package/dist/access-operations.d.ts +6 -6
  18. package/dist/access-operations.js +26 -26
  19. package/dist/access-recall-surface.d.ts +6 -6
  20. package/dist/access-recall-surface.js +22 -22
  21. package/dist/{access-service-tHd3eQpd.d.ts → access-service-DpcYZ9Nl.d.ts} +4 -4
  22. package/dist/access-service.d.ts +6 -6
  23. package/dist/access-service.js +22 -22
  24. package/dist/access-surface-catalog.d.ts +6 -6
  25. package/dist/action-confidence.d.ts +1 -1
  26. package/dist/active-memory-bridge.d.ts +1 -1
  27. package/dist/active-recall.d.ts +1 -1
  28. package/dist/active-recall.js +1 -1
  29. package/dist/behavior-learner.d.ts +1 -1
  30. package/dist/behavior-signals.d.ts +1 -1
  31. package/dist/bootstrap.d.ts +5 -5
  32. package/dist/briefing.d.ts +2 -2
  33. package/dist/briefing.js +6 -6
  34. package/dist/buffer-surprise-report.d.ts +1 -1
  35. package/dist/buffer.d.ts +2 -2
  36. package/dist/bulk-import/index.d.ts +3 -3
  37. package/dist/calibration.d.ts +1 -1
  38. package/dist/capabilities.d.ts +1 -1
  39. package/dist/{capsule-merge-Z2HUO6IK.js → capsule-merge-5KKJQBWQ.js} +4 -4
  40. package/dist/{catalog-DEMiw6HZ.d.ts → catalog-C9444P10.d.ts} +1 -1
  41. package/dist/causal-behavior.d.ts +1 -1
  42. package/dist/causal-behavior.js +2 -2
  43. package/dist/causal-chain.js +2 -2
  44. package/dist/causal-consolidation.d.ts +1 -1
  45. package/dist/causal-consolidation.js +9 -9
  46. package/dist/causal-retrieval.js +2 -2
  47. package/dist/causal-trajectory-graph.d.ts +1 -1
  48. package/dist/causal-trajectory-graph.js +1 -1
  49. package/dist/causal-trajectory.js +1 -1
  50. package/dist/{chunk-2RSL6YRA.js → chunk-2AZXIZEN.js} +3 -3
  51. package/dist/{chunk-OBMR4VIX.js → chunk-2S6QHZGJ.js} +186 -15
  52. package/dist/chunk-2S6QHZGJ.js.map +1 -0
  53. package/dist/{chunk-5S7Z5SHA.js → chunk-34RC5RT7.js} +2 -2
  54. package/dist/{chunk-JKLJ6A7W.js → chunk-3D6AFGQR.js} +2 -2
  55. package/dist/{chunk-2AL7N3BH.js → chunk-3IY64MV6.js} +4 -2
  56. package/dist/chunk-3IY64MV6.js.map +1 -0
  57. package/dist/{chunk-VZ5OJE4X.js → chunk-5VQJBH2G.js} +2 -2
  58. package/dist/{chunk-N3JDIRBW.js → chunk-74F5STS4.js} +2 -2
  59. package/dist/{chunk-OC35KQIN.js → chunk-A5UXZRRM.js} +3 -3
  60. package/dist/{chunk-OKUEZOGW.js → chunk-AITCS2RC.js} +2 -2
  61. package/dist/{chunk-7XGSIUP5.js → chunk-AJLLIIML.js} +1 -1
  62. package/dist/{chunk-CE3GMHT4.js → chunk-BS4SGLWV.js} +2 -2
  63. package/dist/{chunk-2V5QI6NQ.js → chunk-DCFV7A7H.js} +6 -6
  64. package/dist/{chunk-I3GKTDM2.js → chunk-DMSDUGDM.js} +3 -3
  65. package/dist/{chunk-D4TLWDYR.js → chunk-DPAENM7O.js} +2 -2
  66. package/dist/{chunk-AHLPZ2SF.js → chunk-DYP3YYJJ.js} +7 -3
  67. package/dist/chunk-DYP3YYJJ.js.map +1 -0
  68. package/dist/{chunk-7MW3CVLD.js → chunk-DZKHSDOM.js} +2 -2
  69. package/dist/{chunk-HG32URJJ.js → chunk-E6CBU7XP.js} +2 -2
  70. package/dist/{chunk-675YU5IK.js → chunk-EHCONI6E.js} +2 -2
  71. package/dist/{chunk-NVXQTRBL.js → chunk-FDCTQWLW.js} +2 -2
  72. package/dist/{chunk-PA6DMYEK.js → chunk-FIJUY7GG.js} +9 -9
  73. package/dist/{chunk-UTRJFDP2.js → chunk-FNZQGN76.js} +2 -2
  74. package/dist/{chunk-V53GJUSL.js → chunk-G4CN3ZXL.js} +6 -6
  75. package/dist/{chunk-IQULRIZM.js → chunk-G6XXLKC5.js} +3 -3
  76. package/dist/{chunk-CLFY35ED.js → chunk-HDTQVNWD.js} +3 -3
  77. package/dist/{chunk-TMGK6YE7.js → chunk-HF77M34J.js} +2 -2
  78. package/dist/{chunk-6N7R7TR2.js → chunk-INELTPRG.js} +15 -1
  79. package/dist/chunk-INELTPRG.js.map +1 -0
  80. package/dist/{chunk-ZC3HG22I.js → chunk-JREB6UFH.js} +2 -2
  81. package/dist/{chunk-TRH533CA.js → chunk-K7LOWSLU.js} +38 -9
  82. package/dist/chunk-K7LOWSLU.js.map +1 -0
  83. package/dist/{chunk-TWGGNUWJ.js → chunk-KDXVOFYY.js} +2 -2
  84. package/dist/{chunk-RT7IP3D3.js → chunk-KSNVDPDF.js} +2 -2
  85. package/dist/{chunk-ZIDSD6VC.js → chunk-LRNMRXJO.js} +2 -2
  86. package/dist/{chunk-QPZCAVDC.js → chunk-LZOV7MX7.js} +12 -12
  87. package/dist/{chunk-VY6LKKVN.js → chunk-NWTGD4AN.js} +6 -1
  88. package/dist/chunk-NWTGD4AN.js.map +1 -0
  89. package/dist/{chunk-Z6WN2KSH.js → chunk-OAZVR2BV.js} +3 -3
  90. package/dist/{chunk-KQMKF4G2.js → chunk-OVJ2HIPE.js} +21 -21
  91. package/dist/{chunk-OCY3CRPG.js → chunk-PBNZVPFS.js} +3 -3
  92. package/dist/{chunk-G5PVJJRA.js → chunk-PBSAPHHA.js} +1 -1
  93. package/dist/chunk-PBSAPHHA.js.map +1 -0
  94. package/dist/{chunk-XVLEGMJI.js → chunk-PLQE6MJA.js} +2 -2
  95. package/dist/{chunk-NKCPUHCI.js → chunk-PQVK2S3J.js} +2 -2
  96. package/dist/{chunk-OIFZII6J.js → chunk-Q5WG43OP.js} +3 -3
  97. package/dist/{chunk-YI7XUXLX.js → chunk-QKQYXZVS.js} +28 -26
  98. package/dist/chunk-QKQYXZVS.js.map +1 -0
  99. package/dist/{chunk-XUQUBZ7O.js → chunk-QQGPUKMT.js} +2 -2
  100. package/dist/{chunk-XUQUBZ7O.js.map → chunk-QQGPUKMT.js.map} +1 -1
  101. package/dist/{chunk-WU46YPY6.js → chunk-RAR2BSEM.js} +1 -1
  102. package/dist/{chunk-WU46YPY6.js.map → chunk-RAR2BSEM.js.map} +1 -1
  103. package/dist/{chunk-3SIC3E65.js → chunk-RO4LQGT3.js} +2 -2
  104. package/dist/{chunk-BQTSGK5Y.js → chunk-RVZRYUQH.js} +2 -2
  105. package/dist/{chunk-T6U7HDIY.js → chunk-T7Y5KPLN.js} +2 -2
  106. package/dist/{chunk-HLTAPHZZ.js → chunk-UDQUSU27.js} +2 -2
  107. package/dist/{chunk-2ZPQUTMW.js → chunk-VDW4VU7B.js} +5 -5
  108. package/dist/{chunk-ZRXSHGXT.js → chunk-VJVNLNDQ.js} +3 -3
  109. package/dist/{chunk-UTZW3X2R.js → chunk-XDSXEKYK.js} +2 -2
  110. package/dist/{chunk-X47LWMKA.js → chunk-XMMESCPD.js} +26 -1
  111. package/dist/chunk-XMMESCPD.js.map +1 -0
  112. package/dist/{chunk-FUUF7FMX.js → chunk-ZHIVCODT.js} +2 -2
  113. package/dist/{cli-D8Tsa4Sh.d.ts → cli-Cd5fMmhq.d.ts} +5 -5
  114. package/dist/cli.d.ts +8 -8
  115. package/dist/cli.js +42 -42
  116. package/dist/compounding/engine.d.ts +2 -2
  117. package/dist/compounding/engine.js +6 -6
  118. package/dist/compounding/preference-consolidator.d.ts +1 -1
  119. package/dist/compression-optimizer.d.ts +1 -1
  120. package/dist/config.d.ts +1 -1
  121. package/dist/config.js +1 -1
  122. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  123. package/dist/connectors/codex-materialize-runner.js +6 -6
  124. package/dist/connectors/codex-materialize.d.ts +1 -1
  125. package/dist/connectors/index.d.ts +1 -1
  126. package/dist/connectors/index.js +6 -6
  127. package/dist/consolidation-provenance-check.d.ts +2 -2
  128. package/dist/consolidation-provenance-check.js +4 -4
  129. package/dist/consolidation-undo.d.ts +2 -2
  130. package/dist/consolidation-undo.js +3 -3
  131. package/dist/contradiction/index.d.ts +2 -2
  132. package/dist/conversation-index/backend.d.ts +1 -1
  133. package/dist/conversation-index/chunker.d.ts +1 -1
  134. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  135. package/dist/conversation-index/indexer.d.ts +1 -1
  136. package/dist/conversation-index/search.d.ts +1 -1
  137. package/dist/day-summary.d.ts +1 -1
  138. package/dist/delinearize.d.ts +1 -1
  139. package/dist/direct-answer-wiring.d.ts +1 -1
  140. package/dist/direct-answer.d.ts +1 -1
  141. package/dist/embedding-fallback.d.ts +1 -1
  142. package/dist/enrichment/index.d.ts +1 -1
  143. package/dist/entity-retrieval.d.ts +2 -2
  144. package/dist/entity-retrieval.js +6 -6
  145. package/dist/entity-schema.d.ts +1 -1
  146. package/dist/explicit-capture.d.ts +5 -5
  147. package/dist/extraction-faithfulness.d.ts +1 -1
  148. package/dist/extraction-judge-telemetry.d.ts +1 -1
  149. package/dist/extraction-judge-training.d.ts +1 -1
  150. package/dist/extraction-judge.d.ts +1 -1
  151. package/dist/extraction.d.ts +1 -1
  152. package/dist/extraction.js +2 -2
  153. package/dist/fallback-llm.d.ts +1 -1
  154. package/dist/graph-dashboard-diff.d.ts +1 -1
  155. package/dist/graph-dashboard-key.d.ts +1 -1
  156. package/dist/graph-dashboard-parser.d.ts +1 -1
  157. package/dist/{graph-edge-decay-FO7AM4UR.js → graph-edge-decay-IOGBBAGC.js} +3 -3
  158. package/dist/graph-edge-reinforcement.d.ts +1 -1
  159. package/dist/graph-snapshot.d.ts +1 -1
  160. package/dist/graph-snapshot.js +2 -2
  161. package/dist/graph.d.ts +55 -10
  162. package/dist/graph.js +1 -1
  163. package/dist/identity-continuity.d.ts +1 -1
  164. package/dist/importance.d.ts +1 -1
  165. package/dist/importers/index.d.ts +1 -1
  166. package/dist/in-flight-reads.d.ts +1 -1
  167. package/dist/index.d.ts +14 -14
  168. package/dist/index.js +51 -51
  169. package/dist/intent.d.ts +1 -1
  170. package/dist/lcm/engine.d.ts +1 -1
  171. package/dist/lcm/index.d.ts +1 -1
  172. package/dist/lcm/tools.d.ts +1 -1
  173. package/dist/lifecycle.d.ts +1 -1
  174. package/dist/live-connectors-runner.d.ts +1 -1
  175. package/dist/local-llm.d.ts +1 -1
  176. package/dist/local-llm.js +1 -1
  177. package/dist/local-model-endpoint.d.ts +1 -1
  178. package/dist/maintenance/memory-governance.d.ts +1 -1
  179. package/dist/maintenance/memory-governance.js +6 -6
  180. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +6 -6
  181. package/dist/maintenance/rebuild-memory-projection.js +7 -7
  182. package/dist/mcp-memory-inspector-app.d.ts +6 -6
  183. package/dist/memory-action-policy.d.ts +1 -1
  184. package/dist/memory-cache.d.ts +44 -2
  185. package/dist/memory-cache.js +7 -1
  186. package/dist/memory-corpus-version.js +2 -2
  187. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  188. package/dist/memory-projection-store.d.ts +1 -1
  189. package/dist/memory-provenance.d.ts +1 -1
  190. package/dist/memory-worth-outcomes.d.ts +2 -2
  191. package/dist/models-json.d.ts +1 -1
  192. package/dist/namespaces/migrate.d.ts +3 -3
  193. package/dist/namespaces/migrate.js +15 -15
  194. package/dist/namespaces/principal.d.ts +1 -1
  195. package/dist/namespaces/search.d.ts +1 -1
  196. package/dist/namespaces/search.js +9 -9
  197. package/dist/namespaces/storage.d.ts +3 -3
  198. package/dist/namespaces/storage.js +6 -6
  199. package/dist/native-knowledge.d.ts +1 -1
  200. package/dist/operator-toolkit.d.ts +2 -2
  201. package/dist/operator-toolkit.js +21 -21
  202. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  203. package/dist/orchestration/maintenance.d.ts +2 -2
  204. package/dist/orchestration/maintenance.js +8 -8
  205. package/dist/{orchestrator-B_WXBmuE.d.ts → orchestrator-Bp_jeBHK.d.ts} +4 -4
  206. package/dist/orchestrator.d.ts +5 -5
  207. package/dist/orchestrator.js +51 -51
  208. package/dist/page-versioning.js +3 -3
  209. package/dist/patterns-cli.d.ts +1 -1
  210. package/dist/{pipeline-Dvtvt141.d.ts → pipeline-BAj5bx7g.d.ts} +1 -1
  211. package/dist/policy-runtime.d.ts +1 -1
  212. package/dist/provenance.d.ts +1 -1
  213. package/dist/{qmd-DwBLh_hl.d.ts → qmd-_MDSlj1m.d.ts} +1 -1
  214. package/dist/qmd-preflight.d.ts +2 -2
  215. package/dist/qmd-recall-cache.d.ts +1 -1
  216. package/dist/qmd.d.ts +2 -2
  217. package/dist/qmd.js +3 -3
  218. package/dist/recall-disclosure-escalation.d.ts +1 -1
  219. package/dist/recall-explain-renderer.d.ts +1 -1
  220. package/dist/recall-explain-renderer.js +3 -3
  221. package/dist/recall-planner-llm.d.ts +1 -1
  222. package/dist/recall-state.d.ts +1 -1
  223. package/dist/recall-tag-filter.d.ts +1 -1
  224. package/dist/recall-timings.d.ts +1 -1
  225. package/dist/recall-xray-cli.d.ts +1 -1
  226. package/dist/recall-xray-cli.js +4 -4
  227. package/dist/recall-xray-renderer.d.ts +1 -1
  228. package/dist/recall-xray-renderer.js +3 -3
  229. package/dist/recall-xray.d.ts +1 -1
  230. package/dist/recall-xray.js +2 -2
  231. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  232. package/dist/replay/normalizers/claude.d.ts +1 -1
  233. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  234. package/dist/replay/normalizers/shared.d.ts +1 -1
  235. package/dist/replay/runner.d.ts +1 -1
  236. package/dist/replay/types.d.ts +1 -1
  237. package/dist/resolve-auth-token.d.ts +1 -1
  238. package/dist/resume-bundles.js +2 -2
  239. package/dist/retrieval-agents.d.ts +2 -2
  240. package/dist/retrieval-tiers.d.ts +1 -1
  241. package/dist/routing/engine.d.ts +1 -1
  242. package/dist/routing/store.d.ts +1 -1
  243. package/dist/search/embed-helper.d.ts +1 -1
  244. package/dist/search/factory.d.ts +1 -1
  245. package/dist/search/factory.js +8 -8
  246. package/dist/search/index.d.ts +1 -1
  247. package/dist/search/index.js +8 -8
  248. package/dist/search/lancedb-backend.d.ts +1 -1
  249. package/dist/search/lancedb-backend.js +2 -2
  250. package/dist/search/meilisearch-backend.d.ts +1 -1
  251. package/dist/search/meilisearch-backend.js +2 -2
  252. package/dist/search/noop-backend.d.ts +1 -1
  253. package/dist/search/orama-backend.d.ts +1 -1
  254. package/dist/search/orama-backend.js +2 -2
  255. package/dist/search/port.d.ts +12 -1
  256. package/dist/search/port.js +1 -1
  257. package/dist/search/remote-backend.d.ts +1 -1
  258. package/dist/search/remote-backend.js +2 -2
  259. package/dist/{semantic-consolidation-Dia_6FOn.d.ts → semantic-consolidation-C2m5eHHE.d.ts} +1 -1
  260. package/dist/semantic-consolidation.d.ts +2 -2
  261. package/dist/semantic-consolidation.js +7 -7
  262. package/dist/semantic-rule-promotion.js +6 -6
  263. package/dist/semantic-rule-verifier.d.ts +1 -1
  264. package/dist/semantic-rule-verifier.js +6 -6
  265. package/dist/session-observer-bands.d.ts +1 -1
  266. package/dist/session-observer-state.d.ts +1 -1
  267. package/dist/shared-context/manager.d.ts +1 -1
  268. package/dist/signal.d.ts +1 -1
  269. package/dist/{storage-RshM7i8-.d.ts → storage-hr_Y2z5R.d.ts} +17 -1
  270. package/dist/storage.d.ts +2 -2
  271. package/dist/storage.js +5 -5
  272. package/dist/summarizer.d.ts +1 -1
  273. package/dist/summarizer.js +2 -2
  274. package/dist/summary-snapshot.d.ts +1 -1
  275. package/dist/temporal-supersession.d.ts +2 -2
  276. package/dist/temporal-timeline-recall.d.ts +1 -1
  277. package/dist/temporal-validity.d.ts +1 -1
  278. package/dist/threading.d.ts +1 -1
  279. package/dist/tier-migration.d.ts +2 -2
  280. package/dist/tier-routing.d.ts +1 -1
  281. package/dist/topics.d.ts +1 -1
  282. package/dist/transcript.d.ts +1 -1
  283. package/dist/transfer/capsule-import.js +4 -4
  284. package/dist/trust-score-stage.d.ts +1 -1
  285. package/dist/trust-score.d.ts +1 -1
  286. package/dist/{types-CHSXOq7s.d.ts → types-Cg8q7W7_.d.ts} +16 -0
  287. package/dist/types.d.ts +1 -1
  288. package/dist/types.js +1 -1
  289. package/dist/utility-runtime.d.ts +1 -1
  290. package/dist/verified-recall.js +6 -6
  291. package/package.json +2 -2
  292. package/src/config.ts +18 -0
  293. package/src/graph.test.ts +137 -1
  294. package/src/graph.ts +274 -17
  295. package/src/local-llm-thinking.test.ts +55 -1
  296. package/src/local-llm.ts +14 -0
  297. package/src/memory-cache.ts +70 -0
  298. package/src/namespaces/storage.ts +2 -0
  299. package/src/orchestration/maintenance.ts +7 -0
  300. package/src/orchestration/workspace-ops.ts +4 -0
  301. package/src/orchestrator.ts +6 -1
  302. package/src/qmd.test.ts +22 -0
  303. package/src/qmd.ts +13 -1
  304. package/src/search/port.ts +11 -0
  305. package/src/storage.ts +60 -13
  306. package/src/types.ts +21 -0
  307. package/dist/chunk-2AL7N3BH.js.map +0 -1
  308. package/dist/chunk-6N7R7TR2.js.map +0 -1
  309. package/dist/chunk-AHLPZ2SF.js.map +0 -1
  310. package/dist/chunk-G5PVJJRA.js.map +0 -1
  311. package/dist/chunk-OBMR4VIX.js.map +0 -1
  312. package/dist/chunk-TRH533CA.js.map +0 -1
  313. package/dist/chunk-VY6LKKVN.js.map +0 -1
  314. package/dist/chunk-X47LWMKA.js.map +0 -1
  315. package/dist/chunk-YI7XUXLX.js.map +0 -1
  316. /package/dist/{capsule-merge-Z2HUO6IK.js.map → capsule-merge-5KKJQBWQ.js.map} +0 -0
  317. /package/dist/{chunk-2RSL6YRA.js.map → chunk-2AZXIZEN.js.map} +0 -0
  318. /package/dist/{chunk-5S7Z5SHA.js.map → chunk-34RC5RT7.js.map} +0 -0
  319. /package/dist/{chunk-JKLJ6A7W.js.map → chunk-3D6AFGQR.js.map} +0 -0
  320. /package/dist/{chunk-VZ5OJE4X.js.map → chunk-5VQJBH2G.js.map} +0 -0
  321. /package/dist/{chunk-N3JDIRBW.js.map → chunk-74F5STS4.js.map} +0 -0
  322. /package/dist/{chunk-OC35KQIN.js.map → chunk-A5UXZRRM.js.map} +0 -0
  323. /package/dist/{chunk-OKUEZOGW.js.map → chunk-AITCS2RC.js.map} +0 -0
  324. /package/dist/{chunk-7XGSIUP5.js.map → chunk-AJLLIIML.js.map} +0 -0
  325. /package/dist/{chunk-CE3GMHT4.js.map → chunk-BS4SGLWV.js.map} +0 -0
  326. /package/dist/{chunk-2V5QI6NQ.js.map → chunk-DCFV7A7H.js.map} +0 -0
  327. /package/dist/{chunk-I3GKTDM2.js.map → chunk-DMSDUGDM.js.map} +0 -0
  328. /package/dist/{chunk-D4TLWDYR.js.map → chunk-DPAENM7O.js.map} +0 -0
  329. /package/dist/{chunk-7MW3CVLD.js.map → chunk-DZKHSDOM.js.map} +0 -0
  330. /package/dist/{chunk-HG32URJJ.js.map → chunk-E6CBU7XP.js.map} +0 -0
  331. /package/dist/{chunk-675YU5IK.js.map → chunk-EHCONI6E.js.map} +0 -0
  332. /package/dist/{chunk-NVXQTRBL.js.map → chunk-FDCTQWLW.js.map} +0 -0
  333. /package/dist/{chunk-PA6DMYEK.js.map → chunk-FIJUY7GG.js.map} +0 -0
  334. /package/dist/{chunk-UTRJFDP2.js.map → chunk-FNZQGN76.js.map} +0 -0
  335. /package/dist/{chunk-V53GJUSL.js.map → chunk-G4CN3ZXL.js.map} +0 -0
  336. /package/dist/{chunk-IQULRIZM.js.map → chunk-G6XXLKC5.js.map} +0 -0
  337. /package/dist/{chunk-CLFY35ED.js.map → chunk-HDTQVNWD.js.map} +0 -0
  338. /package/dist/{chunk-TMGK6YE7.js.map → chunk-HF77M34J.js.map} +0 -0
  339. /package/dist/{chunk-ZC3HG22I.js.map → chunk-JREB6UFH.js.map} +0 -0
  340. /package/dist/{chunk-TWGGNUWJ.js.map → chunk-KDXVOFYY.js.map} +0 -0
  341. /package/dist/{chunk-RT7IP3D3.js.map → chunk-KSNVDPDF.js.map} +0 -0
  342. /package/dist/{chunk-ZIDSD6VC.js.map → chunk-LRNMRXJO.js.map} +0 -0
  343. /package/dist/{chunk-QPZCAVDC.js.map → chunk-LZOV7MX7.js.map} +0 -0
  344. /package/dist/{chunk-Z6WN2KSH.js.map → chunk-OAZVR2BV.js.map} +0 -0
  345. /package/dist/{chunk-KQMKF4G2.js.map → chunk-OVJ2HIPE.js.map} +0 -0
  346. /package/dist/{chunk-OCY3CRPG.js.map → chunk-PBNZVPFS.js.map} +0 -0
  347. /package/dist/{chunk-XVLEGMJI.js.map → chunk-PLQE6MJA.js.map} +0 -0
  348. /package/dist/{chunk-NKCPUHCI.js.map → chunk-PQVK2S3J.js.map} +0 -0
  349. /package/dist/{chunk-OIFZII6J.js.map → chunk-Q5WG43OP.js.map} +0 -0
  350. /package/dist/{chunk-3SIC3E65.js.map → chunk-RO4LQGT3.js.map} +0 -0
  351. /package/dist/{chunk-BQTSGK5Y.js.map → chunk-RVZRYUQH.js.map} +0 -0
  352. /package/dist/{chunk-T6U7HDIY.js.map → chunk-T7Y5KPLN.js.map} +0 -0
  353. /package/dist/{chunk-HLTAPHZZ.js.map → chunk-UDQUSU27.js.map} +0 -0
  354. /package/dist/{chunk-2ZPQUTMW.js.map → chunk-VDW4VU7B.js.map} +0 -0
  355. /package/dist/{chunk-ZRXSHGXT.js.map → chunk-VJVNLNDQ.js.map} +0 -0
  356. /package/dist/{chunk-UTZW3X2R.js.map → chunk-XDSXEKYK.js.map} +0 -0
  357. /package/dist/{chunk-FUUF7FMX.js.map → chunk-ZHIVCODT.js.map} +0 -0
  358. /package/dist/{graph-edge-decay-FO7AM4UR.js.map → graph-edge-decay-IOGBBAGC.js.map} +0 -0
package/src/graph.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * All writes are fail-open: errors are caught/logged, never thrown.
11
11
  */
12
12
 
13
- import { appendFile, mkdir, readFile } from "node:fs/promises";
13
+ import { appendFile, mkdir, readFile, stat } from "node:fs/promises";
14
14
  import * as path from "node:path";
15
15
 
16
16
  import { readEdgeConfidence } from "./graph-edge-reinforcement.js";
@@ -59,6 +59,18 @@ export interface GraphConfig {
59
59
  * and return raw BFS scores. Default 8.
60
60
  */
61
61
  graphTraversalPageRankIterations: number;
62
+ /**
63
+ * Issue #1904 — incremental GraphIndex edge cache. When true (the default,
64
+ * and the value when omitted), a single-writer edge append is pushed into the
65
+ * warm edge cache in place (revalidated by file size for cross-process
66
+ * coherence) instead of nulling the cache and paying a full 6 MB re-read +
67
+ * parse on the next traversal. Set false to restore the pre-#1904
68
+ * null-on-every-write behavior. Optional here (unlike the required
69
+ * PluginConfig field parseConfig always sets) so GraphConfig can still be
70
+ * constructed directly with a partial config; only an explicit `false` opts
71
+ * out, so an omitted field keeps the incremental default.
72
+ */
73
+ graphEdgeCacheIncrementalEnabled?: boolean;
62
74
  }
63
75
 
64
76
  /** Default minimum edge confidence required for traversal (issue #681 PR 3/3). */
@@ -370,6 +382,18 @@ export function detectCausalPhrase(text: string): string | null {
370
382
  * // After each memory write:
371
383
  * await this.graphIndex.onMemoryWritten(memoryPath, frontmatter, threadId, recentInThread);
372
384
  */
385
+ /**
386
+ * Identity of one graph JSONL file used to validate the incremental edge cache
387
+ * (issue #1904). An absent file (ENOENT) is recorded as the all-zero sentinel
388
+ * `{ size: 0, mtimeMs: 0, ino: 0 }` so a later peer creation (ino becomes
389
+ * non-zero) is detected as a change.
390
+ */
391
+ interface GraphFileMeta {
392
+ size: number;
393
+ mtimeMs: number;
394
+ ino: number;
395
+ }
396
+
373
397
  export class GraphIndex {
374
398
  private readonly memoryDir: string;
375
399
  private readonly cfg: GraphConfig;
@@ -378,7 +402,20 @@ export class GraphIndex {
378
402
  // file read + JSON parse takes 2-4 s per call. This instance-level cache
379
403
  // eliminates that overhead on every spreadingActivation() call; it is
380
404
  // invalidated (set to null) in onMemoryWritten() so new edges appear promptly.
381
- private edgeCache: { allEdges: GraphEdge[]; loadedAt: number } | null = null;
405
+ private edgeCache: {
406
+ allEdges: GraphEdge[];
407
+ loadedAt: number;
408
+ // Per-file identity of each enabled graph file observed at load (issue
409
+ // #1904). An incremental push is trusted only when every file's identity is
410
+ // explained entirely by OUR own append: size grows by exactly the bytes we
411
+ // appended and the inode is unchanged (a local append keeps the inode; an
412
+ // external atomic temp+rename rewrite — e.g. the edge-decay job — changes
413
+ // it). Untouched files must match size AND mtime AND inode exactly. This
414
+ // closes the size-only holes Codex flagged: a peer append racing cache
415
+ // construction, and an equal-length atomic rewrite (e.g. confidence
416
+ // 0.9->0.8) that preserves byte length. Any divergence forces a full reload.
417
+ meta: Partial<Record<GraphType, GraphFileMeta>>;
418
+ } | null = null;
382
419
  private static readonly EDGE_CACHE_TTL_MS = 300_000; // 5 minutes
383
420
 
384
421
  constructor(memoryDir: string, cfg: GraphConfig) {
@@ -396,15 +433,120 @@ export class GraphIndex {
396
433
  if (this.edgeCache && Date.now() - this.edgeCache.loadedAt < GraphIndex.EDGE_CACHE_TTL_MS) {
397
434
  return this.edgeCache.allEdges;
398
435
  }
399
- const allEdges = await readAllEdges(this.memoryDir, {
400
- entityGraph: this.cfg.entityGraphEnabled,
401
- timeGraph: this.cfg.timeGraphEnabled,
402
- causalGraph: this.cfg.causalGraphEnabled,
403
- });
404
- this.edgeCache = { allEdges, loadedAt: Date.now() };
436
+ // Consistent snapshot (issue #1904, Codex): capture file identity BEFORE and
437
+ // AFTER reading edges and retry if it changed, so `allEdges` and the recorded
438
+ // `meta` baseline reflect the SAME on-disk state. Without this a peer append
439
+ // landing mid-read would leave the cache holding edges that exclude the peer
440
+ // edge while the baseline already counts its bytes — a later local append
441
+ // would then pass the size-delta check and silently omit the peer edge.
442
+ let allEdges: GraphEdge[] = [];
443
+ let meta = await this.readEnabledGraphFileMeta();
444
+ let stable = false;
445
+ for (let attempt = 0; attempt < 3; attempt++) {
446
+ const before = meta;
447
+ allEdges = await readAllEdges(this.memoryDir, {
448
+ entityGraph: this.cfg.entityGraphEnabled,
449
+ timeGraph: this.cfg.timeGraphEnabled,
450
+ causalGraph: this.cfg.causalGraphEnabled,
451
+ });
452
+ meta = await this.readEnabledGraphFileMeta();
453
+ if (this.graphFileMetaEqual(before, meta)) {
454
+ stable = true;
455
+ break;
456
+ }
457
+ // A write landed during the read; the just-captured `meta` is the new
458
+ // baseline for the next attempt's before-snapshot.
459
+ }
460
+ if (stable) {
461
+ this.edgeCache = { allEdges, loadedAt: Date.now(), meta };
462
+ } else {
463
+ // Contended: a peer kept writing across every attempt, so the final
464
+ // allEdges/meta pair is not guaranteed consistent. Serve THIS read's edges
465
+ // (best effort) but do NOT poison the cache — leave it null so the next
466
+ // call re-reads, instead of installing a baseline that could hide a peer
467
+ // edge for the TTL (Codex).
468
+ this.edgeCache = null;
469
+ }
405
470
  return allEdges;
406
471
  }
407
472
 
473
+ /**
474
+ * Identity ({ size, mtimeMs, ino }) of each ENABLED graph file (issue #1904).
475
+ * Missing files (ENOENT) and any stat error map to the all-zero sentinel so
476
+ * the baseline is fail-open: a later revalidation sees a mismatch (or a peer
477
+ * creation flipping ino from 0 to non-zero) and forces a full reload rather
478
+ * than trusting a stale incremental push.
479
+ */
480
+ private async readEnabledGraphFileMeta(): Promise<Partial<Record<GraphType, GraphFileMeta>>> {
481
+ const enabled: GraphType[] = [];
482
+ if (this.cfg.entityGraphEnabled) enabled.push("entity");
483
+ if (this.cfg.timeGraphEnabled) enabled.push("time");
484
+ if (this.cfg.causalGraphEnabled) enabled.push("causal");
485
+ const meta: Partial<Record<GraphType, GraphFileMeta>> = {};
486
+ for (const type of enabled) {
487
+ try {
488
+ const st = await stat(graphFilePath(this.memoryDir, type));
489
+ meta[type] = { size: st.size, mtimeMs: st.mtimeMs, ino: Number(st.ino) };
490
+ } catch {
491
+ meta[type] = { size: 0, mtimeMs: 0, ino: 0 };
492
+ }
493
+ }
494
+ return meta;
495
+ }
496
+
497
+ /** True iff both metadata maps carry identical identity for every key. */
498
+ private graphFileMetaEqual(
499
+ a: Partial<Record<GraphType, GraphFileMeta>>,
500
+ b: Partial<Record<GraphType, GraphFileMeta>>,
501
+ ): boolean {
502
+ const types: GraphType[] = ["entity", "time", "causal"];
503
+ for (const type of types) {
504
+ const x = a[type];
505
+ const y = b[type];
506
+ if (!x && !y) continue;
507
+ if (!x || !y) return false;
508
+ if (x.size !== y.size || x.mtimeMs !== y.mtimeMs || x.ino !== y.ino) return false;
509
+ }
510
+ return true;
511
+ }
512
+
513
+ /**
514
+ * Re-stat every graph file whose identity the cache just committed and return
515
+ * false if any has moved since (issue #1904, Codex). Closes the validation→
516
+ * in-place-push TOCTOU window: a peer process (concurrent CLI / daemon /
517
+ * decay job) can append between edgeFilesUnchangedExceptOurAppends()'s final
518
+ * stat and the allEdges.push(...). Without this re-check the cache would be
519
+ * accepted with identity predating the peer write and miss the peer edge for
520
+ * the TTL. A residual infinitesimal window (peer writes during this re-stat)
521
+ * remains and is TTL-bounded.
522
+ */
523
+ private async edgeCacheIdentityUnchangedSinceCommit(): Promise<boolean> {
524
+ if (!this.edgeCache) return false;
525
+ for (const type of Object.keys(this.edgeCache.meta) as GraphType[]) {
526
+ const committed = this.edgeCache.meta[type];
527
+ if (!committed) continue;
528
+ let cur: GraphFileMeta;
529
+ try {
530
+ const st = await stat(graphFilePath(this.memoryDir, type));
531
+ cur = { size: st.size, mtimeMs: st.mtimeMs, ino: Number(st.ino) };
532
+ } catch (err) {
533
+ if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
534
+ cur = { size: 0, mtimeMs: 0, ino: 0 };
535
+ } else {
536
+ return false;
537
+ }
538
+ }
539
+ if (
540
+ cur.size !== committed.size ||
541
+ cur.mtimeMs !== committed.mtimeMs ||
542
+ cur.ino !== committed.ino
543
+ ) {
544
+ return false;
545
+ }
546
+ }
547
+ return true;
548
+ }
549
+
408
550
  /**
409
551
  * Called after a memory is written to disk.
410
552
  *
@@ -444,20 +586,25 @@ export class GraphIndex {
444
586
  const timeOn = g ? g.timeGraph : this.cfg.timeGraphEnabled;
445
587
  const causalOn = g ? g.causalGraph : this.cfg.causalGraphEnabled;
446
588
  const ts = new Date().toISOString();
589
+ // Collect the edges appended this call so a coherent single-writer push can
590
+ // extend the warm edge cache in place instead of nulling it (issue #1904).
591
+ const appended: GraphEdge[] = [];
447
592
 
448
593
  try {
449
594
  // Entity graph
450
595
  if (entityOn && opts.entityRef && opts.entitySiblings?.length) {
451
596
  const siblings = opts.entitySiblings.slice(0, this.cfg.maxEntityGraphEdgesPerMemory);
452
597
  for (const sibling of siblings) {
453
- await appendEdge(this.memoryDir, {
598
+ const edge: GraphEdge = {
454
599
  from: opts.memoryPath,
455
600
  to: sibling,
456
601
  type: "entity",
457
602
  weight: 1.0,
458
603
  label: opts.entityRef,
459
604
  ts,
460
- });
605
+ };
606
+ appended.push(edge);
607
+ await appendEdge(this.memoryDir, edge);
461
608
  }
462
609
  }
463
610
 
@@ -465,14 +612,16 @@ export class GraphIndex {
465
612
  if (timeOn && opts.threadId && opts.recentInThread?.length) {
466
613
  const predecessor = opts.recentInThread[opts.recentInThread.length - 1];
467
614
  if (predecessor && predecessor !== opts.memoryPath) {
468
- await appendEdge(this.memoryDir, {
615
+ const edge: GraphEdge = {
469
616
  from: predecessor,
470
617
  to: opts.memoryPath,
471
618
  type: "time",
472
619
  weight: 1.0,
473
620
  label: opts.threadId,
474
621
  ts,
475
- });
622
+ };
623
+ appended.push(edge);
624
+ await appendEdge(this.memoryDir, edge);
476
625
  }
477
626
  }
478
627
 
@@ -480,14 +629,16 @@ export class GraphIndex {
480
629
  if (causalOn && opts.causalPredecessor) {
481
630
  const phrase = detectCausalPhrase(opts.content);
482
631
  if (phrase) {
483
- await appendEdge(this.memoryDir, {
632
+ const edge: GraphEdge = {
484
633
  from: opts.causalPredecessor,
485
634
  to: opts.memoryPath,
486
635
  type: "causal",
487
636
  weight: 1.0,
488
637
  label: phrase,
489
638
  ts,
490
- });
639
+ };
640
+ appended.push(edge);
641
+ await appendEdge(this.memoryDir, edge);
491
642
  }
492
643
  }
493
644
  } catch (err) {
@@ -495,10 +646,116 @@ export class GraphIndex {
495
646
  const { log } = await import("./logger.js");
496
647
  log.warn(`[graph] onMemoryWritten error: ${err}`);
497
648
  } finally {
498
- // Invalidate edge cache so spreadingActivation() picks up new edges.
499
- // In `finally` so the cache is cleared even on partial write failure.
500
- this.edgeCache = null;
649
+ // Edge-cache coherence (issue #1904). Legacy behavior nulled the cache on
650
+ // every write, paying a 2-4 s full re-read + parse on the next traversal.
651
+ if (!this.edgeCache || Date.now() - this.edgeCache.loadedAt >= GraphIndex.EDGE_CACHE_TTL_MS) {
652
+ // Nothing warm, or the entry is past its 5-min TTL backstop — reload.
653
+ this.edgeCache = null;
654
+ } else if (this.cfg.graphEdgeCacheIncrementalEnabled === false) {
655
+ // Rollback lever: restore the pre-#1904 null-on-every-write behavior.
656
+ this.edgeCache = null;
657
+ } else {
658
+ // Trust an in-place push ONLY when WE are the sole writer since load:
659
+ // every enabled graph file's on-disk size must equal our baseline plus
660
+ // exactly the bytes we appended to it. A divergence means a peer process
661
+ // (backup daemon / CLI / decay job) also wrote — force a full reload for
662
+ // cross-process coherence. Any stat error fails open to a full reload.
663
+ try {
664
+ if (await this.edgeFilesUnchangedExceptOurAppends(appended)) {
665
+ if (appended.length > 0) {
666
+ this.edgeCache.allEdges.push(...appended);
667
+ // Close the validation→push TOCTOU window (#1904, Codex): a peer
668
+ // process can append between the validation stat above and this
669
+ // in-place push. Re-verify every committed identity; if any file
670
+ // moved, the cache would miss the peer edge for the TTL, so null
671
+ // it and let the next traversal reload. (Skipped when this call
672
+ // pushed nothing — no new window was opened, and the validation
673
+ // already confirmed no peer wrote.)
674
+ if (this.edgeCache && !(await this.edgeCacheIdentityUnchangedSinceCommit())) {
675
+ this.edgeCache = null;
676
+ }
677
+ }
678
+ } else {
679
+ this.edgeCache = null;
680
+ }
681
+ } catch {
682
+ this.edgeCache = null;
683
+ }
684
+ }
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Cross-process coherence check for the incremental edge cache (issue #1904).
690
+ *
691
+ * Returns true iff every ENABLED graph file's current identity is explained
692
+ * ENTIRELY by THIS call's own append — no other process wrote to any graph
693
+ * file since load. Per file:
694
+ * - inode must be unchanged. A local append (`appendFile`) keeps the inode;
695
+ * an external atomic temp+rename rewrite (e.g. the edge-decay job) changes
696
+ * it, so an inode change — even one that preserves byte length, such as
697
+ * decaying confidence 0.9->0.8 — forces a reload (Codex).
698
+ * - a file WE appended to (delta > 0): size must equal baseline + our exact
699
+ * appended bytes. If it was absent at load (sentinel ino 0), our append
700
+ * created it, so we accept size === delta and adopt the new identity.
701
+ * - a file we did NOT touch (delta 0): size AND mtime must be unchanged, so
702
+ * an in-place equal-length rewrite (mtime bump) or a peer append (size
703
+ * bump) both force a reload.
704
+ * On success it commits the new identity baseline so the next append
705
+ * revalidates against post-this-write truth. Fail-open: an unreadable file
706
+ * (non-ENOENT) returns false, forcing a full reload.
707
+ */
708
+ private async edgeFilesUnchangedExceptOurAppends(appended: GraphEdge[]): Promise<boolean> {
709
+ if (!this.edgeCache) return false;
710
+ const expectedDelta: Partial<Record<GraphType, number>> = {};
711
+ for (const edge of appended) {
712
+ const bytes = Buffer.byteLength(`${JSON.stringify(edge)}\n`, "utf8");
713
+ expectedDelta[edge.type] = (expectedDelta[edge.type] ?? 0) + bytes;
714
+ }
715
+ const enabled: GraphType[] = [];
716
+ if (this.cfg.entityGraphEnabled) enabled.push("entity");
717
+ if (this.cfg.timeGraphEnabled) enabled.push("time");
718
+ if (this.cfg.causalGraphEnabled) enabled.push("causal");
719
+ const observed: Partial<Record<GraphType, GraphFileMeta>> = {};
720
+ for (const type of enabled) {
721
+ let cur: GraphFileMeta;
722
+ try {
723
+ const st = await stat(graphFilePath(this.memoryDir, type));
724
+ cur = { size: st.size, mtimeMs: st.mtimeMs, ino: Number(st.ino) };
725
+ } catch (err) {
726
+ // An absent graph file (ENOENT) is the normal partially-populated state
727
+ // (e.g. entity edges written before any causal edge): treat it as the
728
+ // all-zero sentinel so the incremental cache stays warm. Only a genuine
729
+ // stat failure forces a full reload (#1904, Codex).
730
+ if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
731
+ cur = { size: 0, mtimeMs: 0, ino: 0 };
732
+ } else {
733
+ return false;
734
+ }
735
+ }
736
+ const base = this.edgeCache.meta[type] ?? { size: 0, mtimeMs: 0, ino: 0 };
737
+ const delta = expectedDelta[type] ?? 0;
738
+ if (delta > 0) {
739
+ if (base.ino === 0) {
740
+ // File was absent at load; our append created it. Accept iff its whole
741
+ // size is exactly our appended bytes (nothing else wrote it).
742
+ if (cur.ino === 0 || cur.size !== delta) return false;
743
+ } else {
744
+ // Existing file: our append keeps the inode and grows size by exactly
745
+ // our bytes. An atomic rewrite (ino change) or a peer append (size
746
+ // mismatch) both fail here.
747
+ if (cur.ino !== base.ino || cur.size !== base.size + delta) return false;
748
+ }
749
+ } else {
750
+ // Untouched file: identity must be byte-for-byte unchanged.
751
+ if (cur.ino !== base.ino || cur.size !== base.size || cur.mtimeMs !== base.mtimeMs) {
752
+ return false;
753
+ }
754
+ }
755
+ observed[type] = cur;
501
756
  }
757
+ this.edgeCache.meta = observed;
758
+ return true;
502
759
  }
503
760
 
504
761
  /**
@@ -18,6 +18,7 @@ function createConfig(): PluginConfig {
18
18
  localLlm400TripThreshold: 3,
19
19
  localLlm400CooldownMs: 60_000,
20
20
  debug: false,
21
+ localLlmReasoningEffort: "none",
21
22
  slowLogEnabled: false,
22
23
  slowLogThresholdMs: 1_000,
23
24
  } as unknown as PluginConfig;
@@ -255,7 +256,7 @@ test("disableThinking fails open for generic backend — no kwarg sent (#548 Cod
255
256
  );
256
257
  });
257
258
 
258
- test("disableThinking fails open for ollama backend no kwarg sent", async () => {
259
+ test("ollama backend: suppression injects reasoning_effort, never chat_template_kwargs (#1996)", async () => {
259
260
  const client = new LocalLlmClient(createConfig());
260
261
  primeClient(client, { thinking: true, detected: "ollama" });
261
262
  const { restore, bodies } = captureFetchBodies();
@@ -265,6 +266,59 @@ test("disableThinking fails open for ollama backend — no kwarg sent", async ()
265
266
  restore();
266
267
  }
267
268
  assert.equal("chat_template_kwargs" in (bodies[0] ?? {}), false);
269
+ assert.equal(bodies[0]?.reasoning_effort, "none");
270
+ });
271
+
272
+ test("ollama backend: non-suppressed operation gets no reasoning_effort (#1996)", async () => {
273
+ const client = new LocalLlmClient(createConfig());
274
+ primeClient(client, { thinking: true, detected: "ollama" });
275
+ const { restore, bodies } = captureFetchBodies();
276
+ try {
277
+ await runOneChatCompletion(client, "consolidation");
278
+ } finally {
279
+ restore();
280
+ }
281
+ assert.equal("reasoning_effort" in (bodies[0] ?? {}), false);
282
+ });
283
+
284
+ test("ollama backend: empty localLlmReasoningEffort disables injection (#1996)", async () => {
285
+ const config = { ...createConfig(), localLlmReasoningEffort: "" } as PluginConfig;
286
+ const client = new LocalLlmClient(config);
287
+ primeClient(client, { thinking: true, detected: "ollama" });
288
+ const { restore, bodies } = captureFetchBodies();
289
+ try {
290
+ await runOneChatCompletion(client);
291
+ } finally {
292
+ restore();
293
+ }
294
+ assert.equal("reasoning_effort" in (bodies[0] ?? {}), false);
295
+ assert.equal("chat_template_kwargs" in (bodies[0] ?? {}), false);
296
+ });
297
+
298
+ test("ollama backend: configured effort value is passed through (#1996)", async () => {
299
+ const config = { ...createConfig(), localLlmReasoningEffort: "low" } as PluginConfig;
300
+ const client = new LocalLlmClient(config);
301
+ primeClient(client, { thinking: true, detected: "ollama" });
302
+ const { restore, bodies } = captureFetchBodies();
303
+ try {
304
+ await runOneChatCompletion(client);
305
+ } finally {
306
+ restore();
307
+ }
308
+ assert.equal(bodies[0]?.reasoning_effort, "low");
309
+ });
310
+
311
+ test("lmstudio backend never receives reasoning_effort (#1996)", async () => {
312
+ const client = new LocalLlmClient(createConfig());
313
+ primeClient(client, { thinking: true, detected: "lmstudio" });
314
+ const { restore, bodies } = captureFetchBodies();
315
+ try {
316
+ await runOneChatCompletion(client);
317
+ } finally {
318
+ restore();
319
+ }
320
+ assert.equal("reasoning_effort" in (bodies[0] ?? {}), false);
321
+ assert.deepEqual(bodies[0]?.chat_template_kwargs, { enable_thinking: false });
268
322
  });
269
323
 
270
324
  test("disableThinking fails open when backend is undetected (null)", async () => {
package/src/local-llm.ts CHANGED
@@ -999,6 +999,20 @@ export class LocalLlmClient {
999
999
  THINKING_COMPATIBLE_BACKENDS.has(this.detectedType)
1000
1000
  ) {
1001
1001
  requestBody.chat_template_kwargs = { enable_thinking: false };
1002
+ } else if (shouldSuppressThinking && this.detectedType === "ollama") {
1003
+ // Ollama's /v1 endpoint silently drops `chat_template_kwargs` AND
1004
+ // `enable_thinking` — the ONLY field it maps to think-off is
1005
+ // `reasoning_effort` ("none" → ThinkValue{false} in openai.go;
1006
+ // verified against Ollama 0.32.0 source and live probes,
1007
+ // issue #1996). Without this, thinking-capable models (Gemma 4,
1008
+ // Qwen 3.5+) burn 1,700-2,000 reasoning tokens per extraction on
1009
+ // Ollama, exceed the client timeout, and retry forever. The effort
1010
+ // is configurable via `localLlmReasoningEffort` (default "none");
1011
+ // an empty value disables injection (pre-#1996 behavior).
1012
+ const effort = this.config.localLlmReasoningEffort;
1013
+ if (effort) {
1014
+ requestBody.reasoning_effort = effort;
1015
+ }
1002
1016
  }
1003
1017
 
1004
1018
  // Normalize URL (use 127.0.0.1 instead of localhost)
@@ -284,6 +284,19 @@ export function setCachedQmdSearch(cacheKey: string, results: unknown[]): void {
284
284
  }
285
285
  }
286
286
 
287
+ /**
288
+ * Mutation scopes (issue #1904). A cache layer declares (via `evictOn`) which
289
+ * of these mutation kinds evict it, so the invalidation chokepoint can clear
290
+ * only the layers a given write actually affects instead of nuking every
291
+ * layer on every write (the permanently-cold-cache regression #1904 fixes).
292
+ */
293
+ export type MemoryMutationScope =
294
+ | "memory-create" // a new memory doc added; no existing doc's recall-visibility changes
295
+ | "memory-mutate" // existing doc changed/moved/removed/status-changed (update, supersede,
296
+ // archive, invalidate, frontmatter, cold-tier change, bulk delete)
297
+ | "entity-write" // an entity file changed
298
+ | "secure-key-change"; // encryption key rotated; everything is now unreadable → evict all
299
+
287
300
  /**
288
301
  * Cache-layer registry (issue #1535).
289
302
  *
@@ -305,6 +318,11 @@ export interface MemoryCacheLayer {
305
318
  * the simple-and-correct choice — these caches are re-warmable
306
319
  * (issue #1535, implementation guide option (b)). */
307
320
  readonly scope: "dir" | "global";
321
+ /** Which mutation scopes evict this layer (issue #1904). A `memory-create`
322
+ * only refreshes the full-memory-list views (hot + derived episode/rule); a
323
+ * `memory-mutate`/`entity-write`/`secure-key-change` evicts every layer whose
324
+ * content it can affect. Adding a layer REQUIRES declaring this. */
325
+ readonly evictOn: Partial<Record<MemoryMutationScope, true>>;
308
326
  /** Evict entries for baseDir ("dir" scope) or everything ("global" scope). */
309
327
  readonly invalidateForDir: (baseDir: string) => void;
310
328
  /** Evict every entry across all dirs. */
@@ -318,6 +336,7 @@ export const ALL_CACHE_LAYERS: readonly MemoryCacheLayer[] = [
318
336
  {
319
337
  name: "hot-memories",
320
338
  scope: "dir",
339
+ evictOn: { "memory-create": true, "memory-mutate": true, "secure-key-change": true },
321
340
  invalidateForDir: (baseDir) => void hotCacheByDir.delete(baseDir),
322
341
  clearAll: () => hotCacheByDir.clear(),
323
342
  hasEntriesFor: (baseDir) => hotCacheByDir.has(baseDir),
@@ -325,6 +344,7 @@ export const ALL_CACHE_LAYERS: readonly MemoryCacheLayer[] = [
325
344
  {
326
345
  name: "archive-memories",
327
346
  scope: "dir",
347
+ evictOn: { "memory-mutate": true, "secure-key-change": true },
328
348
  invalidateForDir: (baseDir) => void archiveCacheByDir.delete(baseDir),
329
349
  clearAll: () => archiveCacheByDir.clear(),
330
350
  hasEntriesFor: (baseDir) => archiveCacheByDir.has(baseDir),
@@ -332,6 +352,7 @@ export const ALL_CACHE_LAYERS: readonly MemoryCacheLayer[] = [
332
352
  {
333
353
  name: "entities",
334
354
  scope: "dir",
355
+ evictOn: { "entity-write": true, "secure-key-change": true },
335
356
  invalidateForDir: (baseDir) => invalidateCachedEntities(baseDir),
336
357
  clearAll: () => entityCacheByDir.clear(),
337
358
  hasEntriesFor: (baseDir) => {
@@ -345,6 +366,7 @@ export const ALL_CACHE_LAYERS: readonly MemoryCacheLayer[] = [
345
366
  {
346
367
  name: "derived-episode-map",
347
368
  scope: "dir",
369
+ evictOn: { "memory-create": true, "memory-mutate": true, "secure-key-change": true },
348
370
  invalidateForDir: (baseDir) => void episodeMapByDir.delete(baseDir),
349
371
  clearAll: () => episodeMapByDir.clear(),
350
372
  hasEntriesFor: (baseDir) => episodeMapByDir.has(baseDir),
@@ -352,6 +374,7 @@ export const ALL_CACHE_LAYERS: readonly MemoryCacheLayer[] = [
352
374
  {
353
375
  name: "derived-rule-memories",
354
376
  scope: "dir",
377
+ evictOn: { "memory-create": true, "memory-mutate": true, "secure-key-change": true },
355
378
  invalidateForDir: (baseDir) => void ruleMemoriesByDir.delete(baseDir),
356
379
  clearAll: () => ruleMemoriesByDir.clear(),
357
380
  hasEntriesFor: (baseDir) => ruleMemoriesByDir.has(baseDir),
@@ -359,6 +382,7 @@ export const ALL_CACHE_LAYERS: readonly MemoryCacheLayer[] = [
359
382
  {
360
383
  name: "qmd-search",
361
384
  scope: "global",
385
+ evictOn: { "memory-mutate": true, "entity-write": true, "secure-key-change": true },
362
386
  invalidateForDir: () => qmdSearchCache.clear(),
363
387
  clearAll: () => qmdSearchCache.clear(),
364
388
  hasEntriesFor: () => qmdSearchCache.size > 0,
@@ -366,6 +390,7 @@ export const ALL_CACHE_LAYERS: readonly MemoryCacheLayer[] = [
366
390
  {
367
391
  name: "qmd-recall",
368
392
  scope: "global",
393
+ evictOn: { "memory-mutate": true, "entity-write": true, "secure-key-change": true },
369
394
  invalidateForDir: () => clearQmdRecallCache(),
370
395
  clearAll: () => clearQmdRecallCache(),
371
396
  hasEntriesFor: () => qmdRecallCacheSize() > 0,
@@ -389,6 +414,37 @@ export function invalidateAllForDir(baseDir: string): void {
389
414
  }
390
415
  }
391
416
 
417
+ /**
418
+ * Scope-aware invalidation chokepoint (issue #1904). Evicts only the layers
419
+ * whose `evictOn` includes `scope`, so the highest-frequency write
420
+ * (`memory-create`, fired continuously by extraction) stops nuking the QMD
421
+ * result caches and the version-keyed entity cache it cannot affect. This is
422
+ * the single scoped entry point — like `invalidateAllForDir`, it iterates the
423
+ * `ALL_CACHE_LAYERS` registry rather than clearing any layer ad hoc, so the
424
+ * single-chokepoint invariant (#1535) holds: a new layer only participates by
425
+ * declaring `evictOn`.
426
+ */
427
+ export function invalidateForScope(baseDir: string, scope: MemoryMutationScope): void {
428
+ for (const layer of ALL_CACHE_LAYERS) {
429
+ if (layer.evictOn[scope]) layer.invalidateForDir(baseDir);
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Companion to {@link invalidateForScope} for the single-file write fast path
435
+ * (issue #1902 + #1904): it patches the hot-memories entry in place and
436
+ * re-keys it, so this evicts every OTHER layer the scope would clear but leaves
437
+ * hot-memories alone. A `memory-create` thus evicts only the derived
438
+ * episode/rule views (hot patched, QMD/entities/archive kept warm), which is
439
+ * the whole point of #1904 — a create must not evict the QMD result caches.
440
+ */
441
+ export function invalidateForScopeExceptHot(baseDir: string, scope: MemoryMutationScope): void {
442
+ for (const layer of ALL_CACHE_LAYERS) {
443
+ if (layer.name === "hot-memories") continue;
444
+ if (layer.evictOn[scope]) layer.invalidateForDir(baseDir);
445
+ }
446
+ }
447
+
392
448
  /**
393
449
  * Invalidate every cache layer for `baseDir` EXCEPT the hot-memories layer
394
450
  * (issue #1902). Single-file write paths use this instead of
@@ -429,6 +485,20 @@ export function clearMemoryCache(baseDir?: string): void {
429
485
  }
430
486
  }
431
487
 
488
+ /**
489
+ * Clear ONLY the two global QMD result caches (qmd-search + qmd-recall), leaving
490
+ * every dir-scoped layer (hot/archive/entities/derived) untouched. Called after
491
+ * a successful QMD maintenance update+embed (#1904, Codex): a newly-persisted
492
+ * fact becomes searchable at that moment, so any cached pre-index recall/search
493
+ * bundle is now stale and must not be served. This lets the create path keep the
494
+ * QMD caches warm during index lag (perf win) while guaranteeing invalidation
495
+ * the moment the fact is actually indexed — no extended stale window.
496
+ */
497
+ export function clearQmdResultCaches(): void {
498
+ qmdSearchCache.clear();
499
+ clearQmdRecallCache();
500
+ }
501
+
432
502
  export function getMemoryCacheStats(baseDir: string): {
433
503
  hotSize: number;
434
504
  archiveSize: number;
@@ -312,6 +312,8 @@ export class NamespaceStorageRouter {
312
312
  this.config.hotMemoriesCacheEnabled,
313
313
  this.config.hotMemoriesCacheTtlMs,
314
314
  );
315
+ const { scopedCacheInvalidationEnabled } = this.config;
316
+ StorageManager.setScopedCacheInvalidationDefault(root, scopedCacheInvalidationEnabled);
315
317
  // Propagate the inline-attribution template so that router-created storages
316
318
  // (used by extraction and shared-promotion paths) strip citations consistently,
317
319
  // matching the behaviour of the primary this.storage instance in the orchestrator.
@@ -523,6 +523,13 @@ export class MaintenanceScheduler {
523
523
  this.lastQmdEmbedAtMs = now;
524
524
  }
525
525
  }
526
+ // Note: a successful QMD update/embed clears the global QMD result caches
527
+ // itself (QmdClient.runUpdateForCollection / runEmbedForCollection), so
528
+ // this scheduler does not invalidate them here — every refresh path
529
+ // (direct update, namespace router, wearable/OpenClaw sync) funnels through
530
+ // the backend and clears centrally. A no-op pass (throttle/unavailable/
531
+ // backoff) does not advance lastUpdateRanAtMs, so it leaves caches warm
532
+ // (#1904, Codex/Cursor).
526
533
  } finally {
527
534
  this.qmdMaintenanceInFlight = false;
528
535
  if (this.qmdMaintenancePending) {
@@ -646,6 +646,10 @@ export class WorkspaceOpsCoordinator {
646
646
  },
647
647
  },
648
648
  reindexSearch: async () => {
649
+ // qmd.update() clears the global QMD result caches itself on success
650
+ // (QmdClient.runUpdateForCollection), so newly-indexed wearable
651
+ // transcript/fact content is never hidden by a pre-sync cache entry
652
+ // (#1904, Codex).
649
653
  await this.deps.qmd.update();
650
654
  },
651
655
  });