@remnic/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (551) hide show
  1. package/dist/abstraction-nodes.d.ts +52 -0
  2. package/dist/abstraction-nodes.js +15 -0
  3. package/dist/abstraction-nodes.js.map +1 -0
  4. package/dist/access-cli.d.ts +5 -0
  5. package/dist/access-cli.js +308 -0
  6. package/dist/access-cli.js.map +1 -0
  7. package/dist/access-http.d.ts +158 -0
  8. package/dist/access-http.js +32 -0
  9. package/dist/access-http.js.map +1 -0
  10. package/dist/access-idempotency.d.ts +31 -0
  11. package/dist/access-idempotency.js +11 -0
  12. package/dist/access-idempotency.js.map +1 -0
  13. package/dist/access-mcp.d.ts +76 -0
  14. package/dist/access-mcp.js +8 -0
  15. package/dist/access-mcp.js.map +1 -0
  16. package/dist/access-schema.d.ts +266 -0
  17. package/dist/access-schema.js +29 -0
  18. package/dist/access-schema.js.map +1 -0
  19. package/dist/access-service.d.ts +614 -0
  20. package/dist/access-service.js +32 -0
  21. package/dist/access-service.js.map +1 -0
  22. package/dist/behavior-learner.d.ts +16 -0
  23. package/dist/behavior-learner.js +124 -0
  24. package/dist/behavior-learner.js.map +1 -0
  25. package/dist/behavior-signals.d.ts +15 -0
  26. package/dist/behavior-signals.js +11 -0
  27. package/dist/behavior-signals.js.map +1 -0
  28. package/dist/bootstrap.d.ts +46 -0
  29. package/dist/bootstrap.js +9 -0
  30. package/dist/bootstrap.js.map +1 -0
  31. package/dist/boxes.d.ts +93 -0
  32. package/dist/boxes.js +14 -0
  33. package/dist/boxes.js.map +1 -0
  34. package/dist/buffer.d.ts +22 -0
  35. package/dist/buffer.js +9 -0
  36. package/dist/buffer.js.map +1 -0
  37. package/dist/calibration.d.ts +81 -0
  38. package/dist/calibration.js +239 -0
  39. package/dist/calibration.js.map +1 -0
  40. package/dist/causal-behavior.d.ts +79 -0
  41. package/dist/causal-behavior.js +190 -0
  42. package/dist/causal-behavior.js.map +1 -0
  43. package/dist/causal-chain.d.ts +61 -0
  44. package/dist/causal-chain.js +24 -0
  45. package/dist/causal-chain.js.map +1 -0
  46. package/dist/causal-consolidation.d.ts +71 -0
  47. package/dist/causal-consolidation.js +211 -0
  48. package/dist/causal-consolidation.js.map +1 -0
  49. package/dist/causal-retrieval.d.ts +44 -0
  50. package/dist/causal-retrieval.js +184 -0
  51. package/dist/causal-retrieval.js.map +1 -0
  52. package/dist/causal-trajectory-graph.d.ts +13 -0
  53. package/dist/causal-trajectory-graph.js +59 -0
  54. package/dist/causal-trajectory-graph.js.map +1 -0
  55. package/dist/causal-trajectory.d.ts +68 -0
  56. package/dist/causal-trajectory.js +18 -0
  57. package/dist/causal-trajectory.js.map +1 -0
  58. package/dist/chunk-2CJCWDMR.js +87 -0
  59. package/dist/chunk-2CJCWDMR.js.map +1 -0
  60. package/dist/chunk-2NMMFZ5T.js +216 -0
  61. package/dist/chunk-2NMMFZ5T.js.map +1 -0
  62. package/dist/chunk-2PO5ZRKV.js +103 -0
  63. package/dist/chunk-2PO5ZRKV.js.map +1 -0
  64. package/dist/chunk-3QKK7QOS.js +154 -0
  65. package/dist/chunk-3QKK7QOS.js.map +1 -0
  66. package/dist/chunk-3SLRNYNG.js +26 -0
  67. package/dist/chunk-3SLRNYNG.js.map +1 -0
  68. package/dist/chunk-4A24LIM2.js +68 -0
  69. package/dist/chunk-4A24LIM2.js.map +1 -0
  70. package/dist/chunk-6HZ6AO2P.js +164 -0
  71. package/dist/chunk-6HZ6AO2P.js.map +1 -0
  72. package/dist/chunk-763GUIOU.js +302 -0
  73. package/dist/chunk-763GUIOU.js.map +1 -0
  74. package/dist/chunk-AAI7JARD.js +173 -0
  75. package/dist/chunk-AAI7JARD.js.map +1 -0
  76. package/dist/chunk-B7LOFDVE.js +112 -0
  77. package/dist/chunk-B7LOFDVE.js.map +1 -0
  78. package/dist/chunk-BDFZXRSO.js +318 -0
  79. package/dist/chunk-BDFZXRSO.js.map +1 -0
  80. package/dist/chunk-BOUYNNYD.js +707 -0
  81. package/dist/chunk-BOUYNNYD.js.map +1 -0
  82. package/dist/chunk-BRK4ODMI.js +60 -0
  83. package/dist/chunk-BRK4ODMI.js.map +1 -0
  84. package/dist/chunk-C6QPK5GG.js +111 -0
  85. package/dist/chunk-C6QPK5GG.js.map +1 -0
  86. package/dist/chunk-C7VW7C3F.js +117 -0
  87. package/dist/chunk-C7VW7C3F.js.map +1 -0
  88. package/dist/chunk-CDW777AI.js +621 -0
  89. package/dist/chunk-CDW777AI.js.map +1 -0
  90. package/dist/chunk-CULXMQJH.js +185 -0
  91. package/dist/chunk-CULXMQJH.js.map +1 -0
  92. package/dist/chunk-CXWFUJR2.js +1203 -0
  93. package/dist/chunk-CXWFUJR2.js.map +1 -0
  94. package/dist/chunk-DGXUHMOV.js +61 -0
  95. package/dist/chunk-DGXUHMOV.js.map +1 -0
  96. package/dist/chunk-DM2T26WE.js +61 -0
  97. package/dist/chunk-DM2T26WE.js.map +1 -0
  98. package/dist/chunk-DORBM6OB.js +81 -0
  99. package/dist/chunk-DORBM6OB.js.map +1 -0
  100. package/dist/chunk-DT5TVLJE.js +32 -0
  101. package/dist/chunk-DT5TVLJE.js.map +1 -0
  102. package/dist/chunk-EEQLFRUM.js +89 -0
  103. package/dist/chunk-EEQLFRUM.js.map +1 -0
  104. package/dist/chunk-EQINRHYR.js +672 -0
  105. package/dist/chunk-EQINRHYR.js.map +1 -0
  106. package/dist/chunk-ESSMF2FR.js +146 -0
  107. package/dist/chunk-ESSMF2FR.js.map +1 -0
  108. package/dist/chunk-ETOW6ACV.js +158 -0
  109. package/dist/chunk-ETOW6ACV.js.map +1 -0
  110. package/dist/chunk-FYIYMQ5N.js +221 -0
  111. package/dist/chunk-FYIYMQ5N.js.map +1 -0
  112. package/dist/chunk-G3AG3KZN.js +78 -0
  113. package/dist/chunk-G3AG3KZN.js.map +1 -0
  114. package/dist/chunk-GJR6D6KC.js +61 -0
  115. package/dist/chunk-GJR6D6KC.js.map +1 -0
  116. package/dist/chunk-GPGBSNKM.js +380 -0
  117. package/dist/chunk-GPGBSNKM.js.map +1 -0
  118. package/dist/chunk-H63EDPFJ.js +57 -0
  119. package/dist/chunk-H63EDPFJ.js.map +1 -0
  120. package/dist/chunk-HG2NKWR2.js +185 -0
  121. package/dist/chunk-HG2NKWR2.js.map +1 -0
  122. package/dist/chunk-HL4DB7TO.js +13 -0
  123. package/dist/chunk-HL4DB7TO.js.map +1 -0
  124. package/dist/chunk-HLBYLYRD.js +346 -0
  125. package/dist/chunk-HLBYLYRD.js.map +1 -0
  126. package/dist/chunk-HLXVTBF3.js +109 -0
  127. package/dist/chunk-HLXVTBF3.js.map +1 -0
  128. package/dist/chunk-IFFFR3MR.js +68 -0
  129. package/dist/chunk-IFFFR3MR.js.map +1 -0
  130. package/dist/chunk-ISY75RLM.js +1027 -0
  131. package/dist/chunk-ISY75RLM.js.map +1 -0
  132. package/dist/chunk-IZME7KW2.js +1886 -0
  133. package/dist/chunk-IZME7KW2.js.map +1 -0
  134. package/dist/chunk-J3BT33K7.js +720 -0
  135. package/dist/chunk-J3BT33K7.js.map +1 -0
  136. package/dist/chunk-J47FNDR7.js +113 -0
  137. package/dist/chunk-J47FNDR7.js.map +1 -0
  138. package/dist/chunk-JWPLJLDU.js +63 -0
  139. package/dist/chunk-JWPLJLDU.js.map +1 -0
  140. package/dist/chunk-K6WK37A6.js +865 -0
  141. package/dist/chunk-K6WK37A6.js.map +1 -0
  142. package/dist/chunk-KL4CP4SB.js +130 -0
  143. package/dist/chunk-KL4CP4SB.js.map +1 -0
  144. package/dist/chunk-KT4NEUNF.js +315 -0
  145. package/dist/chunk-KT4NEUNF.js.map +1 -0
  146. package/dist/chunk-KWBU5S5U.js +42 -0
  147. package/dist/chunk-KWBU5S5U.js.map +1 -0
  148. package/dist/chunk-L5RPWGFK.js +59 -0
  149. package/dist/chunk-L5RPWGFK.js.map +1 -0
  150. package/dist/chunk-L7WO3MZ4.js +128 -0
  151. package/dist/chunk-L7WO3MZ4.js.map +1 -0
  152. package/dist/chunk-LIRZNNUP.js +74 -0
  153. package/dist/chunk-LIRZNNUP.js.map +1 -0
  154. package/dist/chunk-LK6SGL53.js +22 -0
  155. package/dist/chunk-LK6SGL53.js.map +1 -0
  156. package/dist/chunk-LOBRX7VD.js +200 -0
  157. package/dist/chunk-LOBRX7VD.js.map +1 -0
  158. package/dist/chunk-LPSF4OQH.js +47 -0
  159. package/dist/chunk-LPSF4OQH.js.map +1 -0
  160. package/dist/chunk-LU3GQNDQ.js +152 -0
  161. package/dist/chunk-LU3GQNDQ.js.map +1 -0
  162. package/dist/chunk-M5KEYE5E.js +350 -0
  163. package/dist/chunk-M5KEYE5E.js.map +1 -0
  164. package/dist/chunk-M62O4P4T.js +41 -0
  165. package/dist/chunk-M62O4P4T.js.map +1 -0
  166. package/dist/chunk-MARWOCVP.js +48 -0
  167. package/dist/chunk-MARWOCVP.js.map +1 -0
  168. package/dist/chunk-MDDAA2AO.js +925 -0
  169. package/dist/chunk-MDDAA2AO.js.map +1 -0
  170. package/dist/chunk-MWGVGUIS.js +198 -0
  171. package/dist/chunk-MWGVGUIS.js.map +1 -0
  172. package/dist/chunk-N5AKDXAI.js +74 -0
  173. package/dist/chunk-N5AKDXAI.js.map +1 -0
  174. package/dist/chunk-NGAVDO7E.js +115 -0
  175. package/dist/chunk-NGAVDO7E.js.map +1 -0
  176. package/dist/chunk-NTTLPF7F.js +283 -0
  177. package/dist/chunk-NTTLPF7F.js.map +1 -0
  178. package/dist/chunk-ONRU4L2N.js +240 -0
  179. package/dist/chunk-ONRU4L2N.js.map +1 -0
  180. package/dist/chunk-ORZMT74A.js +209 -0
  181. package/dist/chunk-ORZMT74A.js.map +1 -0
  182. package/dist/chunk-OTAVQCSF.js +268 -0
  183. package/dist/chunk-OTAVQCSF.js.map +1 -0
  184. package/dist/chunk-PGK3VUHN.js +160 -0
  185. package/dist/chunk-PGK3VUHN.js.map +1 -0
  186. package/dist/chunk-Q6FETXJA.js +1362 -0
  187. package/dist/chunk-Q6FETXJA.js.map +1 -0
  188. package/dist/chunk-QANCTXQF.js +271 -0
  189. package/dist/chunk-QANCTXQF.js.map +1 -0
  190. package/dist/chunk-QCCCQT3O.js +189 -0
  191. package/dist/chunk-QCCCQT3O.js.map +1 -0
  192. package/dist/chunk-QDOSNLB4.js +1048 -0
  193. package/dist/chunk-QDOSNLB4.js.map +1 -0
  194. package/dist/chunk-QFQVZOGA.js +2168 -0
  195. package/dist/chunk-QFQVZOGA.js.map +1 -0
  196. package/dist/chunk-QPKFPHOO.js +178 -0
  197. package/dist/chunk-QPKFPHOO.js.map +1 -0
  198. package/dist/chunk-QSVPYQPG.js +268 -0
  199. package/dist/chunk-QSVPYQPG.js.map +1 -0
  200. package/dist/chunk-QWUUMMIK.js +3045 -0
  201. package/dist/chunk-QWUUMMIK.js.map +1 -0
  202. package/dist/chunk-QY2BHY5O.js +2378 -0
  203. package/dist/chunk-QY2BHY5O.js.map +1 -0
  204. package/dist/chunk-SCHEKPYH.js +349 -0
  205. package/dist/chunk-SCHEKPYH.js.map +1 -0
  206. package/dist/chunk-SCU65EZI.js +15 -0
  207. package/dist/chunk-SCU65EZI.js.map +1 -0
  208. package/dist/chunk-T4WRIV2C.js +170 -0
  209. package/dist/chunk-T4WRIV2C.js.map +1 -0
  210. package/dist/chunk-TKO4HZCK.js +1852 -0
  211. package/dist/chunk-TKO4HZCK.js.map +1 -0
  212. package/dist/chunk-TP4FZJIZ.js +93 -0
  213. package/dist/chunk-TP4FZJIZ.js.map +1 -0
  214. package/dist/chunk-TPB3I2AC.js +403 -0
  215. package/dist/chunk-TPB3I2AC.js.map +1 -0
  216. package/dist/chunk-TVVVQQAK.js +1431 -0
  217. package/dist/chunk-TVVVQQAK.js.map +1 -0
  218. package/dist/chunk-U4PV25RD.js +14 -0
  219. package/dist/chunk-U4PV25RD.js.map +1 -0
  220. package/dist/chunk-UCYSTFZR.js +284 -0
  221. package/dist/chunk-UCYSTFZR.js.map +1 -0
  222. package/dist/chunk-UHGBNIOS.js +205 -0
  223. package/dist/chunk-UHGBNIOS.js.map +1 -0
  224. package/dist/chunk-UIYZ5T3I.js +108 -0
  225. package/dist/chunk-UIYZ5T3I.js.map +1 -0
  226. package/dist/chunk-UV2FO7J4.js +747 -0
  227. package/dist/chunk-UV2FO7J4.js.map +1 -0
  228. package/dist/chunk-UZB5KHKX.js +63 -0
  229. package/dist/chunk-UZB5KHKX.js.map +1 -0
  230. package/dist/chunk-V3RXWQIE.js +626 -0
  231. package/dist/chunk-V3RXWQIE.js.map +1 -0
  232. package/dist/chunk-V4YC4LUK.js +444 -0
  233. package/dist/chunk-V4YC4LUK.js.map +1 -0
  234. package/dist/chunk-VEWZZM3H.js +133 -0
  235. package/dist/chunk-VEWZZM3H.js.map +1 -0
  236. package/dist/chunk-WWIQTB2Y.js +98 -0
  237. package/dist/chunk-WWIQTB2Y.js.map +1 -0
  238. package/dist/chunk-X7XN6YU4.js +24 -0
  239. package/dist/chunk-X7XN6YU4.js.map +1 -0
  240. package/dist/chunk-XKECPATV.js +202 -0
  241. package/dist/chunk-XKECPATV.js.map +1 -0
  242. package/dist/chunk-XYIK4LF6.js +75 -0
  243. package/dist/chunk-XYIK4LF6.js.map +1 -0
  244. package/dist/chunk-Y27UJK6V.js +39 -0
  245. package/dist/chunk-Y27UJK6V.js.map +1 -0
  246. package/dist/chunk-Y4Z4I6WK.js +9 -0
  247. package/dist/chunk-Y4Z4I6WK.js.map +1 -0
  248. package/dist/chunk-YAPUAHAY.js +10761 -0
  249. package/dist/chunk-YAPUAHAY.js.map +1 -0
  250. package/dist/chunk-YAZNBMNF.js +92 -0
  251. package/dist/chunk-YAZNBMNF.js.map +1 -0
  252. package/dist/chunk-YCN4BVDK.js +66 -0
  253. package/dist/chunk-YCN4BVDK.js.map +1 -0
  254. package/dist/chunk-YNCQ7E4M.js +388 -0
  255. package/dist/chunk-YNCQ7E4M.js.map +1 -0
  256. package/dist/chunk-YNI4S5WT.js +143 -0
  257. package/dist/chunk-YNI4S5WT.js.map +1 -0
  258. package/dist/chunk-YRMVARQP.js +406 -0
  259. package/dist/chunk-YRMVARQP.js.map +1 -0
  260. package/dist/chunk-Z5AAYHUC.js +79 -0
  261. package/dist/chunk-Z5AAYHUC.js.map +1 -0
  262. package/dist/chunk-Z5LAYHGJ.js +15 -0
  263. package/dist/chunk-Z5LAYHGJ.js.map +1 -0
  264. package/dist/chunk-ZJLY4QSU.js +823 -0
  265. package/dist/chunk-ZJLY4QSU.js.map +1 -0
  266. package/dist/chunk-ZKYI7UVO.js +276 -0
  267. package/dist/chunk-ZKYI7UVO.js.map +1 -0
  268. package/dist/chunk-ZPKBYX2F.js +297 -0
  269. package/dist/chunk-ZPKBYX2F.js.map +1 -0
  270. package/dist/chunking.d.ts +48 -0
  271. package/dist/chunking.js +11 -0
  272. package/dist/chunking.js.map +1 -0
  273. package/dist/cli.d.ts +1162 -0
  274. package/dist/cli.js +7187 -0
  275. package/dist/cli.js.map +1 -0
  276. package/dist/commitment-ledger.d.ts +83 -0
  277. package/dist/commitment-ledger.js +19 -0
  278. package/dist/commitment-ledger.js.map +1 -0
  279. package/dist/compression-optimizer.d.ts +37 -0
  280. package/dist/compression-optimizer.js +13 -0
  281. package/dist/compression-optimizer.js.map +1 -0
  282. package/dist/config.d.ts +6 -0
  283. package/dist/config.js +12 -0
  284. package/dist/config.js.map +1 -0
  285. package/dist/cue-anchors.d.ts +50 -0
  286. package/dist/cue-anchors.js +15 -0
  287. package/dist/cue-anchors.js.map +1 -0
  288. package/dist/dashboard-runtime.d.ts +46 -0
  289. package/dist/dashboard-runtime.js +10 -0
  290. package/dist/dashboard-runtime.js.map +1 -0
  291. package/dist/day-summary.d.ts +6 -0
  292. package/dist/day-summary.js +10 -0
  293. package/dist/day-summary.js.map +1 -0
  294. package/dist/delinearize.d.ts +34 -0
  295. package/dist/delinearize.js +11 -0
  296. package/dist/delinearize.js.map +1 -0
  297. package/dist/embedding-fallback.d.ts +22 -0
  298. package/dist/embedding-fallback.js +8 -0
  299. package/dist/embedding-fallback.js.map +1 -0
  300. package/dist/engine-P26JFSVY.js +19 -0
  301. package/dist/engine-P26JFSVY.js.map +1 -0
  302. package/dist/entity-retrieval.d.ts +23 -0
  303. package/dist/entity-retrieval.js +24 -0
  304. package/dist/entity-retrieval.js.map +1 -0
  305. package/dist/evals.d.ts +282 -0
  306. package/dist/evals.js +32 -0
  307. package/dist/evals.js.map +1 -0
  308. package/dist/explicit-capture.d.ts +60 -0
  309. package/dist/explicit-capture.js +23 -0
  310. package/dist/explicit-capture.js.map +1 -0
  311. package/dist/extraction.d.ts +141 -0
  312. package/dist/extraction.js +22 -0
  313. package/dist/extraction.js.map +1 -0
  314. package/dist/fallback-llm.d.ts +95 -0
  315. package/dist/fallback-llm.js +12 -0
  316. package/dist/fallback-llm.js.map +1 -0
  317. package/dist/graph-dashboard-diff.d.ts +12 -0
  318. package/dist/graph-dashboard-diff.js +8 -0
  319. package/dist/graph-dashboard-diff.js.map +1 -0
  320. package/dist/graph-dashboard-key.d.ts +5 -0
  321. package/dist/graph-dashboard-key.js +7 -0
  322. package/dist/graph-dashboard-key.js.map +1 -0
  323. package/dist/graph-dashboard-parser.d.ts +20 -0
  324. package/dist/graph-dashboard-parser.js +8 -0
  325. package/dist/graph-dashboard-parser.js.map +1 -0
  326. package/dist/graph.d.ts +157 -0
  327. package/dist/graph.js +27 -0
  328. package/dist/graph.js.map +1 -0
  329. package/dist/harmonic-retrieval.d.ts +27 -0
  330. package/dist/harmonic-retrieval.js +12 -0
  331. package/dist/harmonic-retrieval.js.map +1 -0
  332. package/dist/himem.d.ts +23 -0
  333. package/dist/himem.js +7 -0
  334. package/dist/himem.js.map +1 -0
  335. package/dist/hygiene.d.ts +24 -0
  336. package/dist/hygiene.js +9 -0
  337. package/dist/hygiene.js.map +1 -0
  338. package/dist/identity-continuity.d.ts +17 -0
  339. package/dist/identity-continuity.js +19 -0
  340. package/dist/identity-continuity.js.map +1 -0
  341. package/dist/importance.d.ts +25 -0
  342. package/dist/importance.js +11 -0
  343. package/dist/importance.js.map +1 -0
  344. package/dist/index.d.ts +923 -0
  345. package/dist/index.js +2512 -0
  346. package/dist/index.js.map +1 -0
  347. package/dist/intent.d.ts +8 -0
  348. package/dist/intent.js +13 -0
  349. package/dist/intent.js.map +1 -0
  350. package/dist/json-extract.d.ts +14 -0
  351. package/dist/json-extract.js +9 -0
  352. package/dist/json-extract.js.map +1 -0
  353. package/dist/json-store.d.ts +5 -0
  354. package/dist/json-store.js +11 -0
  355. package/dist/json-store.js.map +1 -0
  356. package/dist/legacy-hook-compat.d.ts +3 -0
  357. package/dist/legacy-hook-compat.js +35 -0
  358. package/dist/legacy-hook-compat.js.map +1 -0
  359. package/dist/lifecycle.d.ts +52 -0
  360. package/dist/lifecycle.js +21 -0
  361. package/dist/lifecycle.js.map +1 -0
  362. package/dist/local-llm.d.ts +154 -0
  363. package/dist/local-llm.js +10 -0
  364. package/dist/local-llm.js.map +1 -0
  365. package/dist/logger.d.ts +15 -0
  366. package/dist/logger.js +9 -0
  367. package/dist/logger.js.map +1 -0
  368. package/dist/memory-action-policy.d.ts +13 -0
  369. package/dist/memory-action-policy.js +7 -0
  370. package/dist/memory-action-policy.js.map +1 -0
  371. package/dist/memory-cache.d.ts +35 -0
  372. package/dist/memory-cache.js +37 -0
  373. package/dist/memory-cache.js.map +1 -0
  374. package/dist/memory-lifecycle-ledger-utils.d.ts +13 -0
  375. package/dist/memory-lifecycle-ledger-utils.js +23 -0
  376. package/dist/memory-lifecycle-ledger-utils.js.map +1 -0
  377. package/dist/memory-projection-format.d.ts +4 -0
  378. package/dist/memory-projection-format.js +9 -0
  379. package/dist/memory-projection-format.js.map +1 -0
  380. package/dist/memory-projection-store-NxMkbocT.d.ts +221 -0
  381. package/dist/memory-projection-store.d.ts +3 -0
  382. package/dist/memory-projection-store.js +31 -0
  383. package/dist/memory-projection-store.js.map +1 -0
  384. package/dist/model-registry.d.ts +60 -0
  385. package/dist/model-registry.js +8 -0
  386. package/dist/model-registry.js.map +1 -0
  387. package/dist/native-knowledge.d.ts +94 -0
  388. package/dist/native-knowledge.js +26 -0
  389. package/dist/native-knowledge.js.map +1 -0
  390. package/dist/negative.d.ts +26 -0
  391. package/dist/negative.js +8 -0
  392. package/dist/negative.js.map +1 -0
  393. package/dist/objective-state-writers.d.ts +22 -0
  394. package/dist/objective-state-writers.js +313 -0
  395. package/dist/objective-state-writers.js.map +1 -0
  396. package/dist/objective-state.d.ts +75 -0
  397. package/dist/objective-state.js +17 -0
  398. package/dist/objective-state.js.map +1 -0
  399. package/dist/openai-chat-compat.d.ts +13 -0
  400. package/dist/openai-chat-compat.js +11 -0
  401. package/dist/openai-chat-compat.js.map +1 -0
  402. package/dist/operator-toolkit.d.ts +304 -0
  403. package/dist/operator-toolkit.js +41 -0
  404. package/dist/operator-toolkit.js.map +1 -0
  405. package/dist/opik-exporter.d.ts +72 -0
  406. package/dist/opik-exporter.js +361 -0
  407. package/dist/opik-exporter.js.map +1 -0
  408. package/dist/orchestrator-zTa-Qo-1.d.ts +1104 -0
  409. package/dist/orchestrator.d.ts +21 -0
  410. package/dist/orchestrator.js +145 -0
  411. package/dist/orchestrator.js.map +1 -0
  412. package/dist/policy-runtime.d.ts +37 -0
  413. package/dist/policy-runtime.js +13 -0
  414. package/dist/policy-runtime.js.map +1 -0
  415. package/dist/port-C1GZFv8h.d.ts +41 -0
  416. package/dist/profiling.d.ts +80 -0
  417. package/dist/profiling.js +10 -0
  418. package/dist/profiling.js.map +1 -0
  419. package/dist/qmd-recall-cache.d.ts +29 -0
  420. package/dist/qmd-recall-cache.js +13 -0
  421. package/dist/qmd-recall-cache.js.map +1 -0
  422. package/dist/qmd.d.ts +105 -0
  423. package/dist/qmd.js +13 -0
  424. package/dist/qmd.js.map +1 -0
  425. package/dist/recall-qos.d.ts +33 -0
  426. package/dist/recall-qos.js +10 -0
  427. package/dist/recall-qos.js.map +1 -0
  428. package/dist/recall-query-policy.d.ts +20 -0
  429. package/dist/recall-query-policy.js +11 -0
  430. package/dist/recall-query-policy.js.map +1 -0
  431. package/dist/recall-state.d.ts +113 -0
  432. package/dist/recall-state.js +12 -0
  433. package/dist/recall-state.js.map +1 -0
  434. package/dist/recall-tokenization.d.ts +4 -0
  435. package/dist/recall-tokenization.js +9 -0
  436. package/dist/recall-tokenization.js.map +1 -0
  437. package/dist/reconstruct.d.ts +16 -0
  438. package/dist/reconstruct.js +7 -0
  439. package/dist/reconstruct.js.map +1 -0
  440. package/dist/release-changelog.d.ts +7 -0
  441. package/dist/release-changelog.js +30 -0
  442. package/dist/release-changelog.js.map +1 -0
  443. package/dist/relevance.d.ts +18 -0
  444. package/dist/relevance.js +8 -0
  445. package/dist/relevance.js.map +1 -0
  446. package/dist/rerank.d.ts +57 -0
  447. package/dist/rerank.js +11 -0
  448. package/dist/rerank.js.map +1 -0
  449. package/dist/resolve-provider-secret.d.ts +16 -0
  450. package/dist/resolve-provider-secret.js +11 -0
  451. package/dist/resolve-provider-secret.js.map +1 -0
  452. package/dist/resume-bundles.d.ts +66 -0
  453. package/dist/resume-bundles.js +27 -0
  454. package/dist/resume-bundles.js.map +1 -0
  455. package/dist/retrieval-agents.d.ts +129 -0
  456. package/dist/retrieval-agents.js +23 -0
  457. package/dist/retrieval-agents.js.map +1 -0
  458. package/dist/retrieval.d.ts +19 -0
  459. package/dist/retrieval.js +10 -0
  460. package/dist/retrieval.js.map +1 -0
  461. package/dist/sanitize.d.ts +9 -0
  462. package/dist/sanitize.js +9 -0
  463. package/dist/sanitize.js.map +1 -0
  464. package/dist/schemas.d.ts +688 -0
  465. package/dist/schemas.js +51 -0
  466. package/dist/schemas.js.map +1 -0
  467. package/dist/sdk-compat.d.ts +21 -0
  468. package/dist/sdk-compat.js +28 -0
  469. package/dist/sdk-compat.js.map +1 -0
  470. package/dist/semantic-consolidation.d.ts +42 -0
  471. package/dist/semantic-consolidation.js +12 -0
  472. package/dist/semantic-consolidation.js.map +1 -0
  473. package/dist/semantic-rule-promotion.d.ts +28 -0
  474. package/dist/semantic-rule-promotion.js +17 -0
  475. package/dist/semantic-rule-promotion.js.map +1 -0
  476. package/dist/semantic-rule-verifier.d.ts +19 -0
  477. package/dist/semantic-rule-verifier.js +18 -0
  478. package/dist/semantic-rule-verifier.js.map +1 -0
  479. package/dist/session-integrity.d.ts +67 -0
  480. package/dist/session-integrity.js +11 -0
  481. package/dist/session-integrity.js.map +1 -0
  482. package/dist/session-observer-bands.d.ts +6 -0
  483. package/dist/session-observer-bands.js +9 -0
  484. package/dist/session-observer-bands.js.map +1 -0
  485. package/dist/session-observer-state.d.ts +40 -0
  486. package/dist/session-observer-state.js +11 -0
  487. package/dist/session-observer-state.js.map +1 -0
  488. package/dist/signal.d.ts +6 -0
  489. package/dist/signal.js +9 -0
  490. package/dist/signal.js.map +1 -0
  491. package/dist/storage.d.ts +453 -0
  492. package/dist/storage.js +24 -0
  493. package/dist/storage.js.map +1 -0
  494. package/dist/store-contract.d.ts +10 -0
  495. package/dist/store-contract.js +21 -0
  496. package/dist/store-contract.js.map +1 -0
  497. package/dist/summarizer.d.ts +35 -0
  498. package/dist/summarizer.js +17 -0
  499. package/dist/summarizer.js.map +1 -0
  500. package/dist/summary-snapshot.d.ts +8 -0
  501. package/dist/summary-snapshot.js +13 -0
  502. package/dist/summary-snapshot.js.map +1 -0
  503. package/dist/temporal-index.d.ts +139 -0
  504. package/dist/temporal-index.js +29 -0
  505. package/dist/temporal-index.js.map +1 -0
  506. package/dist/threading.d.ts +62 -0
  507. package/dist/threading.js +8 -0
  508. package/dist/threading.js.map +1 -0
  509. package/dist/tier-migration.d.ts +44 -0
  510. package/dist/tier-migration.js +7 -0
  511. package/dist/tier-migration.js.map +1 -0
  512. package/dist/tier-routing.d.ts +21 -0
  513. package/dist/tier-routing.js +10 -0
  514. package/dist/tier-routing.js.map +1 -0
  515. package/dist/tmt.d.ts +79 -0
  516. package/dist/tmt.js +29 -0
  517. package/dist/tmt.js.map +1 -0
  518. package/dist/tokens.d.ts +24 -0
  519. package/dist/tokens.js +21 -0
  520. package/dist/tokens.js.map +1 -0
  521. package/dist/topics.d.ts +29 -0
  522. package/dist/topics.js +9 -0
  523. package/dist/topics.js.map +1 -0
  524. package/dist/transcript.d.ts +171 -0
  525. package/dist/transcript.js +9 -0
  526. package/dist/transcript.js.map +1 -0
  527. package/dist/trust-zones.d.ts +170 -0
  528. package/dist/trust-zones.js +32 -0
  529. package/dist/trust-zones.js.map +1 -0
  530. package/dist/types.d.ts +1243 -0
  531. package/dist/types.js +9 -0
  532. package/dist/types.js.map +1 -0
  533. package/dist/utility-learner.d.ts +59 -0
  534. package/dist/utility-learner.js +17 -0
  535. package/dist/utility-learner.js.map +1 -0
  536. package/dist/utility-runtime.d.ts +21 -0
  537. package/dist/utility-runtime.js +16 -0
  538. package/dist/utility-runtime.js.map +1 -0
  539. package/dist/utility-telemetry.d.ts +68 -0
  540. package/dist/utility-telemetry.js +17 -0
  541. package/dist/utility-telemetry.js.map +1 -0
  542. package/dist/verified-recall.d.ts +17 -0
  543. package/dist/verified-recall.js +19 -0
  544. package/dist/verified-recall.js.map +1 -0
  545. package/dist/version-utils.d.ts +4 -0
  546. package/dist/version-utils.js +7 -0
  547. package/dist/version-utils.js.map +1 -0
  548. package/dist/work-product-ledger.d.ts +65 -0
  549. package/dist/work-product-ledger.js +18 -0
  550. package/dist/work-product-ledger.js.map +1 -0
  551. package/package.json +58 -0
@@ -0,0 +1,2168 @@
1
+ import {
2
+ ConsolidationResultSchema,
3
+ DaySummaryResultSchema,
4
+ ExtractionResultSchema,
5
+ IdentityConsolidationResultSchema,
6
+ ProactiveExtractionResultSchema,
7
+ ProactiveQuestionsResultSchema,
8
+ buildProfileConsolidationResultSchema
9
+ } from "./chunk-MWGVGUIS.js";
10
+ import {
11
+ ProfilingCollector
12
+ } from "./chunk-763GUIOU.js";
13
+ import {
14
+ ModelRegistry
15
+ } from "./chunk-ONRU4L2N.js";
16
+ import {
17
+ LocalLlmClient
18
+ } from "./chunk-MDDAA2AO.js";
19
+ import {
20
+ formatDaySummaryMemories,
21
+ loadDaySummaryPrompt
22
+ } from "./chunk-2PO5ZRKV.js";
23
+ import {
24
+ delinearize
25
+ } from "./chunk-VEWZZM3H.js";
26
+ import {
27
+ FallbackLlmClient
28
+ } from "./chunk-KT4NEUNF.js";
29
+ import {
30
+ buildChatCompletionTokenLimit,
31
+ shouldAssumeOpenAiChatCompletions
32
+ } from "./chunk-Y27UJK6V.js";
33
+ import {
34
+ extractJsonCandidates
35
+ } from "./chunk-UZB5KHKX.js";
36
+ import {
37
+ applyWorkExtractionBoundary
38
+ } from "./chunk-EEQLFRUM.js";
39
+ import {
40
+ sanitizeMemoryContent
41
+ } from "./chunk-M62O4P4T.js";
42
+ import {
43
+ log
44
+ } from "./chunk-KWBU5S5U.js";
45
+
46
+ // src/extraction.ts
47
+ import OpenAI from "openai";
48
+ var PROACTIVE_MIN_CONFIDENCE = 0.8;
49
+ function normalizeQuestion(question) {
50
+ const priority = Number.isFinite(question.priority) ? Math.max(0, Math.min(1, question.priority)) : 0.5;
51
+ return {
52
+ question: typeof question.question === "string" ? question.question.trim() : "",
53
+ context: typeof question.context === "string" ? question.context.trim() : "",
54
+ priority
55
+ };
56
+ }
57
+ function normalizeFactKey(fact) {
58
+ return `${fact.category}:${fact.content.trim().toLowerCase()}`;
59
+ }
60
+ function normalizeEntityKey(entity) {
61
+ return `${entity.type}:${entity.name.trim().toLowerCase()}`;
62
+ }
63
+ function normalizeRelationshipKey(relationship) {
64
+ return `${relationship.source.trim().toLowerCase()}=>${relationship.target.trim().toLowerCase()}:${relationship.label.trim().toLowerCase()}`;
65
+ }
66
+ function normalizeProfileUpdateKey(update) {
67
+ return update.trim().toLowerCase();
68
+ }
69
+ var ExtractionEngine = class {
70
+ constructor(config, profilerArg, localLlm, gatewayConfig, modelRegistry) {
71
+ this.config = config;
72
+ this.profiler = profilerArg ?? new ProfilingCollector({ enabled: false, storageDir: "/tmp/engram-profiler-disabled", maxTraces: 0 });
73
+ if (config.openaiApiKey) {
74
+ this.client = new OpenAI({
75
+ apiKey: config.openaiApiKey,
76
+ ...config.openaiBaseUrl ? { baseURL: config.openaiBaseUrl } : {}
77
+ });
78
+ } else {
79
+ this.client = null;
80
+ log.warn("no OpenAI API key \u2014 direct OpenAI client disabled; local and gateway fallback paths remain available");
81
+ }
82
+ this.localLlm = localLlm ?? new LocalLlmClient(config, modelRegistry);
83
+ this.fallbackLlm = new FallbackLlmClient(gatewayConfig);
84
+ this.modelRegistry = modelRegistry ?? new ModelRegistry(config.memoryDir);
85
+ if (config.modelSource === "gateway") {
86
+ log.debug(
87
+ `extraction engine: gateway model source active; extraction uses the gateway chain as its primary path` + (config.gatewayAgentId ? ` (agent: ${config.gatewayAgentId})` : " (defaults)")
88
+ );
89
+ }
90
+ }
91
+ config;
92
+ client;
93
+ localLlm;
94
+ fallbackLlm;
95
+ modelRegistry;
96
+ profiler;
97
+ /**
98
+ * Whether LLM calls should be routed through the gateway model chain
99
+ * instead of the plugin's own local/OpenAI clients.
100
+ */
101
+ get useGatewayModelSource() {
102
+ return this.config.modelSource === "gateway";
103
+ }
104
+ /**
105
+ * Whether the local LLM path should be attempted.
106
+ * Disabled when gateway model source is active (gateway chain replaces local).
107
+ */
108
+ get shouldUseLocalLlm() {
109
+ return this.config.localLlmEnabled && !this.useGatewayModelSource;
110
+ }
111
+ /**
112
+ * Whether the direct OpenAI client should be used.
113
+ * Disabled when gateway model source is active.
114
+ */
115
+ get shouldUseDirectClient() {
116
+ return !this.useGatewayModelSource && this.client !== null;
117
+ }
118
+ /**
119
+ * Build FallbackLlmOptions with the configured gateway agent ID injected.
120
+ */
121
+ withGatewayAgent(options) {
122
+ if (!this.useGatewayModelSource) return options;
123
+ const agentId = this.config.gatewayAgentId || void 0;
124
+ return agentId ? { ...options, agentId } : options;
125
+ }
126
+ emit(event) {
127
+ try {
128
+ const cb = globalThis.__openclawEngramTrace;
129
+ if (typeof cb === "function") cb(event);
130
+ } catch {
131
+ }
132
+ }
133
+ directClientUsesOpenAiTokenSemantics() {
134
+ return shouldAssumeOpenAiChatCompletions(this.config.openaiBaseUrl);
135
+ }
136
+ sanitizeExtractionResult(result, messageTimestamp) {
137
+ const ts = messageTimestamp ?? /* @__PURE__ */ new Date();
138
+ const facts = result.facts.map((fact) => {
139
+ const sanitized = sanitizeMemoryContent(fact.content);
140
+ if (!sanitized.clean) {
141
+ log.warn(`extraction fact sanitized; violations=${sanitized.violations.join(", ")}`);
142
+ }
143
+ let content = sanitized.text;
144
+ if (this.config.delinearizeEnabled) {
145
+ content = delinearize(content, result.entities, ts);
146
+ }
147
+ return { ...fact, content };
148
+ });
149
+ return { ...result, facts };
150
+ }
151
+ hasExtractionOutputs(result) {
152
+ return result.facts.length > 0 || result.entities.length > 0 || result.questions.length > 0 || result.profileUpdates.length > 0 || (result.relationships?.length ?? 0) > 0;
153
+ }
154
+ normalizeExtractionResultPayload(parsed) {
155
+ const entities = Array.isArray(parsed?.entities) ? parsed.entities.map((e) => ({
156
+ name: typeof e?.name === "string" ? e.name : "",
157
+ type: typeof e?.type === "string" ? e.type : "other",
158
+ facts: Array.isArray(e?.facts) ? e.facts.filter((f) => typeof f === "string") : [],
159
+ promptedByQuestion: typeof e?.promptedByQuestion === "string" ? e.promptedByQuestion : void 0
160
+ })).filter((e) => e.name.length > 0) : [];
161
+ const facts = Array.isArray(parsed?.facts) ? parsed.facts.map((f) => ({
162
+ category: typeof f?.category === "string" ? f.category : "fact",
163
+ content: typeof f?.content === "string" ? f.content : typeof f?.text === "string" ? f.text : "",
164
+ confidence: typeof f?.confidence === "number" ? f.confidence : 0.7,
165
+ tags: Array.isArray(f?.tags) ? f.tags.filter((t) => typeof t === "string") : [],
166
+ entityRef: typeof f?.entityRef === "string" ? f.entityRef : void 0,
167
+ promptedByQuestion: typeof f?.promptedByQuestion === "string" ? f.promptedByQuestion : void 0,
168
+ structuredAttributes: f?.structuredAttributes && typeof f.structuredAttributes === "object" && !Array.isArray(f.structuredAttributes) ? Object.fromEntries(
169
+ Object.entries(f.structuredAttributes).filter(([k, v]) => typeof k === "string" && typeof v === "string")
170
+ ) : void 0
171
+ })).filter((f) => f.content.length > 0) : [];
172
+ const questions = Array.isArray(parsed?.questions) ? parsed.questions.map((q) => {
173
+ if (typeof q === "string") return { question: q, context: "", priority: 0.5 };
174
+ return {
175
+ question: typeof q?.question === "string" ? q.question : typeof q?.text === "string" ? q.text : "",
176
+ context: typeof q?.context === "string" ? q.context : "",
177
+ priority: typeof q?.priority === "number" ? q.priority : 0.5
178
+ };
179
+ }).filter((q) => q.question.length > 0) : [];
180
+ return {
181
+ facts,
182
+ entities,
183
+ profileUpdates: Array.isArray(parsed?.profileUpdates) ? parsed.profileUpdates.filter((u) => typeof u === "string" && u.trim().length > 0) : [],
184
+ questions,
185
+ identityReflection: parsed?.identityReflection ?? void 0,
186
+ relationships: Array.isArray(parsed?.relationships) ? parsed.relationships.filter(
187
+ (r) => typeof r?.source === "string" && typeof r?.target === "string" && typeof r?.label === "string"
188
+ ).map((r) => ({
189
+ source: r.source,
190
+ target: r.target,
191
+ label: r.label,
192
+ promptedByQuestion: typeof r?.promptedByQuestion === "string" ? r.promptedByQuestion : void 0
193
+ })) : void 0
194
+ };
195
+ }
196
+ parseJsonObject(content) {
197
+ const trimmed = content?.trim();
198
+ if (!trimmed) return null;
199
+ for (const candidate of extractJsonCandidates(trimmed)) {
200
+ try {
201
+ return JSON.parse(candidate);
202
+ } catch {
203
+ }
204
+ }
205
+ return null;
206
+ }
207
+ normalizeContradictionVerificationResult(parsed) {
208
+ if (!parsed || typeof parsed.isContradiction !== "boolean") return null;
209
+ const rawWhich = parsed.whichIsNewer ?? parsed.winner;
210
+ const normalizedWhich = rawWhich === "first" || rawWhich === "existing" ? "first" : rawWhich === "second" || rawWhich === "new" ? "second" : "unclear";
211
+ return {
212
+ isContradiction: Boolean(parsed.isContradiction),
213
+ confidence: typeof parsed.confidence === "number" ? parsed.confidence : 0.5,
214
+ reasoning: typeof parsed.reasoning === "string" ? parsed.reasoning : typeof parsed.explanation === "string" ? parsed.explanation : "",
215
+ whichIsNewer: normalizedWhich
216
+ };
217
+ }
218
+ normalizeSuggestedLinksResult(parsed) {
219
+ if (!parsed || !Array.isArray(parsed.links)) {
220
+ return null;
221
+ }
222
+ const normalizedLinks = parsed.links.map((link) => {
223
+ const rawLinkType = link?.linkType ?? link?.type;
224
+ return {
225
+ targetId: typeof link?.targetId === "string" ? link.targetId : "",
226
+ linkType: rawLinkType === "follows" || rawLinkType === "references" || rawLinkType === "contradicts" || rawLinkType === "supports" || rawLinkType === "related" ? rawLinkType : "related",
227
+ strength: typeof link?.strength === "number" ? Math.max(0, Math.min(1, link.strength)) : 0.5,
228
+ reason: typeof link?.reason === "string" ? link.reason : void 0
229
+ };
230
+ }).filter((link) => link.targetId.length > 0);
231
+ return { links: normalizedLinks };
232
+ }
233
+ normalizeMemorySummaryResult(parsed) {
234
+ if (!parsed) return null;
235
+ const normalized = {
236
+ summaryText: typeof parsed.summaryText === "string" ? parsed.summaryText : typeof parsed.summary === "string" ? parsed.summary : "",
237
+ keyFacts: Array.isArray(parsed.keyFacts) ? parsed.keyFacts.filter((f) => typeof f === "string") : [],
238
+ keyEntities: Array.isArray(parsed.keyEntities) ? parsed.keyEntities.filter((e) => typeof e === "string") : Array.isArray(parsed.entities) ? parsed.entities.filter((e) => typeof e === "string") : []
239
+ };
240
+ return normalized.summaryText.length > 0 ? normalized : null;
241
+ }
242
+ normalizeDaySummaryResult(parsed) {
243
+ if (!parsed) return null;
244
+ const normalized = {
245
+ summary: typeof parsed.summary === "string" ? parsed.summary.trim() : "",
246
+ bullets: Array.isArray(parsed.bullets) ? parsed.bullets.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [],
247
+ next_actions: Array.isArray(parsed.next_actions) ? parsed.next_actions.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [],
248
+ risks_or_open_loops: Array.isArray(parsed.risks_or_open_loops) ? parsed.risks_or_open_loops.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : []
249
+ };
250
+ return normalized.summary.length > 0 ? normalized : null;
251
+ }
252
+ sanitizeConsolidationResult(result) {
253
+ const items = result.items.map((item) => {
254
+ if (!item.updatedContent) return item;
255
+ const sanitized = sanitizeMemoryContent(item.updatedContent);
256
+ if (!sanitized.clean) {
257
+ log.warn(`consolidation item sanitized (${item.existingId}); violations=${sanitized.violations.join(", ")}`);
258
+ }
259
+ return { ...item, updatedContent: sanitized.text };
260
+ });
261
+ return { ...result, items };
262
+ }
263
+ async applyProactiveQuestionPass(conversation, base) {
264
+ if (!this.config.proactiveExtractionEnabled) return base;
265
+ const maxAdditional = Math.max(0, Math.floor(this.config.maxProactiveQuestionsPerExtraction));
266
+ if (maxAdditional === 0) return base;
267
+ if (this.config.proactiveExtractionTimeoutMs === 0) return base;
268
+ if (this.config.proactiveExtractionMaxTokens === 0) return base;
269
+ try {
270
+ const proactive = await this.generateProactiveQuestions(conversation, base, maxAdditional);
271
+ if (proactive.length === 0) return base;
272
+ const proactiveAdditions = await this.answerProactiveQuestions(
273
+ conversation,
274
+ base,
275
+ proactive,
276
+ maxAdditional
277
+ );
278
+ if (!this.hasExtractionOutputs(proactiveAdditions)) return base;
279
+ return this.mergeProactiveExtractionPass(base, proactiveAdditions, maxAdditional);
280
+ } catch (err) {
281
+ log.debug(`proactive extraction question pass failed (ignored): ${err}`);
282
+ return base;
283
+ }
284
+ }
285
+ parseProactiveQuestionsFromText(content, existingQuestionKeys) {
286
+ for (const candidate of extractJsonCandidates(content)) {
287
+ try {
288
+ const parsed = JSON.parse(candidate);
289
+ if (!Array.isArray(parsed.questions)) continue;
290
+ return parsed.questions.map((q) => normalizeQuestion(q)).filter((q) => q.question.length > 0).filter((q) => !existingQuestionKeys.has(q.question.toLowerCase()));
291
+ } catch {
292
+ }
293
+ }
294
+ return [];
295
+ }
296
+ parseProactiveExtractionResultFromText(content) {
297
+ for (const candidate of extractJsonCandidates(content)) {
298
+ try {
299
+ const parsed = ProactiveExtractionResultSchema.parse(JSON.parse(candidate));
300
+ return this.normalizeExtractionResultPayload({
301
+ ...parsed,
302
+ questions: []
303
+ });
304
+ } catch {
305
+ }
306
+ }
307
+ return null;
308
+ }
309
+ async generateProactiveQuestions(conversation, base, maxAdditional) {
310
+ const existingQuestionKeys = new Set(
311
+ (base.questions ?? []).map((q) => q.question.trim().toLowerCase()).filter((q) => q.length > 0)
312
+ );
313
+ const factsPreview = base.facts.slice(0, 8).map((f) => `- (${f.category}) ${f.content}`).join("\n");
314
+ const existingQuestionsPreview = (base.questions ?? []).slice(0, 8).map((q) => `- ${q.question}`).join("\n");
315
+ const prompt = [
316
+ "You are doing a proactive second-pass memory extraction.",
317
+ `Generate up to ${maxAdditional} additional high-value follow-up questions not already covered.`,
318
+ "Return only valid JSON with this shape:",
319
+ '{"questions":[{"question":"...","context":"...","priority":0.0}]}',
320
+ "",
321
+ "Current extracted facts:",
322
+ factsPreview || "(none)",
323
+ "",
324
+ "Questions already extracted (do not repeat):",
325
+ existingQuestionsPreview || "(none)",
326
+ "",
327
+ "Conversation:",
328
+ conversation
329
+ ].join("\n");
330
+ if (this.shouldUseLocalLlm) {
331
+ try {
332
+ const localResponse = await this.localLlm.chatCompletion(
333
+ [
334
+ {
335
+ role: "system",
336
+ content: "You are a proactive memory extraction assistant. Output valid JSON only."
337
+ },
338
+ { role: "user", content: prompt }
339
+ ],
340
+ {
341
+ temperature: 0.2,
342
+ maxTokens: this.config.proactiveExtractionMaxTokens,
343
+ timeoutMs: this.config.proactiveExtractionTimeoutMs,
344
+ operation: "proactive_extraction",
345
+ priority: "background"
346
+ }
347
+ );
348
+ if (localResponse?.content) {
349
+ const localParsed = this.parseProactiveQuestionsFromText(
350
+ localResponse.content.trim(),
351
+ existingQuestionKeys
352
+ );
353
+ if (localParsed.length > 0) {
354
+ return localParsed.slice(0, maxAdditional);
355
+ }
356
+ }
357
+ if (!this.config.localLlmFallback) {
358
+ return [];
359
+ }
360
+ } catch (err) {
361
+ if (!this.config.localLlmFallback) {
362
+ throw err;
363
+ }
364
+ }
365
+ }
366
+ const fallbackResult = await this.fallbackLlm.parseWithSchema(
367
+ [
368
+ {
369
+ role: "system",
370
+ content: "Generate additional proactive memory follow-up questions. Return valid JSON only."
371
+ },
372
+ { role: "user", content: prompt }
373
+ ],
374
+ ProactiveQuestionsResultSchema,
375
+ this.withGatewayAgent({
376
+ temperature: 0.2,
377
+ maxTokens: this.config.proactiveExtractionMaxTokens,
378
+ timeoutMs: this.config.proactiveExtractionTimeoutMs
379
+ })
380
+ );
381
+ if (!fallbackResult?.questions) return [];
382
+ return fallbackResult.questions.map((q) => normalizeQuestion(q)).filter((q) => q.question.length > 0).filter((q) => !existingQuestionKeys.has(q.question.toLowerCase())).slice(0, maxAdditional);
383
+ }
384
+ async answerProactiveQuestions(conversation, base, proactiveQuestions, maxAdditional) {
385
+ const factsPreview = base.facts.slice(0, 8).map((f) => `- (${f.category}) ${f.content}`).join("\n");
386
+ const entitiesPreview = base.entities.slice(0, 8).map((entity) => `- (${entity.type}) ${entity.name}: ${entity.facts.join("; ") || "(no facts)"}`).join("\n");
387
+ const proactivePreview = proactiveQuestions.slice(0, maxAdditional).map((question, index) => `${index + 1}. ${question.question}${question.context ? `
388
+ context: ${question.context}` : ""}`).join("\n");
389
+ const prompt = [
390
+ "You are answering proactive memory follow-up questions using only the provided buffered conversation.",
391
+ `Return at most ${maxAdditional} additional high-confidence memory candidates that were omitted from the base extraction.`,
392
+ "Only include information directly supported by the conversation. Do not speculate. Do not repeat the base extraction.",
393
+ "Return only valid JSON with this shape:",
394
+ '{"facts":[{"category":"fact","content":"...","confidence":0.0,"tags":["..."],"entityRef":"optional","promptedByQuestion":"optional"}],"profileUpdates":["..."],"entities":[{"name":"...","type":"person","facts":["..."],"promptedByQuestion":"optional"}],"relationships":[{"source":"...","target":"...","label":"...","promptedByQuestion":"optional"}]}',
395
+ "",
396
+ "Base extracted facts (do not repeat):",
397
+ factsPreview || "(none)",
398
+ "",
399
+ "Base extracted entities (do not repeat):",
400
+ entitiesPreview || "(none)",
401
+ "",
402
+ "Answer these follow-up questions from the same conversation only:",
403
+ proactivePreview || "(none)",
404
+ "",
405
+ "Conversation:",
406
+ conversation
407
+ ].join("\n");
408
+ if (this.shouldUseLocalLlm) {
409
+ try {
410
+ const localResponse = await this.localLlm.chatCompletion(
411
+ [
412
+ {
413
+ role: "system",
414
+ content: "You are a proactive memory extraction assistant. Output valid JSON only."
415
+ },
416
+ { role: "user", content: prompt }
417
+ ],
418
+ {
419
+ temperature: 0.2,
420
+ maxTokens: this.config.proactiveExtractionMaxTokens,
421
+ timeoutMs: this.config.proactiveExtractionTimeoutMs,
422
+ operation: "proactive_extraction",
423
+ priority: "background"
424
+ }
425
+ );
426
+ if (localResponse?.content) {
427
+ const parsed = this.parseProactiveExtractionResultFromText(localResponse.content.trim());
428
+ if (parsed) {
429
+ return this.sanitizeExtractionResult(parsed);
430
+ }
431
+ }
432
+ if (!this.config.localLlmFallback) {
433
+ return { facts: [], profileUpdates: [], entities: [], questions: [] };
434
+ }
435
+ } catch (err) {
436
+ if (!this.config.localLlmFallback) {
437
+ throw err;
438
+ }
439
+ }
440
+ }
441
+ const fallbackResult = await this.fallbackLlm.parseWithSchema(
442
+ [
443
+ {
444
+ role: "system",
445
+ content: "Answer proactive memory follow-up questions from the provided conversation only. Return valid JSON only."
446
+ },
447
+ { role: "user", content: prompt }
448
+ ],
449
+ ProactiveExtractionResultSchema,
450
+ this.withGatewayAgent({
451
+ temperature: 0.2,
452
+ maxTokens: this.config.proactiveExtractionMaxTokens,
453
+ timeoutMs: this.config.proactiveExtractionTimeoutMs
454
+ })
455
+ );
456
+ if (!fallbackResult) {
457
+ return { facts: [], profileUpdates: [], entities: [], questions: [] };
458
+ }
459
+ return this.sanitizeExtractionResult(
460
+ this.normalizeExtractionResultPayload({
461
+ ...fallbackResult,
462
+ questions: []
463
+ })
464
+ );
465
+ }
466
+ mergeProactiveExtractionPass(base, proactive, maxAdditional) {
467
+ const allowlist = this.config.proactiveExtractionCategoryAllowlist;
468
+ let remainingBudget = Math.max(0, Math.floor(maxAdditional));
469
+ const mergedFacts = [...base.facts];
470
+ const seenFacts = new Set(base.facts.map((fact) => normalizeFactKey(fact)));
471
+ for (const fact of proactive.facts) {
472
+ if (remainingBudget <= 0) break;
473
+ if (fact.confidence < PROACTIVE_MIN_CONFIDENCE) continue;
474
+ if (allowlist && !allowlist.includes(fact.category)) continue;
475
+ const key = normalizeFactKey(fact);
476
+ if (seenFacts.has(key)) continue;
477
+ seenFacts.add(key);
478
+ mergedFacts.push({ ...fact, source: "proactive" });
479
+ remainingBudget -= 1;
480
+ }
481
+ const mergedEntities = base.entities.map((entity) => ({
482
+ ...entity,
483
+ facts: [...entity.facts]
484
+ }));
485
+ const entityIndex = new Map(mergedEntities.map((entity, index) => [normalizeEntityKey(entity), index]));
486
+ for (const entity of proactive.entities) {
487
+ if (remainingBudget <= 0) break;
488
+ const key = normalizeEntityKey(entity);
489
+ const existingIndex = entityIndex.get(key);
490
+ if (typeof existingIndex === "number") {
491
+ const existing = mergedEntities[existingIndex];
492
+ const nextFacts = new Set(existing.facts.map((fact) => fact.trim()));
493
+ let changed = false;
494
+ for (const fact of entity.facts) {
495
+ const trimmed = fact.trim();
496
+ if (!trimmed || nextFacts.has(trimmed)) continue;
497
+ nextFacts.add(trimmed);
498
+ changed = true;
499
+ }
500
+ if (changed) {
501
+ mergedEntities[existingIndex] = {
502
+ ...existing,
503
+ facts: Array.from(nextFacts),
504
+ source: "proactive",
505
+ promptedByQuestion: existing.promptedByQuestion ?? entity.promptedByQuestion
506
+ };
507
+ remainingBudget -= 1;
508
+ }
509
+ continue;
510
+ }
511
+ mergedEntities.push({ ...entity, source: "proactive" });
512
+ entityIndex.set(key, mergedEntities.length - 1);
513
+ remainingBudget -= 1;
514
+ }
515
+ const mergedProfileUpdates = [...base.profileUpdates];
516
+ const seenProfileUpdates = new Set(base.profileUpdates.map((update) => normalizeProfileUpdateKey(update)));
517
+ for (const update of proactive.profileUpdates) {
518
+ if (remainingBudget <= 0) break;
519
+ const key = normalizeProfileUpdateKey(update);
520
+ if (!key || seenProfileUpdates.has(key)) continue;
521
+ seenProfileUpdates.add(key);
522
+ mergedProfileUpdates.push(update.trim());
523
+ remainingBudget -= 1;
524
+ }
525
+ const mergedRelationships = [...base.relationships ?? []];
526
+ const seenRelationships = new Set(mergedRelationships.map((relationship) => normalizeRelationshipKey(relationship)));
527
+ for (const relationship of proactive.relationships ?? []) {
528
+ if (remainingBudget <= 0) break;
529
+ const key = normalizeRelationshipKey(relationship);
530
+ if (seenRelationships.has(key)) continue;
531
+ seenRelationships.add(key);
532
+ mergedRelationships.push({ ...relationship, extractionSource: "proactive" });
533
+ remainingBudget -= 1;
534
+ }
535
+ return {
536
+ ...base,
537
+ facts: mergedFacts,
538
+ entities: mergedEntities,
539
+ profileUpdates: mergedProfileUpdates,
540
+ relationships: mergedRelationships
541
+ };
542
+ }
543
+ async parseWithGatewayFallback(traceId, operation, startedAtMs, schema, messages, options = {}) {
544
+ const detailed = await this.fallbackLlm.parseWithSchemaDetailed(messages, schema, this.withGatewayAgent(options));
545
+ if (detailed?.result) {
546
+ const durationMs = Date.now() - startedAtMs;
547
+ this.emit({
548
+ kind: "llm_end",
549
+ traceId,
550
+ model: detailed.modelUsed,
551
+ operation,
552
+ durationMs,
553
+ output: JSON.stringify(detailed.result).slice(0, 2e3)
554
+ });
555
+ return detailed.result;
556
+ }
557
+ return null;
558
+ }
559
+ async extract(turns, existingEntities) {
560
+ const substantiveTurns = turns.filter((t) => t.content.trim().length > 0);
561
+ if (substantiveTurns.length === 0) {
562
+ log.debug("extraction skipped \u2014 no substantive turns in buffer");
563
+ return { facts: [], profileUpdates: [], entities: [], questions: [] };
564
+ }
565
+ const boundedTurns = substantiveTurns.map((turn) => ({
566
+ ...turn,
567
+ content: turn.role === "assistant" ? applyWorkExtractionBoundary(turn.content) : turn.content
568
+ })).filter((turn) => turn.content.trim().length > 0);
569
+ const conversation = boundedTurns.map((t) => `[${t.role}] ${t.content}`).join("\n\n");
570
+ if (conversation.trim().length === 0) {
571
+ log.debug("extraction skipped \u2014 conversation only contained non-memory work-layer context");
572
+ return { facts: [], profileUpdates: [], entities: [], questions: [] };
573
+ }
574
+ const lastTurnTs = boundedTurns.length > 0 ? new Date(boundedTurns[boundedTurns.length - 1].timestamp) : void 0;
575
+ const messageTimestamp = lastTurnTs && !isNaN(lastTurnTs.getTime()) ? lastTurnTs : void 0;
576
+ const traceId = crypto.randomUUID();
577
+ const emittedDirectStart = !!(this.shouldUseDirectClient || this.shouldUseLocalLlm);
578
+ if (emittedDirectStart) {
579
+ this.emit({ kind: "llm_start", traceId, model: this.config.model, operation: "extraction", input: conversation });
580
+ }
581
+ let closedDirectTrace = false;
582
+ const startTime = Date.now();
583
+ const extractionTraceId = this.profiler.startTrace("extraction", void 0, {
584
+ model: this.config.model,
585
+ localLlm: this.config.localLlmEnabled
586
+ });
587
+ this.profiler.startSpan("total", extractionTraceId);
588
+ try {
589
+ if (this.shouldUseLocalLlm) {
590
+ this.profiler.startSpan("local-llm", extractionTraceId);
591
+ try {
592
+ const localResult = await this.extractWithLocalLlm(conversation, existingEntities);
593
+ if (localResult) {
594
+ const durationMs = Date.now() - startTime;
595
+ this.profiler.endSpan("local-llm", extractionTraceId);
596
+ this.emit({ kind: "llm_end", traceId, model: this.config.localLlmModel, operation: "extraction", durationMs });
597
+ log.debug(`extraction: used local LLM \u2014 ${localResult.facts.length} facts, ${localResult.entities.length} entities`);
598
+ const sanitized = this.sanitizeExtractionResult(localResult, messageTimestamp);
599
+ return await this.applyProactiveQuestionPass(conversation, sanitized);
600
+ }
601
+ if (!this.config.localLlmFallback) {
602
+ log.warn("extraction: local LLM failed and fallback disabled");
603
+ return { facts: [], profileUpdates: [], entities: [], questions: [] };
604
+ }
605
+ log.info("extraction: local LLM unavailable, falling back to gateway default AI");
606
+ } catch (err) {
607
+ if (!this.config.localLlmFallback) {
608
+ log.warn("extraction: local LLM error and fallback disabled:", err);
609
+ return { facts: [], profileUpdates: [], entities: [], questions: [] };
610
+ }
611
+ log.info("extraction: local LLM error, falling back to gateway default AI:", err);
612
+ } finally {
613
+ try {
614
+ this.profiler.endSpan("local-llm", extractionTraceId);
615
+ } catch {
616
+ }
617
+ }
618
+ }
619
+ if (this.shouldUseDirectClient) {
620
+ this.profiler.startSpan("direct-client", extractionTraceId);
621
+ try {
622
+ const directResult = await this.extractWithDirectClient(conversation, existingEntities);
623
+ if (directResult) {
624
+ const durationMs = Date.now() - startTime;
625
+ this.profiler.endSpan("direct-client", extractionTraceId);
626
+ this.emit({ kind: "llm_end", traceId, model: this.config.model, operation: "extraction", durationMs });
627
+ log.debug(`extraction: used direct client (${this.config.model}) \u2014 ${directResult.facts.length} facts, ${directResult.entities.length} entities`);
628
+ const sanitized = this.sanitizeExtractionResult(directResult, messageTimestamp);
629
+ return await this.applyProactiveQuestionPass(conversation, sanitized);
630
+ }
631
+ try {
632
+ this.emit({
633
+ kind: "llm_error",
634
+ traceId,
635
+ model: this.config.model,
636
+ operation: "extraction",
637
+ durationMs: Date.now() - startTime,
638
+ error: "direct client returned no result"
639
+ });
640
+ } catch {
641
+ }
642
+ closedDirectTrace = true;
643
+ log.info("extraction: direct client returned no result, falling back to gateway AI");
644
+ } catch (err) {
645
+ try {
646
+ this.emit({
647
+ kind: "llm_error",
648
+ traceId,
649
+ model: this.config.model,
650
+ operation: "extraction",
651
+ durationMs: Date.now() - startTime,
652
+ error: String(err)
653
+ });
654
+ } catch {
655
+ }
656
+ closedDirectTrace = true;
657
+ log.info("extraction: direct client failed, falling back to gateway AI:", err);
658
+ } finally {
659
+ try {
660
+ this.profiler.endSpan("direct-client", extractionTraceId);
661
+ } catch {
662
+ }
663
+ }
664
+ }
665
+ if (emittedDirectStart && !closedDirectTrace) {
666
+ try {
667
+ this.emit({
668
+ kind: "llm_error",
669
+ traceId,
670
+ model: this.config.model,
671
+ operation: "extraction",
672
+ durationMs: Date.now() - startTime,
673
+ error: "local LLM failed, handing off to gateway fallback"
674
+ });
675
+ } catch {
676
+ }
677
+ }
678
+ const fallbackTraceId = crypto.randomUUID();
679
+ const fallbackStartTime = Date.now();
680
+ if (this.useGatewayModelSource) {
681
+ log.debug(
682
+ `extraction: using gateway model chain as primary path` + (this.config.gatewayAgentId ? ` (agent: ${this.config.gatewayAgentId})` : " (defaults)")
683
+ );
684
+ } else {
685
+ log.info("extraction: falling back to gateway default AI");
686
+ }
687
+ this.profiler.startSpan("gateway-fallback", extractionTraceId);
688
+ try {
689
+ const messages = [
690
+ { role: "system", content: this.buildExtractionInstructions(existingEntities) },
691
+ { role: "user", content: conversation }
692
+ ];
693
+ this.emit({ kind: "llm_start", traceId: fallbackTraceId, model: "fallback", operation: "extraction", input: conversation });
694
+ const detailed = await this.fallbackLlm.parseWithSchemaDetailed(
695
+ messages,
696
+ ExtractionResultSchema,
697
+ this.withGatewayAgent({ temperature: 0.3, maxTokens: 4096, timeoutMs: 3e4 })
698
+ );
699
+ const fallbackDurationMs = Date.now() - fallbackStartTime;
700
+ if (detailed?.result && Array.isArray(detailed.result.facts)) {
701
+ const result = detailed.result;
702
+ this.emit({
703
+ kind: "llm_end",
704
+ traceId: fallbackTraceId,
705
+ model: detailed.modelUsed,
706
+ operation: "extraction",
707
+ durationMs: fallbackDurationMs,
708
+ output: JSON.stringify(result).slice(0, 2e3)
709
+ });
710
+ log.debug(
711
+ `extracted ${result.facts.length} facts, ${result.entities.length} entities, ${(result.questions ?? []).length} questions via fallback (${detailed.modelUsed})`
712
+ );
713
+ const sanitized = this.sanitizeExtractionResult({
714
+ ...result,
715
+ questions: result.questions ?? [],
716
+ identityReflection: result.identityReflection ?? void 0
717
+ }, messageTimestamp);
718
+ return await this.applyProactiveQuestionPass(conversation, sanitized);
719
+ }
720
+ this.emit({
721
+ kind: "llm_error",
722
+ traceId: fallbackTraceId,
723
+ model: "fallback",
724
+ operation: "extraction",
725
+ durationMs: fallbackDurationMs,
726
+ error: "fallback returned no parsed output"
727
+ });
728
+ log.warn("extraction fallback returned no parsed output");
729
+ return { facts: [], profileUpdates: [], entities: [], questions: [] };
730
+ } catch (err) {
731
+ this.emit({
732
+ kind: "llm_error",
733
+ traceId: fallbackTraceId,
734
+ model: "fallback",
735
+ operation: "extraction",
736
+ durationMs: Date.now() - fallbackStartTime,
737
+ error: String(err)
738
+ });
739
+ log.error("extraction fallback failed", err);
740
+ return { facts: [], profileUpdates: [], entities: [], questions: [] };
741
+ } finally {
742
+ try {
743
+ this.profiler.endSpan("gateway-fallback", extractionTraceId);
744
+ } catch {
745
+ }
746
+ }
747
+ } finally {
748
+ this.profiler.endSpan("total", extractionTraceId);
749
+ this.profiler.endTrace(extractionTraceId);
750
+ }
751
+ }
752
+ /**
753
+ * Extract memories using local LLM with JSON mode.
754
+ * Uses a minimal prompt to fit within local model context limits (typically 4k-8k).
755
+ */
756
+ async extractWithLocalLlm(conversation, existingEntities) {
757
+ log.debug(
758
+ `extractWithLocalLlm: starting extraction, localLlmEnabled=${this.shouldUseLocalLlm}, model=${this.config.localLlmModel}`
759
+ );
760
+ const contextSizes = this.modelRegistry.calculateContextSizes(
761
+ this.config.localLlmModel,
762
+ this.config.localLlmMaxContext
763
+ );
764
+ log.debug(`Model context: ${contextSizes.description}`);
765
+ const maxConversationChars = contextSizes.maxInputChars;
766
+ const truncatedConversation = conversation.length > maxConversationChars ? conversation.slice(0, maxConversationChars) + "\n\n[truncated]" : conversation;
767
+ const localPrompt = `You are a memory extraction system. Extract durable, reusable memories from this conversation.
768
+
769
+ Memory categories \u2014 use the MOST SPECIFIC category that fits:
770
+ - fact: Objective information about the world
771
+ - preference: User likes, dislikes, or stylistic choices
772
+ - correction: User correcting a mistake (highest priority)
773
+ - entity: People, projects, tools, companies (use canonical hyphenated names like "my-project")
774
+ - decision: Choices made with rationale
775
+ - relationship: How entities relate (e.g., "Alice manages Bob")
776
+ - principle: Durable rules or operating beliefs (e.g., "never use X API")
777
+ - commitment: Promises, obligations, deadlines
778
+ - moment: Emotionally significant events
779
+ - skill: Demonstrated capabilities
780
+
781
+ IMPORTANT: Do NOT label everything as "fact". Use "decision" for architectural choices, "commitment" for deadlines/promises, "principle" for reusable rules, "correction" for when the user rejects a suggestion, etc.
782
+
783
+ === DO NOT EXTRACT (negative examples) ===
784
+ These are operational noise - skip them:
785
+ - "The user has a cron job that runs every 30 minutes" (scheduled task descriptions)
786
+ - "The user encountered error XYZ at 3:45 PM" (temporary error states)
787
+ - "The file is located at /path/to/project/file" (transient file paths)
788
+ - "The system is using 4GB of memory" (current resource usage)
789
+ - "The user ran the 'git status' command" (individual command executions)
790
+ - "The conversation took place on Tuesday" (session metadata)
791
+ - "The agent read the file at /path/to/file.txt" (agent's own actions)
792
+ - "The user's OpenClaw automation posts to #channel on failures" (automation behavior descriptions)
793
+ - "The user stores state in /path/to/state.json" (implementation details)
794
+ - "The X-watch automation has been stalled for 58 hours" (system status updates)
795
+ - "The user processed 5 batch files and extracted insights" (processing summaries)
796
+ - "The user has a cron job that runs a Checkpoint Loop every 2 hours" (automation schedules)
797
+ - "The user runs a Morning Surprise cron job daily at 7:30 AM" (automation schedules)
798
+ - "The user runs an X Bookmarks \u2192 Insights pipeline hourly at :13" (automation schedules)
799
+ - "The user's system mines X/Twitter mentions for ideas every 10a/2p/6p" (automation schedules)
800
+ - "The user runs a Health Insights cron job weekday mornings" (automation schedules)
801
+ - "The system monitors the showcase page every 12 hours" (system monitoring configurations)
802
+
803
+ === DO EXTRACT (positive examples) ===
804
+ These are durable insights - capture them:
805
+ - "The user prefers dark mode interfaces and finds light mode uncomfortable" (preference)
806
+ - "The user works primarily with TypeScript and avoids Python for frontend code" (long-term fact)
807
+ - "The user's side project 'alpha-trader' uses a custom algorithm for arbitrage" (entity + detail)
808
+ - "The user corrected that PostgreSQL 15 is required, not version 14" (correction)
809
+ - "The user never commits code without running tests first" (principle)
810
+ - "The user has a meeting with the design team every Friday at 2pm" (commitment)
811
+
812
+ === Rules ===
813
+ - Extract only NEW information worth remembering across sessions
814
+ - Skip transient details (file paths, current errors, temporary states, agent actions)
815
+ - Confidence: Explicit (0.95-1.0), Implied (0.70-0.94), Inferred (0.40-0.69), Speculative (0.00-0.39)
816
+ - Corrections get highest confidence (0.95+)
817
+ - Each fact should be standalone and self-contained
818
+ - CRITICAL: Use canonical hyphenated entity names (e.g., "jane-doe" not "janedoe")
819
+ - CRITICAL: NEVER extract the same fact twice - check for duplicates before adding to facts array
820
+ - CRITICAL: NEVER extract cron job schedules, automation configurations, or system monitoring details (these are operational noise)
821
+ - If uncertain about relevance, prefer NOT extracting
822
+
823
+ === Structured Attributes ===
824
+ When a fact contains measurable, categorical, or precisely valued data, add a "structuredAttributes" object with key-value string pairs. This captures exact values for precise retrieval later.
825
+ Examples of when to add structuredAttributes:
826
+ - Product details: {"price": "29.99", "brand": "Sony", "color": "black", "rating": "4.5"}
827
+ - Person details: {"age": "32", "occupation": "engineer", "city": "Austin"}
828
+ - Events with dates: {"date": "2024-03-15", "location": "San Francisco"}
829
+ - Decisions: {"chosen": "PostgreSQL", "rejected": "MongoDB", "reason": "ACID compliance"}
830
+ - Quantities/measurements: {"budget": "50000", "team_size": "5", "deadline": "2024-06-01"}
831
+ Only add structuredAttributes when there are concrete values. Skip for abstract or narrative facts.
832
+
833
+ Also generate:
834
+ 1. 1-3 genuine questions you're curious about from this conversation
835
+ 2. Profile updates about user patterns/behaviors (if any)
836
+ 3. Relationships between entities (max 5). Use normalized names like "person-jane-doe", "company-acme-corp".
837
+
838
+ Output JSON:
839
+ {
840
+ "facts": [{"category": "decision", "content": "Chose PostgreSQL over MongoDB for the user service", "importance": 8, "confidence": 0.9, "structuredAttributes": {"chosen": "PostgreSQL", "rejected": "MongoDB"}}, {"category": "commitment", "content": "Must ship v2.0 API by end of March", "importance": 10, "confidence": 1.0, "structuredAttributes": {"deadline": "end of March", "deliverable": "v2.0 API"}}, {"category": "fact", "content": "The store backend uses Redis for session caching", "importance": 6, "confidence": 0.95, "entityRef": "project-acme-store"}, {"category": "principle", "content": "Always run migrations in a transaction to avoid partial schema updates", "importance": 8, "confidence": 0.9}],
841
+ "entities": [{"name": "person-jane-doe", "type": "person", "facts": ["Works at Acme Corp", "Prefers Python over JavaScript"]}, {"name": "project-acme-store", "type": "project", "facts": ["Built with Next.js", "Deployed on Vercel"]}],
842
+ "profileUpdates": ["User prefers dark mode in all editors"],
843
+ "questions": [{"question": "Which cloud provider hosts the staging environment?", "context": "Came up during deployment discussion", "priority": 0.5}],
844
+ "relationships": [{"source": "person-jane-doe", "target": "company-acme-corp", "label": "works at"}]
845
+ }
846
+
847
+ Conversation:
848
+ ${truncatedConversation}`;
849
+ log.debug(
850
+ `extractWithLocalLlm: calling localLlm.chatCompletion with prompt length ${localPrompt.length}...`
851
+ );
852
+ const response = await this.localLlm.chatCompletion(
853
+ [
854
+ { role: "system", content: "You are a memory extraction system. Output valid JSON only." },
855
+ { role: "user", content: localPrompt }
856
+ ],
857
+ {
858
+ temperature: 0.1,
859
+ maxTokens: contextSizes.maxOutputTokens,
860
+ operation: "extraction",
861
+ priority: "background"
862
+ }
863
+ );
864
+ if (!response?.content) {
865
+ log.debug("extractWithLocalLlm: chatCompletion returned null or empty content");
866
+ return null;
867
+ }
868
+ const content = response.content.trim();
869
+ log.debug(`extractWithLocalLlm: got response content, length=${content.length}`);
870
+ try {
871
+ for (const candidate of extractJsonCandidates(content)) {
872
+ try {
873
+ log.debug(`extractWithLocalLlm: attempting JSON parse, candidate length=${candidate.length}`);
874
+ const parsed = JSON.parse(candidate);
875
+ const result = this.normalizeExtractionResultPayload(parsed);
876
+ log.debug(
877
+ `extractWithLocalLlm: successfully parsed response, facts=${result.facts.length}, entities=${result.entities.length}, profileUpdates=${result.profileUpdates.length}, questions=${result.questions.length}`
878
+ );
879
+ return result;
880
+ } catch {
881
+ }
882
+ }
883
+ return null;
884
+ } catch (err) {
885
+ log.debug("extractWithLocalLlm: JSON parse failed, attempting partial extraction...");
886
+ const partial = this.extractPartialFacts(content);
887
+ if (partial.facts.length > 0 || partial.entities.length > 0) {
888
+ log.debug(
889
+ `extractWithLocalLlm: extracted ${partial.facts.length} partial facts from truncated JSON`
890
+ );
891
+ return partial;
892
+ }
893
+ const errMsg = err instanceof Error ? err.message : String(err);
894
+ log.debug(`extractWithLocalLlm: JSON parse error: ${errMsg}`);
895
+ return null;
896
+ }
897
+ }
898
+ /**
899
+ * Extract memories using direct OpenAI-compatible client (Chat Completions API).
900
+ * Works with Scryr, OpenRouter, and other OpenAI-compatible endpoints.
901
+ */
902
+ async extractWithDirectClient(conversation, existingEntities) {
903
+ if (!this.client) return null;
904
+ const tokenParams = buildChatCompletionTokenLimit(this.config.model, this.config.extractionMaxOutputTokens, {
905
+ assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
906
+ });
907
+ log.debug(`extractWithDirectClient: calling model=${this.config.model} tokenParams=${JSON.stringify(tokenParams)}`);
908
+ const response = await this.client.chat.completions.create({
909
+ model: this.config.model,
910
+ messages: [
911
+ {
912
+ role: "system",
913
+ content: this.buildExtractionInstructions(existingEntities) + `
914
+
915
+ Respond with valid JSON matching this schema:
916
+ {
917
+ "facts": [{"category": "decision", "content": "Chose React over Vue for the dashboard rewrite", "importance": 8, "confidence": 0.9, "tags": ["frontend"], "structuredAttributes": {"chosen": "React", "rejected": "Vue"}}, {"category": "fact", "content": "The API gateway uses rate limiting at 1000 req/min", "importance": 6, "confidence": 0.95, "tags": ["infra"], "entityRef": "project-dashboard", "structuredAttributes": {"rate_limit": "1000 req/min"}}],
918
+ "entities": [{"name": "person-sarah-chen", "type": "person", "facts": ["Leads the backend team", "Joined from Google in 2024"]}, {"name": "project-dashboard", "type": "project", "facts": ["React-based admin panel", "Deployed on AWS ECS"]}],
919
+ "profileUpdates": ["User prefers TypeScript over plain JavaScript"],
920
+ "questions": [{"question": "What database does the analytics service use?", "context": "Came up during discussion of migration plan", "priority": 0.5}],
921
+ "relationships": [{"source": "person-sarah-chen", "target": "project-dashboard", "label": "leads development of"}]
922
+ }`
923
+ },
924
+ { role: "user", content: conversation }
925
+ ],
926
+ ...tokenParams
927
+ });
928
+ const content = response.choices?.[0]?.message?.content?.trim();
929
+ if (!content) {
930
+ log.info(`extractWithDirectClient: empty response \u2014 choices=${JSON.stringify(response.choices?.length ?? 0)} finishReason=${response.choices?.[0]?.finish_reason ?? "n/a"}`);
931
+ return null;
932
+ }
933
+ log.info(
934
+ `extractWithDirectClient: got response, length=${content.length}`
935
+ );
936
+ for (const candidate of extractJsonCandidates(content)) {
937
+ try {
938
+ const parsed = JSON.parse(candidate);
939
+ return this.normalizeExtractionResultPayload(parsed);
940
+ } catch {
941
+ }
942
+ }
943
+ log.info(`extractWithDirectClient: failed to parse JSON from response (first 200 chars: ${content.slice(0, 200)})`);
944
+ return null;
945
+ }
946
+ /**
947
+ * Extract partial facts from truncated JSON responses.
948
+ * Local LLMs sometimes hit token limits mid-JSON. This tries to salvage valid facts.
949
+ */
950
+ extractPartialFacts(jsonStr) {
951
+ const allowedCategories = /* @__PURE__ */ new Set([
952
+ "fact",
953
+ "preference",
954
+ "correction",
955
+ "entity",
956
+ "decision",
957
+ "relationship",
958
+ "principle",
959
+ "commitment",
960
+ "moment",
961
+ "skill"
962
+ ]);
963
+ const allowedEntityTypes = /* @__PURE__ */ new Set([
964
+ "person",
965
+ "project",
966
+ "tool",
967
+ "company",
968
+ "place",
969
+ "other"
970
+ ]);
971
+ const facts = [];
972
+ const entities = [];
973
+ try {
974
+ const factRegex = /\{\s*"category"\s*:\s*"([^"]+)"\s*,\s*"content"\s*:\s*"([^"]+)"\s*,\s*"confidence"\s*:\s*([0-9.]+)/g;
975
+ let match;
976
+ while ((match = factRegex.exec(jsonStr)) !== null) {
977
+ const rawCat = match[1];
978
+ const category = allowedCategories.has(rawCat) ? rawCat : "fact";
979
+ facts.push({
980
+ category,
981
+ content: match[2].replace(/\\n/g, "\n").replace(/\\"/g, '"'),
982
+ confidence: parseFloat(match[3]),
983
+ tags: []
984
+ });
985
+ }
986
+ const entityRegex = /\{\s*"name"\s*:\s*"([^"]+)"\s*,\s*"type"\s*:\s*"([^"]+)"/g;
987
+ while ((match = entityRegex.exec(jsonStr)) !== null) {
988
+ const rawType = match[2];
989
+ const type = allowedEntityTypes.has(rawType) ? rawType : "other";
990
+ entities.push({
991
+ name: match[1],
992
+ type,
993
+ facts: []
994
+ });
995
+ }
996
+ } catch {
997
+ }
998
+ return { facts, entities, profileUpdates: [], questions: [] };
999
+ }
1000
+ /**
1001
+ * Build extraction instructions shared between local and cloud LLM.
1002
+ */
1003
+ buildExtractionInstructions(existingEntities) {
1004
+ return `You are a memory extraction system. Analyze the following conversation and extract durable, reusable memories.
1005
+
1006
+ Memory categories:
1007
+ - fact: Objective information about the world
1008
+ - preference: User likes, dislikes, or stylistic choices
1009
+ - correction: User correcting a mistake or misconception (highest priority)
1010
+ - entity: Information about a specific person, project, tool, or company
1011
+ - decision: A choice that was made with rationale
1012
+ - relationship: How two entities relate to each other (e.g., "Alice is Bob's manager", "Acme Corp uses Shopify")
1013
+ - principle: Durable rules, values, or operating beliefs (e.g., "never use Chat Completions API")
1014
+ - commitment: Promises, obligations, or deadlines (e.g., "deploy by Friday", "call accountant Monday")
1015
+ - moment: Emotionally significant events or milestones (e.g., "first successful deployment of engram")
1016
+ - skill: Capabilities the user or agent has demonstrated (e.g., "user is proficient with Kubernetes")${this.config.causalRuleExtractionEnabled ? `
1017
+ - rule: Causal rules discovered through experience (format: "IF <condition> THEN <action/outcome>", e.g., "IF Shopify API returns 401 THEN the admin token is missing read_products scope")` : ""}
1018
+
1019
+ Rules:
1020
+ - Only extract genuinely NEW information worth remembering across sessions
1021
+ - Skip transient task details (file paths being edited, current errors, etc.)
1022
+ - Priority: corrections > principles${this.config.causalRuleExtractionEnabled ? " > rules" : ""} > preferences > commitments > decisions > relationships > entities > moments > skills > facts
1023
+ - Corrections (user saying "actually, don't do X" or "I prefer Y") get highest confidence
1024
+ - Each fact should be a standalone, self-contained statement
1025
+ - Entity references should use normalized names (lowercase, hyphenated: "jane-doe", "acme-corp")
1026
+ - CRITICAL: Entity names must be CANONICAL. Always use the hyphenated multi-word form: "acme-corp" NOT "acmecorp" or "acme". "jane-doe" NOT "janedoe" or "jane". If unsure, prefer the most specific full name.
1027
+ - Avoid creating entities typed as "other" when a more specific type fits (company, project, tool, person, place)
1028
+ - Tags should be concise and reusable (e.g., "coding-style", "personal", "tools")
1029
+ - When a fact contains measurable, categorical, or precisely valued data, include a "structuredAttributes" field with key-value string pairs (e.g., {"price": "29.99", "brand": "Sony"}, {"date": "2024-03-15", "location": "SF"}, {"chosen": "PostgreSQL", "rejected": "MongoDB"}). Only for concrete values, not narrative content.
1030
+ - Set confidence using these tiers:
1031
+ * Explicit (0.95-1.0): Direct user statements \u2014 "I prefer X", "my name is Y"
1032
+ * Implied (0.70-0.94): Strong contextual inference \u2014 user consistently does X, clear from conversation flow
1033
+ * Inferred (0.40-0.69): Pattern recognition \u2014 reasonable guess from limited evidence
1034
+ * Speculative (0.00-0.39): Tentative hypothesis \u2014 weak signal, needs future confirmation. Speculative memories auto-expire after 30 days if not confirmed.
1035
+ - For commitments: include any deadline or timeframe mentioned
1036
+
1037
+ Entity creation rules (STRICT):
1038
+ - Only create entities for DURABLE things: real people, companies, products, tools, ongoing projects
1039
+ - NEVER create entities for transient items: individual PRs, branches, Jira tickets, meetings, agent task IDs, log files, database tables, cron job runs, sessions
1040
+ - When you learn something about a transient item (e.g., PR #58 fixed a bug), store it as a FACT with an entityRef to the parent project \u2014 do NOT create an entity for the PR itself
1041
+ - Prefer attaching facts to broad parent entities rather than creating sub-entities. E.g., "acme-store uses Algolia for search" is a fact on entity "acme-store", NOT a new entity "acme-store-algolia-connector"
1042
+ - The entity list should be SHORT \u2014 think "things that would have their own Wikipedia page" not "things mentioned in passing"
1043
+
1044
+ ${existingEntities && existingEntities.length > 0 ? `
1045
+ KNOWN ENTITIES (use these exact names when referencing existing things):
1046
+ ${existingEntities.join(", ")}
1047
+
1048
+ When you see something that matches a known entity, use THAT name exactly. Only create a NEW entity if nothing in this list represents it.
1049
+ ` : ""}
1050
+ Also extract relationships between entities mentioned in the conversation.
1051
+ - Format: {source: "entity-name", target: "entity-name", label: "relationship description"}
1052
+ - Max 5 relationships per extraction
1053
+ - Only include clear, durable relationships (e.g., "works at", "created", "manages", "uses")
1054
+ - Use normalized entity names (e.g., "person-jane-doe", "company-acme-corp")
1055
+
1056
+ Also generate 1-3 genuine questions you're curious about based on this conversation. These should be things you'd actually want answers to in future sessions \u2014 not prompts, but real curiosity.
1057
+
1058
+ Finally, write a brief identity reflection about the AGENT who had this conversation (not about you, the extraction system). Based on what the agent said and did in the conversation:
1059
+ - What communication patterns did the agent show? (e.g., proactive vs reactive, verbose vs concise)
1060
+ - Did the agent handle the user's needs well or miss something?
1061
+ - What behavioral tendencies are visible? (e.g., cautious, creative, thorough, impatient)
1062
+ - What could the agent improve next time?
1063
+ Do NOT write about the extraction process itself. Do NOT say things like "I extracted durable facts" \u2014 that's about YOUR job, not the agent's behavior.`;
1064
+ }
1065
+ async consolidate(newMemories, existingMemories, currentProfile) {
1066
+ const newList = newMemories.map(
1067
+ (m) => `[${m.frontmatter.id}] (${m.frontmatter.category}) ${m.content}`
1068
+ ).join("\n");
1069
+ const existingList = existingMemories.slice(-50).map(
1070
+ (m) => `[${m.frontmatter.id}] (${m.frontmatter.category}) ${m.content}`
1071
+ ).join("\n");
1072
+ const cTraceId = crypto.randomUUID();
1073
+ this.emit({ kind: "llm_start", traceId: cTraceId, model: this.config.model, operation: "consolidation", input: newList });
1074
+ const cStartTime = Date.now();
1075
+ if (this.shouldUseLocalLlm) {
1076
+ try {
1077
+ const localResult = await this.consolidateWithLocalLlm(newList, existingList, currentProfile);
1078
+ if (localResult) {
1079
+ const durationMs = Date.now() - cStartTime;
1080
+ this.emit({ kind: "llm_end", traceId: cTraceId, model: this.config.localLlmModel, operation: "consolidation", durationMs });
1081
+ log.debug(`consolidation: used local LLM \u2014 ${localResult.items.length} decisions`);
1082
+ return this.sanitizeConsolidationResult(localResult);
1083
+ }
1084
+ if (!this.config.localLlmFallback) {
1085
+ log.warn("consolidation: local LLM failed and fallback disabled");
1086
+ return { items: [], profileUpdates: [], entityUpdates: [] };
1087
+ }
1088
+ log.info("consolidation: local LLM unavailable, falling back to gateway AI");
1089
+ } catch (err) {
1090
+ if (!this.config.localLlmFallback) {
1091
+ log.warn("consolidation: local LLM error and fallback disabled:", err);
1092
+ return { items: [], profileUpdates: [], entityUpdates: [] };
1093
+ }
1094
+ log.info("consolidation: local LLM error, falling back to gateway AI:", err);
1095
+ }
1096
+ }
1097
+ const fallbackResult = await this.parseWithGatewayFallback(
1098
+ cTraceId,
1099
+ "consolidation",
1100
+ cStartTime,
1101
+ ConsolidationResultSchema,
1102
+ [
1103
+ {
1104
+ role: "system",
1105
+ content: `You are a memory consolidation system. Compare new memories against existing ones and decide what to do with each.
1106
+
1107
+ Actions:
1108
+ - ADD: Keep the new memory as-is (no duplicate exists)
1109
+ - MERGE: Combine with an existing memory (provide mergeWith ID and updated content)
1110
+ - UPDATE: Replace existing memory content (provide updated content)
1111
+ - INVALIDATE: Remove existing memory (it's been superseded or is wrong)
1112
+ - SKIP: This new memory is redundant (exact duplicate or subset of existing)
1113
+
1114
+ Also:
1115
+ - Suggest profile updates based on patterns across memories
1116
+ - Identify entity updates for entity tracking${this.config.causalRuleExtractionEnabled ? `
1117
+ - When merging or updating memories, look for IF\u2192THEN causal patterns. If a memory describes "X failed/succeeded because Y" or "doing X led to Y", rewrite its content to make the causal rule explicit in the form "IF <condition> THEN <action/outcome>".` : ""}`
1118
+ },
1119
+ {
1120
+ role: "user",
1121
+ content: `Current behavioral profile:
1122
+ ${currentProfile || "(empty)"}
1123
+
1124
+ Existing memories:
1125
+ ${existingList || "(none)"}
1126
+
1127
+ New memories to consolidate:
1128
+ ${newList}
1129
+
1130
+ Consolidate the new memories against existing ones.`
1131
+ }
1132
+ ],
1133
+ { temperature: 0.3, maxTokens: 4096 }
1134
+ );
1135
+ if (fallbackResult) {
1136
+ log.debug(`consolidation: ${fallbackResult.items.length} decisions via fallback`);
1137
+ const normalizedEntityUpdates = fallbackResult.entityUpdates.map((entity) => ({
1138
+ ...entity,
1139
+ promptedByQuestion: typeof entity.promptedByQuestion === "string" ? entity.promptedByQuestion : void 0
1140
+ }));
1141
+ return this.sanitizeConsolidationResult({
1142
+ items: fallbackResult.items.map((item) => ({
1143
+ ...item,
1144
+ mergeWith: item.mergeWith ?? void 0,
1145
+ updatedContent: item.updatedContent ?? void 0
1146
+ })),
1147
+ profileUpdates: fallbackResult.profileUpdates,
1148
+ entityUpdates: normalizedEntityUpdates
1149
+ });
1150
+ }
1151
+ if (!this.client) {
1152
+ log.warn("consolidation skipped \u2014 no OpenAI API key and local LLM failed/disabled");
1153
+ return { items: [], profileUpdates: [], entityUpdates: [] };
1154
+ }
1155
+ try {
1156
+ const systemPrompt = `You are a memory consolidation system. Compare new memories against existing ones and decide what to do with each.
1157
+
1158
+ Actions:
1159
+ - ADD: Keep the new memory as-is (no duplicate exists)
1160
+ - MERGE: Combine with an existing memory (provide mergeWith ID and updated content)
1161
+ - UPDATE: Replace existing memory content (provide updated content)
1162
+ - INVALIDATE: Remove existing memory (it's been superseded or is wrong)
1163
+ - SKIP: This new memory is redundant (exact duplicate or subset of existing)
1164
+
1165
+ Also:
1166
+ - Suggest profile updates based on patterns across memories
1167
+ - Identify entity updates for entity tracking${this.config.causalRuleExtractionEnabled ? `
1168
+ - When merging or updating memories, look for IF\u2192THEN causal patterns. If a memory describes "X failed/succeeded because Y" or "doing X led to Y", rewrite its content to make the causal rule explicit in the form "IF <condition> THEN <action/outcome>".` : ""}
1169
+
1170
+ Current behavioral profile:
1171
+ ${currentProfile || "(empty)"}
1172
+
1173
+ Existing memories:
1174
+ ${existingList || "(none)"}
1175
+
1176
+ New memories to consolidate:
1177
+ ${newList}
1178
+
1179
+ Respond with valid JSON only, matching this schema:
1180
+ {
1181
+ "items": [
1182
+ {
1183
+ "existingId": "id",
1184
+ "action": "ADD",
1185
+ "mergeWith": "optional-existing-id",
1186
+ "updatedContent": "optional replacement content",
1187
+ "reason": "brief reason for this action"
1188
+ }
1189
+ ],
1190
+ "profileUpdates": ["optional profile update"],
1191
+ "entityUpdates": [{"name": "person-jane-doe", "type": "person", "facts": ["Now leads the backend team", "Recently migrated the user service to TypeScript"]}]
1192
+ }`;
1193
+ const response = await this.client.chat.completions.create({
1194
+ model: this.config.model,
1195
+ messages: [
1196
+ { role: "system", content: systemPrompt },
1197
+ { role: "user", content: "Consolidate the new memories against existing ones." }
1198
+ ],
1199
+ ...this.config.reasoningEffort !== "none" ? { reasoning_effort: this.config.reasoningEffort } : {},
1200
+ ...buildChatCompletionTokenLimit(this.config.model, 4096, {
1201
+ assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
1202
+ })
1203
+ });
1204
+ const rawContent = response.choices?.[0]?.message?.content?.trim();
1205
+ const cDurationMs = Date.now() - cStartTime;
1206
+ const cUsage = response.usage;
1207
+ let parsed = null;
1208
+ if (rawContent) {
1209
+ for (const candidate of extractJsonCandidates(rawContent)) {
1210
+ try {
1211
+ parsed = JSON.parse(candidate);
1212
+ break;
1213
+ } catch {
1214
+ }
1215
+ }
1216
+ }
1217
+ this.emit({
1218
+ kind: "llm_end",
1219
+ traceId: cTraceId,
1220
+ model: this.config.model,
1221
+ operation: "consolidation",
1222
+ durationMs: cDurationMs,
1223
+ output: parsed ? JSON.stringify(parsed).slice(0, 2e3) : void 0,
1224
+ tokenUsage: cUsage ? { input: cUsage.prompt_tokens, output: cUsage.completion_tokens, total: cUsage.total_tokens } : void 0
1225
+ });
1226
+ if (parsed && Array.isArray(parsed.items)) {
1227
+ const normalizedItems = parsed.items.map((item) => {
1228
+ const rawAction = typeof item?.action === "string" ? item.action.toUpperCase() : "SKIP";
1229
+ const action = rawAction === "ADD" || rawAction === "MERGE" || rawAction === "UPDATE" || rawAction === "INVALIDATE" || rawAction === "SKIP" ? rawAction : "SKIP";
1230
+ return {
1231
+ existingId: typeof item?.existingId === "string" ? item.existingId : typeof item?.newMemoryId === "string" ? item.newMemoryId : "",
1232
+ action,
1233
+ mergeWith: typeof item?.mergeWith === "string" ? item.mergeWith : void 0,
1234
+ updatedContent: typeof item?.updatedContent === "string" ? item.updatedContent : void 0,
1235
+ reason: typeof item?.reason === "string" ? item.reason : ""
1236
+ };
1237
+ }).filter((item) => item.existingId.length > 0);
1238
+ const normalizedEntityUpdates = Array.isArray(parsed.entityUpdates) ? parsed.entityUpdates.map((entity) => ({
1239
+ name: typeof entity?.name === "string" ? entity.name : "",
1240
+ type: typeof entity?.type === "string" ? entity.type : "other",
1241
+ facts: Array.isArray(entity?.facts) ? entity.facts.filter((fact) => typeof fact === "string") : []
1242
+ })).filter((entity) => entity.name.length > 0) : [];
1243
+ log.debug(
1244
+ `consolidation: ${normalizedItems.length} decisions`
1245
+ );
1246
+ return this.sanitizeConsolidationResult({
1247
+ items: normalizedItems,
1248
+ profileUpdates: Array.isArray(parsed.profileUpdates) ? parsed.profileUpdates.filter((update) => typeof update === "string" && update.trim().length > 0) : [],
1249
+ entityUpdates: normalizedEntityUpdates
1250
+ });
1251
+ }
1252
+ log.warn("consolidation returned no parsed output");
1253
+ return { items: [], profileUpdates: [], entityUpdates: [] };
1254
+ } catch (err) {
1255
+ this.emit({
1256
+ kind: "llm_error",
1257
+ traceId: cTraceId,
1258
+ model: this.config.model,
1259
+ operation: "consolidation",
1260
+ durationMs: Date.now() - cStartTime,
1261
+ error: String(err)
1262
+ });
1263
+ log.error("consolidation failed", err);
1264
+ return { items: [], profileUpdates: [], entityUpdates: [] };
1265
+ }
1266
+ }
1267
+ /**
1268
+ * Consolidate memories using local LLM.
1269
+ */
1270
+ async consolidateWithLocalLlm(newList, existingList, currentProfile) {
1271
+ const contextSizes = this.modelRegistry.calculateContextSizes(
1272
+ this.config.localLlmModel,
1273
+ this.config.localLlmMaxContext
1274
+ );
1275
+ log.debug(`Consolidation model context: ${contextSizes.description}`);
1276
+ const prompt = `You are a memory consolidation system. Compare new memories against existing ones and decide what to do with each.
1277
+
1278
+ Actions:
1279
+ - ADD: Keep the new memory as-is (no duplicate exists)
1280
+ - MERGE: Combine with an existing memory (provide mergeWith ID and updated content)
1281
+ - UPDATE: Replace existing memory content (provide updated content)
1282
+ - INVALIDATE: Remove existing memory (it's been superseded or is wrong)
1283
+ - SKIP: This new memory is redundant (exact duplicate or subset of existing)
1284
+
1285
+ Also:
1286
+ - Suggest profile updates based on patterns across memories
1287
+ - Identify entity updates for entity tracking${this.config.causalRuleExtractionEnabled ? `
1288
+ - When merging or updating memories, look for IF\u2192THEN causal patterns. If a memory describes "X failed/succeeded because Y" or "doing X led to Y", rewrite its content to make the causal rule explicit in the form "IF <condition> THEN <action/outcome>".` : ""}
1289
+
1290
+ Current behavioral profile:
1291
+ ${currentProfile || "(empty)"}
1292
+
1293
+ Existing memories:
1294
+ ${existingList || "(none)"}
1295
+
1296
+ New memories to consolidate:
1297
+ ${newList}
1298
+
1299
+ Respond with valid JSON matching this schema:
1300
+ {
1301
+ "items": [
1302
+ {"memoryId": "id", "action": "ADD|MERGE|UPDATE|INVALIDATE|SKIP", "reason": "why", "updatedContent": "optional new content"}
1303
+ ],
1304
+ "profileUpdates": [{"section": "section name", "content": "new bullet"}],
1305
+ "entityUpdates": [{"entityId": "id", "updates": {"field": "value"}}]
1306
+ }`;
1307
+ const response = await this.localLlm.chatCompletion(
1308
+ [
1309
+ { role: "system", content: "You are a memory consolidation system. Output valid JSON only." },
1310
+ { role: "user", content: prompt }
1311
+ ],
1312
+ {
1313
+ temperature: 0.3,
1314
+ maxTokens: contextSizes.maxOutputTokens,
1315
+ operation: "consolidation",
1316
+ priority: "background"
1317
+ }
1318
+ );
1319
+ if (!response?.content) {
1320
+ return null;
1321
+ }
1322
+ try {
1323
+ const content = response.content.trim();
1324
+ for (const candidate of extractJsonCandidates(content)) {
1325
+ try {
1326
+ const parsed = JSON.parse(candidate);
1327
+ return {
1328
+ items: Array.isArray(parsed.items) ? parsed.items : [],
1329
+ profileUpdates: Array.isArray(parsed.profileUpdates) ? parsed.profileUpdates : [],
1330
+ entityUpdates: Array.isArray(parsed.entityUpdates) ? parsed.entityUpdates : []
1331
+ };
1332
+ } catch {
1333
+ }
1334
+ }
1335
+ return null;
1336
+ } catch (err) {
1337
+ log.warn("local LLM consolidation: failed to parse JSON response:", err);
1338
+ return null;
1339
+ }
1340
+ }
1341
+ /**
1342
+ * Consolidate a bloated profile.md into a compact version.
1343
+ * The LLM merges duplicates, removes stale info, and preserves section structure.
1344
+ * Returns the consolidated markdown or null on failure.
1345
+ */
1346
+ async consolidateProfile(fullProfileContent, targetLines = 50) {
1347
+ const pTraceId = crypto.randomUUID();
1348
+ this.emit({ kind: "llm_start", traceId: pTraceId, model: this.config.model, operation: "profile_consolidation", input: fullProfileContent.slice(0, 2e3) });
1349
+ const pStartTime = Date.now();
1350
+ if (this.shouldUseLocalLlm) {
1351
+ try {
1352
+ const localResult = await this.consolidateProfileWithLocalLlm(fullProfileContent, targetLines);
1353
+ if (localResult) {
1354
+ const durationMs = Date.now() - pStartTime;
1355
+ this.emit({ kind: "llm_end", traceId: pTraceId, model: this.config.localLlmModel, operation: "profile_consolidation", durationMs });
1356
+ log.debug(`profile consolidation: used local LLM \u2014 removed ${localResult.removedCount} items`);
1357
+ return localResult;
1358
+ }
1359
+ if (!this.config.localLlmFallback) {
1360
+ log.warn("profile consolidation: local LLM failed and fallback disabled");
1361
+ return null;
1362
+ }
1363
+ log.info("profile consolidation: local LLM unavailable, falling back to gateway AI");
1364
+ } catch (err) {
1365
+ if (!this.config.localLlmFallback) {
1366
+ log.warn("profile consolidation: local LLM error and fallback disabled:", err);
1367
+ return null;
1368
+ }
1369
+ log.info("profile consolidation: local LLM error, falling back to gateway AI:", err);
1370
+ }
1371
+ }
1372
+ const profileFallback = await this.parseWithGatewayFallback(
1373
+ pTraceId,
1374
+ "profile_consolidation",
1375
+ pStartTime,
1376
+ buildProfileConsolidationResultSchema(targetLines),
1377
+ [
1378
+ {
1379
+ role: "system",
1380
+ content: `You are a profile consolidation system. You are given a behavioral profile (markdown) that has grown too large. Your job is to produce a CONSOLIDATED version that:
1381
+
1382
+ 1. PRESERVES all ## section headers and their structure
1383
+ 2. MERGES duplicate or near-duplicate bullet points into single, clear statements
1384
+ 3. REMOVES stale information that has been superseded by newer bullets
1385
+ 4. REMOVES trivial or overly specific operational details that won't be useful across sessions
1386
+ 5. KEEPS the most important, durable observations about the user's preferences, habits, identity, and working style
1387
+ 6. Target roughly ${targetLines} lines \u2014 this is a soft target, prioritize quality over length
1388
+ 7. Write in the same style as the existing profile \u2014 concise bullets, no fluff
1389
+
1390
+ The output should be the COMPLETE consolidated profile as valid markdown, starting with "# Behavioral Profile".`
1391
+ },
1392
+ { role: "user", content: fullProfileContent }
1393
+ ],
1394
+ { temperature: 0.3, maxTokens: 4096 }
1395
+ );
1396
+ if (profileFallback) {
1397
+ log.debug(
1398
+ `profile consolidation: removed ${profileFallback.removedCount} items \u2014 ${profileFallback.summary} (fallback)`
1399
+ );
1400
+ return profileFallback;
1401
+ }
1402
+ if (!this.client) {
1403
+ log.warn("profile consolidation skipped \u2014 no OpenAI API key and local LLM failed/disabled");
1404
+ return null;
1405
+ }
1406
+ try {
1407
+ const systemPrompt = `You are a profile consolidation system. You are given a behavioral profile (markdown) that has grown too large. Your job is to produce a CONSOLIDATED version that:
1408
+
1409
+ 1. PRESERVES all ## section headers and their structure
1410
+ 2. MERGES duplicate or near-duplicate bullet points into single, clear statements
1411
+ 3. REMOVES stale information that has been superseded by newer bullets
1412
+ 4. REMOVES trivial or overly specific operational details that won't be useful across sessions
1413
+ 5. KEEPS the most important, durable observations about the user's preferences, habits, identity, and working style
1414
+ 6. Target roughly ${targetLines} lines \u2014 this is a soft target, prioritize quality over length
1415
+ 7. Write in the same style as the existing profile \u2014 concise bullets, no fluff
1416
+
1417
+ The output should be the COMPLETE consolidated profile as valid markdown, starting with "# Behavioral Profile".
1418
+
1419
+ Respond with valid JSON matching this schema:
1420
+ {
1421
+ "consolidatedProfile": "# Behavioral Profile\\n\\n... (complete markdown)",
1422
+ "removedCount": 42,
1423
+ "summary": "brief summary of what was consolidated"
1424
+ }`;
1425
+ const response = await this.client.chat.completions.create({
1426
+ model: this.config.model,
1427
+ messages: [
1428
+ { role: "system", content: systemPrompt },
1429
+ { role: "user", content: fullProfileContent }
1430
+ ],
1431
+ ...this.config.reasoningEffort !== "none" ? { reasoning_effort: this.config.reasoningEffort } : {},
1432
+ ...buildChatCompletionTokenLimit(this.config.model, 4096, {
1433
+ assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
1434
+ })
1435
+ });
1436
+ const rawContent = response.choices?.[0]?.message?.content?.trim();
1437
+ const pDurationMs = Date.now() - pStartTime;
1438
+ const pUsage = response.usage;
1439
+ let parsed = null;
1440
+ if (rawContent) {
1441
+ for (const candidate of extractJsonCandidates(rawContent)) {
1442
+ try {
1443
+ parsed = JSON.parse(candidate);
1444
+ break;
1445
+ } catch {
1446
+ }
1447
+ }
1448
+ }
1449
+ this.emit({
1450
+ kind: "llm_end",
1451
+ traceId: pTraceId,
1452
+ model: this.config.model,
1453
+ operation: "profile_consolidation",
1454
+ durationMs: pDurationMs,
1455
+ output: parsed ? parsed.summary : void 0,
1456
+ tokenUsage: pUsage ? { input: pUsage.prompt_tokens, output: pUsage.completion_tokens, total: pUsage.total_tokens } : void 0
1457
+ });
1458
+ if (parsed && typeof parsed.consolidatedProfile === "string") {
1459
+ log.debug(
1460
+ `profile consolidation: removed ${parsed.removedCount ?? 0} items \u2014 ${parsed.summary ?? ""}`
1461
+ );
1462
+ return {
1463
+ consolidatedProfile: parsed.consolidatedProfile,
1464
+ removedCount: Number(parsed.removedCount || 0),
1465
+ summary: String(parsed.summary || "")
1466
+ };
1467
+ }
1468
+ log.warn("profile consolidation returned no parsed output");
1469
+ return null;
1470
+ } catch (err) {
1471
+ this.emit({
1472
+ kind: "llm_error",
1473
+ traceId: pTraceId,
1474
+ model: this.config.model,
1475
+ operation: "profile_consolidation",
1476
+ durationMs: Date.now() - pStartTime,
1477
+ error: String(err)
1478
+ });
1479
+ log.error("profile consolidation failed", err);
1480
+ return null;
1481
+ }
1482
+ }
1483
+ /**
1484
+ * Consolidate profile using local LLM.
1485
+ */
1486
+ async consolidateProfileWithLocalLlm(fullProfileContent, targetLines = 50) {
1487
+ const contextSizes = this.modelRegistry.calculateContextSizes(
1488
+ this.config.localLlmModel,
1489
+ this.config.localLlmMaxContext
1490
+ );
1491
+ log.debug(`Profile consolidation model context: ${contextSizes.description}`);
1492
+ const prompt = `You are a profile consolidation system. You are given a behavioral profile (markdown) that has grown too large. Your job is to produce a CONSOLIDATED version that:
1493
+
1494
+ 1. PRESERVES all ## section headers and their structure
1495
+ 2. MERGES duplicate or near-duplicate bullet points into single, clear statements
1496
+ 3. REMOVES stale information that has been superseded by newer bullets
1497
+ 4. REMOVES trivial or overly specific operational details that won't be useful across sessions
1498
+ 5. KEEPS the most important, durable observations about the user's preferences, habits, identity, and working style
1499
+ 6. Target roughly ${targetLines} lines \u2014 this is a soft target, prioritize quality over length
1500
+ 7. Write in the same style as the existing profile \u2014 concise bullets, no fluff
1501
+
1502
+ Profile to consolidate:
1503
+ ${fullProfileContent}
1504
+
1505
+ Respond with valid JSON matching this schema:
1506
+ {
1507
+ "consolidatedProfile": "# Behavioral Profile\\n\\n... (complete markdown)",
1508
+ "removedCount": 42,
1509
+ "summary": "brief summary of what was consolidated"
1510
+ }`;
1511
+ const response = await this.localLlm.chatCompletion(
1512
+ [
1513
+ { role: "system", content: "You are a profile consolidation system. Output valid JSON only." },
1514
+ { role: "user", content: prompt }
1515
+ ],
1516
+ {
1517
+ temperature: 0.3,
1518
+ maxTokens: contextSizes.maxOutputTokens,
1519
+ operation: "profile_consolidation",
1520
+ priority: "background"
1521
+ }
1522
+ );
1523
+ if (!response?.content) {
1524
+ return null;
1525
+ }
1526
+ try {
1527
+ const content = response.content.trim();
1528
+ for (const candidate of extractJsonCandidates(content)) {
1529
+ try {
1530
+ const parsed = JSON.parse(candidate);
1531
+ return {
1532
+ consolidatedProfile: String(parsed.consolidatedProfile || ""),
1533
+ removedCount: Number(parsed.removedCount || 0),
1534
+ summary: String(parsed.summary || "")
1535
+ };
1536
+ } catch {
1537
+ }
1538
+ }
1539
+ return null;
1540
+ } catch (err) {
1541
+ log.warn("local LLM profile consolidation: failed to parse JSON response:", err);
1542
+ return null;
1543
+ }
1544
+ }
1545
+ /**
1546
+ * Consolidate IDENTITY.md reflections into a concise "Learned Patterns" section.
1547
+ * Returns the new content for the IDENTITY.md file (everything below the static header).
1548
+ */
1549
+ async consolidateIdentity(fullIdentityContent, staticHeaderEndMarker) {
1550
+ const iTraceId = crypto.randomUUID();
1551
+ this.emit({ kind: "llm_start", traceId: iTraceId, model: this.config.model, operation: "identity_consolidation", input: fullIdentityContent.slice(0, 2e3) });
1552
+ const iStartTime = Date.now();
1553
+ if (this.shouldUseLocalLlm) {
1554
+ try {
1555
+ const localResult = await this.consolidateIdentityWithLocalLlm(fullIdentityContent);
1556
+ if (localResult) {
1557
+ const durationMs = Date.now() - iStartTime;
1558
+ this.emit({ kind: "llm_end", traceId: iTraceId, model: this.config.localLlmModel, operation: "identity_consolidation", durationMs });
1559
+ log.debug(`identity consolidation: used local LLM \u2014 ${localResult.learnedPatterns.length} patterns`);
1560
+ return localResult;
1561
+ }
1562
+ if (!this.config.localLlmFallback) {
1563
+ log.warn("identity consolidation: local LLM failed and fallback disabled");
1564
+ return null;
1565
+ }
1566
+ log.info("identity consolidation: local LLM unavailable, falling back to gateway AI");
1567
+ } catch (err) {
1568
+ if (!this.config.localLlmFallback) {
1569
+ log.warn("identity consolidation: local LLM error and fallback disabled:", err);
1570
+ return null;
1571
+ }
1572
+ log.info("identity consolidation: local LLM error, falling back to gateway AI:", err);
1573
+ }
1574
+ }
1575
+ const identityFallback = await this.parseWithGatewayFallback(
1576
+ iTraceId,
1577
+ "identity_consolidation",
1578
+ iStartTime,
1579
+ IdentityConsolidationResultSchema,
1580
+ [
1581
+ {
1582
+ role: "system",
1583
+ content: `You are an identity consolidation system. You are given the full contents of an IDENTITY.md file that contains many individual reflection entries. Your job is to:
1584
+
1585
+ 1. Read all the reflection entries (sections starting with "## Reflection")
1586
+ 2. Extract the most important, durable behavioral patterns and lessons learned
1587
+ 3. Consolidate them into concise, standalone statements (aim for 10-25 key patterns)
1588
+ 4. Remove redundancy \u2014 if multiple reflections say the same thing, merge into one clear statement
1589
+ 5. Prioritize patterns that are actionable and recurring over one-off observations
1590
+ 6. Write a brief summary paragraph
1591
+
1592
+ The goal is to reduce a bloated file to a compact, high-signal set of learned patterns while preserving all genuinely useful self-knowledge.`
1593
+ },
1594
+ { role: "user", content: fullIdentityContent }
1595
+ ],
1596
+ { temperature: 0.3, maxTokens: 4096 }
1597
+ );
1598
+ if (identityFallback) {
1599
+ log.debug(
1600
+ `identity consolidation: ${identityFallback.learnedPatterns.length} patterns (fallback)`
1601
+ );
1602
+ return identityFallback;
1603
+ }
1604
+ if (!this.client) {
1605
+ log.warn("identity consolidation skipped \u2014 no OpenAI API key and local LLM failed/disabled");
1606
+ return null;
1607
+ }
1608
+ try {
1609
+ const systemPrompt = `You are an identity consolidation system. You are given the full contents of an IDENTITY.md file that contains many individual reflection entries. Your job is to:
1610
+
1611
+ 1. Read all the reflection entries (sections starting with "## Reflection")
1612
+ 2. Extract the most important, durable behavioral patterns and lessons learned
1613
+ 3. Consolidate them into concise, standalone statements (aim for 10-25 key patterns)
1614
+ 4. Remove redundancy \u2014 if multiple reflections say the same thing, merge into one clear statement
1615
+ 5. Prioritize patterns that are actionable and recurring over one-off observations
1616
+ 6. Write a brief summary paragraph
1617
+
1618
+ The goal is to reduce a bloated file to a compact, high-signal set of learned patterns while preserving all genuinely useful self-knowledge.
1619
+
1620
+ Respond with valid JSON matching this schema:
1621
+ {
1622
+ "learnedPatterns": ["pattern 1", "pattern 2", "pattern 3"],
1623
+ "summary": "brief summary of consolidation"
1624
+ }`;
1625
+ const response = await this.client.chat.completions.create({
1626
+ model: this.config.model,
1627
+ messages: [
1628
+ { role: "system", content: systemPrompt },
1629
+ { role: "user", content: fullIdentityContent }
1630
+ ],
1631
+ ...this.config.reasoningEffort !== "none" ? { reasoning_effort: this.config.reasoningEffort } : {},
1632
+ ...buildChatCompletionTokenLimit(this.config.model, 4096, {
1633
+ assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
1634
+ })
1635
+ });
1636
+ const rawContent = response.choices?.[0]?.message?.content?.trim();
1637
+ const iDurationMs = Date.now() - iStartTime;
1638
+ const iUsage = response.usage;
1639
+ let parsed = null;
1640
+ if (rawContent) {
1641
+ for (const candidate of extractJsonCandidates(rawContent)) {
1642
+ try {
1643
+ parsed = JSON.parse(candidate);
1644
+ break;
1645
+ } catch {
1646
+ }
1647
+ }
1648
+ }
1649
+ this.emit({
1650
+ kind: "llm_end",
1651
+ traceId: iTraceId,
1652
+ model: this.config.model,
1653
+ operation: "identity_consolidation",
1654
+ durationMs: iDurationMs,
1655
+ output: parsed ? parsed.summary : void 0,
1656
+ tokenUsage: iUsage ? { input: iUsage.prompt_tokens, output: iUsage.completion_tokens, total: iUsage.total_tokens } : void 0
1657
+ });
1658
+ if (parsed && Array.isArray(parsed.learnedPatterns)) {
1659
+ const learnedPatterns = parsed.learnedPatterns.filter((pattern) => typeof pattern === "string").map((pattern) => pattern.trim()).filter((pattern) => pattern.length > 0);
1660
+ log.debug(
1661
+ `identity consolidation: ${learnedPatterns.length} patterns`
1662
+ );
1663
+ return {
1664
+ learnedPatterns,
1665
+ summary: String(parsed.summary || "")
1666
+ };
1667
+ }
1668
+ log.warn("identity consolidation returned no parsed output");
1669
+ return null;
1670
+ } catch (err) {
1671
+ this.emit({
1672
+ kind: "llm_error",
1673
+ traceId: iTraceId,
1674
+ model: this.config.model,
1675
+ operation: "identity_consolidation",
1676
+ durationMs: Date.now() - iStartTime,
1677
+ error: String(err)
1678
+ });
1679
+ log.error("identity consolidation failed", err);
1680
+ return null;
1681
+ }
1682
+ }
1683
+ /**
1684
+ * Consolidate identity using local LLM.
1685
+ */
1686
+ async consolidateIdentityWithLocalLlm(fullIdentityContent) {
1687
+ const contextSizes = this.modelRegistry.calculateContextSizes(
1688
+ this.config.localLlmModel,
1689
+ this.config.localLlmMaxContext
1690
+ );
1691
+ log.debug(`Identity consolidation model context: ${contextSizes.description}`);
1692
+ const prompt = `You are an identity consolidation system. You are given the full contents of an IDENTITY.md file that contains many individual reflection entries. Your job is to:
1693
+
1694
+ 1. Read all the reflection entries (sections starting with "## Reflection")
1695
+ 2. Extract the most important, durable behavioral patterns and lessons learned
1696
+ 3. Consolidate them into concise, standalone statements (aim for 10-25 key patterns)
1697
+ 4. Remove redundancy \u2014 if multiple reflections say the same thing, merge into one clear statement
1698
+ 5. Prioritize patterns that are actionable and recurring over one-off observations
1699
+ 6. Write a brief summary paragraph
1700
+
1701
+ The goal is to reduce a bloated file to a compact, high-signal set of learned patterns while preserving all genuinely useful self-knowledge.
1702
+
1703
+ IDENTITY.md content:
1704
+ ${fullIdentityContent}
1705
+
1706
+ Respond with valid JSON matching this schema:
1707
+ {
1708
+ "learnedPatterns": ["pattern 1", "pattern 2", "pattern 3"],
1709
+ "summary": "brief summary of consolidation"
1710
+ }`;
1711
+ const response = await this.localLlm.chatCompletion(
1712
+ [
1713
+ { role: "system", content: "You are an identity consolidation system. Output valid JSON only." },
1714
+ { role: "user", content: prompt }
1715
+ ],
1716
+ {
1717
+ temperature: 0.3,
1718
+ maxTokens: contextSizes.maxOutputTokens,
1719
+ operation: "identity_consolidation",
1720
+ priority: "background"
1721
+ }
1722
+ );
1723
+ if (!response?.content) {
1724
+ return null;
1725
+ }
1726
+ try {
1727
+ const content = response.content.trim();
1728
+ for (const candidate of extractJsonCandidates(content)) {
1729
+ try {
1730
+ const parsed = JSON.parse(candidate);
1731
+ return {
1732
+ learnedPatterns: Array.isArray(parsed.learnedPatterns) ? parsed.learnedPatterns : [],
1733
+ summary: String(parsed.summary || "")
1734
+ };
1735
+ } catch {
1736
+ }
1737
+ }
1738
+ return null;
1739
+ } catch (err) {
1740
+ log.warn("local LLM identity consolidation: failed to parse JSON response:", err);
1741
+ return null;
1742
+ }
1743
+ }
1744
+ /**
1745
+ * Verify if two memories contradict each other using LLM.
1746
+ * Called when QMD finds semantically similar memories (Phase 2B).
1747
+ */
1748
+ async verifyContradiction(newMemory, existingMemory) {
1749
+ const input = `Memory 1 (existing, created ${existingMemory.created}):
1750
+ Category: ${existingMemory.category}
1751
+ Content: ${existingMemory.content}
1752
+
1753
+ Memory 2 (new):
1754
+ Category: ${newMemory.category}
1755
+ Content: ${newMemory.content}`;
1756
+ try {
1757
+ const systemPrompt = `You are a contradiction detection system. Analyze whether two memories contradict each other.
1758
+
1759
+ IMPORTANT: Not all similar memories are contradictions!
1760
+ - "User likes TypeScript" and "User likes Python" are NOT contradictions (preferences can coexist)
1761
+ - "User prefers dark mode" and "User prefers light mode" ARE contradictions (mutually exclusive)
1762
+ - "User's email is a@b.com" and "User's email is c@d.com" ARE contradictions (only one email)
1763
+ - "User works at Acme" and "User used to work at Acme" might be a contradiction (temporal change)
1764
+
1765
+ Only mark as contradiction if the two statements CANNOT both be true at the same time.
1766
+
1767
+ If they ARE contradictory, determine which represents the more recent/current state based on:
1768
+ - Explicit time references ("now", "currently", "used to", "no longer")
1769
+ - The fact that newer corrections often start with "actually" or "correction"
1770
+ - Context clues about change over time
1771
+
1772
+ Respond with valid JSON matching this schema:
1773
+ {
1774
+ "isContradiction": true,
1775
+ "confidence": 0.95,
1776
+ "reasoning": "why they contradict or don't",
1777
+ "whichIsNewer": "first"
1778
+ }`;
1779
+ if (this.shouldUseLocalLlm) {
1780
+ try {
1781
+ const localResponse = await this.localLlm.chatCompletion(
1782
+ [
1783
+ { role: "system", content: systemPrompt },
1784
+ { role: "user", content: input }
1785
+ ],
1786
+ {
1787
+ temperature: 0.3,
1788
+ maxTokens: 2048,
1789
+ operation: "contradiction_verification",
1790
+ priority: "background"
1791
+ }
1792
+ );
1793
+ const normalized2 = this.normalizeContradictionVerificationResult(
1794
+ this.parseJsonObject(localResponse?.content)
1795
+ );
1796
+ if (normalized2) {
1797
+ log.debug(
1798
+ `contradiction check via local LLM: ${normalized2.isContradiction ? "YES" : "NO"} (confidence: ${normalized2.confidence})`
1799
+ );
1800
+ return normalized2;
1801
+ }
1802
+ if (!this.config.localLlmFallback) {
1803
+ log.warn("contradiction verification skipped \u2014 local LLM returned invalid JSON and cloud fallback is disabled");
1804
+ return null;
1805
+ }
1806
+ } catch (err) {
1807
+ if (!this.config.localLlmFallback) {
1808
+ log.warn(`contradiction verification skipped \u2014 local LLM failed and cloud fallback is disabled: ${err}`);
1809
+ return null;
1810
+ }
1811
+ }
1812
+ }
1813
+ if (!this.shouldUseDirectClient) {
1814
+ const fallbackResponse = await this.fallbackLlm.chatCompletion(
1815
+ [
1816
+ { role: "system", content: systemPrompt },
1817
+ { role: "user", content: input }
1818
+ ],
1819
+ this.withGatewayAgent({ temperature: 0.3, maxTokens: 2048 })
1820
+ );
1821
+ const normalized2 = this.normalizeContradictionVerificationResult(
1822
+ this.parseJsonObject(fallbackResponse?.content)
1823
+ );
1824
+ if (normalized2) {
1825
+ log.debug(
1826
+ `contradiction check via fallback: ${normalized2.isContradiction ? "YES" : "NO"} (confidence: ${normalized2.confidence})`
1827
+ );
1828
+ return normalized2;
1829
+ }
1830
+ log.warn("contradiction verification skipped \u2014 no OpenAI API key and fallback unavailable");
1831
+ return null;
1832
+ }
1833
+ const response = await this.client.chat.completions.create({
1834
+ model: this.config.model,
1835
+ messages: [
1836
+ { role: "system", content: systemPrompt },
1837
+ { role: "user", content: input }
1838
+ ],
1839
+ ...buildChatCompletionTokenLimit(this.config.model, 2048, {
1840
+ assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
1841
+ })
1842
+ });
1843
+ const normalized = this.normalizeContradictionVerificationResult(
1844
+ this.parseJsonObject(response.choices?.[0]?.message?.content)
1845
+ );
1846
+ if (normalized) {
1847
+ log.debug(
1848
+ `contradiction check: ${normalized.isContradiction ? "YES" : "NO"} (confidence: ${normalized.confidence})`
1849
+ );
1850
+ return normalized;
1851
+ }
1852
+ return null;
1853
+ } catch (err) {
1854
+ log.error("contradiction verification failed", err);
1855
+ return null;
1856
+ }
1857
+ }
1858
+ /**
1859
+ * Suggest links between a new memory and existing memories (Phase 3A).
1860
+ * Called during extraction to build the knowledge graph.
1861
+ */
1862
+ async suggestLinks(newMemory, candidateMemories) {
1863
+ if (candidateMemories.length === 0) {
1864
+ return { links: [] };
1865
+ }
1866
+ const candidateList = candidateMemories.map((m, i) => `[${i + 1}] ID: ${m.id}
1867
+ Category: ${m.category}
1868
+ Content: ${m.content}`).join("\n\n");
1869
+ const input = `New memory:
1870
+ Category: ${newMemory.category}
1871
+ Content: ${newMemory.content}
1872
+
1873
+ Candidate memories to link to:
1874
+ ${candidateList}`;
1875
+ try {
1876
+ const systemPrompt = `You are a memory linking system. Analyze the new memory and suggest relationships to existing memories.
1877
+
1878
+ Link types:
1879
+ - follows: This memory is a continuation or next step (e.g., decision follows discussion)
1880
+ - references: This memory mentions or refers to the other (e.g., fact references entity)
1881
+ - contradicts: This memory conflicts with the other (use sparingly, only for true contradictions)
1882
+ - supports: This memory provides evidence or reinforcement (e.g., example supports principle)
1883
+ - related: General topical relationship
1884
+
1885
+ Rules:
1886
+ - Only suggest links with strength > 0.5
1887
+ - Quality over quantity \u2014 0-3 links is typical
1888
+ - Prefer specific link types over generic "related"
1889
+ - Consider entity references, topics, and causal relationships
1890
+
1891
+ Respond with valid JSON matching this schema:
1892
+ {
1893
+ "links": [{"targetId": "memory-id", "linkType": "follows|references|contradicts|supports|related", "strength": 0.8, "reason": "why"}]
1894
+ }`;
1895
+ if (this.shouldUseLocalLlm) {
1896
+ try {
1897
+ const localResponse = await this.localLlm.chatCompletion(
1898
+ [
1899
+ { role: "system", content: systemPrompt },
1900
+ { role: "user", content: input }
1901
+ ],
1902
+ {
1903
+ temperature: 0.3,
1904
+ maxTokens: 2048,
1905
+ operation: "link_suggestion",
1906
+ priority: "background"
1907
+ }
1908
+ );
1909
+ const normalized2 = this.normalizeSuggestedLinksResult(this.parseJsonObject(localResponse?.content));
1910
+ if (normalized2) {
1911
+ log.debug(`suggested ${normalized2.links.length} links via local LLM`);
1912
+ return normalized2;
1913
+ }
1914
+ if (!this.config.localLlmFallback) {
1915
+ log.warn("link suggestion skipped \u2014 local LLM returned invalid JSON and cloud fallback is disabled");
1916
+ return null;
1917
+ }
1918
+ } catch (err) {
1919
+ if (!this.config.localLlmFallback) {
1920
+ log.warn(`link suggestion skipped \u2014 local LLM failed and cloud fallback is disabled: ${err}`);
1921
+ return null;
1922
+ }
1923
+ }
1924
+ }
1925
+ if (!this.shouldUseDirectClient) {
1926
+ const fallbackResponse = await this.fallbackLlm.chatCompletion(
1927
+ [
1928
+ { role: "system", content: systemPrompt },
1929
+ { role: "user", content: input }
1930
+ ],
1931
+ this.withGatewayAgent({ temperature: 0.3, maxTokens: 2048 })
1932
+ );
1933
+ const normalized2 = this.normalizeSuggestedLinksResult(this.parseJsonObject(fallbackResponse?.content));
1934
+ if (normalized2) {
1935
+ log.debug(`suggested ${normalized2.links.length} links via fallback`);
1936
+ return normalized2;
1937
+ }
1938
+ log.warn("link suggestion skipped \u2014 no OpenAI API key and fallback unavailable");
1939
+ return null;
1940
+ }
1941
+ const response = await this.client.chat.completions.create({
1942
+ model: this.config.model,
1943
+ messages: [
1944
+ { role: "system", content: systemPrompt },
1945
+ { role: "user", content: input }
1946
+ ],
1947
+ ...buildChatCompletionTokenLimit(this.config.model, 2048, {
1948
+ assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
1949
+ })
1950
+ });
1951
+ const normalized = this.normalizeSuggestedLinksResult(
1952
+ this.parseJsonObject(response.choices?.[0]?.message?.content)
1953
+ );
1954
+ if (normalized) {
1955
+ log.debug(`suggested ${normalized.links.length} links`);
1956
+ return normalized;
1957
+ }
1958
+ return null;
1959
+ } catch (err) {
1960
+ log.error("link suggestion failed", err);
1961
+ return null;
1962
+ }
1963
+ }
1964
+ async generateDaySummary(memories) {
1965
+ if (!this.config.daySummaryEnabled) {
1966
+ log.warn("day summary skipped \u2014 disabled by config");
1967
+ return null;
1968
+ }
1969
+ const memoryContext = formatDaySummaryMemories(memories);
1970
+ if (memoryContext.length === 0) return null;
1971
+ const systemPrompt = await loadDaySummaryPrompt();
1972
+ const userPrompt = `Generate an end-of-day summary from this Remnic memory context:
1973
+
1974
+ ${memoryContext}`;
1975
+ const traceId = crypto.randomUUID();
1976
+ const startedAt = Date.now();
1977
+ this.emit({ kind: "llm_start", traceId, model: this.config.model, operation: "day_summary", input: memoryContext.slice(0, 4e3) });
1978
+ if (this.shouldUseLocalLlm) {
1979
+ try {
1980
+ const localResponse = await this.localLlm.chatCompletion(
1981
+ [
1982
+ { role: "system", content: `${systemPrompt}
1983
+
1984
+ Return valid JSON only.` },
1985
+ { role: "user", content: userPrompt }
1986
+ ],
1987
+ {
1988
+ temperature: 0.2,
1989
+ maxTokens: 2048,
1990
+ operation: "day_summary",
1991
+ priority: "background"
1992
+ }
1993
+ );
1994
+ const normalized = this.normalizeDaySummaryResult(this.parseJsonObject(localResponse?.content));
1995
+ if (normalized) {
1996
+ this.emit({ kind: "llm_end", traceId, model: this.config.localLlmModel, operation: "day_summary", durationMs: Date.now() - startedAt, output: JSON.stringify(normalized).slice(0, 2e3) });
1997
+ log.debug(`generated day summary via local LLM (${normalized.bullets.length} bullets)`);
1998
+ return normalized;
1999
+ }
2000
+ if (!this.config.localLlmFallback) {
2001
+ this.emit({ kind: "llm_error", traceId, model: this.config.localLlmModel, operation: "day_summary", durationMs: Date.now() - startedAt, error: "local LLM returned invalid JSON and fallback disabled" });
2002
+ log.warn("day summary skipped \u2014 local LLM returned invalid JSON and fallback disabled");
2003
+ return null;
2004
+ }
2005
+ } catch (err) {
2006
+ if (!this.config.localLlmFallback) {
2007
+ this.emit({ kind: "llm_error", traceId, model: this.config.localLlmModel, operation: "day_summary", durationMs: Date.now() - startedAt, error: String(err) });
2008
+ log.warn(`day summary skipped \u2014 local LLM failed and fallback disabled: ${err}`);
2009
+ return null;
2010
+ }
2011
+ }
2012
+ }
2013
+ const fallbackResult = await this.parseWithGatewayFallback(
2014
+ traceId,
2015
+ "day_summary",
2016
+ startedAt,
2017
+ DaySummaryResultSchema,
2018
+ [
2019
+ { role: "system", content: `${systemPrompt}
2020
+
2021
+ Return valid JSON only.` },
2022
+ { role: "user", content: userPrompt }
2023
+ ],
2024
+ { temperature: 0.2, maxTokens: 2048 }
2025
+ );
2026
+ if (fallbackResult) {
2027
+ const normalized = this.normalizeDaySummaryResult(fallbackResult);
2028
+ if (normalized) {
2029
+ log.debug(`generated day summary via fallback (${normalized.bullets.length} bullets)`);
2030
+ return normalized;
2031
+ }
2032
+ }
2033
+ if (this.shouldUseDirectClient) {
2034
+ try {
2035
+ const response = await this.client.responses.create({
2036
+ model: this.config.model,
2037
+ instructions: `${systemPrompt}
2038
+
2039
+ Return valid JSON only.`,
2040
+ input: userPrompt,
2041
+ max_output_tokens: 2048
2042
+ });
2043
+ const rawText = typeof response.output_text === "string" ? response.output_text : JSON.stringify(response.output_text ?? "");
2044
+ const normalized = this.normalizeDaySummaryResult(this.parseJsonObject(rawText));
2045
+ if (normalized) {
2046
+ this.emit({ kind: "llm_end", traceId, model: this.config.model, operation: "day_summary", durationMs: Date.now() - startedAt, output: JSON.stringify(normalized).slice(0, 2e3) });
2047
+ log.debug(`generated day summary via Responses API (${normalized.bullets.length} bullets)`);
2048
+ return normalized;
2049
+ }
2050
+ this.emit({ kind: "llm_error", traceId, model: this.config.model, operation: "day_summary", durationMs: Date.now() - startedAt, error: "Responses API returned unparseable output" });
2051
+ } catch (err) {
2052
+ this.emit({ kind: "llm_error", traceId, model: this.config.model, operation: "day_summary", durationMs: Date.now() - startedAt, error: `Responses API failed: ${err}` });
2053
+ }
2054
+ }
2055
+ this.emit({ kind: "llm_error", traceId, model: this.config.model, operation: "day_summary", durationMs: Date.now() - startedAt, error: "all generation paths exhausted (local LLM + gateway + Responses API)" });
2056
+ log.warn("day summary skipped \u2014 all generation paths exhausted");
2057
+ return null;
2058
+ }
2059
+ /**
2060
+ * Summarize a batch of old memories into a compact summary (Phase 4A).
2061
+ */
2062
+ async summarizeMemories(memories) {
2063
+ if (memories.length === 0) return null;
2064
+ const memoryList = memories.map((m) => `[${m.id}] (${m.category}, ${m.created.slice(0, 10)})
2065
+ ${m.content}`).join("\n\n");
2066
+ try {
2067
+ const systemPrompt = `You are a memory summarization system. You are given a batch of old memories that need to be compressed into a summary.
2068
+
2069
+ Your task:
2070
+ 1. Write a concise summary paragraph (2-4 sentences) capturing the essence of these memories
2071
+ 2. Extract the 5-10 most important facts that should be preserved
2072
+ 3. List the key entities mentioned
2073
+
2074
+ Guidelines:
2075
+ - Preserve specific, actionable information
2076
+ - Merge redundant details into single statements
2077
+ - Focus on durable insights, not transient details
2078
+ - Maintain any preferences, decisions, or corrections as key facts
2079
+
2080
+ Respond with valid JSON matching this schema:
2081
+ {
2082
+ "summaryText": "concise summary paragraph",
2083
+ "keyFacts": ["fact 1", "fact 2"],
2084
+ "keyEntities": ["entity-1", "entity-2"]
2085
+ }`;
2086
+ if (this.shouldUseLocalLlm) {
2087
+ try {
2088
+ const localResponse = await this.localLlm.chatCompletion(
2089
+ [
2090
+ { role: "system", content: systemPrompt },
2091
+ { role: "user", content: `Summarize these ${memories.length} memories:
2092
+
2093
+ ${memoryList}` }
2094
+ ],
2095
+ {
2096
+ temperature: 0.3,
2097
+ maxTokens: 4096,
2098
+ operation: "memory_summarization",
2099
+ priority: "background"
2100
+ }
2101
+ );
2102
+ const normalized2 = this.normalizeMemorySummaryResult(this.parseJsonObject(localResponse?.content));
2103
+ if (normalized2) {
2104
+ log.debug(
2105
+ `summarized ${memories.length} memories into ${normalized2.keyFacts.length} key facts via local LLM`
2106
+ );
2107
+ return normalized2;
2108
+ }
2109
+ if (!this.config.localLlmFallback) {
2110
+ log.warn("summarization skipped \u2014 local LLM returned invalid JSON and cloud fallback is disabled");
2111
+ return null;
2112
+ }
2113
+ } catch (err) {
2114
+ if (!this.config.localLlmFallback) {
2115
+ log.warn(`summarization skipped \u2014 local LLM failed and cloud fallback is disabled: ${err}`);
2116
+ return null;
2117
+ }
2118
+ }
2119
+ }
2120
+ if (!this.shouldUseDirectClient) {
2121
+ const fallbackResponse = await this.fallbackLlm.chatCompletion(
2122
+ [
2123
+ { role: "system", content: systemPrompt },
2124
+ { role: "user", content: `Summarize these ${memories.length} memories:
2125
+
2126
+ ${memoryList}` }
2127
+ ],
2128
+ this.withGatewayAgent({ temperature: 0.3, maxTokens: 4096 })
2129
+ );
2130
+ const normalized2 = this.normalizeMemorySummaryResult(this.parseJsonObject(fallbackResponse?.content));
2131
+ if (normalized2) {
2132
+ log.debug(`summarized ${memories.length} memories into ${normalized2.keyFacts.length} key facts via fallback`);
2133
+ return normalized2;
2134
+ }
2135
+ log.warn("summarization skipped \u2014 no OpenAI API key and fallback unavailable");
2136
+ return null;
2137
+ }
2138
+ const response = await this.client.chat.completions.create({
2139
+ model: this.config.model,
2140
+ messages: [
2141
+ { role: "system", content: systemPrompt },
2142
+ { role: "user", content: `Summarize these ${memories.length} memories:
2143
+
2144
+ ${memoryList}` }
2145
+ ],
2146
+ ...buildChatCompletionTokenLimit(this.config.model, 4096, {
2147
+ assumeOpenAI: this.directClientUsesOpenAiTokenSemantics()
2148
+ })
2149
+ });
2150
+ const normalized = this.normalizeMemorySummaryResult(
2151
+ this.parseJsonObject(response.choices?.[0]?.message?.content)
2152
+ );
2153
+ if (normalized) {
2154
+ log.debug(`summarized ${memories.length} memories into ${normalized.keyFacts.length} key facts`);
2155
+ return normalized;
2156
+ }
2157
+ return null;
2158
+ } catch (err) {
2159
+ log.error("memory summarization failed", err);
2160
+ return null;
2161
+ }
2162
+ }
2163
+ };
2164
+
2165
+ export {
2166
+ ExtractionEngine
2167
+ };
2168
+ //# sourceMappingURL=chunk-QFQVZOGA.js.map