@psiclawops/hypermem 0.8.5 → 0.9.1

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 (933) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/CHANGELOG.md +36 -1
  3. package/INSTALL.md +132 -9
  4. package/README.md +119 -272
  5. package/bench/README.md +42 -0
  6. package/bench/data-access-bench.mjs +380 -0
  7. package/bin/hypermem-bench.mjs +2 -0
  8. package/bin/hypermem-doctor.mjs +412 -0
  9. package/bin/hypermem-model-audit.mjs +339 -0
  10. package/bin/hypermem-status.mjs +491 -70
  11. package/docs/DIAGNOSTICS.md +205 -0
  12. package/docs/INTEGRATION_VALIDATION.md +186 -0
  13. package/docs/MIGRATION.md +9 -6
  14. package/docs/MIGRATION_GUIDE.md +125 -101
  15. package/docs/ROADMAP.md +247 -20
  16. package/docs/TUNING.md +19 -5
  17. package/install.sh +152 -401
  18. package/memory-plugin/LICENSE +190 -0
  19. package/memory-plugin/README.md +20 -0
  20. package/memory-plugin/package.json +3 -3
  21. package/package.json +19 -4
  22. package/plugin/LICENSE +190 -0
  23. package/plugin/README.md +20 -0
  24. package/plugin/package.json +4 -4
  25. package/scripts/install-runtime.mjs +12 -1
  26. package/dist/background-indexer.d.ts +0 -161
  27. package/dist/background-indexer.d.ts.map +0 -1
  28. package/dist/background-indexer.js +0 -1263
  29. package/dist/budget-policy.d.ts +0 -22
  30. package/dist/budget-policy.d.ts.map +0 -1
  31. package/dist/budget-policy.js +0 -27
  32. package/dist/cache.d.ts +0 -147
  33. package/dist/cache.d.ts.map +0 -1
  34. package/dist/cache.js +0 -569
  35. package/dist/compaction-fence.d.ts +0 -97
  36. package/dist/compaction-fence.d.ts.map +0 -1
  37. package/dist/compaction-fence.js +0 -174
  38. package/dist/compositor-utils.d.ts +0 -31
  39. package/dist/compositor-utils.d.ts.map +0 -1
  40. package/dist/compositor-utils.js +0 -47
  41. package/dist/compositor.d.ts +0 -394
  42. package/dist/compositor.d.ts.map +0 -1
  43. package/dist/compositor.js +0 -3632
  44. package/dist/content-hash.d.ts +0 -43
  45. package/dist/content-hash.d.ts.map +0 -1
  46. package/dist/content-hash.js +0 -75
  47. package/dist/content-type-classifier.d.ts +0 -41
  48. package/dist/content-type-classifier.d.ts.map +0 -1
  49. package/dist/content-type-classifier.js +0 -181
  50. package/dist/context-backfill.d.ts +0 -46
  51. package/dist/context-backfill.d.ts.map +0 -1
  52. package/dist/context-backfill.js +0 -113
  53. package/dist/context-store.d.ts +0 -131
  54. package/dist/context-store.d.ts.map +0 -1
  55. package/dist/context-store.js +0 -279
  56. package/dist/contradiction-audit-store.d.ts +0 -54
  57. package/dist/contradiction-audit-store.d.ts.map +0 -1
  58. package/dist/contradiction-audit-store.js +0 -88
  59. package/dist/contradiction-detector.d.ts +0 -78
  60. package/dist/contradiction-detector.d.ts.map +0 -1
  61. package/dist/contradiction-detector.js +0 -362
  62. package/dist/contradiction-resolution-policy.d.ts +0 -21
  63. package/dist/contradiction-resolution-policy.d.ts.map +0 -1
  64. package/dist/contradiction-resolution-policy.js +0 -17
  65. package/dist/cross-agent.d.ts +0 -74
  66. package/dist/cross-agent.d.ts.map +0 -1
  67. package/dist/cross-agent.js +0 -271
  68. package/dist/db.d.ts +0 -131
  69. package/dist/db.d.ts.map +0 -1
  70. package/dist/db.js +0 -410
  71. package/dist/degradation.d.ts +0 -102
  72. package/dist/degradation.d.ts.map +0 -1
  73. package/dist/degradation.js +0 -141
  74. package/dist/desired-state-store.d.ts +0 -100
  75. package/dist/desired-state-store.d.ts.map +0 -1
  76. package/dist/desired-state-store.js +0 -222
  77. package/dist/doc-chunk-store.d.ts +0 -140
  78. package/dist/doc-chunk-store.d.ts.map +0 -1
  79. package/dist/doc-chunk-store.js +0 -391
  80. package/dist/doc-chunker.d.ts +0 -99
  81. package/dist/doc-chunker.d.ts.map +0 -1
  82. package/dist/doc-chunker.js +0 -324
  83. package/dist/dreaming-promoter.d.ts +0 -124
  84. package/dist/dreaming-promoter.d.ts.map +0 -1
  85. package/dist/dreaming-promoter.js +0 -447
  86. package/dist/episode-store.d.ts +0 -49
  87. package/dist/episode-store.d.ts.map +0 -1
  88. package/dist/episode-store.js +0 -135
  89. package/dist/expertise-store.d.ts +0 -129
  90. package/dist/expertise-store.d.ts.map +0 -1
  91. package/dist/expertise-store.js +0 -342
  92. package/dist/fact-store.d.ts +0 -90
  93. package/dist/fact-store.d.ts.map +0 -1
  94. package/dist/fact-store.js +0 -283
  95. package/dist/fleet-store.d.ts +0 -144
  96. package/dist/fleet-store.d.ts.map +0 -1
  97. package/dist/fleet-store.js +0 -276
  98. package/dist/fos-mod.d.ts +0 -178
  99. package/dist/fos-mod.d.ts.map +0 -1
  100. package/dist/fos-mod.js +0 -416
  101. package/dist/hybrid-retrieval.d.ts +0 -64
  102. package/dist/hybrid-retrieval.d.ts.map +0 -1
  103. package/dist/hybrid-retrieval.js +0 -344
  104. package/dist/image-eviction.d.ts +0 -49
  105. package/dist/image-eviction.d.ts.map +0 -1
  106. package/dist/image-eviction.js +0 -251
  107. package/dist/index.d.ts +0 -718
  108. package/dist/index.d.ts.map +0 -1
  109. package/dist/index.js +0 -1469
  110. package/dist/keystone-scorer.d.ts +0 -51
  111. package/dist/keystone-scorer.d.ts.map +0 -1
  112. package/dist/keystone-scorer.js +0 -52
  113. package/dist/knowledge-graph.d.ts +0 -110
  114. package/dist/knowledge-graph.d.ts.map +0 -1
  115. package/dist/knowledge-graph.js +0 -305
  116. package/dist/knowledge-lint.d.ts +0 -31
  117. package/dist/knowledge-lint.d.ts.map +0 -1
  118. package/dist/knowledge-lint.js +0 -155
  119. package/dist/knowledge-store.d.ts +0 -72
  120. package/dist/knowledge-store.d.ts.map +0 -1
  121. package/dist/knowledge-store.js +0 -247
  122. package/dist/library-schema.d.ts +0 -27
  123. package/dist/library-schema.d.ts.map +0 -1
  124. package/dist/library-schema.js +0 -1343
  125. package/dist/message-store.d.ts +0 -181
  126. package/dist/message-store.d.ts.map +0 -1
  127. package/dist/message-store.js +0 -573
  128. package/dist/metrics-dashboard.d.ts +0 -114
  129. package/dist/metrics-dashboard.d.ts.map +0 -1
  130. package/dist/metrics-dashboard.js +0 -260
  131. package/dist/obsidian-exporter.d.ts +0 -57
  132. package/dist/obsidian-exporter.d.ts.map +0 -1
  133. package/dist/obsidian-exporter.js +0 -274
  134. package/dist/obsidian-watcher.d.ts +0 -147
  135. package/dist/obsidian-watcher.d.ts.map +0 -1
  136. package/dist/obsidian-watcher.js +0 -403
  137. package/dist/open-domain.d.ts +0 -46
  138. package/dist/open-domain.d.ts.map +0 -1
  139. package/dist/open-domain.js +0 -125
  140. package/dist/preference-store.d.ts +0 -54
  141. package/dist/preference-store.d.ts.map +0 -1
  142. package/dist/preference-store.js +0 -109
  143. package/dist/preservation-gate.d.ts +0 -82
  144. package/dist/preservation-gate.d.ts.map +0 -1
  145. package/dist/preservation-gate.js +0 -150
  146. package/dist/proactive-pass.d.ts +0 -63
  147. package/dist/proactive-pass.d.ts.map +0 -1
  148. package/dist/proactive-pass.js +0 -293
  149. package/dist/profiles.d.ts +0 -46
  150. package/dist/profiles.d.ts.map +0 -1
  151. package/dist/profiles.js +0 -262
  152. package/dist/provider-translator.d.ts +0 -50
  153. package/dist/provider-translator.d.ts.map +0 -1
  154. package/dist/provider-translator.js +0 -403
  155. package/dist/rate-limiter.d.ts +0 -76
  156. package/dist/rate-limiter.d.ts.map +0 -1
  157. package/dist/rate-limiter.js +0 -179
  158. package/dist/repair-tool-pairs.d.ts +0 -38
  159. package/dist/repair-tool-pairs.d.ts.map +0 -1
  160. package/dist/repair-tool-pairs.js +0 -209
  161. package/dist/replay-recovery.d.ts +0 -29
  162. package/dist/replay-recovery.d.ts.map +0 -1
  163. package/dist/replay-recovery.js +0 -82
  164. package/dist/reranker.d.ts +0 -95
  165. package/dist/reranker.d.ts.map +0 -1
  166. package/dist/reranker.js +0 -308
  167. package/dist/retrieval-policy.d.ts +0 -51
  168. package/dist/retrieval-policy.d.ts.map +0 -1
  169. package/dist/retrieval-policy.js +0 -77
  170. package/dist/schema.d.ts +0 -15
  171. package/dist/schema.d.ts.map +0 -1
  172. package/dist/schema.js +0 -300
  173. package/dist/secret-scanner.d.ts +0 -51
  174. package/dist/secret-scanner.d.ts.map +0 -1
  175. package/dist/secret-scanner.js +0 -248
  176. package/dist/seed.d.ts +0 -108
  177. package/dist/seed.d.ts.map +0 -1
  178. package/dist/seed.js +0 -177
  179. package/dist/session-flusher.d.ts +0 -53
  180. package/dist/session-flusher.d.ts.map +0 -1
  181. package/dist/session-flusher.js +0 -69
  182. package/dist/session-topic-map.d.ts +0 -41
  183. package/dist/session-topic-map.d.ts.map +0 -1
  184. package/dist/session-topic-map.js +0 -77
  185. package/dist/spawn-context.d.ts +0 -54
  186. package/dist/spawn-context.d.ts.map +0 -1
  187. package/dist/spawn-context.js +0 -159
  188. package/dist/system-store.d.ts +0 -73
  189. package/dist/system-store.d.ts.map +0 -1
  190. package/dist/system-store.js +0 -182
  191. package/dist/temporal-store.d.ts +0 -81
  192. package/dist/temporal-store.d.ts.map +0 -1
  193. package/dist/temporal-store.js +0 -149
  194. package/dist/tool-artifact-store.d.ts +0 -98
  195. package/dist/tool-artifact-store.d.ts.map +0 -1
  196. package/dist/tool-artifact-store.js +0 -244
  197. package/dist/topic-detector.d.ts +0 -35
  198. package/dist/topic-detector.d.ts.map +0 -1
  199. package/dist/topic-detector.js +0 -249
  200. package/dist/topic-store.d.ts +0 -51
  201. package/dist/topic-store.d.ts.map +0 -1
  202. package/dist/topic-store.js +0 -175
  203. package/dist/topic-synthesizer.d.ts +0 -51
  204. package/dist/topic-synthesizer.d.ts.map +0 -1
  205. package/dist/topic-synthesizer.js +0 -316
  206. package/dist/trigger-registry.d.ts +0 -63
  207. package/dist/trigger-registry.d.ts.map +0 -1
  208. package/dist/trigger-registry.js +0 -163
  209. package/dist/types.d.ts +0 -815
  210. package/dist/types.d.ts.map +0 -1
  211. package/dist/types.js +0 -9
  212. package/dist/vector-store.d.ts +0 -180
  213. package/dist/vector-store.d.ts.map +0 -1
  214. package/dist/vector-store.js +0 -1041
  215. package/dist/version.d.ts +0 -34
  216. package/dist/version.d.ts.map +0 -1
  217. package/dist/version.js +0 -34
  218. package/dist/wiki-page-emitter.d.ts +0 -65
  219. package/dist/wiki-page-emitter.d.ts.map +0 -1
  220. package/dist/wiki-page-emitter.js +0 -258
  221. package/dist/work-store.d.ts +0 -112
  222. package/dist/work-store.d.ts.map +0 -1
  223. package/dist/work-store.js +0 -273
  224. package/memory-plugin/dist/index.d.ts +0 -24
  225. package/memory-plugin/dist/index.d.ts.map +0 -1
  226. package/memory-plugin/dist/index.js +0 -300
  227. package/memory-plugin/dist/index.js.map +0 -1
  228. package/node_modules/sqlite-vec/README.md +0 -1
  229. package/node_modules/sqlite-vec/index.cjs +0 -46
  230. package/node_modules/sqlite-vec/index.d.ts +0 -17
  231. package/node_modules/sqlite-vec/index.mjs +0 -47
  232. package/node_modules/sqlite-vec/package.json +0 -1
  233. package/node_modules/sqlite-vec-linux-x64/README.md +0 -1
  234. package/node_modules/sqlite-vec-linux-x64/package.json +0 -1
  235. package/node_modules/sqlite-vec-linux-x64/vec0.so +0 -0
  236. package/node_modules/zod/LICENSE +0 -21
  237. package/node_modules/zod/README.md +0 -208
  238. package/node_modules/zod/index.cjs +0 -33
  239. package/node_modules/zod/index.d.cts +0 -4
  240. package/node_modules/zod/index.d.ts +0 -4
  241. package/node_modules/zod/index.js +0 -4
  242. package/node_modules/zod/locales/index.cjs +0 -17
  243. package/node_modules/zod/locales/index.d.cts +0 -1
  244. package/node_modules/zod/locales/index.d.ts +0 -1
  245. package/node_modules/zod/locales/index.js +0 -1
  246. package/node_modules/zod/locales/package.json +0 -6
  247. package/node_modules/zod/mini/index.cjs +0 -32
  248. package/node_modules/zod/mini/index.d.cts +0 -3
  249. package/node_modules/zod/mini/index.d.ts +0 -3
  250. package/node_modules/zod/mini/index.js +0 -3
  251. package/node_modules/zod/mini/package.json +0 -6
  252. package/node_modules/zod/package.json +0 -135
  253. package/node_modules/zod/src/index.ts +0 -4
  254. package/node_modules/zod/src/locales/index.ts +0 -1
  255. package/node_modules/zod/src/mini/index.ts +0 -3
  256. package/node_modules/zod/src/v3/ZodError.ts +0 -330
  257. package/node_modules/zod/src/v3/benchmarks/datetime.ts +0 -58
  258. package/node_modules/zod/src/v3/benchmarks/discriminatedUnion.ts +0 -80
  259. package/node_modules/zod/src/v3/benchmarks/index.ts +0 -59
  260. package/node_modules/zod/src/v3/benchmarks/ipv4.ts +0 -57
  261. package/node_modules/zod/src/v3/benchmarks/object.ts +0 -69
  262. package/node_modules/zod/src/v3/benchmarks/primitives.ts +0 -162
  263. package/node_modules/zod/src/v3/benchmarks/realworld.ts +0 -63
  264. package/node_modules/zod/src/v3/benchmarks/string.ts +0 -55
  265. package/node_modules/zod/src/v3/benchmarks/union.ts +0 -80
  266. package/node_modules/zod/src/v3/errors.ts +0 -13
  267. package/node_modules/zod/src/v3/external.ts +0 -6
  268. package/node_modules/zod/src/v3/helpers/enumUtil.ts +0 -17
  269. package/node_modules/zod/src/v3/helpers/errorUtil.ts +0 -8
  270. package/node_modules/zod/src/v3/helpers/parseUtil.ts +0 -176
  271. package/node_modules/zod/src/v3/helpers/partialUtil.ts +0 -34
  272. package/node_modules/zod/src/v3/helpers/typeAliases.ts +0 -2
  273. package/node_modules/zod/src/v3/helpers/util.ts +0 -224
  274. package/node_modules/zod/src/v3/index.ts +0 -4
  275. package/node_modules/zod/src/v3/locales/en.ts +0 -124
  276. package/node_modules/zod/src/v3/standard-schema.ts +0 -113
  277. package/node_modules/zod/src/v3/tests/Mocker.ts +0 -54
  278. package/node_modules/zod/src/v3/tests/all-errors.test.ts +0 -157
  279. package/node_modules/zod/src/v3/tests/anyunknown.test.ts +0 -28
  280. package/node_modules/zod/src/v3/tests/array.test.ts +0 -71
  281. package/node_modules/zod/src/v3/tests/async-parsing.test.ts +0 -388
  282. package/node_modules/zod/src/v3/tests/async-refinements.test.ts +0 -46
  283. package/node_modules/zod/src/v3/tests/base.test.ts +0 -29
  284. package/node_modules/zod/src/v3/tests/bigint.test.ts +0 -55
  285. package/node_modules/zod/src/v3/tests/branded.test.ts +0 -53
  286. package/node_modules/zod/src/v3/tests/catch.test.ts +0 -220
  287. package/node_modules/zod/src/v3/tests/coerce.test.ts +0 -133
  288. package/node_modules/zod/src/v3/tests/complex.test.ts +0 -70
  289. package/node_modules/zod/src/v3/tests/custom.test.ts +0 -31
  290. package/node_modules/zod/src/v3/tests/date.test.ts +0 -32
  291. package/node_modules/zod/src/v3/tests/deepmasking.test.ts +0 -186
  292. package/node_modules/zod/src/v3/tests/default.test.ts +0 -112
  293. package/node_modules/zod/src/v3/tests/description.test.ts +0 -33
  294. package/node_modules/zod/src/v3/tests/discriminated-unions.test.ts +0 -315
  295. package/node_modules/zod/src/v3/tests/enum.test.ts +0 -80
  296. package/node_modules/zod/src/v3/tests/error.test.ts +0 -551
  297. package/node_modules/zod/src/v3/tests/firstparty.test.ts +0 -87
  298. package/node_modules/zod/src/v3/tests/firstpartyschematypes.test.ts +0 -21
  299. package/node_modules/zod/src/v3/tests/function.test.ts +0 -261
  300. package/node_modules/zod/src/v3/tests/generics.test.ts +0 -48
  301. package/node_modules/zod/src/v3/tests/instanceof.test.ts +0 -37
  302. package/node_modules/zod/src/v3/tests/intersection.test.ts +0 -110
  303. package/node_modules/zod/src/v3/tests/language-server.source.ts +0 -76
  304. package/node_modules/zod/src/v3/tests/language-server.test.ts +0 -207
  305. package/node_modules/zod/src/v3/tests/literal.test.ts +0 -36
  306. package/node_modules/zod/src/v3/tests/map.test.ts +0 -110
  307. package/node_modules/zod/src/v3/tests/masking.test.ts +0 -4
  308. package/node_modules/zod/src/v3/tests/mocker.test.ts +0 -19
  309. package/node_modules/zod/src/v3/tests/nan.test.ts +0 -24
  310. package/node_modules/zod/src/v3/tests/nativeEnum.test.ts +0 -87
  311. package/node_modules/zod/src/v3/tests/nullable.test.ts +0 -42
  312. package/node_modules/zod/src/v3/tests/number.test.ts +0 -176
  313. package/node_modules/zod/src/v3/tests/object-augmentation.test.ts +0 -29
  314. package/node_modules/zod/src/v3/tests/object-in-es5-env.test.ts +0 -29
  315. package/node_modules/zod/src/v3/tests/object.test.ts +0 -434
  316. package/node_modules/zod/src/v3/tests/optional.test.ts +0 -42
  317. package/node_modules/zod/src/v3/tests/parseUtil.test.ts +0 -23
  318. package/node_modules/zod/src/v3/tests/parser.test.ts +0 -41
  319. package/node_modules/zod/src/v3/tests/partials.test.ts +0 -243
  320. package/node_modules/zod/src/v3/tests/pickomit.test.ts +0 -111
  321. package/node_modules/zod/src/v3/tests/pipeline.test.ts +0 -29
  322. package/node_modules/zod/src/v3/tests/preprocess.test.ts +0 -186
  323. package/node_modules/zod/src/v3/tests/primitive.test.ts +0 -440
  324. package/node_modules/zod/src/v3/tests/promise.test.ts +0 -90
  325. package/node_modules/zod/src/v3/tests/readonly.test.ts +0 -194
  326. package/node_modules/zod/src/v3/tests/record.test.ts +0 -171
  327. package/node_modules/zod/src/v3/tests/recursive.test.ts +0 -197
  328. package/node_modules/zod/src/v3/tests/refine.test.ts +0 -313
  329. package/node_modules/zod/src/v3/tests/safeparse.test.ts +0 -27
  330. package/node_modules/zod/src/v3/tests/set.test.ts +0 -142
  331. package/node_modules/zod/src/v3/tests/standard-schema.test.ts +0 -83
  332. package/node_modules/zod/src/v3/tests/string.test.ts +0 -916
  333. package/node_modules/zod/src/v3/tests/transformer.test.ts +0 -233
  334. package/node_modules/zod/src/v3/tests/tuple.test.ts +0 -90
  335. package/node_modules/zod/src/v3/tests/unions.test.ts +0 -57
  336. package/node_modules/zod/src/v3/tests/validations.test.ts +0 -133
  337. package/node_modules/zod/src/v3/tests/void.test.ts +0 -15
  338. package/node_modules/zod/src/v3/types.ts +0 -5138
  339. package/node_modules/zod/src/v4/classic/checks.ts +0 -32
  340. package/node_modules/zod/src/v4/classic/coerce.ts +0 -27
  341. package/node_modules/zod/src/v4/classic/compat.ts +0 -70
  342. package/node_modules/zod/src/v4/classic/errors.ts +0 -82
  343. package/node_modules/zod/src/v4/classic/external.ts +0 -51
  344. package/node_modules/zod/src/v4/classic/from-json-schema.ts +0 -643
  345. package/node_modules/zod/src/v4/classic/index.ts +0 -5
  346. package/node_modules/zod/src/v4/classic/iso.ts +0 -90
  347. package/node_modules/zod/src/v4/classic/parse.ts +0 -82
  348. package/node_modules/zod/src/v4/classic/schemas.ts +0 -2409
  349. package/node_modules/zod/src/v4/classic/tests/anyunknown.test.ts +0 -26
  350. package/node_modules/zod/src/v4/classic/tests/apply.test.ts +0 -59
  351. package/node_modules/zod/src/v4/classic/tests/array.test.ts +0 -264
  352. package/node_modules/zod/src/v4/classic/tests/assignability.test.ts +0 -210
  353. package/node_modules/zod/src/v4/classic/tests/async-parsing.test.ts +0 -381
  354. package/node_modules/zod/src/v4/classic/tests/async-refinements.test.ts +0 -68
  355. package/node_modules/zod/src/v4/classic/tests/base.test.ts +0 -7
  356. package/node_modules/zod/src/v4/classic/tests/bigint.test.ts +0 -54
  357. package/node_modules/zod/src/v4/classic/tests/brand.test.ts +0 -106
  358. package/node_modules/zod/src/v4/classic/tests/catch.test.ts +0 -276
  359. package/node_modules/zod/src/v4/classic/tests/coalesce.test.ts +0 -20
  360. package/node_modules/zod/src/v4/classic/tests/codec-examples.test.ts +0 -573
  361. package/node_modules/zod/src/v4/classic/tests/codec.test.ts +0 -562
  362. package/node_modules/zod/src/v4/classic/tests/coerce.test.ts +0 -160
  363. package/node_modules/zod/src/v4/classic/tests/continuability.test.ts +0 -374
  364. package/node_modules/zod/src/v4/classic/tests/custom.test.ts +0 -40
  365. package/node_modules/zod/src/v4/classic/tests/date.test.ts +0 -62
  366. package/node_modules/zod/src/v4/classic/tests/datetime.test.ts +0 -302
  367. package/node_modules/zod/src/v4/classic/tests/default.test.ts +0 -365
  368. package/node_modules/zod/src/v4/classic/tests/describe-meta-checks.test.ts +0 -27
  369. package/node_modules/zod/src/v4/classic/tests/description.test.ts +0 -32
  370. package/node_modules/zod/src/v4/classic/tests/discriminated-unions.test.ts +0 -661
  371. package/node_modules/zod/src/v4/classic/tests/enum.test.ts +0 -285
  372. package/node_modules/zod/src/v4/classic/tests/error-utils.test.ts +0 -595
  373. package/node_modules/zod/src/v4/classic/tests/error.test.ts +0 -711
  374. package/node_modules/zod/src/v4/classic/tests/file.test.ts +0 -96
  375. package/node_modules/zod/src/v4/classic/tests/firstparty.test.ts +0 -179
  376. package/node_modules/zod/src/v4/classic/tests/fix-json-issue.test.ts +0 -26
  377. package/node_modules/zod/src/v4/classic/tests/from-json-schema.test.ts +0 -734
  378. package/node_modules/zod/src/v4/classic/tests/function.test.ts +0 -360
  379. package/node_modules/zod/src/v4/classic/tests/generics.test.ts +0 -72
  380. package/node_modules/zod/src/v4/classic/tests/hash.test.ts +0 -68
  381. package/node_modules/zod/src/v4/classic/tests/index.test.ts +0 -939
  382. package/node_modules/zod/src/v4/classic/tests/instanceof.test.ts +0 -60
  383. package/node_modules/zod/src/v4/classic/tests/intersection.test.ts +0 -198
  384. package/node_modules/zod/src/v4/classic/tests/json.test.ts +0 -109
  385. package/node_modules/zod/src/v4/classic/tests/lazy.test.ts +0 -227
  386. package/node_modules/zod/src/v4/classic/tests/literal.test.ts +0 -117
  387. package/node_modules/zod/src/v4/classic/tests/map.test.ts +0 -330
  388. package/node_modules/zod/src/v4/classic/tests/nan.test.ts +0 -21
  389. package/node_modules/zod/src/v4/classic/tests/nested-refine.test.ts +0 -168
  390. package/node_modules/zod/src/v4/classic/tests/nonoptional.test.ts +0 -101
  391. package/node_modules/zod/src/v4/classic/tests/nullable.test.ts +0 -22
  392. package/node_modules/zod/src/v4/classic/tests/number.test.ts +0 -270
  393. package/node_modules/zod/src/v4/classic/tests/object.test.ts +0 -640
  394. package/node_modules/zod/src/v4/classic/tests/optional.test.ts +0 -223
  395. package/node_modules/zod/src/v4/classic/tests/partial.test.ts +0 -427
  396. package/node_modules/zod/src/v4/classic/tests/pickomit.test.ts +0 -211
  397. package/node_modules/zod/src/v4/classic/tests/pipe.test.ts +0 -101
  398. package/node_modules/zod/src/v4/classic/tests/prefault.test.ts +0 -74
  399. package/node_modules/zod/src/v4/classic/tests/preprocess.test.ts +0 -282
  400. package/node_modules/zod/src/v4/classic/tests/primitive.test.ts +0 -175
  401. package/node_modules/zod/src/v4/classic/tests/promise.test.ts +0 -81
  402. package/node_modules/zod/src/v4/classic/tests/prototypes.test.ts +0 -23
  403. package/node_modules/zod/src/v4/classic/tests/readonly.test.ts +0 -252
  404. package/node_modules/zod/src/v4/classic/tests/record.test.ts +0 -632
  405. package/node_modules/zod/src/v4/classic/tests/recursive-types.test.ts +0 -582
  406. package/node_modules/zod/src/v4/classic/tests/refine.test.ts +0 -570
  407. package/node_modules/zod/src/v4/classic/tests/registries.test.ts +0 -243
  408. package/node_modules/zod/src/v4/classic/tests/set.test.ts +0 -181
  409. package/node_modules/zod/src/v4/classic/tests/standard-schema.test.ts +0 -134
  410. package/node_modules/zod/src/v4/classic/tests/string-formats.test.ts +0 -125
  411. package/node_modules/zod/src/v4/classic/tests/string.test.ts +0 -1175
  412. package/node_modules/zod/src/v4/classic/tests/stringbool.test.ts +0 -106
  413. package/node_modules/zod/src/v4/classic/tests/template-literal.test.ts +0 -771
  414. package/node_modules/zod/src/v4/classic/tests/to-json-schema-methods.test.ts +0 -438
  415. package/node_modules/zod/src/v4/classic/tests/to-json-schema.test.ts +0 -2990
  416. package/node_modules/zod/src/v4/classic/tests/transform.test.ts +0 -361
  417. package/node_modules/zod/src/v4/classic/tests/tuple.test.ts +0 -183
  418. package/node_modules/zod/src/v4/classic/tests/union.test.ts +0 -219
  419. package/node_modules/zod/src/v4/classic/tests/url.test.ts +0 -13
  420. package/node_modules/zod/src/v4/classic/tests/validations.test.ts +0 -283
  421. package/node_modules/zod/src/v4/classic/tests/void.test.ts +0 -12
  422. package/node_modules/zod/src/v4/core/api.ts +0 -1798
  423. package/node_modules/zod/src/v4/core/checks.ts +0 -1293
  424. package/node_modules/zod/src/v4/core/config.ts +0 -15
  425. package/node_modules/zod/src/v4/core/core.ts +0 -138
  426. package/node_modules/zod/src/v4/core/doc.ts +0 -44
  427. package/node_modules/zod/src/v4/core/errors.ts +0 -448
  428. package/node_modules/zod/src/v4/core/index.ts +0 -16
  429. package/node_modules/zod/src/v4/core/json-schema-generator.ts +0 -126
  430. package/node_modules/zod/src/v4/core/json-schema-processors.ts +0 -667
  431. package/node_modules/zod/src/v4/core/json-schema.ts +0 -147
  432. package/node_modules/zod/src/v4/core/parse.ts +0 -195
  433. package/node_modules/zod/src/v4/core/regexes.ts +0 -183
  434. package/node_modules/zod/src/v4/core/registries.ts +0 -105
  435. package/node_modules/zod/src/v4/core/schemas.ts +0 -4538
  436. package/node_modules/zod/src/v4/core/standard-schema.ts +0 -159
  437. package/node_modules/zod/src/v4/core/tests/extend.test.ts +0 -59
  438. package/node_modules/zod/src/v4/core/tests/index.test.ts +0 -46
  439. package/node_modules/zod/src/v4/core/tests/locales/be.test.ts +0 -124
  440. package/node_modules/zod/src/v4/core/tests/locales/en.test.ts +0 -22
  441. package/node_modules/zod/src/v4/core/tests/locales/es.test.ts +0 -181
  442. package/node_modules/zod/src/v4/core/tests/locales/he.test.ts +0 -379
  443. package/node_modules/zod/src/v4/core/tests/locales/nl.test.ts +0 -46
  444. package/node_modules/zod/src/v4/core/tests/locales/ru.test.ts +0 -128
  445. package/node_modules/zod/src/v4/core/tests/locales/tr.test.ts +0 -69
  446. package/node_modules/zod/src/v4/core/tests/locales/uz.test.ts +0 -83
  447. package/node_modules/zod/src/v4/core/tests/record-constructor.test.ts +0 -67
  448. package/node_modules/zod/src/v4/core/tests/recursive-tuples.test.ts +0 -45
  449. package/node_modules/zod/src/v4/core/to-json-schema.ts +0 -613
  450. package/node_modules/zod/src/v4/core/util.ts +0 -966
  451. package/node_modules/zod/src/v4/core/versions.ts +0 -5
  452. package/node_modules/zod/src/v4/core/zsf.ts +0 -323
  453. package/node_modules/zod/src/v4/index.ts +0 -4
  454. package/node_modules/zod/src/v4/locales/ar.ts +0 -115
  455. package/node_modules/zod/src/v4/locales/az.ts +0 -111
  456. package/node_modules/zod/src/v4/locales/be.ts +0 -176
  457. package/node_modules/zod/src/v4/locales/bg.ts +0 -128
  458. package/node_modules/zod/src/v4/locales/ca.ts +0 -116
  459. package/node_modules/zod/src/v4/locales/cs.ts +0 -118
  460. package/node_modules/zod/src/v4/locales/da.ts +0 -123
  461. package/node_modules/zod/src/v4/locales/de.ts +0 -116
  462. package/node_modules/zod/src/v4/locales/en.ts +0 -119
  463. package/node_modules/zod/src/v4/locales/eo.ts +0 -118
  464. package/node_modules/zod/src/v4/locales/es.ts +0 -141
  465. package/node_modules/zod/src/v4/locales/fa.ts +0 -126
  466. package/node_modules/zod/src/v4/locales/fi.ts +0 -121
  467. package/node_modules/zod/src/v4/locales/fr-CA.ts +0 -116
  468. package/node_modules/zod/src/v4/locales/fr.ts +0 -116
  469. package/node_modules/zod/src/v4/locales/he.ts +0 -246
  470. package/node_modules/zod/src/v4/locales/hu.ts +0 -117
  471. package/node_modules/zod/src/v4/locales/hy.ts +0 -164
  472. package/node_modules/zod/src/v4/locales/id.ts +0 -115
  473. package/node_modules/zod/src/v4/locales/index.ts +0 -49
  474. package/node_modules/zod/src/v4/locales/is.ts +0 -119
  475. package/node_modules/zod/src/v4/locales/it.ts +0 -116
  476. package/node_modules/zod/src/v4/locales/ja.ts +0 -114
  477. package/node_modules/zod/src/v4/locales/ka.ts +0 -123
  478. package/node_modules/zod/src/v4/locales/kh.ts +0 -7
  479. package/node_modules/zod/src/v4/locales/km.ts +0 -119
  480. package/node_modules/zod/src/v4/locales/ko.ts +0 -121
  481. package/node_modules/zod/src/v4/locales/lt.ts +0 -239
  482. package/node_modules/zod/src/v4/locales/mk.ts +0 -118
  483. package/node_modules/zod/src/v4/locales/ms.ts +0 -115
  484. package/node_modules/zod/src/v4/locales/nl.ts +0 -121
  485. package/node_modules/zod/src/v4/locales/no.ts +0 -116
  486. package/node_modules/zod/src/v4/locales/ota.ts +0 -117
  487. package/node_modules/zod/src/v4/locales/pl.ts +0 -118
  488. package/node_modules/zod/src/v4/locales/ps.ts +0 -126
  489. package/node_modules/zod/src/v4/locales/pt.ts +0 -116
  490. package/node_modules/zod/src/v4/locales/ru.ts +0 -176
  491. package/node_modules/zod/src/v4/locales/sl.ts +0 -118
  492. package/node_modules/zod/src/v4/locales/sv.ts +0 -119
  493. package/node_modules/zod/src/v4/locales/ta.ts +0 -118
  494. package/node_modules/zod/src/v4/locales/th.ts +0 -119
  495. package/node_modules/zod/src/v4/locales/tr.ts +0 -111
  496. package/node_modules/zod/src/v4/locales/ua.ts +0 -7
  497. package/node_modules/zod/src/v4/locales/uk.ts +0 -117
  498. package/node_modules/zod/src/v4/locales/ur.ts +0 -119
  499. package/node_modules/zod/src/v4/locales/uz.ts +0 -116
  500. package/node_modules/zod/src/v4/locales/vi.ts +0 -117
  501. package/node_modules/zod/src/v4/locales/yo.ts +0 -124
  502. package/node_modules/zod/src/v4/locales/zh-CN.ts +0 -116
  503. package/node_modules/zod/src/v4/locales/zh-TW.ts +0 -115
  504. package/node_modules/zod/src/v4/mini/checks.ts +0 -32
  505. package/node_modules/zod/src/v4/mini/coerce.ts +0 -27
  506. package/node_modules/zod/src/v4/mini/external.ts +0 -40
  507. package/node_modules/zod/src/v4/mini/index.ts +0 -3
  508. package/node_modules/zod/src/v4/mini/iso.ts +0 -66
  509. package/node_modules/zod/src/v4/mini/parse.ts +0 -14
  510. package/node_modules/zod/src/v4/mini/schemas.ts +0 -1916
  511. package/node_modules/zod/src/v4/mini/tests/apply.test.ts +0 -24
  512. package/node_modules/zod/src/v4/mini/tests/assignability.test.ts +0 -129
  513. package/node_modules/zod/src/v4/mini/tests/brand.test.ts +0 -94
  514. package/node_modules/zod/src/v4/mini/tests/checks.test.ts +0 -144
  515. package/node_modules/zod/src/v4/mini/tests/codec.test.ts +0 -529
  516. package/node_modules/zod/src/v4/mini/tests/computed.test.ts +0 -36
  517. package/node_modules/zod/src/v4/mini/tests/error.test.ts +0 -22
  518. package/node_modules/zod/src/v4/mini/tests/functions.test.ts +0 -5
  519. package/node_modules/zod/src/v4/mini/tests/index.test.ts +0 -963
  520. package/node_modules/zod/src/v4/mini/tests/number.test.ts +0 -95
  521. package/node_modules/zod/src/v4/mini/tests/object.test.ts +0 -227
  522. package/node_modules/zod/src/v4/mini/tests/prototypes.test.ts +0 -43
  523. package/node_modules/zod/src/v4/mini/tests/recursive-types.test.ts +0 -275
  524. package/node_modules/zod/src/v4/mini/tests/standard-schema.test.ts +0 -50
  525. package/node_modules/zod/src/v4/mini/tests/string.test.ts +0 -347
  526. package/node_modules/zod/src/v4-mini/index.ts +0 -3
  527. package/node_modules/zod/v3/ZodError.cjs +0 -138
  528. package/node_modules/zod/v3/ZodError.d.cts +0 -164
  529. package/node_modules/zod/v3/ZodError.d.ts +0 -164
  530. package/node_modules/zod/v3/ZodError.js +0 -133
  531. package/node_modules/zod/v3/errors.cjs +0 -17
  532. package/node_modules/zod/v3/errors.d.cts +0 -5
  533. package/node_modules/zod/v3/errors.d.ts +0 -5
  534. package/node_modules/zod/v3/errors.js +0 -9
  535. package/node_modules/zod/v3/external.cjs +0 -22
  536. package/node_modules/zod/v3/external.d.cts +0 -6
  537. package/node_modules/zod/v3/external.d.ts +0 -6
  538. package/node_modules/zod/v3/external.js +0 -6
  539. package/node_modules/zod/v3/helpers/enumUtil.cjs +0 -2
  540. package/node_modules/zod/v3/helpers/enumUtil.d.cts +0 -8
  541. package/node_modules/zod/v3/helpers/enumUtil.d.ts +0 -8
  542. package/node_modules/zod/v3/helpers/enumUtil.js +0 -1
  543. package/node_modules/zod/v3/helpers/errorUtil.cjs +0 -9
  544. package/node_modules/zod/v3/helpers/errorUtil.d.cts +0 -9
  545. package/node_modules/zod/v3/helpers/errorUtil.d.ts +0 -9
  546. package/node_modules/zod/v3/helpers/errorUtil.js +0 -6
  547. package/node_modules/zod/v3/helpers/parseUtil.cjs +0 -124
  548. package/node_modules/zod/v3/helpers/parseUtil.d.cts +0 -78
  549. package/node_modules/zod/v3/helpers/parseUtil.d.ts +0 -78
  550. package/node_modules/zod/v3/helpers/parseUtil.js +0 -109
  551. package/node_modules/zod/v3/helpers/partialUtil.cjs +0 -2
  552. package/node_modules/zod/v3/helpers/partialUtil.d.cts +0 -8
  553. package/node_modules/zod/v3/helpers/partialUtil.d.ts +0 -8
  554. package/node_modules/zod/v3/helpers/partialUtil.js +0 -1
  555. package/node_modules/zod/v3/helpers/typeAliases.cjs +0 -2
  556. package/node_modules/zod/v3/helpers/typeAliases.d.cts +0 -2
  557. package/node_modules/zod/v3/helpers/typeAliases.d.ts +0 -2
  558. package/node_modules/zod/v3/helpers/typeAliases.js +0 -1
  559. package/node_modules/zod/v3/helpers/util.cjs +0 -137
  560. package/node_modules/zod/v3/helpers/util.d.cts +0 -85
  561. package/node_modules/zod/v3/helpers/util.d.ts +0 -85
  562. package/node_modules/zod/v3/helpers/util.js +0 -133
  563. package/node_modules/zod/v3/index.cjs +0 -33
  564. package/node_modules/zod/v3/index.d.cts +0 -4
  565. package/node_modules/zod/v3/index.d.ts +0 -4
  566. package/node_modules/zod/v3/index.js +0 -4
  567. package/node_modules/zod/v3/locales/en.cjs +0 -112
  568. package/node_modules/zod/v3/locales/en.d.cts +0 -3
  569. package/node_modules/zod/v3/locales/en.d.ts +0 -3
  570. package/node_modules/zod/v3/locales/en.js +0 -109
  571. package/node_modules/zod/v3/package.json +0 -6
  572. package/node_modules/zod/v3/standard-schema.cjs +0 -2
  573. package/node_modules/zod/v3/standard-schema.d.cts +0 -102
  574. package/node_modules/zod/v3/standard-schema.d.ts +0 -102
  575. package/node_modules/zod/v3/standard-schema.js +0 -1
  576. package/node_modules/zod/v3/types.cjs +0 -3777
  577. package/node_modules/zod/v3/types.d.cts +0 -1034
  578. package/node_modules/zod/v3/types.d.ts +0 -1034
  579. package/node_modules/zod/v3/types.js +0 -3695
  580. package/node_modules/zod/v4/classic/checks.cjs +0 -33
  581. package/node_modules/zod/v4/classic/checks.d.cts +0 -1
  582. package/node_modules/zod/v4/classic/checks.d.ts +0 -1
  583. package/node_modules/zod/v4/classic/checks.js +0 -1
  584. package/node_modules/zod/v4/classic/coerce.cjs +0 -47
  585. package/node_modules/zod/v4/classic/coerce.d.cts +0 -17
  586. package/node_modules/zod/v4/classic/coerce.d.ts +0 -17
  587. package/node_modules/zod/v4/classic/coerce.js +0 -17
  588. package/node_modules/zod/v4/classic/compat.cjs +0 -61
  589. package/node_modules/zod/v4/classic/compat.d.cts +0 -50
  590. package/node_modules/zod/v4/classic/compat.d.ts +0 -50
  591. package/node_modules/zod/v4/classic/compat.js +0 -31
  592. package/node_modules/zod/v4/classic/errors.cjs +0 -74
  593. package/node_modules/zod/v4/classic/errors.d.cts +0 -30
  594. package/node_modules/zod/v4/classic/errors.d.ts +0 -30
  595. package/node_modules/zod/v4/classic/errors.js +0 -48
  596. package/node_modules/zod/v4/classic/external.cjs +0 -73
  597. package/node_modules/zod/v4/classic/external.d.cts +0 -15
  598. package/node_modules/zod/v4/classic/external.d.ts +0 -15
  599. package/node_modules/zod/v4/classic/external.js +0 -20
  600. package/node_modules/zod/v4/classic/from-json-schema.cjs +0 -610
  601. package/node_modules/zod/v4/classic/from-json-schema.d.cts +0 -12
  602. package/node_modules/zod/v4/classic/from-json-schema.d.ts +0 -12
  603. package/node_modules/zod/v4/classic/from-json-schema.js +0 -584
  604. package/node_modules/zod/v4/classic/index.cjs +0 -33
  605. package/node_modules/zod/v4/classic/index.d.cts +0 -4
  606. package/node_modules/zod/v4/classic/index.d.ts +0 -4
  607. package/node_modules/zod/v4/classic/index.js +0 -4
  608. package/node_modules/zod/v4/classic/iso.cjs +0 -60
  609. package/node_modules/zod/v4/classic/iso.d.cts +0 -22
  610. package/node_modules/zod/v4/classic/iso.d.ts +0 -22
  611. package/node_modules/zod/v4/classic/iso.js +0 -30
  612. package/node_modules/zod/v4/classic/package.json +0 -6
  613. package/node_modules/zod/v4/classic/parse.cjs +0 -41
  614. package/node_modules/zod/v4/classic/parse.d.cts +0 -31
  615. package/node_modules/zod/v4/classic/parse.d.ts +0 -31
  616. package/node_modules/zod/v4/classic/parse.js +0 -15
  617. package/node_modules/zod/v4/classic/schemas.cjs +0 -1272
  618. package/node_modules/zod/v4/classic/schemas.d.cts +0 -739
  619. package/node_modules/zod/v4/classic/schemas.d.ts +0 -739
  620. package/node_modules/zod/v4/classic/schemas.js +0 -1157
  621. package/node_modules/zod/v4/core/api.cjs +0 -1222
  622. package/node_modules/zod/v4/core/api.d.cts +0 -304
  623. package/node_modules/zod/v4/core/api.d.ts +0 -304
  624. package/node_modules/zod/v4/core/api.js +0 -1082
  625. package/node_modules/zod/v4/core/checks.cjs +0 -601
  626. package/node_modules/zod/v4/core/checks.d.cts +0 -278
  627. package/node_modules/zod/v4/core/checks.d.ts +0 -278
  628. package/node_modules/zod/v4/core/checks.js +0 -575
  629. package/node_modules/zod/v4/core/core.cjs +0 -83
  630. package/node_modules/zod/v4/core/core.d.cts +0 -70
  631. package/node_modules/zod/v4/core/core.d.ts +0 -70
  632. package/node_modules/zod/v4/core/core.js +0 -76
  633. package/node_modules/zod/v4/core/doc.cjs +0 -39
  634. package/node_modules/zod/v4/core/doc.d.cts +0 -14
  635. package/node_modules/zod/v4/core/doc.d.ts +0 -14
  636. package/node_modules/zod/v4/core/doc.js +0 -35
  637. package/node_modules/zod/v4/core/errors.cjs +0 -213
  638. package/node_modules/zod/v4/core/errors.d.cts +0 -220
  639. package/node_modules/zod/v4/core/errors.d.ts +0 -220
  640. package/node_modules/zod/v4/core/errors.js +0 -182
  641. package/node_modules/zod/v4/core/index.cjs +0 -47
  642. package/node_modules/zod/v4/core/index.d.cts +0 -16
  643. package/node_modules/zod/v4/core/index.d.ts +0 -16
  644. package/node_modules/zod/v4/core/index.js +0 -16
  645. package/node_modules/zod/v4/core/json-schema-generator.cjs +0 -99
  646. package/node_modules/zod/v4/core/json-schema-generator.d.cts +0 -65
  647. package/node_modules/zod/v4/core/json-schema-generator.d.ts +0 -65
  648. package/node_modules/zod/v4/core/json-schema-generator.js +0 -95
  649. package/node_modules/zod/v4/core/json-schema-processors.cjs +0 -648
  650. package/node_modules/zod/v4/core/json-schema-processors.d.cts +0 -49
  651. package/node_modules/zod/v4/core/json-schema-processors.d.ts +0 -49
  652. package/node_modules/zod/v4/core/json-schema-processors.js +0 -605
  653. package/node_modules/zod/v4/core/json-schema.cjs +0 -2
  654. package/node_modules/zod/v4/core/json-schema.d.cts +0 -88
  655. package/node_modules/zod/v4/core/json-schema.d.ts +0 -88
  656. package/node_modules/zod/v4/core/json-schema.js +0 -1
  657. package/node_modules/zod/v4/core/package.json +0 -6
  658. package/node_modules/zod/v4/core/parse.cjs +0 -131
  659. package/node_modules/zod/v4/core/parse.d.cts +0 -49
  660. package/node_modules/zod/v4/core/parse.d.ts +0 -49
  661. package/node_modules/zod/v4/core/parse.js +0 -93
  662. package/node_modules/zod/v4/core/regexes.cjs +0 -166
  663. package/node_modules/zod/v4/core/regexes.d.cts +0 -79
  664. package/node_modules/zod/v4/core/regexes.d.ts +0 -79
  665. package/node_modules/zod/v4/core/regexes.js +0 -133
  666. package/node_modules/zod/v4/core/registries.cjs +0 -56
  667. package/node_modules/zod/v4/core/registries.d.cts +0 -35
  668. package/node_modules/zod/v4/core/registries.d.ts +0 -35
  669. package/node_modules/zod/v4/core/registries.js +0 -51
  670. package/node_modules/zod/v4/core/schemas.cjs +0 -2124
  671. package/node_modules/zod/v4/core/schemas.d.cts +0 -1146
  672. package/node_modules/zod/v4/core/schemas.d.ts +0 -1146
  673. package/node_modules/zod/v4/core/schemas.js +0 -2093
  674. package/node_modules/zod/v4/core/standard-schema.cjs +0 -2
  675. package/node_modules/zod/v4/core/standard-schema.d.cts +0 -126
  676. package/node_modules/zod/v4/core/standard-schema.d.ts +0 -126
  677. package/node_modules/zod/v4/core/standard-schema.js +0 -1
  678. package/node_modules/zod/v4/core/to-json-schema.cjs +0 -446
  679. package/node_modules/zod/v4/core/to-json-schema.d.cts +0 -114
  680. package/node_modules/zod/v4/core/to-json-schema.d.ts +0 -114
  681. package/node_modules/zod/v4/core/to-json-schema.js +0 -437
  682. package/node_modules/zod/v4/core/util.cjs +0 -710
  683. package/node_modules/zod/v4/core/util.d.cts +0 -199
  684. package/node_modules/zod/v4/core/util.d.ts +0 -199
  685. package/node_modules/zod/v4/core/util.js +0 -651
  686. package/node_modules/zod/v4/core/versions.cjs +0 -8
  687. package/node_modules/zod/v4/core/versions.d.cts +0 -5
  688. package/node_modules/zod/v4/core/versions.d.ts +0 -5
  689. package/node_modules/zod/v4/core/versions.js +0 -5
  690. package/node_modules/zod/v4/index.cjs +0 -22
  691. package/node_modules/zod/v4/index.d.cts +0 -3
  692. package/node_modules/zod/v4/index.d.ts +0 -3
  693. package/node_modules/zod/v4/index.js +0 -3
  694. package/node_modules/zod/v4/locales/ar.cjs +0 -133
  695. package/node_modules/zod/v4/locales/ar.d.cts +0 -5
  696. package/node_modules/zod/v4/locales/ar.d.ts +0 -4
  697. package/node_modules/zod/v4/locales/ar.js +0 -106
  698. package/node_modules/zod/v4/locales/az.cjs +0 -132
  699. package/node_modules/zod/v4/locales/az.d.cts +0 -5
  700. package/node_modules/zod/v4/locales/az.d.ts +0 -4
  701. package/node_modules/zod/v4/locales/az.js +0 -105
  702. package/node_modules/zod/v4/locales/be.cjs +0 -183
  703. package/node_modules/zod/v4/locales/be.d.cts +0 -5
  704. package/node_modules/zod/v4/locales/be.d.ts +0 -4
  705. package/node_modules/zod/v4/locales/be.js +0 -156
  706. package/node_modules/zod/v4/locales/bg.cjs +0 -147
  707. package/node_modules/zod/v4/locales/bg.d.cts +0 -5
  708. package/node_modules/zod/v4/locales/bg.d.ts +0 -4
  709. package/node_modules/zod/v4/locales/bg.js +0 -120
  710. package/node_modules/zod/v4/locales/ca.cjs +0 -134
  711. package/node_modules/zod/v4/locales/ca.d.cts +0 -5
  712. package/node_modules/zod/v4/locales/ca.d.ts +0 -4
  713. package/node_modules/zod/v4/locales/ca.js +0 -107
  714. package/node_modules/zod/v4/locales/cs.cjs +0 -138
  715. package/node_modules/zod/v4/locales/cs.d.cts +0 -5
  716. package/node_modules/zod/v4/locales/cs.d.ts +0 -4
  717. package/node_modules/zod/v4/locales/cs.js +0 -111
  718. package/node_modules/zod/v4/locales/da.cjs +0 -142
  719. package/node_modules/zod/v4/locales/da.d.cts +0 -5
  720. package/node_modules/zod/v4/locales/da.d.ts +0 -4
  721. package/node_modules/zod/v4/locales/da.js +0 -115
  722. package/node_modules/zod/v4/locales/de.cjs +0 -135
  723. package/node_modules/zod/v4/locales/de.d.cts +0 -5
  724. package/node_modules/zod/v4/locales/de.d.ts +0 -4
  725. package/node_modules/zod/v4/locales/de.js +0 -108
  726. package/node_modules/zod/v4/locales/en.cjs +0 -136
  727. package/node_modules/zod/v4/locales/en.d.cts +0 -5
  728. package/node_modules/zod/v4/locales/en.d.ts +0 -4
  729. package/node_modules/zod/v4/locales/en.js +0 -109
  730. package/node_modules/zod/v4/locales/eo.cjs +0 -136
  731. package/node_modules/zod/v4/locales/eo.d.cts +0 -5
  732. package/node_modules/zod/v4/locales/eo.d.ts +0 -4
  733. package/node_modules/zod/v4/locales/eo.js +0 -109
  734. package/node_modules/zod/v4/locales/es.cjs +0 -159
  735. package/node_modules/zod/v4/locales/es.d.cts +0 -5
  736. package/node_modules/zod/v4/locales/es.d.ts +0 -4
  737. package/node_modules/zod/v4/locales/es.js +0 -132
  738. package/node_modules/zod/v4/locales/fa.cjs +0 -141
  739. package/node_modules/zod/v4/locales/fa.d.cts +0 -5
  740. package/node_modules/zod/v4/locales/fa.d.ts +0 -4
  741. package/node_modules/zod/v4/locales/fa.js +0 -114
  742. package/node_modules/zod/v4/locales/fi.cjs +0 -139
  743. package/node_modules/zod/v4/locales/fi.d.cts +0 -5
  744. package/node_modules/zod/v4/locales/fi.d.ts +0 -4
  745. package/node_modules/zod/v4/locales/fi.js +0 -112
  746. package/node_modules/zod/v4/locales/fr-CA.cjs +0 -134
  747. package/node_modules/zod/v4/locales/fr-CA.d.cts +0 -5
  748. package/node_modules/zod/v4/locales/fr-CA.d.ts +0 -4
  749. package/node_modules/zod/v4/locales/fr-CA.js +0 -107
  750. package/node_modules/zod/v4/locales/fr.cjs +0 -135
  751. package/node_modules/zod/v4/locales/fr.d.cts +0 -5
  752. package/node_modules/zod/v4/locales/fr.d.ts +0 -4
  753. package/node_modules/zod/v4/locales/fr.js +0 -108
  754. package/node_modules/zod/v4/locales/he.cjs +0 -241
  755. package/node_modules/zod/v4/locales/he.d.cts +0 -5
  756. package/node_modules/zod/v4/locales/he.d.ts +0 -4
  757. package/node_modules/zod/v4/locales/he.js +0 -214
  758. package/node_modules/zod/v4/locales/hu.cjs +0 -135
  759. package/node_modules/zod/v4/locales/hu.d.cts +0 -5
  760. package/node_modules/zod/v4/locales/hu.d.ts +0 -4
  761. package/node_modules/zod/v4/locales/hu.js +0 -108
  762. package/node_modules/zod/v4/locales/hy.cjs +0 -174
  763. package/node_modules/zod/v4/locales/hy.d.cts +0 -5
  764. package/node_modules/zod/v4/locales/hy.d.ts +0 -4
  765. package/node_modules/zod/v4/locales/hy.js +0 -147
  766. package/node_modules/zod/v4/locales/id.cjs +0 -133
  767. package/node_modules/zod/v4/locales/id.d.cts +0 -5
  768. package/node_modules/zod/v4/locales/id.d.ts +0 -4
  769. package/node_modules/zod/v4/locales/id.js +0 -106
  770. package/node_modules/zod/v4/locales/index.cjs +0 -104
  771. package/node_modules/zod/v4/locales/index.d.cts +0 -49
  772. package/node_modules/zod/v4/locales/index.d.ts +0 -49
  773. package/node_modules/zod/v4/locales/index.js +0 -49
  774. package/node_modules/zod/v4/locales/is.cjs +0 -136
  775. package/node_modules/zod/v4/locales/is.d.cts +0 -5
  776. package/node_modules/zod/v4/locales/is.d.ts +0 -4
  777. package/node_modules/zod/v4/locales/is.js +0 -109
  778. package/node_modules/zod/v4/locales/it.cjs +0 -135
  779. package/node_modules/zod/v4/locales/it.d.cts +0 -5
  780. package/node_modules/zod/v4/locales/it.d.ts +0 -4
  781. package/node_modules/zod/v4/locales/it.js +0 -108
  782. package/node_modules/zod/v4/locales/ja.cjs +0 -134
  783. package/node_modules/zod/v4/locales/ja.d.cts +0 -5
  784. package/node_modules/zod/v4/locales/ja.d.ts +0 -4
  785. package/node_modules/zod/v4/locales/ja.js +0 -107
  786. package/node_modules/zod/v4/locales/ka.cjs +0 -139
  787. package/node_modules/zod/v4/locales/ka.d.cts +0 -5
  788. package/node_modules/zod/v4/locales/ka.d.ts +0 -4
  789. package/node_modules/zod/v4/locales/ka.js +0 -112
  790. package/node_modules/zod/v4/locales/kh.cjs +0 -12
  791. package/node_modules/zod/v4/locales/kh.d.cts +0 -5
  792. package/node_modules/zod/v4/locales/kh.d.ts +0 -5
  793. package/node_modules/zod/v4/locales/kh.js +0 -5
  794. package/node_modules/zod/v4/locales/km.cjs +0 -137
  795. package/node_modules/zod/v4/locales/km.d.cts +0 -5
  796. package/node_modules/zod/v4/locales/km.d.ts +0 -4
  797. package/node_modules/zod/v4/locales/km.js +0 -110
  798. package/node_modules/zod/v4/locales/ko.cjs +0 -138
  799. package/node_modules/zod/v4/locales/ko.d.cts +0 -5
  800. package/node_modules/zod/v4/locales/ko.d.ts +0 -4
  801. package/node_modules/zod/v4/locales/ko.js +0 -111
  802. package/node_modules/zod/v4/locales/lt.cjs +0 -230
  803. package/node_modules/zod/v4/locales/lt.d.cts +0 -5
  804. package/node_modules/zod/v4/locales/lt.d.ts +0 -4
  805. package/node_modules/zod/v4/locales/lt.js +0 -203
  806. package/node_modules/zod/v4/locales/mk.cjs +0 -136
  807. package/node_modules/zod/v4/locales/mk.d.cts +0 -5
  808. package/node_modules/zod/v4/locales/mk.d.ts +0 -4
  809. package/node_modules/zod/v4/locales/mk.js +0 -109
  810. package/node_modules/zod/v4/locales/ms.cjs +0 -134
  811. package/node_modules/zod/v4/locales/ms.d.cts +0 -5
  812. package/node_modules/zod/v4/locales/ms.d.ts +0 -4
  813. package/node_modules/zod/v4/locales/ms.js +0 -107
  814. package/node_modules/zod/v4/locales/nl.cjs +0 -137
  815. package/node_modules/zod/v4/locales/nl.d.cts +0 -5
  816. package/node_modules/zod/v4/locales/nl.d.ts +0 -4
  817. package/node_modules/zod/v4/locales/nl.js +0 -110
  818. package/node_modules/zod/v4/locales/no.cjs +0 -135
  819. package/node_modules/zod/v4/locales/no.d.cts +0 -5
  820. package/node_modules/zod/v4/locales/no.d.ts +0 -4
  821. package/node_modules/zod/v4/locales/no.js +0 -108
  822. package/node_modules/zod/v4/locales/ota.cjs +0 -136
  823. package/node_modules/zod/v4/locales/ota.d.cts +0 -5
  824. package/node_modules/zod/v4/locales/ota.d.ts +0 -4
  825. package/node_modules/zod/v4/locales/ota.js +0 -109
  826. package/node_modules/zod/v4/locales/package.json +0 -6
  827. package/node_modules/zod/v4/locales/pl.cjs +0 -136
  828. package/node_modules/zod/v4/locales/pl.d.cts +0 -5
  829. package/node_modules/zod/v4/locales/pl.d.ts +0 -4
  830. package/node_modules/zod/v4/locales/pl.js +0 -109
  831. package/node_modules/zod/v4/locales/ps.cjs +0 -141
  832. package/node_modules/zod/v4/locales/ps.d.cts +0 -5
  833. package/node_modules/zod/v4/locales/ps.d.ts +0 -4
  834. package/node_modules/zod/v4/locales/ps.js +0 -114
  835. package/node_modules/zod/v4/locales/pt.cjs +0 -135
  836. package/node_modules/zod/v4/locales/pt.d.cts +0 -5
  837. package/node_modules/zod/v4/locales/pt.d.ts +0 -4
  838. package/node_modules/zod/v4/locales/pt.js +0 -108
  839. package/node_modules/zod/v4/locales/ru.cjs +0 -183
  840. package/node_modules/zod/v4/locales/ru.d.cts +0 -5
  841. package/node_modules/zod/v4/locales/ru.d.ts +0 -4
  842. package/node_modules/zod/v4/locales/ru.js +0 -156
  843. package/node_modules/zod/v4/locales/sl.cjs +0 -136
  844. package/node_modules/zod/v4/locales/sl.d.cts +0 -5
  845. package/node_modules/zod/v4/locales/sl.d.ts +0 -4
  846. package/node_modules/zod/v4/locales/sl.js +0 -109
  847. package/node_modules/zod/v4/locales/sv.cjs +0 -137
  848. package/node_modules/zod/v4/locales/sv.d.cts +0 -5
  849. package/node_modules/zod/v4/locales/sv.d.ts +0 -4
  850. package/node_modules/zod/v4/locales/sv.js +0 -110
  851. package/node_modules/zod/v4/locales/ta.cjs +0 -137
  852. package/node_modules/zod/v4/locales/ta.d.cts +0 -5
  853. package/node_modules/zod/v4/locales/ta.d.ts +0 -4
  854. package/node_modules/zod/v4/locales/ta.js +0 -110
  855. package/node_modules/zod/v4/locales/th.cjs +0 -137
  856. package/node_modules/zod/v4/locales/th.d.cts +0 -5
  857. package/node_modules/zod/v4/locales/th.d.ts +0 -4
  858. package/node_modules/zod/v4/locales/th.js +0 -110
  859. package/node_modules/zod/v4/locales/tr.cjs +0 -132
  860. package/node_modules/zod/v4/locales/tr.d.cts +0 -5
  861. package/node_modules/zod/v4/locales/tr.d.ts +0 -4
  862. package/node_modules/zod/v4/locales/tr.js +0 -105
  863. package/node_modules/zod/v4/locales/ua.cjs +0 -12
  864. package/node_modules/zod/v4/locales/ua.d.cts +0 -5
  865. package/node_modules/zod/v4/locales/ua.d.ts +0 -5
  866. package/node_modules/zod/v4/locales/ua.js +0 -5
  867. package/node_modules/zod/v4/locales/uk.cjs +0 -135
  868. package/node_modules/zod/v4/locales/uk.d.cts +0 -5
  869. package/node_modules/zod/v4/locales/uk.d.ts +0 -4
  870. package/node_modules/zod/v4/locales/uk.js +0 -108
  871. package/node_modules/zod/v4/locales/ur.cjs +0 -137
  872. package/node_modules/zod/v4/locales/ur.d.cts +0 -5
  873. package/node_modules/zod/v4/locales/ur.d.ts +0 -4
  874. package/node_modules/zod/v4/locales/ur.js +0 -110
  875. package/node_modules/zod/v4/locales/uz.cjs +0 -136
  876. package/node_modules/zod/v4/locales/uz.d.cts +0 -5
  877. package/node_modules/zod/v4/locales/uz.d.ts +0 -4
  878. package/node_modules/zod/v4/locales/uz.js +0 -109
  879. package/node_modules/zod/v4/locales/vi.cjs +0 -135
  880. package/node_modules/zod/v4/locales/vi.d.cts +0 -5
  881. package/node_modules/zod/v4/locales/vi.d.ts +0 -4
  882. package/node_modules/zod/v4/locales/vi.js +0 -108
  883. package/node_modules/zod/v4/locales/yo.cjs +0 -134
  884. package/node_modules/zod/v4/locales/yo.d.cts +0 -5
  885. package/node_modules/zod/v4/locales/yo.d.ts +0 -4
  886. package/node_modules/zod/v4/locales/yo.js +0 -107
  887. package/node_modules/zod/v4/locales/zh-CN.cjs +0 -136
  888. package/node_modules/zod/v4/locales/zh-CN.d.cts +0 -5
  889. package/node_modules/zod/v4/locales/zh-CN.d.ts +0 -4
  890. package/node_modules/zod/v4/locales/zh-CN.js +0 -109
  891. package/node_modules/zod/v4/locales/zh-TW.cjs +0 -134
  892. package/node_modules/zod/v4/locales/zh-TW.d.cts +0 -5
  893. package/node_modules/zod/v4/locales/zh-TW.d.ts +0 -4
  894. package/node_modules/zod/v4/locales/zh-TW.js +0 -107
  895. package/node_modules/zod/v4/mini/checks.cjs +0 -34
  896. package/node_modules/zod/v4/mini/checks.d.cts +0 -1
  897. package/node_modules/zod/v4/mini/checks.d.ts +0 -1
  898. package/node_modules/zod/v4/mini/checks.js +0 -1
  899. package/node_modules/zod/v4/mini/coerce.cjs +0 -52
  900. package/node_modules/zod/v4/mini/coerce.d.cts +0 -7
  901. package/node_modules/zod/v4/mini/coerce.d.ts +0 -7
  902. package/node_modules/zod/v4/mini/coerce.js +0 -22
  903. package/node_modules/zod/v4/mini/external.cjs +0 -63
  904. package/node_modules/zod/v4/mini/external.d.cts +0 -12
  905. package/node_modules/zod/v4/mini/external.d.ts +0 -12
  906. package/node_modules/zod/v4/mini/external.js +0 -14
  907. package/node_modules/zod/v4/mini/index.cjs +0 -32
  908. package/node_modules/zod/v4/mini/index.d.cts +0 -3
  909. package/node_modules/zod/v4/mini/index.d.ts +0 -3
  910. package/node_modules/zod/v4/mini/index.js +0 -3
  911. package/node_modules/zod/v4/mini/iso.cjs +0 -64
  912. package/node_modules/zod/v4/mini/iso.d.cts +0 -22
  913. package/node_modules/zod/v4/mini/iso.d.ts +0 -22
  914. package/node_modules/zod/v4/mini/iso.js +0 -34
  915. package/node_modules/zod/v4/mini/package.json +0 -6
  916. package/node_modules/zod/v4/mini/parse.cjs +0 -16
  917. package/node_modules/zod/v4/mini/parse.d.cts +0 -1
  918. package/node_modules/zod/v4/mini/parse.d.ts +0 -1
  919. package/node_modules/zod/v4/mini/parse.js +0 -1
  920. package/node_modules/zod/v4/mini/schemas.cjs +0 -1046
  921. package/node_modules/zod/v4/mini/schemas.d.cts +0 -427
  922. package/node_modules/zod/v4/mini/schemas.d.ts +0 -427
  923. package/node_modules/zod/v4/mini/schemas.js +0 -925
  924. package/node_modules/zod/v4/package.json +0 -6
  925. package/node_modules/zod/v4-mini/index.cjs +0 -32
  926. package/node_modules/zod/v4-mini/index.d.cts +0 -3
  927. package/node_modules/zod/v4-mini/index.d.ts +0 -3
  928. package/node_modules/zod/v4-mini/index.js +0 -3
  929. package/node_modules/zod/v4-mini/package.json +0 -6
  930. package/plugin/dist/index.d.ts +0 -179
  931. package/plugin/dist/index.d.ts.map +0 -1
  932. package/plugin/dist/index.js +0 -3209
  933. package/plugin/dist/index.js.map +0 -1
@@ -1,3209 +0,0 @@
1
- /**
2
- * hypermem Context Engine Plugin
3
- *
4
- * Implements OpenClaw's ContextEngine interface backed by hypermem's
5
- * four-layer memory architecture:
6
- *
7
- * L1 Cache — SQLite `:memory:` hot session working memory
8
- * L2 Messages — per-agent conversation history (SQLite)
9
- * L3 Vectors — semantic + keyword search (KNN + FTS5)
10
- * L4 Library — facts, knowledge, episodes, preferences
11
- *
12
- * Lifecycle mapping:
13
- * ingest() → record each message into messages.db
14
- * assemble() → compositor builds context from all four layers
15
- * compact() → delegate to runtime (ownsCompaction: false)
16
- * afterTurn() → trigger background indexer (fire-and-forget)
17
- * bootstrap() → warm hot-cache session, register agent in fleet
18
- * dispose() → close hypermem connections
19
- *
20
- * Session key format expected: "agent:<agentId>:<channel>:<name>"
21
- */
22
- import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
23
- import { buildPluginConfigSchema } from 'openclaw/plugin-sdk/core';
24
- import { z } from 'zod';
25
- import { detectTopicShift, stripMessageMetadata, SessionTopicMap, applyToolGradientToWindow, OPENCLAW_BOOTSTRAP_FILES, rotateSessionContext, TRIM_SOFT_TARGET, TRIM_GROWTH_THRESHOLD, TRIM_HEADROOM_FRACTION, resolveTrimBudgets, formatToolChainStub, decideReplayRecovery, isReplayState, } from '@psiclawops/hypermem';
26
- import { evictStaleContent } from '@psiclawops/hypermem/image-eviction';
27
- import { repairToolPairs } from '@psiclawops/hypermem';
28
- import os from 'os';
29
- import path from 'path';
30
- import fs from 'fs/promises';
31
- import { fileURLToPath } from 'url';
32
- import fsSync from 'fs';
33
- let _telemetryStream = null;
34
- let _telemetryStreamFailed = false;
35
- let _telemetryTurnCounter = 0;
36
- function telemetryEnabled() {
37
- return process.env.HYPERMEM_TELEMETRY === '1';
38
- }
39
- function getTelemetryStream() {
40
- if (_telemetryStream || _telemetryStreamFailed)
41
- return _telemetryStream;
42
- try {
43
- const p = process.env.HYPERMEM_TELEMETRY_PATH || './hypermem-telemetry.jsonl';
44
- _telemetryStream = fsSync.createWriteStream(p, { flags: 'a' });
45
- _telemetryStream.on('error', () => {
46
- _telemetryStreamFailed = true;
47
- _telemetryStream = null;
48
- });
49
- }
50
- catch {
51
- _telemetryStreamFailed = true;
52
- _telemetryStream = null;
53
- }
54
- return _telemetryStream;
55
- }
56
- function trimTelemetry(fields) {
57
- if (!telemetryEnabled())
58
- return;
59
- const stream = getTelemetryStream();
60
- if (!stream)
61
- return;
62
- try {
63
- const record = {
64
- event: 'trim',
65
- ts: new Date().toISOString(),
66
- ...fields,
67
- };
68
- stream.write(JSON.stringify(record) + '\n');
69
- }
70
- catch {
71
- // Telemetry must never throw
72
- }
73
- }
74
- function assembleTrace(fields) {
75
- if (!telemetryEnabled())
76
- return;
77
- const stream = getTelemetryStream();
78
- if (!stream)
79
- return;
80
- try {
81
- const record = {
82
- event: 'assemble',
83
- ts: new Date().toISOString(),
84
- ...fields,
85
- };
86
- stream.write(JSON.stringify(record) + '\n');
87
- }
88
- catch {
89
- // Telemetry must never throw
90
- }
91
- }
92
- function degradationTelemetry(fields) {
93
- if (!telemetryEnabled())
94
- return;
95
- const stream = getTelemetryStream();
96
- if (!stream)
97
- return;
98
- try {
99
- const record = {
100
- event: 'degradation',
101
- ts: new Date().toISOString(),
102
- ...fields,
103
- };
104
- stream.write(JSON.stringify(record) + '\n');
105
- }
106
- catch {
107
- // Telemetry must never throw
108
- }
109
- }
110
- function nextTurnId() {
111
- _telemetryTurnCounter = (_telemetryTurnCounter + 1) >>> 0;
112
- return `${Date.now().toString(36)}-${_telemetryTurnCounter.toString(36)}`;
113
- }
114
- // ─── Trim Ownership (Phase A Sprint 2) ───────────────────────────
115
- //
116
- // Sprint 2 consolidates trim ownership: the assemble-owned family
117
- // (assemble.normal, assemble.subagent, assemble.toolLoop) is the single
118
- // steady-state trim owner. Compact paths (compact.nuclear, compact.history,
119
- // compact.history2) are exempted — they're exception-only. warmstart,
120
- // reshape, and afterTurn.secondary are demoted in sub-tasks 2.2 and 2.3.
121
- //
122
- // This block adds:
123
- // 1. A per-session turn context (beginTrimOwnerTurn/endTrimOwnerTurn) scoped
124
- // by the main assemble() flow.
125
- // 2. A single shared trimOwner claim helper that lets exactly one **real**
126
- // steady-state trim claim ownership per turn and throws loudly in
127
- // development (NODE_ENV='development') when a second real steady-state
128
- // trim path attempts to claim the same turn.
129
- // 3. A non-counting guard/noop telemetry helper (same JSONL channel) that
130
- // demoted paths can emit to preserve visibility of warm-start/reshape
131
- // without consuming a steady-state owner slot.
132
- //
133
- // Sub-task 2.1 only adds the scaffolding + invariant; no existing trim call
134
- // is removed here. Demotions of warm-start/reshape/afterTurn.secondary land
135
- // in 2.2 and 2.3.
136
- const STEADY_STATE_TRIM_PATHS = new Set([
137
- 'assemble.normal',
138
- 'assemble.subagent',
139
- 'assemble.toolLoop',
140
- ]);
141
- const COMPACT_TRIM_PATHS = new Set([
142
- 'compact.nuclear',
143
- 'compact.history',
144
- 'compact.history2',
145
- ]);
146
- // ─── Guard-telemetry reason enum (Phase A Sprint 2.2a) ──────────────────
147
- // Plugin-local, constant-backed union of allowed `reason` values on
148
- // `event: 'trim-guard'` records. Keeping this bounded prevents ad-hoc
149
- // numeric/user strings from leaking into the telemetry JSONL channel and
150
- // makes downstream reporting stable. Do NOT widen this to arbitrary
151
- // strings — add a new member here first, then reference it at call sites.
152
- //
153
- // Scope note: this union is plugin-local (per planner 2.2 §C). It is not
154
- // re-exported via `src/types.ts` because the shared public types surface
155
- // must not gain a telemetry-reason enum as part of this sprint.
156
- const GUARD_TELEMETRY_REASONS = [
157
- 'warmstart-pressure-demoted',
158
- 'reshape-downshift-demoted',
159
- 'duplicate-claim-suppressed',
160
- 'afterturn-secondary-demoted',
161
- 'window-within-budget-skip',
162
- 'pressure-accounting-anomaly',
163
- ];
164
- // Turn-scoped ownership map (Phase A Sprint 2.2a).
165
- //
166
- // Previously keyed by `sessionKey` alone, which clobbered overlapping same-
167
- // session assemble() flows (Sprint 2.1 security eval, medium finding #1).
168
- // Now keyed by the composite `sessionKey|turnId` so two concurrent turns on
169
- // the same session key remain isolated: each `beginTrimOwnerTurn` gets its
170
- // own slot, `claimTrimOwner` checks the exact turn's slot, and
171
- // `endTrimOwnerTurn` removes only that turn's slot.
172
- const _trimOwnerTurns = new Map();
173
- function _trimOwnerKey(sessionKey, turnId) {
174
- return `${sessionKey}|${turnId}`;
175
- }
176
- function beginTrimOwnerTurn(sessionKey, turnId) {
177
- _trimOwnerTurns.set(_trimOwnerKey(sessionKey, turnId), { turnId });
178
- }
179
- function endTrimOwnerTurn(sessionKey, turnId) {
180
- _trimOwnerTurns.delete(_trimOwnerKey(sessionKey, turnId));
181
- }
182
- /**
183
- * Claim the steady-state trim owner slot for the current turn.
184
- *
185
- * Behavior:
186
- * - compact.* paths are exception-only and pass through without claiming.
187
- * - Non-steady paths (warmstart, reshape, afterTurn.secondary) also pass
188
- * through without claiming. Demoted/no-op sites should normally emit
189
- * via guardTelemetry() instead so they stay visible without contending
190
- * for ownership (sub-tasks 2.2 and 2.3 wire this in).
191
- * - Steady-state paths (assemble.normal, assemble.subagent,
192
- * assemble.toolLoop) claim the single owner slot for the current turn.
193
- * The first such claim succeeds. A second steady-state claim against the
194
- * same turn is a duplicate-turn violation: it throws loudly under
195
- * NODE_ENV='development' and warns in other environments (returning
196
- * false so non-dev runtimes keep working).
197
- *
198
- * Callers should invoke this immediately before the real
199
- * trimHistoryToTokenBudget() call. Guard telemetry does NOT route through
200
- * this helper — it is explicitly excluded from the steady-state invariant.
201
- *
202
- * Returns true when the claim succeeds (or is exempt); false on a swallowed
203
- * duplicate claim in non-development. In development the duplicate throws
204
- * before returning.
205
- */
206
- function claimTrimOwner(sessionKey, turnId, path) {
207
- // Compact paths: exempt — they represent an exceptional pressure path and
208
- // never contend for the steady-state slot.
209
- if (COMPACT_TRIM_PATHS.has(path))
210
- return true;
211
- // Non-steady paths: pass through (warmstart/reshape/afterTurn.secondary).
212
- // Warmstart + reshape are demoted to guardTelemetry in 2.2a.
213
- if (!STEADY_STATE_TRIM_PATHS.has(path))
214
- return true;
215
- const ctx = _trimOwnerTurns.get(_trimOwnerKey(sessionKey, turnId));
216
- if (!ctx)
217
- return true; // No active assemble-turn scope — nothing to enforce here.
218
- if (ctx.claimedPath) {
219
- const msg = `[hypermem-plugin] trimOwner: duplicate steady-state trim claim in turn ` +
220
- `${ctx.turnId} (sessionKey=${sessionKey}): first=${ctx.claimedPath} second=${path}`;
221
- if (process.env.NODE_ENV === 'development') {
222
- throw new Error(msg);
223
- }
224
- // Non-development: do not throw, but leave a loud trail so telemetry
225
- // surfaces the violation. Callers MUST honor the false return and skip
226
- // the second real trim (Sprint 2.2a enforcement).
227
- console.warn(msg);
228
- return false;
229
- }
230
- ctx.claimedPath = path;
231
- return true;
232
- }
233
- /**
234
- * Non-counting guard / noop telemetry.
235
- *
236
- * Emits a `trim-guard` record on the same JSONL channel as trimTelemetry()
237
- * but with a distinct event name so per-turn reporting (scripts/trim-report.mjs,
238
- * future ownership dashboards) can keep it out of `trimCount`. Used by
239
- * demoted/no-op call sites in 2.2 and 2.3 so their path labels stay visible
240
- * in telemetry without consuming a steady-state owner slot.
241
- *
242
- * Zero-cost when telemetry is off. Never throws.
243
- */
244
- function guardTelemetry(fields) {
245
- if (!telemetryEnabled())
246
- return;
247
- const stream = getTelemetryStream();
248
- if (!stream)
249
- return;
250
- try {
251
- const record = {
252
- event: 'trim-guard',
253
- ts: new Date().toISOString(),
254
- ...fields,
255
- };
256
- stream.write(JSON.stringify(record) + '\n');
257
- }
258
- catch {
259
- // Telemetry must never throw
260
- }
261
- }
262
- // ─── B3: Batch trim with growth allowance ────────────────────────────────
263
- // Trim fires only when window usage exceeds the soft target by this fraction.
264
- // Small natural growth (e.g. a short assistant reply) never triggers a trim;
265
- // only genuine spikes (model switch, cold-start, multi-tool overrun) do.
266
- // When trim fires, the target is (softTarget * (1 - headroomFraction)) so the
267
- // window has room to grow for several turns before the next trim fires.
268
- //
269
- // softTarget (0.65): matches refreshRedisGradient → steady state never trims
270
- // growthThreshold (0.05): 5% overage buffer before trim fires
271
- // headroomFraction (0.10): trim target = softTarget * 0.90 → ~58.5% of budget
272
- // Canonical values live in the core package so plugin trim guards and compose
273
- // paths cannot drift.
274
- // Test-only: expose emitters so the unit test can exercise them directly
275
- // without standing up a real session. Wrapped in a getter object so the flag
276
- // guard still runs (zero-cost when off).
277
- export const __telemetryForTests = {
278
- trimTelemetry,
279
- assembleTrace,
280
- degradationTelemetry,
281
- guardTelemetry,
282
- nextTurnId,
283
- beginTrimOwnerTurn,
284
- endTrimOwnerTurn,
285
- claimTrimOwner,
286
- // B3/C0.1: Expose the canonical policy surface so tests can assert against
287
- // the shared source of truth instead of embedding formulas locally.
288
- TRIM_SOFT_TARGET,
289
- TRIM_GROWTH_THRESHOLD,
290
- TRIM_HEADROOM_FRACTION,
291
- resolveTrimBudgets,
292
- reset() {
293
- if (_telemetryStream) {
294
- try {
295
- _telemetryStream.end();
296
- }
297
- catch { /* ignore */ }
298
- }
299
- _telemetryStream = null;
300
- _telemetryStreamFailed = false;
301
- _telemetryTurnCounter = 0;
302
- _trimOwnerTurns.clear();
303
- },
304
- };
305
- // ─── hypermem singleton ────────────────────────────────────────
306
- // Runtime load is dynamic (hypermem is a sibling package loaded from repo dist,
307
- // not installed via npm). Types come from the core package devDependency.
308
- // This pattern keeps the runtime path stable while TypeScript resolves types
309
- // from the canonical source — no more local shim drift.
310
- // Resolved at init time: pluginConfig.hyperMemPath > import.meta.resolve('@psiclawops/hypermem') > dev fallback
311
- let HYPERMEM_PATH = '';
312
- let _hm = null;
313
- let _hmInitPromise = null;
314
- let _indexer = null;
315
- let _fleetStore = null;
316
- let _generateEmbeddings = null;
317
- let _embeddingConfig = null;
318
- // P1.7: TaskFlow runtime reference — bound at registration time, best-effort.
319
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
320
- let _taskFlowRuntime = null;
321
- // ─── Eviction config cache ────────────────────────────────────
322
- // Populated from user config during hypermem init. Stored here so
323
- // assemble() (which can't await loadUserConfig) can read it without
324
- // re-reading disk on every turn.
325
- let _evictionConfig;
326
- // ─── Context window reserve cache ────────────────────────────
327
- // Populated from user config during hypermem init. Ensures hypermem leaves
328
- // a guaranteed headroom fraction for system prompts, tool results, and
329
- // incoming data — preventing the trim tiers from firing too close to the edge.
330
- //
331
- // contextWindowSize: full model context window in tokens (default: 128_000)
332
- // contextWindowReserve: fraction [0.0–0.5] to keep free (default: 0.25)
333
- //
334
- // Effective history budget = (windowSize * (1 - reserve)) - overheadFallback
335
- // e.g. 128k * 0.75 - 28k = 68k for council agents at 25% reserve
336
- let _contextWindowSize = 128_000;
337
- let _contextWindowReserve = 0.25;
338
- let _deferToolPruning = false;
339
- let _verboseLogging = false;
340
- let _contextWindowOverrides = {};
341
- const _budgetFallbackWarnings = new Set();
342
- export const CONTEXT_WINDOW_OVERRIDE_KEY_REGEX = /^[^/\s]+\/[^/\s]+$/;
343
- const contextWindowOverrideSchema = z.object({
344
- contextTokens: z.number().int().positive().optional(),
345
- contextWindow: z.number().int().positive().optional(),
346
- }).superRefine((value, ctx) => {
347
- if (value.contextTokens == null && value.contextWindow == null) {
348
- ctx.addIssue({
349
- code: z.ZodIssueCode.custom,
350
- message: 'override must declare contextTokens, contextWindow, or both',
351
- });
352
- }
353
- if (value.contextTokens != null &&
354
- value.contextWindow != null &&
355
- value.contextTokens > value.contextWindow) {
356
- ctx.addIssue({
357
- code: z.ZodIssueCode.custom,
358
- message: 'contextTokens must be less than or equal to contextWindow',
359
- });
360
- }
361
- });
362
- export function sanitizeContextWindowOverrides(raw) {
363
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
364
- return { value: {}, warnings: [] };
365
- }
366
- const value = {};
367
- const warnings = [];
368
- for (const [key, candidate] of Object.entries(raw)) {
369
- const normalizedKey = key.trim().toLowerCase();
370
- if (!CONTEXT_WINDOW_OVERRIDE_KEY_REGEX.test(normalizedKey)) {
371
- warnings.push(`ignoring contextWindowOverrides[${JSON.stringify(key)}]: key must be "provider/model"`);
372
- continue;
373
- }
374
- const parsed = contextWindowOverrideSchema.safeParse(candidate);
375
- if (!parsed.success) {
376
- warnings.push(`ignoring contextWindowOverrides[${JSON.stringify(key)}]: ` +
377
- parsed.error.issues.map(issue => issue.message).join('; '));
378
- continue;
379
- }
380
- value[normalizedKey] = parsed.data;
381
- }
382
- return { value, warnings };
383
- }
384
- export function resolveEffectiveBudget(args) {
385
- const { tokenBudget, model, contextWindowSize, contextWindowReserve } = args;
386
- if (tokenBudget) {
387
- return { budget: tokenBudget, source: 'runtime tokenBudget' };
388
- }
389
- const key = normalizeModelKey(model);
390
- const override = key ? args.contextWindowOverrides?.[key] : undefined;
391
- const configuredWindow = override?.contextTokens ?? override?.contextWindow;
392
- if (configuredWindow) {
393
- return {
394
- budget: Math.floor(configuredWindow * (1 - contextWindowReserve)),
395
- source: `contextWindowOverrides[${key}]`,
396
- };
397
- }
398
- return {
399
- budget: Math.floor(contextWindowSize * (1 - contextWindowReserve)),
400
- source: 'fallback contextWindowSize',
401
- };
402
- }
403
- export function resolveModelIdentity(model) {
404
- const modelKey = normalizeModelKey(model);
405
- if (!modelKey) {
406
- return {
407
- rawModel: model ?? null,
408
- modelKey: null,
409
- provider: null,
410
- modelId: null,
411
- };
412
- }
413
- const slash = modelKey.indexOf('/');
414
- return {
415
- rawModel: model ?? null,
416
- modelKey,
417
- provider: slash > 0 ? modelKey.slice(0, slash) : null,
418
- modelId: slash > 0 && slash < modelKey.length - 1 ? modelKey.slice(slash + 1) : modelKey,
419
- };
420
- }
421
- export function diffModelState(previous, current) {
422
- const previousIdentity = previous?.modelKey || previous?.provider || previous?.modelId
423
- ? {
424
- rawModel: previous.model ?? null,
425
- modelKey: previous.modelKey ?? normalizeModelKey(previous.model),
426
- provider: previous.provider ?? resolveModelIdentity(previous.model).provider,
427
- modelId: previous.modelId ?? resolveModelIdentity(previous.model).modelId,
428
- }
429
- : resolveModelIdentity(previous?.model);
430
- const currentIdentity = resolveModelIdentity(current.model);
431
- const previousBudget = previous?.tokenBudget;
432
- const currentBudget = current.tokenBudget;
433
- const budgetChanged = previousBudget != null && currentBudget != null && previousBudget !== currentBudget;
434
- return {
435
- previousIdentity,
436
- currentIdentity,
437
- modelChanged: previousIdentity.modelKey !== currentIdentity.modelKey,
438
- providerChanged: previousIdentity.provider !== currentIdentity.provider,
439
- modelIdChanged: previousIdentity.modelId !== currentIdentity.modelId,
440
- budgetChanged,
441
- budgetDownshift: previousBudget != null && currentBudget != null && currentBudget < previousBudget,
442
- budgetUplift: previousBudget != null && currentBudget != null && currentBudget > previousBudget,
443
- };
444
- }
445
- function normalizeModelKey(model) {
446
- if (!model)
447
- return null;
448
- const key = model.trim().toLowerCase();
449
- return key.length > 0 ? key : null;
450
- }
451
- function verboseLog(message) {
452
- if (_verboseLogging)
453
- console.log(message);
454
- }
455
- function resolveConfiguredWindow(model) {
456
- const key = normalizeModelKey(model);
457
- if (!key)
458
- return null;
459
- const override = _contextWindowOverrides[key];
460
- if (!override)
461
- return null;
462
- return override.contextTokens ?? override.contextWindow ?? null;
463
- }
464
- // Subagent warming mode: 'full' | 'light' | 'off'. Default: 'light'.
465
- // Controls how much HyperMem context is injected into subagent sessions.
466
- let _subagentWarming = 'light';
467
- // Cache replay threshold: 15min default. Set to 0 in user config to disable.
468
- let _cacheReplayThresholdMs = 900_000;
469
- // ─── System overhead cache ────────────────────────────────────
470
- // Caches the non-history token cost (contextBlock + runtime system prompt)
471
- // from the last full compose per session key. Used in tool-loop turns to
472
- // return an honest estimatedTokens without re-running the full compose
473
- // pipeline. Map key = resolved session key.
474
- const _overheadCache = new Map();
475
- // Tier-aware conservative fallback when no cached value exists (cold session,
476
- // first turn after restart). Over-estimates are safer than under-estimates:
477
- // a false-positive compact is cheaper than letting context blow past budget.
478
- const OVERHEAD_FALLBACK = {
479
- council: 28_000,
480
- director: 28_000,
481
- specialist: 18_000,
482
- };
483
- const OVERHEAD_FALLBACK_DEFAULT = 15_000;
484
- function getOverheadFallback(tier) {
485
- if (!tier)
486
- return OVERHEAD_FALLBACK_DEFAULT;
487
- return OVERHEAD_FALLBACK[tier] ?? OVERHEAD_FALLBACK_DEFAULT;
488
- }
489
- /**
490
- * Compute the effective history budget for trim and compact operations.
491
- *
492
- * Priority:
493
- * 1. tokenBudget passed by the runtime (most precise)
494
- * 2. Derived from context window config: windowSize * (1 - reserve)
495
- *
496
- * The reserve fraction (default 0.25 = 25%) guarantees headroom for:
497
- * - System prompt + identity blocks (~28k for council agents)
498
- * - Incoming tool results (can be 10–30k in parallel web_search bursts)
499
- * - Response generation buffer (~4k)
500
- *
501
- * Without the reserve, trim tiers fire at 75–85% of tokenBudget but
502
- * total context (history + system) exceeds the model window before trim
503
- * completes, causing result stripping.
504
- */
505
- function computeEffectiveBudget(tokenBudget, model) {
506
- const resolved = resolveEffectiveBudget({
507
- tokenBudget,
508
- model,
509
- contextWindowSize: _contextWindowSize,
510
- contextWindowReserve: _contextWindowReserve,
511
- contextWindowOverrides: _contextWindowOverrides,
512
- });
513
- if (resolved.source === 'runtime tokenBudget') {
514
- verboseLog(`[hypermem-plugin] budget source: runtime tokenBudget=${tokenBudget}${model ? ` model=${model}` : ''}`);
515
- return resolved.budget;
516
- }
517
- const configuredWindow = resolveConfiguredWindow(model);
518
- if (configuredWindow) {
519
- verboseLog(`[hypermem-plugin] budget source: contextWindowOverrides[${normalizeModelKey(model)}]=${configuredWindow}, ` +
520
- `reserve=${_contextWindowReserve}, effective=${resolved.budget}`);
521
- return resolved.budget;
522
- }
523
- verboseLog(`[hypermem-plugin] budget source: fallback contextWindowSize=${_contextWindowSize}, ` +
524
- `reserve=${_contextWindowReserve}, effective=${resolved.budget}${model ? ` model=${model}` : ''}`);
525
- const warningKey = normalizeModelKey(model) ?? '(unknown-model)';
526
- if (!_budgetFallbackWarnings.has(warningKey)) {
527
- _budgetFallbackWarnings.add(warningKey);
528
- console.warn(`[hypermem-plugin] No runtime tokenBudget${model ? ` for model ${model}` : ''}; ` +
529
- `falling back to contextWindowSize=${_contextWindowSize}. ` +
530
- `Add contextWindowOverrides["provider/model"] to config.json or openclaw.json if detection is wrong.`);
531
- }
532
- return resolved.budget;
533
- }
534
- // ─── Plugin config cache ───────────────────────────────────────
535
- // Populated from openclaw.json plugins.entries.hypercompositor.config
536
- // during register(). loadUserConfig() merges this over config.json.
537
- let _pluginConfig = {};
538
- /**
539
- * Load user config with priority: pluginConfig (openclaw.json) > config.json (legacy).
540
- * pluginConfig values win; config.json provides fallback for keys not set in openclaw.json.
541
- * This allows gradual migration from the shadow config.json to central config.
542
- */
543
- async function loadUserConfig() {
544
- // Resolve data dir: pluginConfig > default
545
- const dataDir = _pluginConfig.dataDir ?? path.join(os.homedir(), '.openclaw/hypermem');
546
- const configPath = path.join(dataDir, 'config.json');
547
- let fileConfig = {};
548
- try {
549
- const raw = await fs.readFile(configPath, 'utf-8');
550
- fileConfig = JSON.parse(raw);
551
- console.log(`[hypermem-plugin] Loaded legacy config from ${configPath}`);
552
- }
553
- catch (err) {
554
- if (err.code !== 'ENOENT') {
555
- console.warn(`[hypermem-plugin] Failed to parse config.json (using defaults):`, err.message);
556
- }
557
- }
558
- // Merge: pluginConfig (openclaw.json) wins over fileConfig (legacy config.json).
559
- // Top-level scalar keys from pluginConfig override fileConfig.
560
- // Nested objects (compositor, eviction, embedding) are shallow-merged.
561
- const merged = { ...fileConfig };
562
- if (_pluginConfig.contextWindowSize != null)
563
- merged.contextWindowSize = _pluginConfig.contextWindowSize;
564
- if (_pluginConfig.contextWindowReserve != null)
565
- merged.contextWindowReserve = _pluginConfig.contextWindowReserve;
566
- if (_pluginConfig.deferToolPruning != null)
567
- merged.deferToolPruning = _pluginConfig.deferToolPruning;
568
- if (_pluginConfig.verboseLogging != null)
569
- merged.verboseLogging = _pluginConfig.verboseLogging;
570
- if (_pluginConfig.contextWindowOverrides != null)
571
- merged.contextWindowOverrides = { ...merged.contextWindowOverrides, ..._pluginConfig.contextWindowOverrides };
572
- if (_pluginConfig.warmCacheReplayThresholdMs != null)
573
- merged.warmCacheReplayThresholdMs = _pluginConfig.warmCacheReplayThresholdMs;
574
- if (_pluginConfig.subagentWarming != null)
575
- merged.subagentWarming = _pluginConfig.subagentWarming;
576
- if (_pluginConfig.compositor)
577
- merged.compositor = { ...merged.compositor, ..._pluginConfig.compositor };
578
- if (_pluginConfig.eviction)
579
- merged.eviction = { ...merged.eviction, ..._pluginConfig.eviction };
580
- if (_pluginConfig.embedding)
581
- merged.embedding = { ...merged.embedding, ..._pluginConfig.embedding };
582
- if (Object.keys(fileConfig).length > 0 && Object.keys(_pluginConfig).filter(k => k !== 'hyperMemPath' && k !== 'dataDir').length > 0) {
583
- console.log('[hypermem-plugin] Note: migrating config.json keys to plugins.entries.hypercompositor.config in openclaw.json is recommended');
584
- }
585
- return merged;
586
- }
587
- async function getHyperMem() {
588
- if (_hm)
589
- return _hm;
590
- if (_hmInitPromise)
591
- return _hmInitPromise;
592
- _hmInitPromise = (async () => {
593
- // Dynamic import — hypermem is loaded from repo dist
594
- const mod = await import(HYPERMEM_PATH);
595
- const HyperMem = mod.HyperMem;
596
- // Capture generateEmbeddings from the dynamic module for use in afterTurn().
597
- // Bind it with the user's embedding config so the pre-compute path uses the
598
- // same provider as the indexer (Ollama vs OpenAI).
599
- if (typeof mod.generateEmbeddings === 'function') {
600
- const rawGenerate = mod.generateEmbeddings;
601
- _generateEmbeddings = (texts) => rawGenerate(texts, _embeddingConfig ?? undefined);
602
- }
603
- // Load optional user config — compositor tuning overrides
604
- const userConfig = await loadUserConfig();
605
- // Build embedding config from user config. Applied to both HyperMem core
606
- // (VectorStore init) and the _generateEmbeddings closure above.
607
- if (userConfig.embedding) {
608
- const ue = userConfig.embedding;
609
- // Provider-specific model/dimension/batch defaults
610
- const providerDefaults = ue.provider === 'gemini'
611
- ? { model: 'gemini-embedding-001', dimensions: 3072, batchSize: 100, timeout: 15000 }
612
- : ue.provider === 'openai'
613
- ? { model: 'text-embedding-3-small', dimensions: 1536, batchSize: 128, timeout: 10000 }
614
- : { model: 'nomic-embed-text', dimensions: 768, batchSize: 32, timeout: 10000 };
615
- _embeddingConfig = {
616
- provider: ue.provider ?? 'ollama',
617
- ollamaUrl: ue.ollamaUrl ?? 'http://localhost:11434',
618
- openaiBaseUrl: ue.openaiBaseUrl ?? 'https://api.openai.com/v1',
619
- openaiApiKey: ue.openaiApiKey,
620
- geminiBaseUrl: ue.geminiBaseUrl,
621
- geminiIndexTaskType: ue.geminiIndexTaskType,
622
- geminiQueryTaskType: ue.geminiQueryTaskType,
623
- model: ue.model ?? providerDefaults.model,
624
- dimensions: ue.dimensions ?? providerDefaults.dimensions,
625
- timeout: ue.timeout ?? providerDefaults.timeout,
626
- batchSize: ue.batchSize ?? providerDefaults.batchSize,
627
- };
628
- console.log(`[hypermem-plugin] Embedding provider: ${_embeddingConfig.provider} ` +
629
- `(model: ${_embeddingConfig.model}, ${_embeddingConfig.dimensions}d, batch: ${_embeddingConfig.batchSize})`);
630
- }
631
- // Cache eviction config at module scope so assemble() can read it
632
- // synchronously without re-reading disk on every turn.
633
- _evictionConfig = userConfig.eviction ?? {};
634
- // Cache context window config so all three trim hotpaths use the same values.
635
- if (typeof userConfig.contextWindowSize === 'number' && userConfig.contextWindowSize > 0) {
636
- _contextWindowSize = userConfig.contextWindowSize;
637
- }
638
- if (typeof userConfig.contextWindowReserve === 'number' &&
639
- userConfig.contextWindowReserve >= 0 && userConfig.contextWindowReserve <= 0.5) {
640
- _contextWindowReserve = userConfig.contextWindowReserve;
641
- }
642
- _deferToolPruning = userConfig.deferToolPruning === true;
643
- if (_deferToolPruning) {
644
- console.log('[hypermem-plugin] deferToolPruning: true — tool gradient deferred to host contextPruning');
645
- }
646
- _verboseLogging = userConfig.verboseLogging === true;
647
- const sanitizedOverrides = sanitizeContextWindowOverrides(userConfig.contextWindowOverrides);
648
- _contextWindowOverrides = sanitizedOverrides.value;
649
- for (const warning of sanitizedOverrides.warnings) {
650
- console.warn(`[hypermem-plugin] ${warning}`);
651
- }
652
- const warmingVal = userConfig.subagentWarming;
653
- if (warmingVal === 'full' || warmingVal === 'light' || warmingVal === 'off') {
654
- _subagentWarming = warmingVal;
655
- console.log(`[hypermem-plugin] subagentWarming: ${_subagentWarming}`);
656
- }
657
- if (typeof userConfig.warmCacheReplayThresholdMs === 'number') {
658
- _cacheReplayThresholdMs = userConfig.warmCacheReplayThresholdMs;
659
- }
660
- const reservedTokens = Math.floor(_contextWindowSize * _contextWindowReserve);
661
- console.log(`[hypermem-plugin] context window: ${_contextWindowSize} tokens, ` +
662
- `${Math.round(_contextWindowReserve * 100)}% reserved (${reservedTokens} tokens), ` +
663
- `effective history budget: ${_contextWindowSize - reservedTokens} tokens`);
664
- verboseLog(`[hypermem-plugin] warmCacheReplayThresholdMs=${_cacheReplayThresholdMs}`);
665
- verboseLog(`[hypermem-plugin] contextWindowOverrides keys=${Object.keys(_contextWindowOverrides).join(', ') || '(none)'}`);
666
- const instance = await HyperMem.create({
667
- dataDir: _pluginConfig.dataDir ?? path.join(os.homedir(), '.openclaw/hypermem'),
668
- cache: {
669
- keyPrefix: 'hm:',
670
- sessionTTL: 14400, // 4h for system/identity/meta slots
671
- historyTTL: 86400, // 24h for history — ages out, not count-trimmed
672
- },
673
- ...(userConfig.compositor ? { compositor: userConfig.compositor } : {}),
674
- ...(_embeddingConfig ? { embedding: _embeddingConfig } : {}),
675
- });
676
- _hm = instance;
677
- // Wire up fleet store and background indexer from dynamic module
678
- const { FleetStore: FleetStoreClass, createIndexer } = mod;
679
- const libraryDb = instance.dbManager.getLibraryDb();
680
- _fleetStore = new FleetStoreClass(libraryDb);
681
- try {
682
- // T1.2: Wire indexer with proper DB accessors and cursor fetcher.
683
- // The cursor fetcher enables priority-based indexing: messages the model
684
- // hasn't seen yet (post-cursor) are processed first.
685
- _indexer = createIndexer((agentId) => instance.dbManager.getMessageDb(agentId), () => instance.dbManager.getLibraryDb(), () => {
686
- // List agents from fleet_agents table (active only)
687
- try {
688
- const rows = instance.dbManager.getLibraryDb()
689
- .prepare("SELECT id FROM fleet_agents WHERE status = 'active'")
690
- .all();
691
- return rows.map(r => r.id);
692
- }
693
- catch {
694
- return [];
695
- }
696
- }, {
697
- enabled: true,
698
- periodicInterval: userConfig?.maintenance?.periodicInterval ?? 300000,
699
- maxActiveConversations: userConfig?.maintenance?.maxActiveConversations ?? 5,
700
- recentConversationCooldownMs: userConfig?.maintenance?.recentConversationCooldownMs ?? 30000,
701
- maxCandidatesPerPass: userConfig?.maintenance?.maxCandidatesPerPass ?? 200,
702
- },
703
- // Cursor fetcher: reads the SQLite-backed session cursor
704
- async (agentId, sessionKey) => {
705
- return instance.getSessionCursor(agentId, sessionKey);
706
- },
707
- // Pass vector store so new facts/episodes are embedded at index time
708
- instance.getVectorStore() ?? undefined,
709
- // Dreaming config — passed from hypermem user config if set
710
- userConfig?.dreaming ?? {},
711
- // KL-01: global write policy — passed from hypermem user config
712
- userConfig?.globalWritePolicy ?? 'deny');
713
- _indexer.start();
714
- if (_verboseLogging) {
715
- const mc = userConfig?.maintenance ?? {};
716
- console.log(`[hypermem-plugin] maintenance settings: periodicInterval=${mc.periodicInterval ?? 300000}ms ` +
717
- `maxActiveConversations=${mc.maxActiveConversations ?? 5} ` +
718
- `cooldown=${mc.recentConversationCooldownMs ?? 30000}ms ` +
719
- `maxCandidatesPerPass=${mc.maxCandidatesPerPass ?? 200}`);
720
- }
721
- }
722
- catch {
723
- // Non-fatal — indexer wiring can fail without breaking context assembly
724
- }
725
- return instance;
726
- })();
727
- return _hmInitPromise;
728
- }
729
- // ─── Session Key Helpers ────────────────────────────────────────
730
- /**
731
- * Extract agentId from a session key.
732
- * Session keys follow: "agent:<agentId>:<channel>:<name>"
733
- * Falls back to "main" if the key doesn't match expected format.
734
- */
735
- function extractAgentId(sessionKey) {
736
- if (!sessionKey)
737
- return 'main';
738
- const parts = sessionKey.split(':');
739
- if (parts[0] === 'agent' && parts.length >= 2) {
740
- return parts[1];
741
- }
742
- return 'main';
743
- }
744
- /**
745
- * Normalize sessionKey — prefer the explicit sessionKey param,
746
- * fall back to sessionId (UUID) which we can't parse as a session key.
747
- * If neither is useful, use a default.
748
- */
749
- function resolveSessionKey(sessionId, sessionKey) {
750
- if (sessionKey)
751
- return sessionKey;
752
- // sessionId is a UUID — not a parseable session key.
753
- // Use a synthetic key so recording works but note it won't resolve to a named session.
754
- return `session:${sessionId}`;
755
- }
756
- const SYNTHETIC_MISSING_TOOL_RESULT_TEXT = 'No result provided';
757
- function extractTextFromInboundContent(content) {
758
- if (typeof content === 'string')
759
- return content;
760
- if (!Array.isArray(content))
761
- return '';
762
- return content
763
- .filter((part) => Boolean(part && typeof part.type === 'string'))
764
- .filter(part => part.type === 'text' && typeof part.text === 'string')
765
- .map(part => part.text ?? '')
766
- .join('\n');
767
- }
768
- function resolveAssistantTokenCount(msg, runtimeContext) {
769
- const usage = msg.usage;
770
- if (usage && typeof usage === 'object') {
771
- const candidates = [
772
- usage.total,
773
- usage.totalTokens,
774
- usage.total_tokens,
775
- usage.output,
776
- usage.outputTokens,
777
- usage.output_tokens,
778
- usage.completionTokens,
779
- usage.completion_tokens,
780
- ];
781
- for (const candidate of candidates) {
782
- if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) {
783
- return Math.floor(candidate);
784
- }
785
- }
786
- }
787
- const runtimeTokenCount = runtimeContext?.currentTokenCount;
788
- if (typeof runtimeTokenCount === 'number' && Number.isFinite(runtimeTokenCount) && runtimeTokenCount > 0) {
789
- return Math.floor(runtimeTokenCount);
790
- }
791
- return undefined;
792
- }
793
- function collectNeutralToolPairStats(messages) {
794
- const callIds = new Set();
795
- const resultIds = new Set();
796
- let toolCallCount = 0;
797
- let toolResultCount = 0;
798
- let syntheticNoResultCount = 0;
799
- for (const msg of messages) {
800
- for (const tc of msg.toolCalls ?? []) {
801
- toolCallCount++;
802
- if (tc.id)
803
- callIds.add(tc.id);
804
- }
805
- for (const tr of msg.toolResults ?? []) {
806
- toolResultCount++;
807
- if (tr.callId)
808
- resultIds.add(tr.callId);
809
- if ((tr.content ?? '').trim() === SYNTHETIC_MISSING_TOOL_RESULT_TEXT)
810
- syntheticNoResultCount++;
811
- }
812
- }
813
- const missingToolResultIds = [...callIds].filter(id => !resultIds.has(id));
814
- const orphanToolResultIds = [...resultIds].filter(id => !callIds.has(id));
815
- return {
816
- toolCallCount,
817
- toolResultCount,
818
- missingToolResultCount: missingToolResultIds.length,
819
- orphanToolResultCount: orphanToolResultIds.length,
820
- syntheticNoResultCount,
821
- missingToolResultIds,
822
- orphanToolResultIds,
823
- };
824
- }
825
- function collectAgentToolPairStats(messages) {
826
- const callIds = new Set();
827
- const resultIds = new Set();
828
- let toolCallCount = 0;
829
- let toolResultCount = 0;
830
- let syntheticNoResultCount = 0;
831
- for (const msg of messages) {
832
- if (msg.role === 'assistant' && Array.isArray(msg.content)) {
833
- for (const block of msg.content) {
834
- if (block.type === 'toolCall' || block.type === 'toolUse') {
835
- toolCallCount++;
836
- if (typeof block.id === 'string' && block.id.length > 0)
837
- callIds.add(block.id);
838
- }
839
- }
840
- }
841
- if (msg.role === 'toolResult') {
842
- toolResultCount++;
843
- const toolCallId = typeof msg.toolCallId === 'string' ? msg.toolCallId : '';
844
- if (toolCallId)
845
- resultIds.add(toolCallId);
846
- if (extractTextFromInboundContent(msg.content).trim() === SYNTHETIC_MISSING_TOOL_RESULT_TEXT) {
847
- syntheticNoResultCount++;
848
- }
849
- }
850
- }
851
- const missingToolResultIds = [...callIds].filter(id => !resultIds.has(id));
852
- const orphanToolResultIds = [...resultIds].filter(id => !callIds.has(id));
853
- return {
854
- toolCallCount,
855
- toolResultCount,
856
- missingToolResultCount: missingToolResultIds.length,
857
- orphanToolResultCount: orphanToolResultIds.length,
858
- syntheticNoResultCount,
859
- missingToolResultIds,
860
- orphanToolResultIds,
861
- };
862
- }
863
- async function bumpToolPairMetrics(hm, agentId, sessionKey, delta, anomaly) {
864
- const slot = 'toolPairMetrics';
865
- let stored = {};
866
- try {
867
- const raw = await hm.cache.getSlot(agentId, sessionKey, slot);
868
- if (raw)
869
- stored = JSON.parse(raw);
870
- }
871
- catch {
872
- stored = {};
873
- }
874
- const next = {
875
- composeCount: (stored.composeCount ?? 0) + (delta.composeCount ?? 0),
876
- syntheticNoResultIngested: (stored.syntheticNoResultIngested ?? 0) + (delta.syntheticNoResultIngested ?? 0),
877
- preBridgeMissingToolResults: (stored.preBridgeMissingToolResults ?? 0) + (delta.preBridgeMissingToolResults ?? 0),
878
- preBridgeOrphanToolResults: (stored.preBridgeOrphanToolResults ?? 0) + (delta.preBridgeOrphanToolResults ?? 0),
879
- postBridgeMissingToolResults: (stored.postBridgeMissingToolResults ?? 0) + (delta.postBridgeMissingToolResults ?? 0),
880
- postBridgeOrphanToolResults: (stored.postBridgeOrphanToolResults ?? 0) + (delta.postBridgeOrphanToolResults ?? 0),
881
- lastUpdatedAt: new Date().toISOString(),
882
- lastAnomaly: anomaly ?? stored.lastAnomaly,
883
- };
884
- await hm.cache.setSlot(agentId, sessionKey, slot, JSON.stringify(next));
885
- }
886
- /**
887
- * Convert an OpenClaw AgentMessage to hypermem's NeutralMessage format.
888
- */
889
- function toNeutralMessage(msg) {
890
- // Extract text content from string or array format
891
- let textContent = null;
892
- if (typeof msg.content === 'string') {
893
- textContent = msg.content;
894
- }
895
- else if (Array.isArray(msg.content)) {
896
- const textParts = msg.content
897
- .filter((c) => c.type === 'text' && typeof c.text === 'string')
898
- .map(c => c.text);
899
- textContent = textParts.length > 0 ? textParts.join('\n') : null;
900
- }
901
- // Detect tool calls/results.
902
- // OpenClaw stores tool calls as content blocks: { type: 'toolCall' | 'toolUse', id, name, input }
903
- // Legacy wire format stores them as a separate msg.tool_calls / msg.toolCalls array
904
- // with OpenAI format: { id, type: 'function', function: { name, arguments } }
905
- // Normalize everything to NeutralToolCall format: { id, name, arguments: string }
906
- const contentBlockToolCalls = Array.isArray(msg.content)
907
- ? msg.content
908
- .filter(c => c.type === 'toolCall' || c.type === 'toolUse')
909
- .map(c => ({
910
- id: c.id ?? 'unknown',
911
- name: c.name ?? 'unknown',
912
- arguments: typeof c.input === 'string' ? c.input : JSON.stringify(c.input ?? {}),
913
- }))
914
- : [];
915
- // Legacy wire format tool calls (OpenAI style)
916
- const rawToolCalls = msg.tool_calls
917
- ?? msg.toolCalls
918
- ?? null;
919
- let toolCalls = null;
920
- if (rawToolCalls && rawToolCalls.length > 0) {
921
- toolCalls = rawToolCalls.map(tc => {
922
- // OpenAI wire format: { id, type: 'function', function: { name, arguments } }
923
- const fn = tc.function;
924
- if (fn) {
925
- return {
926
- id: tc.id ?? 'unknown',
927
- name: fn.name ?? 'unknown',
928
- arguments: typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments ?? {}),
929
- };
930
- }
931
- // Already NeutralToolCall-ish or content block format
932
- return {
933
- id: tc.id ?? 'unknown',
934
- name: tc.name ?? 'unknown',
935
- arguments: typeof tc.arguments === 'string' ? tc.arguments
936
- : typeof tc.input === 'string' ? tc.input
937
- : JSON.stringify(tc.arguments ?? tc.input ?? {}),
938
- };
939
- });
940
- }
941
- else if (contentBlockToolCalls.length > 0) {
942
- toolCalls = contentBlockToolCalls;
943
- }
944
- // OpenClaw uses role 'toolResult' (camelCase). Support all three spellings.
945
- const isToolResultMsg = msg.role === 'tool' || msg.role === 'tool_result' || msg.role === 'toolResult';
946
- // Tool results must stay on the result side of the transcript. If we persist them as
947
- // assistant rows with orphaned toolResults, later replay can retain a tool_result after
948
- // trimming away the matching assistant tool_use, which Anthropic rejects with a 400.
949
- let toolResults = null;
950
- if (isToolResultMsg && textContent) {
951
- const toolCallId = msg.tool_call_id ?? msg.toolCallId ?? 'unknown';
952
- const toolName = msg.name ?? msg.toolName ?? 'tool';
953
- toolResults = [{ callId: toolCallId, name: toolName, content: textContent }];
954
- textContent = null; // owned by toolResults now, not duplicated in textContent
955
- }
956
- const role = isToolResultMsg
957
- ? 'user'
958
- : msg.role;
959
- return {
960
- role,
961
- textContent,
962
- toolCalls: isToolResultMsg ? null : toolCalls,
963
- toolResults,
964
- };
965
- }
966
- // ─── Context Engine Implementation ─────────────────────────────
967
- /**
968
- * In-flight warm dedup map.
969
- * Key: "agentId::sessionKey" — Value: the in-progress warm() Promise.
970
- * Prevents concurrent bootstrap() calls from firing multiple full warms
971
- * for the same session key before the first one sets the Redis history key.
972
- * Cleared on completion (success or failure) so the next cold start retries.
973
- */
974
- const _warmInFlight = new Map();
975
- // ─── Token estimation ──────────────────────────────────────────
976
- /**
977
- * Estimate tokens for a string using the same ~4 chars/token heuristic
978
- * used by the hypermem compositor. Fast and allocation-free — no tokenizer
979
- * library needed for a budget guard.
980
- */
981
- function estimateTokens(text) {
982
- if (!text)
983
- return 0;
984
- return Math.ceil(text.length / 4);
985
- }
986
- function estimateMessagePartTokens(part) {
987
- if (part.type === 'image' || part.type === 'image_url') {
988
- const src = part.source?.data;
989
- const url = part.image_url?.url;
990
- const dataStr = typeof src === 'string' ? src : (typeof url === 'string' ? url : '');
991
- return Math.ceil(dataStr.length / 3);
992
- }
993
- if (part.type === 'toolCall' || part.type === 'tool_use') {
994
- return Math.ceil(JSON.stringify(part).length / 2);
995
- }
996
- const textVal = typeof part.text === 'string' ? part.text
997
- : typeof part.content === 'string' ? part.content
998
- : part.content != null ? JSON.stringify(part.content) : null;
999
- return estimateTokens(textVal);
1000
- }
1001
- function estimateMessageTokens(msg) {
1002
- let total = estimateTokens(typeof msg.textContent === 'string' ? msg.textContent : null);
1003
- if (typeof msg.content === 'string' && typeof msg.textContent !== 'string') {
1004
- total += estimateTokens(msg.content);
1005
- }
1006
- if (msg.toolCalls)
1007
- total += Math.ceil(JSON.stringify(msg.toolCalls).length / 2);
1008
- if (msg.toolResults)
1009
- total += Math.ceil(JSON.stringify(msg.toolResults).length / 2);
1010
- if (Array.isArray(msg.content)) {
1011
- total += msg.content.reduce((sum, part) => sum + estimateMessagePartTokens(part), 0);
1012
- }
1013
- return total;
1014
- }
1015
- function estimateMessageArrayTokens(messages) {
1016
- return messages.reduce((sum, msg) => sum + estimateMessageTokens(msg), 0);
1017
- }
1018
- function maybeLogPressureAccountingAnomaly(fields) {
1019
- const threshold = Math.max(500, Math.floor(fields.budget * 0.05));
1020
- const deltas = {
1021
- runtimeVsComposed: Math.abs(fields.runtimeTokens - fields.composedTokens),
1022
- redisVsComposed: Math.abs(fields.redisTokens - fields.composedTokens),
1023
- runtimeVsRedis: Math.abs(fields.runtimeTokens - fields.redisTokens),
1024
- };
1025
- // Post-0.6.0: "redis" is actually the L1 SQLite cache window, which lags
1026
- // behind the runtime message array between trim passes. Cache-vs-runtime
1027
- // drift is structural and harmless — the runtime array is authoritative
1028
- // (it's what the model sees). Only warn when runtimeVsComposed diverges,
1029
- // which indicates an actual trim accounting bug.
1030
- if (deltas.runtimeVsComposed < threshold) {
1031
- // Log cache drift at debug level for observability, not as a warning.
1032
- if (deltas.redisVsComposed >= threshold || deltas.runtimeVsRedis >= threshold) {
1033
- console.debug(`[hypermem-plugin] cache-drift (non-anomalous): path=${fields.path} ` +
1034
- `runtime=${fields.runtimeTokens} cache=${fields.redisTokens} composed=${fields.composedTokens} ` +
1035
- `budget=${fields.budget}`);
1036
- }
1037
- return;
1038
- }
1039
- console.warn(`[hypermem-plugin] pressure-accounting anomaly: path=${fields.path} ` +
1040
- `runtime=${fields.runtimeTokens} cache=${fields.redisTokens} composed=${fields.composedTokens} ` +
1041
- `budget=${fields.budget} threshold=${threshold}`);
1042
- guardTelemetry({
1043
- path: fields.path,
1044
- agentId: fields.agentId,
1045
- sessionKey: fields.sessionKey,
1046
- reason: 'pressure-accounting-anomaly',
1047
- });
1048
- }
1049
- function normalizeReplayRecoveryState(value) {
1050
- if (value == null)
1051
- return null;
1052
- if (value === '')
1053
- return '';
1054
- return isReplayState(value) ? value : null;
1055
- }
1056
- async function persistReplayRecoveryState(hm, agentId, sessionKey, nextState) {
1057
- try {
1058
- await hm.cache.setSlot(agentId, sessionKey, 'replayRecoveryState', nextState ?? '');
1059
- }
1060
- catch {
1061
- // Non-fatal
1062
- }
1063
- }
1064
- function hasStructuredToolCallMessage(msg) {
1065
- if (Array.isArray(msg.toolCalls) && msg.toolCalls.length > 0)
1066
- return true;
1067
- if (!Array.isArray(msg.content))
1068
- return false;
1069
- return msg.content.some(part => part.type === 'toolCall' || part.type === 'tool_use');
1070
- }
1071
- function hasStructuredToolResultMessage(msg) {
1072
- if (Array.isArray(msg.toolResults) && msg.toolResults.length > 0)
1073
- return true;
1074
- if (msg.role === 'toolResult' || msg.role === 'tool' || msg.role === 'tool_result')
1075
- return true;
1076
- if (!Array.isArray(msg.content))
1077
- return false;
1078
- return msg.content.some(part => part.type === 'tool_result' || part.type === 'toolResult');
1079
- }
1080
- function getToolCallIds(msg) {
1081
- const ids = [];
1082
- if (Array.isArray(msg.toolCalls)) {
1083
- ids.push(...msg.toolCalls.map(tc => tc.id).filter((id) => typeof id === 'string' && id.length > 0));
1084
- }
1085
- if (Array.isArray(msg.content)) {
1086
- for (const part of msg.content) {
1087
- if ((part.type === 'toolCall' || part.type === 'tool_use') && typeof part.id === 'string' && part.id.length > 0) {
1088
- ids.push(part.id);
1089
- }
1090
- }
1091
- }
1092
- return ids;
1093
- }
1094
- function getToolResultIds(msg) {
1095
- const ids = [];
1096
- if (Array.isArray(msg.toolResults)) {
1097
- ids.push(...msg.toolResults.map(tr => tr.callId).filter((id) => typeof id === 'string' && id.length > 0));
1098
- }
1099
- if (typeof msg.toolCallId === 'string' && msg.toolCallId.length > 0) {
1100
- ids.push(msg.toolCallId);
1101
- }
1102
- if (typeof msg.tool_call_id === 'string' && msg.tool_call_id.length > 0) {
1103
- ids.push(msg.tool_call_id);
1104
- }
1105
- return ids;
1106
- }
1107
- function clusterTranscriptMessages(messages) {
1108
- const clusters = [];
1109
- for (let i = 0; i < messages.length; i++) {
1110
- const current = messages[i];
1111
- const cluster = [current];
1112
- if (hasStructuredToolCallMessage(current)) {
1113
- const callIds = new Set(getToolCallIds(current));
1114
- let j = i + 1;
1115
- while (j < messages.length) {
1116
- const candidate = messages[j];
1117
- if (!hasStructuredToolResultMessage(candidate))
1118
- break;
1119
- const resultIds = getToolResultIds(candidate);
1120
- if (callIds.size > 0 && resultIds.length > 0 && !resultIds.some(id => callIds.has(id)))
1121
- break;
1122
- cluster.push(candidate);
1123
- j++;
1124
- }
1125
- i = j - 1;
1126
- }
1127
- else if (hasStructuredToolResultMessage(current)) {
1128
- let j = i + 1;
1129
- while (j < messages.length) {
1130
- const candidate = messages[j];
1131
- if (!hasStructuredToolResultMessage(candidate) || hasStructuredToolCallMessage(candidate))
1132
- break;
1133
- cluster.push(candidate);
1134
- j++;
1135
- }
1136
- i = j - 1;
1137
- }
1138
- clusters.push(cluster);
1139
- }
1140
- return clusters;
1141
- }
1142
- /**
1143
- * Estimate total token cost of the current Redis history window for a session.
1144
- * Counts text content + tool call/result JSON for each message.
1145
- */
1146
- async function estimateWindowTokens(hm, agentId, sessionKey) {
1147
- try {
1148
- // Prefer the hot window cache (set after compaction trims the history).
1149
- // Fall back to the actual history list — the window cache is only populated
1150
- // after compact() calls setWindow(), so a fresh or never-compacted session
1151
- // has no window cache entry. Without this fallback, getWindow returns null
1152
- // → estimateWindowTokens returns 0 → compact() always says within_budget
1153
- // → overflow loop.
1154
- const window = await hm.cache.getWindow(agentId, sessionKey)
1155
- ?? await hm.cache.getHistory(agentId, sessionKey);
1156
- if (!window || window.length === 0)
1157
- return 0;
1158
- return estimateMessageArrayTokens(window);
1159
- }
1160
- catch {
1161
- return 0;
1162
- }
1163
- }
1164
- /**
1165
- * Truncate a JSONL session file to keep only the last `targetDepth` message
1166
- * entries plus all non-message entries (header, compaction, model_change, etc).
1167
- *
1168
- * This is needed because the runtime loads messages from the JSONL file
1169
- * (not from Redis) to build its overflow estimate. When ownsCompaction=true,
1170
- * OpenClaw's truncateSessionAfterCompaction() is never called, so we do it
1171
- * ourselves.
1172
- *
1173
- * Returns true if the file was actually truncated, false if no action was
1174
- * needed or the file didn't exist.
1175
- */
1176
- async function truncateJsonlIfNeeded(sessionFile, targetDepth, force = false, tokenBudgetOverride) {
1177
- if (!sessionFile || typeof sessionFile !== 'string')
1178
- return false;
1179
- try {
1180
- const raw = await fs.readFile(sessionFile, 'utf-8');
1181
- const lines = raw.split('\n').filter(l => l.trim());
1182
- if (lines.length === 0)
1183
- return false;
1184
- const header = lines[0];
1185
- const entries = [];
1186
- for (let i = 1; i < lines.length; i++) {
1187
- try {
1188
- entries.push({ line: lines[i], parsed: JSON.parse(lines[i]) });
1189
- }
1190
- catch {
1191
- entries.push({ line: lines[i], parsed: null });
1192
- }
1193
- // Yield every 100 entries to avoid blocking the event loop
1194
- if (i % 100 === 0)
1195
- await new Promise(r => setImmediate(r));
1196
- }
1197
- const messageEntries = [];
1198
- const metadataEntries = [];
1199
- for (const e of entries) {
1200
- if (e.parsed?.type === 'message') {
1201
- messageEntries.push(e);
1202
- }
1203
- else {
1204
- metadataEntries.push(e);
1205
- }
1206
- }
1207
- // Only rewrite if meaningfully over target — unless force=true (over-budget path)
1208
- if (!force && messageEntries.length <= targetDepth * 1.5)
1209
- return false;
1210
- // If a token budget is specified, keep newest messages within that budget
1211
- let keptMessages;
1212
- if (tokenBudgetOverride) {
1213
- let tokenCount = 0;
1214
- const kept = [];
1215
- for (let i = messageEntries.length - 1; i >= 0 && kept.length < targetDepth; i--) {
1216
- const m = messageEntries[i].parsed?.message ?? messageEntries[i].parsed;
1217
- let t = 0;
1218
- if (m?.content)
1219
- t += Math.ceil(JSON.stringify(m.content).length / 4);
1220
- if (m?.textContent)
1221
- t += Math.ceil(String(m.textContent).length / 4);
1222
- if (m?.toolResults)
1223
- t += Math.ceil(JSON.stringify(m.toolResults).length / 4);
1224
- if (m?.toolCalls)
1225
- t += Math.ceil(JSON.stringify(m.toolCalls).length / 4);
1226
- if (tokenCount + t > tokenBudgetOverride && kept.length > 0)
1227
- break;
1228
- kept.unshift(messageEntries[i]);
1229
- tokenCount += t;
1230
- }
1231
- keptMessages = kept;
1232
- }
1233
- else {
1234
- keptMessages = messageEntries.slice(-targetDepth);
1235
- }
1236
- const keptSet = new Set(keptMessages.map(e => e.line));
1237
- const metaSet = new Set(metadataEntries.map(e => e.line));
1238
- const rebuilt = [header];
1239
- for (const e of entries) {
1240
- if (metaSet.has(e.line) || keptSet.has(e.line)) {
1241
- rebuilt.push(e.line);
1242
- }
1243
- }
1244
- const tmpPath = `${sessionFile}.hm-compact-${process.pid}-${Date.now()}.tmp`;
1245
- await fs.writeFile(tmpPath, rebuilt.join('\n') + '\n', 'utf-8');
1246
- await fs.rename(tmpPath, sessionFile);
1247
- console.log(`[hypermem-plugin] truncateJsonl: ${entries.length} → ${rebuilt.length - 1} entries ` +
1248
- `(kept ${keptMessages.length} messages + ${metadataEntries.length} metadata, file=${sessionFile.split('/').pop()})`);
1249
- return true;
1250
- }
1251
- catch (err) {
1252
- // ENOENT is expected when session file doesn't exist yet — not worth logging
1253
- if (err.code !== 'ENOENT') {
1254
- console.warn('[hypermem-plugin] truncateJsonl failed (non-fatal):', err.message);
1255
- }
1256
- return false;
1257
- }
1258
- }
1259
- function createHyperMemEngine() {
1260
- return {
1261
- info: {
1262
- id: 'hypercompositor',
1263
- name: 'hypermem context engine',
1264
- version: '0.6.3',
1265
- // We own compaction — assemble() trims to budget via the compositor safety
1266
- // valve, so runtime compaction is never needed. compact() handles any
1267
- // explicit calls by trimming the Redis history window directly.
1268
- ownsCompaction: true,
1269
- },
1270
- /**
1271
- * Bootstrap: warm Redis session for this agent, register in fleet if needed.
1272
- *
1273
- * Idempotent — skips warming if the session is already hot in Redis.
1274
- * Without this guard, the OpenClaw runtime calls bootstrap() on every turn
1275
- * (not just session start), causing:
1276
- * 1. A SQLite read + Redis pipeline push on every message (lane lock)
1277
- * 2. 250 messages re-pushed to Redis per turn (dedup in pushHistory helps,
1278
- * but the read cost still runs)
1279
- * 3. Followup queue drain blocked until warm completes
1280
- *
1281
- * With this guard: cold start = full warm; hot session = single EXISTS check.
1282
- */
1283
- async bootstrap({ sessionId, sessionKey }) {
1284
- try {
1285
- const hm = await getHyperMem();
1286
- const sk = resolveSessionKey(sessionId, sessionKey);
1287
- const agentId = extractAgentId(sk);
1288
- // EC1 JSONL truncation moved to maintain() — bootstrap stays fast.
1289
- // B2: Session-restart detection — rotateSessionContext hook.
1290
- // When the runtime starts a new session (new sessionId) for an existing
1291
- // sessionKey, archive the old context head and create a fresh active
1292
- // context so the new conversation starts clean. This prevents the new
1293
- // session from inheriting a stale context head pointer from the prior run.
1294
- //
1295
- // Detection: if a conversation row exists for this sessionKey AND the
1296
- // stored session_id differs from the incoming sessionId (runtime-assigned),
1297
- // treat this as a session restart.
1298
- //
1299
- // Non-fatal: context rotation is best-effort and never blocks bootstrap.
1300
- if (sessionId) {
1301
- try {
1302
- const _msgDb = hm.dbManager.getMessageDb(agentId);
1303
- if (_msgDb) {
1304
- const _existingConv = _msgDb.prepare('SELECT id, session_id FROM conversations WHERE session_key = ? LIMIT 1').get(sk);
1305
- if (_existingConv &&
1306
- _existingConv.session_id !== null &&
1307
- _existingConv.session_id !== sessionId) {
1308
- // Distinct sessionId — this is a session restart for an existing sessionKey.
1309
- rotateSessionContext(_msgDb, agentId, sk, _existingConv.id);
1310
- // Update the stored session_id to the new one.
1311
- try {
1312
- _msgDb.prepare('UPDATE conversations SET session_id = ? WHERE id = ?')
1313
- .run(sessionId, _existingConv.id);
1314
- }
1315
- catch {
1316
- // Best-effort — column may not exist in older schemas
1317
- }
1318
- console.log(`[hypermem-plugin] bootstrap: session restart detected for ${agentId}/${sk} ` +
1319
- `(prev session_id=${_existingConv.session_id}, new=${sessionId}) — context rotated`);
1320
- }
1321
- else if (_existingConv && _existingConv.session_id === null && sessionId) {
1322
- // Conversation exists but session_id was never recorded — stamp it now.
1323
- try {
1324
- _msgDb.prepare('UPDATE conversations SET session_id = ? WHERE id = ?')
1325
- .run(sessionId, _existingConv.id);
1326
- }
1327
- catch {
1328
- // Best-effort
1329
- }
1330
- }
1331
- }
1332
- }
1333
- catch (rotateErr) {
1334
- // Non-fatal — never block bootstrap on context rotation
1335
- console.warn('[hypermem-plugin] bootstrap: rotateSessionContext failed (non-fatal):', rotateErr.message);
1336
- }
1337
- }
1338
- // Fast path: if session already has history in Redis, skip warm entirely.
1339
- // sessionExists() is a single EXISTS call — sub-millisecond cost.
1340
- const alreadyWarm = await hm.cache.sessionExists(agentId, sk);
1341
- if (alreadyWarm) {
1342
- return { bootstrapped: true };
1343
- }
1344
- // In-flight dedup: if a warm is already running for this session key,
1345
- // reuse that promise instead of launching a second concurrent warm.
1346
- const inflightKey = `${agentId}::${sk}`;
1347
- const existing = _warmInFlight.get(inflightKey);
1348
- if (existing) {
1349
- await existing;
1350
- return { bootstrapped: true };
1351
- }
1352
- // Cold start: warm Redis with the session — pre-loads history + slots
1353
- // CRIT-002: Load supplemental identity files (MOTIVATIONS.md, STYLE.md) that are
1354
- // NOT already injected by OpenClaw's contextInjection into the system prompt.
1355
- // SOUL.md and IDENTITY.md are filtered out here because OpenClaw injects them
1356
- // via workspace bootstrap — re-injecting them via the identity slot would cause
1357
- // duplication. Only agent-specific extras (MOTIVATIONS.md, STYLE.md) are included.
1358
- // Non-fatal: missing files are silently skipped.
1359
- let identityBlock;
1360
- try {
1361
- // Council agents live at workspace/<agentId>/
1362
- // Other agents at workspace/<agentId>/ — try council path first
1363
- const homedir = os.homedir();
1364
- const councilPath = path.join(homedir, '.openclaw', 'workspace', agentId);
1365
- const workspacePath = path.join(homedir, '.openclaw', 'workspace', agentId);
1366
- let wsPath = councilPath;
1367
- try {
1368
- await fs.access(councilPath);
1369
- }
1370
- catch {
1371
- wsPath = workspacePath;
1372
- }
1373
- const identityFiles = ['SOUL.md', 'IDENTITY.md', 'MOTIVATIONS.md', 'STYLE.md']
1374
- .filter(f => !OPENCLAW_BOOTSTRAP_FILES.has(f));
1375
- const parts = [];
1376
- for (const fname of identityFiles) {
1377
- try {
1378
- const content = await fs.readFile(path.join(wsPath, fname), 'utf-8');
1379
- if (content.trim())
1380
- parts.push(content.trim());
1381
- }
1382
- catch {
1383
- // File absent — skip silently
1384
- }
1385
- }
1386
- if (parts.length > 0)
1387
- identityBlock = parts.join('\n\n');
1388
- }
1389
- catch {
1390
- // Identity load is best-effort — never block bootstrap on this
1391
- }
1392
- // Capture wsPath for post-warm seeding (declared in the identity block above)
1393
- let _wsPathForSeed;
1394
- try {
1395
- const homedir2 = os.homedir();
1396
- const councilPath2 = path.join(homedir2, '.openclaw', 'workspace', agentId);
1397
- const workspacePath2 = path.join(homedir2, '.openclaw', 'workspace', agentId);
1398
- try {
1399
- await fs.access(councilPath2);
1400
- _wsPathForSeed = councilPath2;
1401
- }
1402
- catch {
1403
- _wsPathForSeed = workspacePath2;
1404
- }
1405
- }
1406
- catch { /* non-fatal */ }
1407
- const warmPromise = hm.warm(agentId, sk, identityBlock ? { identity: identityBlock } : undefined).finally(() => {
1408
- _warmInFlight.delete(inflightKey);
1409
- });
1410
- _warmInFlight.set(inflightKey, warmPromise);
1411
- await warmPromise;
1412
- // ACA doc seeding — fire-and-forget after warm.
1413
- // Idempotent: WorkspaceSeeder skips files whose hash hasn't changed.
1414
- // Seeds SOUL.md, TOOLS.md, AGENTS.md, POLICY.md etc. into library.db
1415
- // doc_chunks so trigger-based retrieval can serve them at compose time.
1416
- if (_wsPathForSeed) {
1417
- const wsPathForSeed = _wsPathForSeed;
1418
- hm.seedWorkspace(wsPathForSeed, { agentId }).then(seedResult => {
1419
- if (seedResult.totalInserted > 0 || seedResult.reindexed > 0) {
1420
- console.log(`[hypermem-plugin] bootstrap: seeded workspace docs for ${agentId} ` +
1421
- `(+${seedResult.totalInserted} chunks, ${seedResult.reindexed} reindexed, ` +
1422
- `${seedResult.skipped} unchanged, ${seedResult.errors.length} errors)`);
1423
- }
1424
- }).catch(err => {
1425
- console.warn('[hypermem-plugin] bootstrap: workspace seeding failed (non-fatal):', err.message);
1426
- });
1427
- }
1428
- // Post-warm pressure check: if messages.db had accumulated history,
1429
- // warm() may have loaded the session straight to 80%+. Pre-trim now
1430
- // so the first turn has headroom instead of starting saturated.
1431
- // This is the "restart at 98%" failure mode reported by Eve 2026-04-05:
1432
- // JSONL truncation + Redis flush isn't enough if messages.db is still full
1433
- // and warm() reloads it. Trim here closes the loop.
1434
- try {
1435
- const postWarmTokens = await estimateWindowTokens(hm, agentId, sk);
1436
- // Use a conservative 90k default; if the session is genuinely large,
1437
- // we'll underestimate budget and trim more aggressively — that's fine.
1438
- const warmBudget = 90_000;
1439
- const warmPressure = postWarmTokens / warmBudget;
1440
- if (warmPressure > 0.80) {
1441
- // Sprint 2.2a: demote warmstart to guard telemetry.
1442
- //
1443
- // Previously this path performed a real trim + invalidateWindow
1444
- // and emitted `event:'trim'` with path='warmstart'. Assemble
1445
- // (tool-loop + normal/subagent) is the steady-state owner now,
1446
- // so the first turn's assemble.* trim absorbs any remaining
1447
- // post-warm pressure. Keeping the pressure check + threshold
1448
- // branch here preserves observability via `event:'trim-guard'`
1449
- // without mutating Redis history or the window cache.
1450
- guardTelemetry({
1451
- path: 'warmstart',
1452
- agentId, sessionKey: sk,
1453
- reason: 'warmstart-pressure-demoted',
1454
- });
1455
- }
1456
- }
1457
- catch {
1458
- // Non-fatal — first turn's tool-loop trim is the fallback
1459
- }
1460
- return { bootstrapped: true };
1461
- }
1462
- catch (err) {
1463
- // Bootstrap failure is non-fatal — log and continue
1464
- console.warn('[hypermem-plugin] bootstrap failed:', err.message);
1465
- return { bootstrapped: false, reason: err.message };
1466
- }
1467
- },
1468
- /**
1469
- * Transcript maintenance — runs after bootstrap, successful turns, or compaction.
1470
- *
1471
- * Moved from bootstrap: proactive JSONL truncation is forward-looking (helps
1472
- * next restart, not current session), so it belongs in maintenance, not init.
1473
- * Also runs tool pair repair on Redis history to fix orphaned pairs from
1474
- * trim/compaction passes.
1475
- */
1476
- async maintain({ sessionId, sessionKey, sessionFile }) {
1477
- let changed = false;
1478
- let bytesFreed = 0;
1479
- let rewrittenEntries = 0;
1480
- try {
1481
- const hm = await getHyperMem();
1482
- const sk = resolveSessionKey(sessionId, sessionKey);
1483
- const agentId = extractAgentId(sk);
1484
- // 1. Proactive JSONL truncation (EC1 guard — next restart loads clean)
1485
- try {
1486
- const EC1_MAX_MESSAGES = 60;
1487
- const EC1_TOKEN_BUDGET = Math.floor(128_000 * 0.40);
1488
- const truncated = await truncateJsonlIfNeeded(sessionFile, EC1_MAX_MESSAGES, false, EC1_TOKEN_BUDGET);
1489
- if (truncated) {
1490
- console.log(`[hypermem-plugin] maintain: proactive JSONL trim for ${agentId} ` +
1491
- `(EC1 guard — next restart will load clean)`);
1492
- changed = true;
1493
- }
1494
- }
1495
- catch {
1496
- // Non-fatal — JSONL truncation is best-effort
1497
- }
1498
- // 2. Redis history tool pair repair
1499
- // Compaction and trim passes can orphan tool_call/tool_result pairs.
1500
- // Anthropic and Gemini reject orphaned pairs with 400 errors.
1501
- try {
1502
- const history = await hm.cache.getHistory(agentId, sk);
1503
- if (history && history.length > 0) {
1504
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1505
- const repairedHistory = repairToolPairs(history);
1506
- const removedCount = history.length - repairedHistory.length;
1507
- if (removedCount > 0) {
1508
- await hm.cache.replaceHistory(agentId, sk, repairedHistory);
1509
- await hm.cache.invalidateWindow(agentId, sk);
1510
- console.log(`[hypermem-plugin] maintain: repaired tool pairs in Redis history ` +
1511
- `for ${agentId} (removed ${removedCount} orphaned messages)`);
1512
- changed = true;
1513
- rewrittenEntries += removedCount;
1514
- // Rough estimate: ~500 bytes per removed message
1515
- bytesFreed += removedCount * 500;
1516
- }
1517
- }
1518
- }
1519
- catch {
1520
- // Non-fatal
1521
- }
1522
- return { changed, bytesFreed, rewrittenEntries };
1523
- }
1524
- catch (err) {
1525
- console.warn('[hypermem-plugin] maintain failed:', err.message);
1526
- return { changed, bytesFreed, rewrittenEntries, reason: err.message };
1527
- }
1528
- },
1529
- /**
1530
- * Ingest a single message into hypermem's message store.
1531
- * Skip heartbeats — they're noise in the memory store.
1532
- */
1533
- async ingest({ sessionId, sessionKey, message, isHeartbeat }) {
1534
- if (isHeartbeat) {
1535
- return { ingested: false };
1536
- }
1537
- // Skip system messages — they come from the runtime, not the conversation
1538
- const msg = message;
1539
- if (msg.role === 'system') {
1540
- return { ingested: false };
1541
- }
1542
- try {
1543
- const hm = await getHyperMem();
1544
- const sk = resolveSessionKey(sessionId, sessionKey);
1545
- const agentId = extractAgentId(sk);
1546
- let neutral = toNeutralMessage(msg);
1547
- // Route to appropriate record method based on role.
1548
- // User messages are intentionally NOT recorded here — afterTurn() handles
1549
- // user recording with proper metadata stripping (stripMessageMetadata).
1550
- // Recording here too causes dual-write: once raw (here), once clean (afterTurn).
1551
- if (neutral.role === 'user') {
1552
- return { ingested: false };
1553
- }
1554
- // ── Pre-ingestion wave guard ──────────────────────────────────────────
1555
- // Tool result payloads can be 10k-50k tokens each. When a parallel tool
1556
- // batch (4-6 results) lands while the session is already at 70%+, storing
1557
- // full payloads pushes the hot window past the nuclear path threshold
1558
- // before the next assemble() can trim. Use current hot-window state as
1559
- // the pressure signal (appropriate here, we're deciding what to write TO
1560
- // the window).
1561
- //
1562
- // Above 70%: truncate toolResult content in transcript, but keep the
1563
- // full payload durable in tool_artifacts (schema v9). Stub carries
1564
- // artifactId so the compositor can hydrate on demand.
1565
- // Above 85%: full stub replacement in transcript, still with artifactId.
1566
- // At all levels: the full payload is persisted durably. No data loss.
1567
- const isInboundToolResult = msg.role === 'tool' || msg.role === 'tool_result' || msg.role === 'toolResult';
1568
- if (isInboundToolResult && neutral.toolResults && neutral.toolResults.length > 0) {
1569
- const windowTokens = await estimateWindowTokens(hm, agentId, sk);
1570
- const effectiveBudget = computeEffectiveBudget(undefined);
1571
- const windowPressure = windowTokens / effectiveBudget;
1572
- // Error tool results are always preserved intact: they're small and
1573
- // the model needs the error signal to understand what went wrong.
1574
- const hasErrorResult = neutral.toolResults.some(tr => tr.isError);
1575
- // Only apply degradation / artifact capture above elevated pressure.
1576
- if (windowPressure > 0.70) {
1577
- const MAX_TOOL_RESULT_CHARS = 500;
1578
- const highPressure = windowPressure > 0.85;
1579
- const reason = highPressure ? 'wave_guard_pressure_high' : 'wave_guard_pressure_elevated';
1580
- // For each non-error tool result, persist the full payload as a
1581
- // durable artifact first, then rewrite the transcript entry to
1582
- // either a full stub (high pressure) or a truncated stub with an
1583
- // artifact pointer (elevated pressure).
1584
- const rewrittenResults = await Promise.all(neutral.toolResults.map(async (tr) => {
1585
- if (tr.isError)
1586
- return tr;
1587
- const content = typeof tr.content === 'string'
1588
- ? tr.content
1589
- : JSON.stringify(tr.content);
1590
- // At elevated pressure, small payloads pass through unchanged.
1591
- if (!highPressure && content.length <= MAX_TOOL_RESULT_CHARS) {
1592
- return tr;
1593
- }
1594
- let artifactId;
1595
- try {
1596
- const record = await hm.recordToolArtifact(agentId, sk, {
1597
- toolName: tr.name || 'tool_result',
1598
- toolCallId: tr.callId || undefined,
1599
- isError: false,
1600
- payload: content,
1601
- summary: content.slice(0, 160),
1602
- });
1603
- artifactId = record.id;
1604
- }
1605
- catch (artErr) {
1606
- console.warn('[hypermem-plugin] tool artifact capture failed (non-fatal):', artErr.message);
1607
- }
1608
- const summary = highPressure
1609
- ? `omitted at ${(windowPressure * 100).toFixed(0)}% window pressure`
1610
- : `truncated at ${(windowPressure * 100).toFixed(0)}% pressure: ${Math.ceil(content.length / 4)} tokens`;
1611
- return {
1612
- ...tr,
1613
- content: formatToolChainStub({
1614
- name: tr.name || 'tool_result',
1615
- id: tr.callId || 'unknown',
1616
- status: 'ejected',
1617
- reason,
1618
- summary,
1619
- artifactId,
1620
- }),
1621
- };
1622
- }));
1623
- neutral = { ...neutral, toolResults: rewrittenResults };
1624
- console.log(`[hypermem] ingest wave-guard: ${highPressure ? 'stubbed' : 'truncated'} toolResult (window pressure ${(windowPressure * 100).toFixed(0)}% > ${highPressure ? 85 : 70}%)${hasErrorResult ? ' + error results preserved' : ''} - full payload persisted to tool_artifacts`);
1625
- }
1626
- }
1627
- await hm.recordAssistantMessage(agentId, sk, neutral);
1628
- return { ingested: true };
1629
- }
1630
- catch (err) {
1631
- // Ingest failure is non-fatal — record is best-effort
1632
- console.warn('[hypermem-plugin] ingest failed:', err.message);
1633
- return { ingested: false };
1634
- }
1635
- },
1636
- /**
1637
- * Batch ingest: process multiple messages in a single call.
1638
- *
1639
- * Note: when afterTurn() is defined (which it is), the runtime calls
1640
- * afterTurn instead of ingest/ingestBatch. This is here for interface
1641
- * completeness and forward compatibility.
1642
- */
1643
- async ingestBatch({ sessionId, sessionKey, messages, isHeartbeat }) {
1644
- if (isHeartbeat) {
1645
- return { ingestedCount: 0 };
1646
- }
1647
- let ingestedCount = 0;
1648
- try {
1649
- const hm = await getHyperMem();
1650
- const sk = resolveSessionKey(sessionId, sessionKey);
1651
- const agentId = extractAgentId(sk);
1652
- for (const message of messages) {
1653
- const msg = message;
1654
- if (msg.role === 'system')
1655
- continue;
1656
- const neutral = toNeutralMessage(msg);
1657
- if (neutral.role === 'user' && !neutral.toolResults?.length) {
1658
- await hm.recordUserMessage(agentId, sk, stripMessageMetadata(neutral.textContent ?? ''));
1659
- }
1660
- else {
1661
- await hm.recordAssistantMessage(agentId, sk, neutral);
1662
- }
1663
- ingestedCount++;
1664
- }
1665
- }
1666
- catch (err) {
1667
- console.warn('[hypermem-plugin] ingestBatch failed:', err.message);
1668
- }
1669
- return { ingestedCount };
1670
- },
1671
- /**
1672
- * Assemble model context from all four hypermem layers.
1673
- *
1674
- * The `messages` param contains the current conversation history from the
1675
- * runtime. We pass the prompt (latest user message) as the retrieval query,
1676
- * and let the compositor build the full context.
1677
- *
1678
- * Returns:
1679
- * messages — full assembled message array for the model
1680
- * estimatedTokens — token count of assembled context
1681
- * systemPromptAddition — facts/recall/episodes injected before runtime system prompt
1682
- */
1683
- async assemble({ sessionId, sessionKey, messages, tokenBudget, prompt, model }) {
1684
- // ── Tool-loop guard ──────────────────────────────────────────────────────
1685
- // When the last message is a toolResult, the runtime is mid tool-loop:
1686
- // the model already has full context from the initial turn assembly.
1687
- // Re-running the full compose pipeline here is wasteful and, in long
1688
- // tool loops, causes cumulative context growth that triggers preemptive
1689
- // context overflow. Pass the messages through as-is.
1690
- //
1691
- // Matches OpenClaw's legacy behavior: the legacy engine's assemble() is a
1692
- // pass-through that never re-injects context on tool-loop calls.
1693
- const lastMsg = messages[messages.length - 1];
1694
- const isToolLoop = lastMsg?.role === 'toolResult' || lastMsg?.role === 'tool';
1695
- // Telemetry: emit one assembleTrace at entry. Path taxonomy:
1696
- // 'subagent' - session key matches the subagent pattern
1697
- // 'cold' - normal full-assembly or tool-loop entry (a separate
1698
- // 'replay' trace is emitted if the cache replay fast
1699
- // path is taken below)
1700
- // Zero-cost when HYPERMEM_TELEMETRY !== '1'.
1701
- //
1702
- // Trim-ownership turn context (Sprint 2): the turnId is also used to
1703
- // scope the shared trim-owner claim helper so duplicate steady-state
1704
- // trims in a single assemble() turn can be detected and (under
1705
- // NODE_ENV='development') throw loudly. We always allocate the turnId
1706
- // and open the scope — the map write is cheap and keeps enforcement
1707
- // active even when telemetry is off. The scope is closed in the
1708
- // finally block wrapping the full assemble body below.
1709
- const _asmSk = resolveSessionKey(sessionId, sessionKey);
1710
- const _asmTurnId = nextTurnId();
1711
- beginTrimOwnerTurn(_asmSk, _asmTurnId);
1712
- if (telemetryEnabled()) {
1713
- const _agentId = extractAgentId(_asmSk);
1714
- const _entryPath = _asmSk.includes('subagent:')
1715
- ? 'subagent'
1716
- : 'cold';
1717
- assembleTrace({
1718
- agentId: _agentId,
1719
- sessionKey: _asmSk,
1720
- turnId: _asmTurnId,
1721
- path: _entryPath,
1722
- toolLoop: isToolLoop,
1723
- msgCount: messages.length,
1724
- });
1725
- }
1726
- try {
1727
- if (isToolLoop) {
1728
- // Tool-loop turns: pass messages through unchanged but still:
1729
- // 1. Run the trim guardrail — tool loops accumulate history as fast
1730
- // as regular turns, and the old path skipped trim entirely, leaving
1731
- // the compaction guard blind (received estimatedTokens=0).
1732
- // 2. Return a real estimatedTokens = windowTokens + cached overhead,
1733
- // so the guard has accurate signal and can fire when needed.
1734
- //
1735
- // Fix (ingestion-wave): use pressure-tiered trim instead of fixed 80%.
1736
- // At 91% with 5 parallel web_search calls incoming (~20-30% of budget),
1737
- // a fixed 80% trim only frees 11% headroom — the wave overflows anyway
1738
- // and results strip silently. Tier the trim target based on pre-trim
1739
- // pressure so high-pressure sessions get real headroom before results land.
1740
- const effectiveBudget = computeEffectiveBudget(tokenBudget, model);
1741
- try {
1742
- const hm = await getHyperMem();
1743
- const sk = resolveSessionKey(sessionId, sessionKey);
1744
- const agentId = extractAgentId(sk);
1745
- // ── Image / heavy-content eviction pre-pass ──────────────────────
1746
- // Evict stale image payloads and large tool results before measuring
1747
- // pressure. This frees tokens without compaction — images alone can
1748
- // account for 30%+ of context from a single screenshot 2 turns ago.
1749
- const evictionCfg = _evictionConfig;
1750
- const evictionEnabled = evictionCfg?.enabled !== false;
1751
- let workingMessages = messages;
1752
- if (evictionEnabled) {
1753
- const { messages: evicted, stats: evStats } = evictStaleContent(messages, {
1754
- imageAgeTurns: evictionCfg?.imageAgeTurns,
1755
- toolResultAgeTurns: evictionCfg?.toolResultAgeTurns,
1756
- minTokensToEvict: evictionCfg?.minTokensToEvict,
1757
- keepPreviewChars: evictionCfg?.keepPreviewChars,
1758
- });
1759
- workingMessages = evicted;
1760
- if (evStats.tokensFreed > 0) {
1761
- console.log(`[hypermem] eviction: ${evStats.imagesEvicted} images, ` +
1762
- `${evStats.toolResultsEvicted} tool results, ` +
1763
- `~${evStats.tokensFreed.toLocaleString()} tokens freed`);
1764
- }
1765
- }
1766
- // Measure pressure from the in-memory message array we are actually about
1767
- // to shape and return. Redis remains a cross-check only.
1768
- const runtimeTokens = estimateMessageArrayTokens(workingMessages);
1769
- const redisTokens = await estimateWindowTokens(hm, agentId, sk);
1770
- const replayRecovery = decideReplayRecovery({
1771
- currentState: normalizeReplayRecoveryState(await hm.cache.getSlot(agentId, sk, 'replayRecoveryState').catch(() => '')),
1772
- runtimeTokens,
1773
- redisTokens,
1774
- effectiveBudget,
1775
- });
1776
- const replayMarkerText = replayRecovery.emittedText;
1777
- const preTrimTokens = runtimeTokens;
1778
- const pressure = preTrimTokens / effectiveBudget;
1779
- // Pressure-tiered trim targets use a single authority: the working
1780
- // message array. Redis drift is logged as an anomaly, never used as
1781
- // a trim trigger. Replay recovery gets its own explicit bounded mode
1782
- // instead of sharing the steady-state pressure heuristics.
1783
- let trimTarget;
1784
- if (typeof replayRecovery.trimTargetOverride === 'number') {
1785
- trimTarget = replayRecovery.trimTargetOverride;
1786
- }
1787
- else if (pressure > 0.85) {
1788
- trimTarget = 0.40; // critical: 60% headroom for incoming wave
1789
- }
1790
- else if (pressure > 0.80) {
1791
- trimTarget = 0.50; // high: 50% headroom
1792
- }
1793
- else if (pressure > 0.75) {
1794
- trimTarget = 0.55; // elevated: 45% headroom
1795
- }
1796
- else {
1797
- trimTarget = 0.65; // normal: 35% headroom
1798
- }
1799
- const trimBudget = Math.floor(effectiveBudget * trimTarget);
1800
- // Steady-state trim owner claim (Sprint 2.2a): route through the
1801
- // shared helper keyed by (sessionKey, turnId). In development a
1802
- // duplicate steady-state trim in the same assemble() turn throws.
1803
- // In non-development a duplicate returns false; the real trim +
1804
- // its `event:'trim'` emission are gated on the successful claim so
1805
- // a duplicate claim is actually suppressed, not just warned.
1806
- // Compact.* paths are exempt; this path is assemble-owned.
1807
- const toolLoopClaimed = claimTrimOwner(sk, _asmTurnId, 'assemble.toolLoop');
1808
- let trimmed = 0;
1809
- let toolLoopCacheInvalidated = false;
1810
- if (toolLoopClaimed) {
1811
- trimmed = await hm.cache.trimHistoryToTokenBudget(agentId, sk, trimBudget);
1812
- if (trimmed > 0) {
1813
- await hm.cache.invalidateWindow(agentId, sk);
1814
- toolLoopCacheInvalidated = true;
1815
- }
1816
- if (telemetryEnabled()) {
1817
- const postTrimTokens = await estimateWindowTokens(hm, agentId, sk).catch(() => 0);
1818
- trimTelemetry({
1819
- path: 'assemble.toolLoop',
1820
- agentId, sessionKey: sk,
1821
- preTokens: preTrimTokens,
1822
- postTokens: postTrimTokens,
1823
- removed: trimmed,
1824
- cacheInvalidated: toolLoopCacheInvalidated,
1825
- reason: `pressure=${(pressure * 100).toFixed(1)}%`,
1826
- });
1827
- }
1828
- }
1829
- else if (telemetryEnabled()) {
1830
- // Surface the suppressed-duplicate as a bounded guard record so
1831
- // downstream reporting can see how often the gate fires. No
1832
- // history or window mutation here.
1833
- guardTelemetry({
1834
- path: 'assemble.toolLoop',
1835
- agentId, sessionKey: sk,
1836
- reason: 'duplicate-claim-suppressed',
1837
- });
1838
- }
1839
- // Also trim the messages array itself to match the budget.
1840
- // Redis trim clears the *next* turn's window. This turn's messages are
1841
- // still the full runtime array — if we return them unchanged at 94%,
1842
- // OpenClaw strips tool results before sending to the model regardless
1843
- // of what estimatedTokens says. We need to return a slimmer array now.
1844
- //
1845
- // Strategy: keep system/identity messages at the front, then fill from
1846
- // the back (most recent) until we hit trimBudget. Drop the middle.
1847
- let trimmedMessages = workingMessages;
1848
- if (pressure > trimTarget) {
1849
- const msgArray = workingMessages;
1850
- // Separate system messages (always keep) from conversation turns
1851
- const systemMsgs = msgArray.filter(m => m.role === 'system');
1852
- const convMsgs = msgArray.filter(m => m.role !== 'system');
1853
- // Pre-process: inline-truncate large tool results before budget-fill drop.
1854
- // A message with a 40k-token tool result that barely misses budget gets dropped
1855
- // entirely. Replacing with a placeholder keeps the turn's metadata in context
1856
- // while freeing the bulk of the tokens.
1857
- const MAX_INLINE_TOOL_CHARS = 2000; // ~500 tokens
1858
- // FIX (Bug 3): handle both NeutralMessage format (m.toolResults) and
1859
- // OpenClaw native format (m.content array with type='tool_result' blocks).
1860
- // Old guard `if (!m.toolResults)` skipped every native-format message.
1861
- // Also fixed: replacement must be valid NeutralToolResult { callId, name, content },
1862
- // not { type, text } which breaks pair-integrity downstream.
1863
- const processedConvMsgs = convMsgs.map(m => {
1864
- // NeutralMessage format
1865
- if (m.toolResults) {
1866
- const resultStr = JSON.stringify(m.toolResults);
1867
- if (resultStr.length <= MAX_INLINE_TOOL_CHARS)
1868
- return m;
1869
- const firstResult = m.toolResults[0];
1870
- return {
1871
- ...m,
1872
- toolResults: [{
1873
- callId: firstResult?.callId ?? 'unknown',
1874
- name: firstResult?.name ?? 'tool',
1875
- content: `[tool result truncated: ${Math.ceil(resultStr.length / 4)} tokens]`,
1876
- }],
1877
- };
1878
- }
1879
- // OpenClaw native format
1880
- if (Array.isArray(m.content)) {
1881
- const content = m.content;
1882
- const hasLarge = content.some(c => {
1883
- if (c.type !== 'tool_result')
1884
- return false;
1885
- const val = typeof c.content === 'string' ? c.content : JSON.stringify(c.content ?? '');
1886
- return val.length > MAX_INLINE_TOOL_CHARS;
1887
- });
1888
- if (!hasLarge)
1889
- return m;
1890
- return {
1891
- ...m,
1892
- content: content.map(c => {
1893
- if (c.type !== 'tool_result')
1894
- return c;
1895
- const val = typeof c.content === 'string' ? c.content : JSON.stringify(c.content ?? '');
1896
- if (val.length <= MAX_INLINE_TOOL_CHARS)
1897
- return c;
1898
- return { ...c, content: `[tool result truncated: ${Math.ceil(val.length / 4)} tokens]` };
1899
- }),
1900
- };
1901
- }
1902
- return m;
1903
- });
1904
- // Fill from the back within budget
1905
- let budget = trimBudget;
1906
- // Reserve tokens for system messages using the same accounting
1907
- // function as the final composed-array estimate.
1908
- for (const sm of systemMsgs) {
1909
- budget -= estimateMessageTokens(sm);
1910
- }
1911
- const msgCost = (m) => estimateMessageTokens(m);
1912
- const clusters = clusterTranscriptMessages(processedConvMsgs);
1913
- const keptClusters = [];
1914
- const tailCluster = clusters.length > 0 ? clusters[clusters.length - 1] : [];
1915
- if (tailCluster.length > 0) {
1916
- budget -= tailCluster.reduce((sum, msg) => sum + msgCost(msg), 0);
1917
- keptClusters.unshift(tailCluster);
1918
- }
1919
- for (let i = clusters.length - 2; i >= 0 && budget > 0; i--) {
1920
- const cluster = clusters[i];
1921
- const clusterCost = cluster.reduce((sum, msg) => sum + msgCost(msg), 0);
1922
- if (budget - clusterCost >= 0) {
1923
- keptClusters.unshift(cluster);
1924
- budget -= clusterCost;
1925
- }
1926
- }
1927
- const kept = keptClusters.flat();
1928
- const keptCount = processedConvMsgs.length - kept.length;
1929
- if (keptCount > 0) {
1930
- console.log(`[hypermem-plugin] tool-loop trim: pressure=${(pressure * 100).toFixed(1)}% → ` +
1931
- `target=${(trimTarget * 100).toFixed(0)}% (redis=${trimmed} msgs, messages=${keptCount} dropped)`);
1932
- trimmedMessages = [...systemMsgs, ...kept];
1933
- }
1934
- else if (trimmed > 0) {
1935
- console.log(`[hypermem-plugin] tool-loop trim: pressure=${(pressure * 100).toFixed(1)}% → ` +
1936
- `target=${(trimTarget * 100).toFixed(0)}% (redis=${trimmed} msgs)`);
1937
- }
1938
- }
1939
- else if (trimmed > 0) {
1940
- console.log(`[hypermem-plugin] tool-loop trim: pressure=${(pressure * 100).toFixed(1)}% → ` +
1941
- `target=${(trimTarget * 100).toFixed(0)}% (redis=${trimmed} msgs)`);
1942
- }
1943
- // Apply tool gradient to compress large tool results before returning.
1944
- // Skip if deferToolPruning is enabled — OpenClaw's contextPruning handles it.
1945
- if (!_deferToolPruning) {
1946
- // The full compose path runs applyToolGradientToWindow during reshaping;
1947
- // the tool-loop path was previously skipping this, leaving a 40k-token
1948
- // web_search result uncompressed every turn.
1949
- try {
1950
- const gradientApplied = applyToolGradientToWindow(trimmedMessages, trimBudget);
1951
- trimmedMessages = gradientApplied;
1952
- }
1953
- catch {
1954
- // Non-fatal: if gradient fails, continue with untouched trimmedMessages
1955
- }
1956
- } // end deferToolPruning gate
1957
- // Repair orphaned tool pairs in the trimmed message list.
1958
- // In-memory trim (cluster drop) can strand tool_result messages whose
1959
- // paired tool_use was in a dropped cluster.
1960
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1961
- trimmedMessages = repairToolPairs(trimmedMessages);
1962
- const composedTokens = estimateMessageArrayTokens(trimmedMessages);
1963
- maybeLogPressureAccountingAnomaly({
1964
- path: 'assemble.toolLoop',
1965
- agentId,
1966
- sessionKey: sk,
1967
- runtimeTokens: preTrimTokens,
1968
- redisTokens,
1969
- composedTokens,
1970
- budget: effectiveBudget,
1971
- });
1972
- await persistReplayRecoveryState(hm, agentId, sk, replayRecovery.nextState);
1973
- degradationTelemetry({
1974
- agentId,
1975
- sessionKey: sk,
1976
- turnId: _asmTurnId,
1977
- path: 'toolLoop',
1978
- toolChainCoEjections: 0,
1979
- toolChainStubReplacements: 0,
1980
- artifactDegradations: 0,
1981
- replayState: replayRecovery.emittedMarker?.state,
1982
- replayReason: replayRecovery.emittedMarker?.reason,
1983
- });
1984
- const overhead = _overheadCache.get(sk) ?? getOverheadFallback();
1985
- return {
1986
- messages: trimmedMessages,
1987
- estimatedTokens: composedTokens + overhead,
1988
- systemPromptAddition: replayMarkerText || undefined,
1989
- };
1990
- }
1991
- catch {
1992
- // Non-fatal: return conservative estimate so guard doesn't go blind
1993
- return {
1994
- messages: messages,
1995
- estimatedTokens: Math.floor(effectiveBudget * 0.8),
1996
- };
1997
- }
1998
- }
1999
- try {
2000
- const hm = await getHyperMem();
2001
- const sk = resolveSessionKey(sessionId, sessionKey);
2002
- const agentId = extractAgentId(sk);
2003
- // ── Subagent warming control ─────────────────────────────────────────
2004
- // Detect subagent sessions by key pattern and apply warming mode.
2005
- // 'off' = passthrough (no HyperMem context at all)
2006
- // 'light' = facts + history only (skip library/wiki/semantic/keystones/doc chunks)
2007
- // 'full' = standard compositor pipeline
2008
- const isSubagent = sk.includes('subagent:');
2009
- if (isSubagent && _subagentWarming === 'off') {
2010
- console.log(`[hypermem-plugin] assemble: subagent warming=off, passthrough (sk: ${sk})`);
2011
- return {
2012
- messages: messages,
2013
- estimatedTokens: estimateMessageArrayTokens(messages),
2014
- };
2015
- }
2016
- if (isSubagent) {
2017
- console.log(`[hypermem-plugin] assemble: subagent warming=${_subagentWarming} (sk: ${sk})`);
2018
- }
2019
- // Resolve agent tier from fleet store (for doc chunk tier filtering)
2020
- let tier;
2021
- try {
2022
- const agent = _fleetStore?.getAgent(agentId);
2023
- tier = agent?.tier;
2024
- }
2025
- catch {
2026
- // Non-fatal — tier filtering just won't apply
2027
- }
2028
- // historyDepth: derive a safe message count from the token budget.
2029
- // Uses 50% of the budget for history (down from 60% — more budget goes to
2030
- // L3/L4 context slots now). Floor at 50, ceiling at 200.
2031
- // This is a preventive guard — the compositor's safety valve still trims
2032
- // by token count post-assembly, but limiting depth up front avoids
2033
- // feeding the compactor a window it can't reduce.
2034
- const effectiveBudget = computeEffectiveBudget(tokenBudget, model);
2035
- const historyDepth = Math.min(250, Math.max(50, Math.floor((effectiveBudget * 0.65) / 500)));
2036
- const runtimeEntryTokens = estimateMessageArrayTokens(messages);
2037
- const redisEntryTokens = await estimateWindowTokens(hm, agentId, sk);
2038
- const replayRecovery = decideReplayRecovery({
2039
- currentState: normalizeReplayRecoveryState(await hm.cache.getSlot(agentId, sk, 'replayRecoveryState').catch(() => '')),
2040
- runtimeTokens: runtimeEntryTokens,
2041
- redisTokens: redisEntryTokens,
2042
- effectiveBudget,
2043
- });
2044
- const replayHistoryDepth = replayRecovery.active && replayRecovery.historyDepthCap
2045
- ? Math.min(historyDepth, replayRecovery.historyDepthCap)
2046
- : historyDepth;
2047
- // ── Redis guardrail: trim history to token budget ────────────────────
2048
- // Prevents model-switch bloat: if an agent previously ran on a larger
2049
- // context window, Redis history may exceed the current model's budget.
2050
- // Trimming here (before compose) ensures the compositor never sees a
2051
- // history window it can't fit.
2052
- //
2053
- // Sprint 3 (AfterTurn Rebuild/Trim Loop Fix): the assemble.normal trim now
2054
- // first checks whether the window is already within trimBudget. When
2055
- // afterTurn's refreshRedisGradient caps the rebuilt window at the same
2056
- // 0.65 fraction (Sprint 3 compositor fix), the steady-state path will
2057
- // find preTokens <= trimBudget and skip the trim entirely. The trim only
2058
- // fires when real excess exists (pressure spikes, model switch, cold start),
2059
- // breaking the unconditional afterTurn→assemble trim churn loop.
2060
- //
2061
- // B3: Batch trim with growth allowance.
2062
- // Trim only fires when the window has grown past the soft target by more
2063
- // than TRIM_GROWTH_THRESHOLD (5%). When it does fire, trim to
2064
- // softTarget * (1 - TRIM_HEADROOM_FRACTION) so the window has room to
2065
- // grow for several turns before the next trim fires. This eliminates
2066
- // per-turn trim churn from minor natural growth (short assistant replies,
2067
- // small tool outputs) while still catching genuine pressure spikes.
2068
- try {
2069
- const { softBudget: trimSoftBudget, triggerBudget: trimTriggerBudget, targetBudget: trimTargetBudget, } = resolveTrimBudgets(effectiveBudget);
2070
- // Always read preTokens so we can make the skip decision and emit telemetry.
2071
- const preTokensNormal = await estimateWindowTokens(hm, agentId, sk).catch(() => 0);
2072
- const normalPath = isSubagent ? 'assemble.subagent' : 'assemble.normal';
2073
- // B3: Skip trim when window is within the growth-allowance envelope.
2074
- // This replaces the Sprint 3 `windowAlreadyFits` check (which only
2075
- // skipped at exactly ≤ softTarget). The growth allowance lets the
2076
- // window float up to +5% before triggering, avoiding trim on every
2077
- // turn that ends a few tokens above 65%.
2078
- const withinGrowthEnvelope = preTokensNormal > 0 && preTokensNormal <= trimTriggerBudget;
2079
- if (withinGrowthEnvelope) {
2080
- if (telemetryEnabled()) {
2081
- guardTelemetry({
2082
- path: normalPath,
2083
- agentId, sessionKey: sk,
2084
- reason: 'window-within-budget-skip',
2085
- });
2086
- }
2087
- }
2088
- else {
2089
- // Steady-state trim owner claim (Sprint 2.2a): route assemble.normal
2090
- // and assemble.subagent through the shared helper keyed by
2091
- // (sessionKey, _asmTurnId). The real trim + its `event:'trim'`
2092
- // emission are gated on the claim so a duplicate steady-state claim
2093
- // in the same turn is actually suppressed in production, not just
2094
- // warned. In development the duplicate throws.
2095
- const normalClaimed = claimTrimOwner(sk, _asmTurnId, normalPath);
2096
- if (normalClaimed) {
2097
- // B3: trim to the headroom target (below soft target) so the
2098
- // window has room to grow before the next trim fires.
2099
- const trimmed = await hm.cache.trimHistoryToTokenBudget(agentId, sk, trimTargetBudget);
2100
- let normalCacheInvalidated = false;
2101
- if (trimmed > 0) {
2102
- // Invalidate window cache since history changed
2103
- await hm.cache.invalidateWindow(agentId, sk);
2104
- normalCacheInvalidated = true;
2105
- }
2106
- if (telemetryEnabled()) {
2107
- const postTokensNormal = await estimateWindowTokens(hm, agentId, sk).catch(() => 0);
2108
- trimTelemetry({
2109
- path: normalPath,
2110
- agentId, sessionKey: sk,
2111
- preTokens: preTokensNormal,
2112
- postTokens: postTokensNormal,
2113
- removed: trimmed,
2114
- cacheInvalidated: normalCacheInvalidated,
2115
- reason: `b3:trigger=${trimTriggerBudget},target=${trimTargetBudget}`,
2116
- });
2117
- }
2118
- }
2119
- else if (telemetryEnabled()) {
2120
- guardTelemetry({
2121
- path: normalPath,
2122
- agentId, sessionKey: sk,
2123
- reason: 'duplicate-claim-suppressed',
2124
- });
2125
- }
2126
- }
2127
- }
2128
- catch (trimErr) {
2129
- // Non-fatal — compositor's budget-fit walk is the second line of defense
2130
- console.warn('[hypermem-plugin] assemble: Redis trim failed (non-fatal):', trimErr.message);
2131
- }
2132
- // ── Budget downshift: proactive reshape pass ───────────────────────────────────────
2133
- // Detect provider/model identity changes as well as raw budget changes.
2134
- // Provider routing matters operationally because the same model family can
2135
- // land on a different effective context window, for example Copilot Sonnet
2136
- // vs direct Anthropic Sonnet. Only budget downshifts trigger the demoted
2137
- // reshape guard, but verbose logs now show provider/model swaps even when
2138
- // the effective budget stays flat or increases.
2139
- let lastState = null;
2140
- try {
2141
- lastState = await hm.cache.getModelState(agentId, sk);
2142
- const DOWNSHIFT_THRESHOLD = 0.10;
2143
- const modelDelta = diffModelState(lastState, {
2144
- model,
2145
- tokenBudget: effectiveBudget,
2146
- });
2147
- const downshiftFraction = lastState?.tokenBudget
2148
- ? (lastState.tokenBudget - effectiveBudget) / lastState.tokenBudget
2149
- : 0;
2150
- const isDownshift = modelDelta.budgetDownshift && downshiftFraction > DOWNSHIFT_THRESHOLD;
2151
- if (lastState && (modelDelta.modelChanged || modelDelta.budgetChanged)) {
2152
- verboseLog(`[hypermem-plugin] model state change: ` +
2153
- `prev=${modelDelta.previousIdentity.modelKey ?? 'unknown'} ` +
2154
- `next=${modelDelta.currentIdentity.modelKey ?? 'unknown'} ` +
2155
- `providerChanged=${modelDelta.providerChanged} ` +
2156
- `modelIdChanged=${modelDelta.modelIdChanged} ` +
2157
- `budget=${lastState.tokenBudget}->${effectiveBudget}`);
2158
- }
2159
- if (isDownshift && !_deferToolPruning) {
2160
- // Sprint 2.2a: demote reshape to guard telemetry.
2161
- //
2162
- // Previously this branch re-ran applyToolGradientToWindow, wrote
2163
- // back via replaceHistory, invalidated the window cache, and
2164
- // stamped `reshapedAt` on model state. Assemble.* is the
2165
- // steady-state owner, so the subsequent assemble.normal /
2166
- // assemble.subagent trim (gated by claimTrimOwner) handles any
2167
- // real downshift pressure. Keeping the detection branch preserves
2168
- // observability; guardTelemetry records the would-be-reshape
2169
- // without mutating history, the window, or model state.
2170
- //
2171
- // CRITICAL: do NOT call setModelState({ reshapedAt, … }) here.
2172
- // compact() skips when reshapedAt is recent, which would cause it
2173
- // to skip on the strength of a reshape that never ran.
2174
- guardTelemetry({
2175
- path: 'reshape',
2176
- agentId, sessionKey: sk,
2177
- reason: 'reshape-downshift-demoted',
2178
- });
2179
- }
2180
- }
2181
- catch (reshapeErr) {
2182
- // Non-fatal — compositor safety valve is still the last defense
2183
- console.warn('[hypermem-plugin] assemble: reshape pass failed (non-fatal):', reshapeErr.message);
2184
- }
2185
- // ── Cache replay fast path ─────────────────────────────────────────────
2186
- // If the session was active recently, return the cached contextBlock
2187
- // (systemPromptAddition) to produce a byte-identical system prompt and
2188
- // hit the provider prefix cache (Anthropic / OpenAI).
2189
- // The message window is always rebuilt fresh — only the compositor output
2190
- // (contextBlock) is cached, since that's what determines prefix identity.
2191
- const cacheReplayThresholdMs = _cacheReplayThresholdMs;
2192
- let cachedContextBlock = null;
2193
- if (cacheReplayThresholdMs > 0 && !replayRecovery.shouldSkipCacheReplay) {
2194
- try {
2195
- const cachedAt = await hm.cache.getSlot(agentId, sk, 'assemblyContextAt');
2196
- if (cachedAt && Date.now() - parseInt(cachedAt) < cacheReplayThresholdMs) {
2197
- cachedContextBlock = await hm.cache.getSlot(agentId, sk, 'assemblyContextBlock');
2198
- if (cachedContextBlock) {
2199
- console.log(`[hypermem-plugin] assemble: cache replay hit for ${agentId} (${Math.round((Date.now() - parseInt(cachedAt)) / 1000)}s old)`);
2200
- if (telemetryEnabled()) {
2201
- assembleTrace({
2202
- agentId,
2203
- sessionKey: sk,
2204
- turnId: _asmTurnId,
2205
- path: 'replay',
2206
- toolLoop: isToolLoop,
2207
- msgCount: messages.length,
2208
- });
2209
- }
2210
- }
2211
- }
2212
- }
2213
- catch {
2214
- // Non-fatal — fall through to full assembly
2215
- }
2216
- }
2217
- // Subagent light mode: skip library/wiki/semantic/keystones/doc chunks.
2218
- // Keeps: system, identity, history, active facts, output profile, tool gradient.
2219
- const subagentLight = isSubagent && _subagentWarming === 'light';
2220
- const request = {
2221
- agentId,
2222
- sessionKey: sk,
2223
- tokenBudget: effectiveBudget,
2224
- historyDepth: lastState?.historyDepth && lastState.historyDepth < replayHistoryDepth
2225
- ? lastState.historyDepth
2226
- : replayHistoryDepth,
2227
- tier,
2228
- model, // pass model for provider detection
2229
- includeDocChunks: subagentLight ? false : !cachedContextBlock, // skip doc retrieval on cache hit or subagent light
2230
- includeLibrary: subagentLight ? false : undefined, // skip wiki/knowledge/preferences
2231
- includeSemanticRecall: subagentLight ? false : undefined, // skip vector/FTS recall
2232
- includeKeystones: subagentLight ? false : undefined, // skip keystone history injection
2233
- prompt,
2234
- skipProviderTranslation: true, // runtime handles provider translation
2235
- };
2236
- const result = await hm.compose(request);
2237
- degradationTelemetry({
2238
- agentId,
2239
- sessionKey: sk,
2240
- turnId: _asmTurnId,
2241
- path: 'compose',
2242
- toolChainCoEjections: result.diagnostics?.toolChainCoEjections ?? 0,
2243
- toolChainStubReplacements: result.diagnostics?.toolChainStubReplacements ?? 0,
2244
- artifactDegradations: result.diagnostics?.artifactDegradations ?? 0,
2245
- artifactOversizeThresholdTokens: result.diagnostics?.artifactOversizeThresholdTokens,
2246
- replayState: replayRecovery.emittedMarker?.state,
2247
- replayReason: replayRecovery.emittedMarker?.reason,
2248
- });
2249
- // Use cached contextBlock if available (cache replay), otherwise use fresh result.
2250
- // After a full compose, write the new contextBlock to cache for the next turn.
2251
- if (cachedContextBlock) {
2252
- result.contextBlock = cachedContextBlock;
2253
- }
2254
- else if (result.contextBlock && cacheReplayThresholdMs > 0 && !replayRecovery.shouldSkipCacheReplay && !replayRecovery.emittedText) {
2255
- // Write cache async — never block the assemble() return on this
2256
- const blockToCache = result.contextBlock;
2257
- const nowStr = Date.now().toString();
2258
- const ttlSec = Math.ceil((cacheReplayThresholdMs * 2) / 1000);
2259
- Promise.all([
2260
- hm.cache.setSlot(agentId, sk, 'assemblyContextBlock', blockToCache),
2261
- hm.cache.setSlot(agentId, sk, 'assemblyContextAt', nowStr),
2262
- ]).then(() => {
2263
- // Extend TTL on the cached keys to 2× the threshold
2264
- // setSlot uses the sessionTTL from RedisLayer config — acceptable fallback
2265
- }).catch(() => { });
2266
- }
2267
- if (replayRecovery.emittedText) {
2268
- result.contextBlock = result.contextBlock
2269
- ? `${result.contextBlock}
2270
- ${replayRecovery.emittedText}`
2271
- : replayRecovery.emittedText;
2272
- }
2273
- // Convert NeutralMessage[] → AgentMessage[] for the OpenClaw runtime.
2274
- // neutralToAgentMessage can return a single message or an array (tool results
2275
- // expand to individual ToolResultMessage objects), so we flatMap.
2276
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2277
- let outputMessages = result.messages
2278
- .filter(m => m.role != null)
2279
- .flatMap(m => neutralToAgentMessage(m));
2280
- const neutralPairStats = collectNeutralToolPairStats(result.messages);
2281
- const agentPairStats = collectAgentToolPairStats(outputMessages);
2282
- const toolPairAnomaly = neutralPairStats.missingToolResultCount > 0 ||
2283
- neutralPairStats.orphanToolResultCount > 0 ||
2284
- agentPairStats.missingToolResultCount > 0 ||
2285
- agentPairStats.orphanToolResultCount > 0 ||
2286
- agentPairStats.syntheticNoResultCount > 0
2287
- ? {
2288
- stage: 'assemble',
2289
- neutralMissingToolResultIds: neutralPairStats.missingToolResultIds.slice(0, 10),
2290
- neutralOrphanToolResultIds: neutralPairStats.orphanToolResultIds.slice(0, 10),
2291
- agentMissingToolResultIds: agentPairStats.missingToolResultIds.slice(0, 10),
2292
- agentOrphanToolResultIds: agentPairStats.orphanToolResultIds.slice(0, 10),
2293
- syntheticNoResultCount: agentPairStats.syntheticNoResultCount,
2294
- }
2295
- : undefined;
2296
- await bumpToolPairMetrics(hm, agentId, sk, {
2297
- composeCount: 1,
2298
- preBridgeMissingToolResults: neutralPairStats.missingToolResultCount,
2299
- preBridgeOrphanToolResults: neutralPairStats.orphanToolResultCount,
2300
- postBridgeMissingToolResults: agentPairStats.missingToolResultCount,
2301
- postBridgeOrphanToolResults: agentPairStats.orphanToolResultCount,
2302
- }, toolPairAnomaly);
2303
- if (toolPairAnomaly) {
2304
- console.warn(`[hypermem-plugin] tool-pair-integrity: ${agentId}/${sk} ` +
2305
- `neutralMissing=${neutralPairStats.missingToolResultCount} neutralOrphan=${neutralPairStats.orphanToolResultCount} ` +
2306
- `agentMissing=${agentPairStats.missingToolResultCount} agentOrphan=${agentPairStats.orphanToolResultCount} ` +
2307
- `synthetic=${agentPairStats.syntheticNoResultCount}`);
2308
- }
2309
- // Repair orphaned tool pairs before returning to provider.
2310
- // compaction/trim passes can remove tool_use blocks without removing their
2311
- // paired tool_result messages — Anthropic and Gemini reject these with 400.
2312
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2313
- outputMessages = repairToolPairs(outputMessages);
2314
- // Cache overhead for tool-loop turns: contextBlock tokens (chars/4) +
2315
- // tier-aware estimate for runtime system prompt (SOUL.md, identity,
2316
- // workspace files — not visible from inside the plugin).
2317
- const contextBlockTokens = Math.ceil((result.contextBlock?.length ?? 0) / 4);
2318
- const runtimeSystemTokens = getOverheadFallback(tier);
2319
- _overheadCache.set(sk, contextBlockTokens + runtimeSystemTokens);
2320
- await persistReplayRecoveryState(hm, agentId, sk, replayRecovery.nextState);
2321
- // Update model state for downshift detection on next turn
2322
- try {
2323
- const modelIdentity = resolveModelIdentity(model);
2324
- await hm.cache.setModelState(agentId, sk, {
2325
- model: model ?? 'unknown',
2326
- modelKey: modelIdentity.modelKey ?? undefined,
2327
- provider: modelIdentity.provider ?? undefined,
2328
- modelId: modelIdentity.modelId ?? undefined,
2329
- tokenBudget: effectiveBudget,
2330
- composedAt: new Date().toISOString(),
2331
- historyDepth,
2332
- });
2333
- }
2334
- catch {
2335
- // Non-fatal
2336
- }
2337
- return {
2338
- messages: outputMessages,
2339
- estimatedTokens: result.tokenCount ?? 0,
2340
- // systemPromptAddition injects hypermem context before the runtime system prompt.
2341
- // This is the facts/recall/episodes block assembled by the compositor.
2342
- systemPromptAddition: result.contextBlock || undefined,
2343
- };
2344
- }
2345
- catch (err) {
2346
- console.error('[hypermem-plugin] assemble error (stack):', err.stack ?? err);
2347
- throw err; // Re-throw so the runtime falls back to legacy pipeline
2348
- }
2349
- }
2350
- finally {
2351
- // End the trim-owner turn scope opened at assemble entry. Paired
2352
- // with beginTrimOwnerTurn(_asmSk, _asmTurnId) above; runs on every
2353
- // exit path (normal return, tool-loop return, replay return, error
2354
- // re-throw). Turn-scoped keying (Sprint 2.2a) means this only
2355
- // removes THIS turn's slot, so concurrent same-session turns remain
2356
- // isolated instead of clobbering each other.
2357
- endTrimOwnerTurn(_asmSk, _asmTurnId);
2358
- }
2359
- },
2360
- /**
2361
- * Compact context. hypermem owns compaction.
2362
- *
2363
- * Strategy: assemble() already trims the composed message list to the token
2364
- * budget via the compositor safety valve, so the model never receives an
2365
- * oversized context. compact() is called by the runtime when it detects
2366
- * overflow — at that point we:
2367
- * 1. Estimate tokens in the current Redis history window
2368
- * 2. If already under budget (compositor already handled it), report clean
2369
- * 3. If over budget (e.g. window was built before budget cap was applied),
2370
- * trim the Redis window to a safe depth and invalidate the compose cache
2371
- *
2372
- * This prevents the runtime from running its own LLM-summarization compaction
2373
- * pass, which would destroy message history we're explicitly managing.
2374
- */
2375
- async compact({ sessionId, sessionKey, sessionFile, tokenBudget, currentTokenCount }) {
2376
- try {
2377
- const hm = await getHyperMem();
2378
- const sk = resolveSessionKey(sessionId, sessionKey);
2379
- const agentId = extractAgentId(sk);
2380
- // Skip if a reshape pass just ran (within last 30s) — avoid double-processing
2381
- // Cache modelState here for reuse in density-aware JSONL truncation below.
2382
- let cachedModelState = null;
2383
- let model;
2384
- try {
2385
- cachedModelState = await hm.cache.getModelState(agentId, sk);
2386
- model = cachedModelState?.model;
2387
- if (cachedModelState?.reshapedAt) {
2388
- const reshapeAge = Date.now() - new Date(cachedModelState.reshapedAt).getTime();
2389
- // Only skip if session is NOT critically full — nuclear path must bypass this guard.
2390
- // If currentTokenCount > 85% budget, fall through to nuclear compaction below.
2391
- const isCriticallyFull = currentTokenCount != null &&
2392
- currentTokenCount > (computeEffectiveBudget(tokenBudget, model) * 0.85);
2393
- if (reshapeAge < 30_000 && !isCriticallyFull) {
2394
- console.log(`[hypermem-plugin] compact: skipping — reshape pass ran ${reshapeAge}ms ago`);
2395
- return { ok: true, compacted: false, reason: 'reshape-recently-ran' };
2396
- }
2397
- }
2398
- }
2399
- catch {
2400
- // Non-fatal — proceed with compaction
2401
- }
2402
- // Re-estimate from the actual Redis window.
2403
- // The runtime's estimate (currentTokenCount) includes the full inbound message
2404
- // and system prompt — our estimate only covers the history window. When they
2405
- // diverge significantly upward, the difference is "inbound overhead" consuming
2406
- // budget the history is competing for. We trim history to make room.
2407
- const effectiveBudget = computeEffectiveBudget(tokenBudget, model);
2408
- const tokensBefore = await estimateWindowTokens(hm, agentId, sk);
2409
- // Target depth for both Redis trimming and JSONL truncation.
2410
- // Target 50% of budget capacity, assume ~500 tokens/message average.
2411
- const targetDepth = Math.max(20, Math.floor((effectiveBudget * 0.5) / 500));
2412
- // ── NUCLEAR COMPACTION ────────────────────────────────────────────────
2413
- // When the runtime reports the session is ≥85% full, trust that signal
2414
- // over our Redis estimate. The JSONL accumulates full tool results that
2415
- // the gradient never sees, so Redis can look fine while the transcript
2416
- // is genuinely saturated. Normal compact() returns compacted=false in
2417
- // this scenario ("within_budget"), which gives the runtime zero relief.
2418
- //
2419
- // Also triggered when reshape ran recently but the session is still
2420
- // critically full — bypass the reshape guard in that case.
2421
- const NUCLEAR_THRESHOLD = 0.85;
2422
- const isNuclear = currentTokenCount != null && currentTokenCount > effectiveBudget * NUCLEAR_THRESHOLD;
2423
- if (isNuclear) {
2424
- // Cut deep: target 20% of normal depth = ~25 messages for a 128k session.
2425
- // Keeps very recent context, clears the long tool-heavy tail.
2426
- const nuclearDepth = Math.max(10, Math.floor(targetDepth * 0.20));
2427
- const nuclearBudget = Math.floor(effectiveBudget * 0.25);
2428
- const nuclearRemoved = await hm.cache.trimHistoryToTokenBudget(agentId, sk, nuclearBudget);
2429
- await hm.cache.invalidateWindow(agentId, sk).catch(() => { });
2430
- await truncateJsonlIfNeeded(sessionFile, nuclearDepth, true);
2431
- const tokensAfter = await estimateWindowTokens(hm, agentId, sk);
2432
- if (telemetryEnabled()) {
2433
- trimTelemetry({
2434
- path: 'compact.nuclear',
2435
- agentId, sessionKey: sk,
2436
- preTokens: tokensBefore,
2437
- postTokens: tokensAfter,
2438
- removed: nuclearRemoved,
2439
- cacheInvalidated: true,
2440
- reason: `currentTokenCount=${currentTokenCount}/${effectiveBudget}`,
2441
- });
2442
- }
2443
- console.log(`[hypermem-plugin] compact: NUCLEAR — session at ${currentTokenCount}/${effectiveBudget} tokens ` +
2444
- `(${Math.round((currentTokenCount / effectiveBudget) * 100)}% full), ` +
2445
- `deep-trimmed JSONL to ${nuclearDepth} messages, Redis ${tokensBefore}→${tokensAfter} tokens`);
2446
- return { ok: true, compacted: true, result: { tokensBefore, tokensAfter } };
2447
- }
2448
- // ── END NUCLEAR ───────────────────────────────────────────────────────
2449
- // Detect large-inbound-content scenario: runtime total significantly exceeds
2450
- // our history estimate. The gap is the inbound message + system prompt overhead.
2451
- // Trim history to leave room for it even if history alone is within budget.
2452
- if (currentTokenCount != null && currentTokenCount > tokensBefore) {
2453
- const inboundOverhead = currentTokenCount - tokensBefore;
2454
- if (inboundOverhead > effectiveBudget * 0.15) {
2455
- // Large inbound content (document review, big tool result, etc.)
2456
- // Trim history so history + inbound fits within 85% of budget.
2457
- const budgetForHistory = Math.floor(effectiveBudget * 0.85) - inboundOverhead;
2458
- if (budgetForHistory < tokensBefore && budgetForHistory > 0) {
2459
- const historyTrimmed = await hm.cache.trimHistoryToTokenBudget(agentId, sk, budgetForHistory);
2460
- await hm.cache.invalidateWindow(agentId, sk).catch(() => { });
2461
- const tokensAfter = await estimateWindowTokens(hm, agentId, sk);
2462
- await truncateJsonlIfNeeded(sessionFile, targetDepth);
2463
- if (telemetryEnabled()) {
2464
- trimTelemetry({
2465
- path: 'compact.history',
2466
- agentId, sessionKey: sk,
2467
- preTokens: tokensBefore,
2468
- postTokens: tokensAfter,
2469
- removed: historyTrimmed,
2470
- cacheInvalidated: true,
2471
- reason: `inbound-overhead=${inboundOverhead}`,
2472
- });
2473
- }
2474
- console.log(`[hypermem-plugin] compact: large-inbound-content (gap=${inboundOverhead} tokens), ` +
2475
- `trimmed history ${tokensBefore}→${tokensAfter} (budget-for-history=${budgetForHistory}, trimmed=${historyTrimmed} messages)`);
2476
- return { ok: true, compacted: true, result: { tokensBefore, tokensAfter } };
2477
- }
2478
- }
2479
- }
2480
- // Under 70% of budget by our own Redis estimate.
2481
- // We still need to check the JSONL — the runtime's overflow is based on JSONL
2482
- // message count, not Redis. If the JSONL is bloated (> targetDepth * 1.5 messages)
2483
- // we truncate it even if Redis looks fine, then return compacted=true so the
2484
- // runtime retries with the trimmed file instead of killing the session.
2485
- if (tokensBefore <= effectiveBudget * 0.7) {
2486
- const jsonlTruncated = await truncateJsonlIfNeeded(sessionFile, targetDepth);
2487
- if (jsonlTruncated) {
2488
- console.log(`[hypermem-plugin] compact: Redis within_budget but JSONL was bloated — truncated to ${targetDepth} messages`);
2489
- return {
2490
- ok: true,
2491
- compacted: true,
2492
- result: { tokensBefore, tokensAfter: tokensBefore },
2493
- };
2494
- }
2495
- return {
2496
- ok: true,
2497
- compacted: false,
2498
- reason: 'within_budget',
2499
- result: { tokensBefore, tokensAfter: tokensBefore },
2500
- };
2501
- }
2502
- // Over budget: trim both the window cache AND the history list.
2503
- // Bug fix: if no window cache exists (fresh/never-compacted session),
2504
- // compact() was only trying to trim the window (which was null) and
2505
- // the history list was left untouched → 0 actual trimming → timeout
2506
- // compaction death spiral.
2507
- const window = await hm.cache.getWindow(agentId, sk);
2508
- if (window && window.length > targetDepth) {
2509
- const trimmed = window.slice(-targetDepth);
2510
- await hm.cache.setWindow(agentId, sk, trimmed);
2511
- }
2512
- // Always trim the underlying history list — this is the source of truth
2513
- // when no window cache exists. trimHistoryToTokenBudget walks newest→oldest
2514
- // and LTRIMs everything beyond the budget.
2515
- const trimBudget = Math.floor(effectiveBudget * 0.5);
2516
- const historyTrimmed = await hm.cache.trimHistoryToTokenBudget(agentId, sk, trimBudget);
2517
- if (historyTrimmed > 0) {
2518
- console.log(`[hypermem-plugin] compact: trimmed ${historyTrimmed} messages from history list`);
2519
- }
2520
- // Invalidate the compose cache so next assemble() re-builds from trimmed data
2521
- await hm.cache.invalidateWindow(agentId, sk).catch(() => { });
2522
- const tokensAfter = await estimateWindowTokens(hm, agentId, sk);
2523
- if (telemetryEnabled()) {
2524
- trimTelemetry({
2525
- path: 'compact.history2',
2526
- agentId, sessionKey: sk,
2527
- preTokens: tokensBefore,
2528
- postTokens: tokensAfter,
2529
- removed: historyTrimmed,
2530
- cacheInvalidated: true,
2531
- reason: `over-budget tokensBefore=${tokensBefore}/${effectiveBudget}`,
2532
- });
2533
- }
2534
- console.log(`[hypermem-plugin] compact: trimmed ${tokensBefore} → ${tokensAfter} tokens (budget: ${effectiveBudget})`);
2535
- // Density-aware JSONL truncation: derive target depth from actual avg tokens/message
2536
- // rather than assuming a fixed 500 tokens/message. This prevents a large-message
2537
- // session (e.g. 145 msgs × 882 tok = 128k) from bypassing the 1.5x guard and
2538
- // leaving the JSONL untouched while Redis is correctly trimmed.
2539
- // force=true bypasses the 1.5x early-exit — over-budget always rewrites.
2540
- const histDepth = cachedModelState?.historyDepth ?? targetDepth;
2541
- const avgTokPerMsg = histDepth > 0 && tokensBefore > 0 ? tokensBefore / histDepth : 500;
2542
- const densityTargetDepth = Math.max(10, Math.floor(trimBudget / avgTokPerMsg));
2543
- await truncateJsonlIfNeeded(sessionFile, densityTargetDepth, true);
2544
- console.log(`[hypermem-plugin] compact: JSONL density-trim targetDepth=${densityTargetDepth} (histDepth=${histDepth}, avg=${Math.round(avgTokPerMsg)} tok/msg)`);
2545
- return {
2546
- ok: true,
2547
- compacted: true,
2548
- result: { tokensBefore, tokensAfter },
2549
- };
2550
- }
2551
- catch (err) {
2552
- console.warn('[hypermem-plugin] compact failed:', err.message);
2553
- // Non-fatal: return ok so the runtime doesn't retry with its own compaction
2554
- return { ok: true, compacted: false, reason: err.message };
2555
- }
2556
- },
2557
- /**
2558
- * After-turn hook: ingest new messages then trigger background indexer.
2559
- *
2560
- * IMPORTANT: When afterTurn is defined, the runtime calls ONLY afterTurn —
2561
- * it never calls ingest() or ingestBatch(). So we must ingest the new
2562
- * messages here, using messages.slice(prePromptMessageCount).
2563
- */
2564
- async afterTurn({ sessionId, sessionKey, messages, prePromptMessageCount, isHeartbeat, runtimeContext }) {
2565
- if (isHeartbeat)
2566
- return;
2567
- try {
2568
- const hm = await getHyperMem();
2569
- const sk = resolveSessionKey(sessionId, sessionKey);
2570
- const agentId = extractAgentId(sk);
2571
- // Ingest only the new messages produced this turn
2572
- const newMessages = messages.slice(prePromptMessageCount);
2573
- for (const msg of newMessages) {
2574
- const m = msg;
2575
- // Skip system messages — they come from the runtime, not the conversation
2576
- if (m.role === 'system')
2577
- continue;
2578
- if (m.role === 'toolResult' && extractTextFromInboundContent(m.content).trim() === SYNTHETIC_MISSING_TOOL_RESULT_TEXT) {
2579
- const toolCallId = typeof m.toolCallId === 'string' ? m.toolCallId : 'unknown';
2580
- const toolName = typeof m.toolName === 'string' ? m.toolName : 'unknown';
2581
- await bumpToolPairMetrics(hm, agentId, sk, { syntheticNoResultIngested: 1 }, {
2582
- stage: 'afterTurn',
2583
- toolCallId,
2584
- toolName,
2585
- });
2586
- console.warn(`[hypermem-plugin] tool-pair-integrity: observed synthetic missing tool result for ${agentId}/${sk} ` +
2587
- `tool=${toolName} callId=${toolCallId}`);
2588
- }
2589
- const neutral = toNeutralMessage(m);
2590
- if (neutral.role === 'user' && !neutral.toolResults?.length) {
2591
- // Record plain user messages here and strip transport envelope metadata
2592
- // before storage so prompt wrappers like:
2593
- // Sender (untrusted metadata): { ... }
2594
- // never enter messages.db / Redis history / downstream facts.
2595
- //
2596
- // recordUserMessage() also strips defensively at core level, but we do
2597
- // it here too so the intended behavior is explicit at the plugin boundary.
2598
- //
2599
- // IMPORTANT: tool results arrive as role='user' carriers (toNeutralMessage
2600
- // sets role='user' + toolResults=[...] + textContent=null). These MUST go
2601
- // through recordAssistantMessage to persist the toolResults array.
2602
- // recordUserMessage takes a plain string and would silently discard them.
2603
- await hm.recordUserMessage(agentId, sk, stripMessageMetadata(neutral.textContent ?? ''));
2604
- }
2605
- else {
2606
- await hm.recordAssistantMessage(agentId, sk, neutral, {
2607
- tokenCount: neutral.role === 'assistant' ? resolveAssistantTokenCount(m, runtimeContext) : undefined,
2608
- });
2609
- }
2610
- }
2611
- // P3.1: Topic detection on the inbound user message
2612
- // Non-fatal: topic detection never blocks afterTurn
2613
- try {
2614
- const inboundUserMsg = newMessages
2615
- .map(m => m)
2616
- .find(m => m.role === 'user');
2617
- if (inboundUserMsg) {
2618
- const neutralUser = toNeutralMessage(inboundUserMsg);
2619
- // Gather recent messages for context (all messages before the new ones)
2620
- const contextMessages = messages.slice(0, prePromptMessageCount)
2621
- .filter(m => m.role !== 'system')
2622
- .slice(-10)
2623
- .map(m => toNeutralMessage(m));
2624
- const db = hm.dbManager.getMessageDb(agentId);
2625
- if (db) {
2626
- const topicMap = new SessionTopicMap(db);
2627
- const activeTopic = topicMap.getActiveTopic(sk);
2628
- const signal = detectTopicShift(neutralUser, contextMessages, activeTopic?.id ?? null);
2629
- if (signal.isNewTopic && signal.topicName) {
2630
- const newTopicId = topicMap.createTopic(sk, signal.topicName);
2631
- // New topic starts with count 1 (the message that triggered the shift)
2632
- topicMap.incrementMessageCount(newTopicId);
2633
- // Write topic_id onto the stored user message (best-effort)
2634
- try {
2635
- const stored = db.prepare(`
2636
- SELECT m.id FROM messages m
2637
- JOIN conversations c ON c.id = m.conversation_id
2638
- WHERE c.session_key = ? AND m.role = 'user'
2639
- ORDER BY m.message_index DESC LIMIT 1
2640
- `).get(sk);
2641
- if (stored) {
2642
- db.prepare('UPDATE messages SET topic_id = ? WHERE id = ?')
2643
- .run(newTopicId, stored.id);
2644
- }
2645
- }
2646
- catch {
2647
- // Best-effort
2648
- }
2649
- }
2650
- else if (activeTopic) {
2651
- topicMap.activateTopic(sk, activeTopic.id);
2652
- topicMap.incrementMessageCount(activeTopic.id);
2653
- }
2654
- }
2655
- }
2656
- }
2657
- catch {
2658
- // Topic detection is entirely non-fatal
2659
- }
2660
- // Recompute the Redis hot history from SQLite so turn-age gradient is
2661
- // materialized after every turn. This prevents warm-compressed history
2662
- // from drifting back to raw payloads during live sessions.
2663
- //
2664
- // Pass the cached model tokenBudget so refreshRedisGradient can cap the
2665
- // gradient-compressed window to budget before writing to Redis. Without
2666
- // this, afterTurn writes up to 250 messages regardless of budget, causing
2667
- // trimHistoryToTokenBudget to fire and trim ~200 messages on every
2668
- // subsequent assemble() — the churn loop seen in Eve's logs.
2669
- if (hm.cache.isConnected) {
2670
- try {
2671
- const modelState = await hm.cache.getModelState(agentId, sk);
2672
- const gradientBudget = modelState?.tokenBudget;
2673
- const gradientDepth = modelState?.historyDepth;
2674
- await hm.refreshRedisGradient(agentId, sk, gradientBudget, gradientDepth);
2675
- }
2676
- catch (refreshErr) {
2677
- console.warn('[hypermem-plugin] afterTurn: refreshRedisGradient failed (non-fatal):', refreshErr.message);
2678
- }
2679
- }
2680
- // Invalidate the window cache after ingesting new messages.
2681
- // The next assemble() call will re-compose with the new data.
2682
- try {
2683
- await hm.cache.invalidateWindow(agentId, sk);
2684
- }
2685
- catch {
2686
- // Window invalidation is best-effort
2687
- }
2688
- // Pre-emptive secondary trim when session exits a turn hot.
2689
- // If a session just finished a turn at >80% pressure, the NEXT turn's
2690
- // incoming tool results (parallel web searches, large exec output, etc.)
2691
- // will hit a window with no headroom — the ingestion wave failure mode
2692
- // (reported by Eve, 2026-04-05). Pre-trim here so the tool-loop
2693
- // assemble() path starts the next turn with meaningful space.
2694
- //
2695
- // Uses modelState.tokenBudget if cached; skips if unavailable (non-fatal).
2696
- try {
2697
- const modelState = await hm.cache.getModelState(agentId, sk);
2698
- if (modelState?.tokenBudget) {
2699
- // Use the runtime message array as the only trim-pressure source.
2700
- // Redis remains a drift signal for anomaly logging.
2701
- const runtimePostTokens = estimateMessageArrayTokens(messages);
2702
- const redisPostTokens = await estimateWindowTokens(hm, agentId, sk);
2703
- const postTurnTokens = runtimePostTokens;
2704
- maybeLogPressureAccountingAnomaly({
2705
- path: 'afterTurn.secondary',
2706
- agentId,
2707
- sessionKey: sk,
2708
- runtimeTokens: runtimePostTokens,
2709
- redisTokens: redisPostTokens,
2710
- composedTokens: postTurnTokens,
2711
- budget: modelState.tokenBudget,
2712
- });
2713
- const postTurnPressure = postTurnTokens / modelState.tokenBudget;
2714
- // Sprint 2.2b: demote afterTurn.secondary to guard-only no-op.
2715
- //
2716
- // Previously this path was a two-tier real trim that fired after
2717
- // every turn ending at >80% pressure, calling
2718
- // trimHistoryToTokenBudget() and emitting `event:'trim'` with
2719
- // path='afterTurn.secondary'. Sprint 2 consolidates steady-state
2720
- // trim ownership in assemble.* (tool-loop + normal/subagent),
2721
- // with compact.* as the only exception family. The afterTurn
2722
- // post-turn pressure path is now redundant: the next turn's
2723
- // assemble.* trim absorbs any residual pressure.
2724
- //
2725
- // Pattern matches the warmstart/reshape demotion from 2.2a:
2726
- // keep the pressure predicate + threshold branch so observability
2727
- // via `event:'trim-guard'` is preserved, but emit NO real trim,
2728
- // NO invalidateWindow, NO mutation. The compact skip-gate stays
2729
- // correct because this path never stamped any model state.
2730
- if (postTurnPressure > 0.80) {
2731
- guardTelemetry({
2732
- path: 'afterTurn.secondary',
2733
- agentId, sessionKey: sk,
2734
- reason: 'afterturn-secondary-demoted',
2735
- });
2736
- }
2737
- }
2738
- }
2739
- catch {
2740
- // Non-fatal — next turn's tool-loop trim is the fallback
2741
- }
2742
- // Pre-compute embedding for the assistant's reply so the next compose()
2743
- // can skip the Ollama round-trip entirely (fire-and-forget).
2744
- //
2745
- // Why the assistant reply, not the current user message:
2746
- // The assistant's reply is the strongest semantic predictor of what the
2747
- // user will ask next — it's the context they're responding to. By the time
2748
- // the next user message arrives and compose() fires, this embedding is
2749
- // already warm in Redis. Cache hit rate: near 100% on normal conversation
2750
- // flow (one reply per turn).
2751
- //
2752
- // The previous approach (embedding the current user message) still missed
2753
- // on every turn because compose() queries against the INCOMING user message,
2754
- // not the one that was just processed.
2755
- //
2756
- // newMessages = messages.slice(prePromptMessageCount) — the assistant reply
2757
- // is always in here. Walk backwards to find the last assistant text turn.
2758
- try {
2759
- let assistantReplyText = null;
2760
- for (let i = newMessages.length - 1; i >= 0; i--) {
2761
- const m = newMessages[i];
2762
- if (m.role === 'assistant') {
2763
- const neutral = toNeutralMessage(m);
2764
- if (neutral.textContent) {
2765
- assistantReplyText = neutral.textContent;
2766
- break;
2767
- }
2768
- }
2769
- }
2770
- if (assistantReplyText && _generateEmbeddings) {
2771
- // Fire-and-forget: don't await, don't block afterTurn
2772
- _generateEmbeddings([assistantReplyText]).then(async ([embedding]) => {
2773
- if (embedding) {
2774
- await hm.cache.setQueryEmbedding(agentId, sk, embedding);
2775
- }
2776
- }).catch(() => {
2777
- // Non-fatal: embedding pre-compute failed, compose() will call Ollama
2778
- });
2779
- }
2780
- }
2781
- catch {
2782
- // Pre-embed is entirely non-fatal
2783
- }
2784
- // P1.7: Direct per-agent tick after each turn — no need to wait for 5-min interval.
2785
- if (_indexer) {
2786
- const _agentIdForTick = agentId;
2787
- const runTick = async () => {
2788
- if (_taskFlowRuntime) {
2789
- // Preflight: only create a managed flow if we can actually tick.
2790
- // Creating a flow we never finish/fail leaves orphaned queued rows.
2791
- let flow = null;
2792
- try {
2793
- // Use createManaged + finish/fail only — do NOT call runTask().
2794
- // runTask() writes a task_run row to runs.sqlite with status='running'
2795
- // and the TaskFlow runtime has no completeTask() method, so those rows
2796
- // would accumulate forever and block clean restarts.
2797
- flow = _taskFlowRuntime.createManaged({
2798
- controllerId: 'hypermem/indexer',
2799
- goal: `Index messages for ${_agentIdForTick}`,
2800
- });
2801
- await _indexer.tick();
2802
- // expectedRevision is required: finishFlow uses optimistic locking.
2803
- // A freshly created managed flow always starts at revision 0.
2804
- // MUST be awaited — finish/fail return Promises. Calling without
2805
- // await lets the Promise get GC'd before the DB write completes,
2806
- // leaving the flow permanently in queued state.
2807
- const finishResult = await Promise.resolve(_taskFlowRuntime.finish({ flowId: flow.flowId, expectedRevision: flow.revision }));
2808
- if (finishResult && !finishResult.applied) {
2809
- console.warn('[hypermem-plugin] TaskFlow finish failed:', finishResult.code ?? finishResult.reason, 'flowId:', flow.flowId, 'revision:', flow.revision);
2810
- }
2811
- }
2812
- catch (tickErr) {
2813
- // Best-effort fail — non-fatal, but always mark the flow so it doesn't leak
2814
- if (flow) {
2815
- try {
2816
- await Promise.resolve(_taskFlowRuntime.fail({ flowId: flow.flowId, expectedRevision: flow.revision }));
2817
- }
2818
- catch { /* ignore */ }
2819
- }
2820
- throw tickErr;
2821
- }
2822
- }
2823
- else {
2824
- await _indexer.tick();
2825
- }
2826
- };
2827
- runTick().catch(() => {
2828
- // Non-fatal: indexer tick failure never blocks afterTurn
2829
- });
2830
- }
2831
- }
2832
- catch (err) {
2833
- // afterTurn is never fatal
2834
- console.warn('[hypermem-plugin] afterTurn failed:', err.message);
2835
- }
2836
- },
2837
- /**
2838
- * Prepare context for a subagent session before it starts.
2839
- *
2840
- * Seeds the child session's Redis with parent context based on the
2841
- * subagentWarming config ('full' | 'light' | 'off').
2842
- * Returns a rollback handle to clean up if spawn fails.
2843
- */
2844
- async prepareSubagentSpawn({ parentSessionKey, childSessionKey }) {
2845
- if (_subagentWarming === 'off') {
2846
- return undefined;
2847
- }
2848
- try {
2849
- const hm = await getHyperMem();
2850
- const parentAgentId = extractAgentId(parentSessionKey);
2851
- const childAgentId = extractAgentId(childSessionKey);
2852
- // Seed child with parent's active facts
2853
- const facts = hm.getActiveFacts(parentAgentId, { limit: 50 });
2854
- if (facts && facts.length > 0) {
2855
- const factBlock = facts
2856
- .map(f => f.content)
2857
- .join('\n');
2858
- await hm.cache.setSlot(childAgentId, childSessionKey, 'parentFacts', factBlock);
2859
- }
2860
- // For 'full' warming, also seed recent history context
2861
- if (_subagentWarming === 'full') {
2862
- const history = await hm.cache.getHistory(parentAgentId, parentSessionKey);
2863
- if (history && history.length > 0) {
2864
- const recentHistory = history.slice(-10);
2865
- await hm.cache.setSlot(childAgentId, childSessionKey, 'parentHistory', JSON.stringify(recentHistory));
2866
- }
2867
- }
2868
- console.log(`[hypermem-plugin] prepareSubagentSpawn: seeded ${childSessionKey} ` +
2869
- `from ${parentSessionKey} (warming=${_subagentWarming})`);
2870
- return {
2871
- async rollback() {
2872
- try {
2873
- const hm = await getHyperMem();
2874
- await hm.cache.setSlot(childAgentId, childSessionKey, 'parentFacts', '');
2875
- await hm.cache.setSlot(childAgentId, childSessionKey, 'parentHistory', '');
2876
- }
2877
- catch {
2878
- // Rollback is best-effort
2879
- }
2880
- },
2881
- };
2882
- }
2883
- catch (err) {
2884
- console.warn('[hypermem-plugin] prepareSubagentSpawn failed (non-fatal):', err.message);
2885
- return undefined;
2886
- }
2887
- },
2888
- /**
2889
- * Clean up after a subagent session ends.
2890
- *
2891
- * Removes Redis slots and invalidates caches for the dead session
2892
- * to prevent stale data accumulation.
2893
- */
2894
- async onSubagentEnded({ childSessionKey, reason }) {
2895
- try {
2896
- const hm = await getHyperMem();
2897
- const childAgentId = extractAgentId(childSessionKey);
2898
- await Promise.all([
2899
- hm.cache.setSlot(childAgentId, childSessionKey, 'parentFacts', ''),
2900
- hm.cache.setSlot(childAgentId, childSessionKey, 'parentHistory', ''),
2901
- hm.cache.setSlot(childAgentId, childSessionKey, 'assemblyContextBlock', ''),
2902
- hm.cache.setSlot(childAgentId, childSessionKey, 'assemblyContextAt', '0'),
2903
- hm.cache.invalidateWindow(childAgentId, childSessionKey).catch(() => { }),
2904
- ]);
2905
- _overheadCache.delete(childSessionKey);
2906
- console.log(`[hypermem-plugin] onSubagentEnded: cleaned up ${childSessionKey} (reason=${reason})`);
2907
- }
2908
- catch (err) {
2909
- console.warn('[hypermem-plugin] onSubagentEnded failed (non-fatal):', err.message);
2910
- }
2911
- },
2912
- /**
2913
- * Dispose: intentionally a no-op.
2914
- *
2915
- * The runtime calls dispose() at the end of every request cycle, but
2916
- * hypermem's Redis connection and SQLite handles are gateway-lifetime
2917
- * singletons — not request-scoped. Closing and nulling _hm here causes
2918
- * a full reconnect + re-init on every turn (~400-800ms latency per turn).
2919
- *
2920
- * ioredis manages its own reconnection on connection loss. If the gateway
2921
- * process exits, Node.js cleans up file handles automatically.
2922
- *
2923
- * If a true shutdown is needed (e.g. gateway restart signal), call
2924
- * _hm.close() directly from a gateway:shutdown hook instead.
2925
- */
2926
- async dispose() {
2927
- // Intentional no-op — see comment above.
2928
- },
2929
- };
2930
- }
2931
- // ─── NeutralMessage → AgentMessage ─────────────────────────────
2932
- /**
2933
- * Convert hypermem's NeutralMessage back to OpenClaw's AgentMessage format.
2934
- *
2935
- * The runtime expects messages conforming to pi-ai's Message union:
2936
- * UserMessage: { role: 'user', content: string | ContentBlock[], timestamp }
2937
- * AssistantMessage: { role: 'assistant', content: ContentBlock[], api, provider, model, usage, stopReason, timestamp }
2938
- * ToolResultMessage: { role: 'toolResult', toolCallId, toolName, content, isError, timestamp }
2939
- *
2940
- * hypermem stores tool results as NeutralMessage with role='user' and toolResults[].
2941
- * These must be expanded into individual ToolResultMessage objects.
2942
- *
2943
- * For assistant messages with tool calls, NeutralToolCall.arguments is a JSON string
2944
- * but the runtime's ToolCall.arguments is Record<string, any>. We parse it here.
2945
- *
2946
- * Missing metadata fields (api, provider, model, usage, stopReason) are filled with
2947
- * sentinel values. The runtime's convertToLlm strips them before the API call, and
2948
- * the session transcript already has the real values. These are just structural stubs
2949
- * so the AgentMessage type is satisfied at runtime.
2950
- */
2951
- function neutralToAgentMessage(msg) {
2952
- const now = Date.now();
2953
- // Tool results: expand to individual ToolResultMessage objects
2954
- if (msg.toolResults && msg.toolResults.length > 0) {
2955
- return msg.toolResults.map(tr => ({
2956
- role: 'toolResult',
2957
- toolCallId: tr.callId,
2958
- toolName: tr.name,
2959
- content: [{ type: 'text', text: tr.content ?? '' }],
2960
- isError: tr.isError ?? false,
2961
- timestamp: now,
2962
- }));
2963
- }
2964
- if (msg.role === 'user') {
2965
- return {
2966
- role: 'user',
2967
- content: msg.textContent ?? '',
2968
- timestamp: now,
2969
- };
2970
- }
2971
- if (msg.role === 'system') {
2972
- // System messages are passed through as-is; the runtime handles them separately
2973
- return {
2974
- role: 'system',
2975
- content: msg.textContent ?? '',
2976
- timestamp: now,
2977
- // Preserve dynamicBoundary metadata for prompt caching
2978
- ...msg.metadata?.dynamicBoundary
2979
- ? { metadata: { dynamicBoundary: true } }
2980
- : {},
2981
- };
2982
- }
2983
- // Assistant message
2984
- const content = [];
2985
- if (msg.textContent) {
2986
- content.push({ type: 'text', text: msg.textContent });
2987
- }
2988
- if (msg.toolCalls && msg.toolCalls.length > 0) {
2989
- for (const tc of msg.toolCalls) {
2990
- // Parse arguments from JSON string → object (runtime expects Record<string, any>)
2991
- let args;
2992
- try {
2993
- args = typeof tc.arguments === 'string' ? JSON.parse(tc.arguments) : (tc.arguments ?? {});
2994
- }
2995
- catch {
2996
- args = {};
2997
- }
2998
- content.push({
2999
- type: 'toolCall',
3000
- id: tc.id,
3001
- name: tc.name,
3002
- arguments: args,
3003
- });
3004
- }
3005
- }
3006
- // Stub metadata fields — the runtime needs these structurally but convertToLlm
3007
- // strips them before the API call. Real values live in the session transcript.
3008
- return {
3009
- role: 'assistant',
3010
- content: content.length > 0 ? content : [{ type: 'text', text: '' }],
3011
- api: 'unknown',
3012
- provider: 'unknown',
3013
- model: 'unknown',
3014
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
3015
- stopReason: 'stop',
3016
- timestamp: now,
3017
- };
3018
- }
3019
- // ─── Cache Bust Utility ────────────────────────────────────────────────────
3020
- /**
3021
- * Bust the assembly cache for a specific agent+session.
3022
- * Call this after writing to identity files (SOUL.md, IDENTITY.md, TOOLS.md,
3023
- * USER.md) to ensure the next assemble() runs full compositor, not a replay.
3024
- */
3025
- export async function bustAssemblyCache(agentId, sessionKey) {
3026
- try {
3027
- const hm = await getHyperMem();
3028
- await Promise.all([
3029
- hm.cache.setSlot(agentId, sessionKey, 'assemblyContextBlock', ''),
3030
- hm.cache.setSlot(agentId, sessionKey, 'assemblyContextAt', '0'),
3031
- ]);
3032
- }
3033
- catch {
3034
- // Non-fatal
3035
- }
3036
- }
3037
- // ─── Plugin Config Schema ────────────────────────────────────────
3038
- // Exposed via openclaw.json → plugins.entries.hypercompositor.config
3039
- // Validated by OpenClaw on gateway start. Visible via `openclaw config get`.
3040
- const hypercompositorConfigSchema = z.object({
3041
- /** Path to HyperMem core dist/index.js. Auto-resolved if omitted. */
3042
- hyperMemPath: z.string().optional(),
3043
- /** HyperMem data directory. Default: ~/.openclaw/hypermem */
3044
- dataDir: z.string().optional(),
3045
- /** Full model context window size in tokens. Default: 128000 */
3046
- contextWindowSize: z.number().int().positive().optional(),
3047
- /** Fraction [0.0–0.5] reserved for system prompts + headroom. Default: 0.25 */
3048
- contextWindowReserve: z.number().min(0).max(0.5).optional(),
3049
- /** Defer tool pruning to OpenClaw's contextPruning. Default: false */
3050
- deferToolPruning: z.boolean().optional(),
3051
- /** Emit detailed budget-source and trim-decision logs. Default: false */
3052
- verboseLogging: z.boolean().optional(),
3053
- /** Manual per-model context window fallback table used when runtime tokenBudget is missing. */
3054
- contextWindowOverrides: z.record(z.string().regex(CONTEXT_WINDOW_OVERRIDE_KEY_REGEX, 'key must be "provider/model"'), contextWindowOverrideSchema).optional(),
3055
- /** Treat cache replay snapshots older than this as stale. Default: 120000ms */
3056
- warmCacheReplayThresholdMs: z.number().int().positive().optional(),
3057
- /** Subagent context injection: 'full' | 'light' | 'off'. Default: 'light' */
3058
- subagentWarming: z.enum(['full', 'light', 'off']).optional(),
3059
- /** Compositor tuning overrides */
3060
- compositor: z.object({
3061
- budgetFraction: z.number().min(0).max(1).optional(),
3062
- reserveFraction: z.number().min(0).max(1).optional(),
3063
- historyFraction: z.number().min(0).max(1).optional(),
3064
- memoryFraction: z.number().min(0).max(1).optional(),
3065
- defaultTokenBudget: z.number().int().positive().optional(),
3066
- maxHistoryMessages: z.number().int().positive().optional(),
3067
- maxFacts: z.number().int().positive().optional(),
3068
- maxExpertisePatterns: z.number().int().positive().optional(),
3069
- maxCrossSessionContext: z.number().int().nonnegative().optional(),
3070
- maxTotalTriggerTokens: z.number().int().nonnegative().optional(),
3071
- maxRecentToolPairs: z.number().int().nonnegative().optional(),
3072
- maxProseToolPairs: z.number().int().nonnegative().optional(),
3073
- warmHistoryBudgetFraction: z.number().min(0).max(1).optional(),
3074
- contextWindowReserve: z.number().min(0).max(1).optional(),
3075
- dynamicReserveTurnHorizon: z.number().int().positive().optional(),
3076
- dynamicReserveMax: z.number().min(0).max(1).optional(),
3077
- dynamicReserveEnabled: z.boolean().optional(),
3078
- keystoneHistoryFraction: z.number().min(0).max(1).optional(),
3079
- keystoneMaxMessages: z.number().int().nonnegative().optional(),
3080
- keystoneMinSignificance: z.number().min(0).max(1).optional(),
3081
- targetBudgetFraction: z.number().min(0).max(1).optional(),
3082
- enableFOS: z.boolean().optional(),
3083
- enableMOD: z.boolean().optional(),
3084
- hyperformProfile: z.enum(['light', 'standard', 'full', 'starter', 'fleet']).optional(),
3085
- outputProfile: z.enum(['light', 'standard', 'full', 'starter', 'fleet']).optional(),
3086
- outputStandard: z.enum(['light', 'standard', 'full', 'starter', 'fleet']).optional(),
3087
- wikiTokenCap: z.number().int().positive().optional(),
3088
- zigzagOrdering: z.boolean().optional(),
3089
- }).optional(),
3090
- /** Image/tool eviction settings */
3091
- eviction: z.object({
3092
- enabled: z.boolean().optional(),
3093
- imageAgeTurns: z.number().int().nonnegative().optional(),
3094
- toolResultAgeTurns: z.number().int().nonnegative().optional(),
3095
- minTokensToEvict: z.number().int().nonnegative().optional(),
3096
- keepPreviewChars: z.number().int().nonnegative().optional(),
3097
- }).optional(),
3098
- /** Embedding provider config */
3099
- embedding: z.object({
3100
- provider: z.enum(['ollama', 'openai', 'gemini']).optional(),
3101
- ollamaUrl: z.string().optional(),
3102
- openaiApiKey: z.string().optional(),
3103
- openaiBaseUrl: z.string().optional(),
3104
- geminiBaseUrl: z.string().optional(),
3105
- geminiIndexTaskType: z.string().optional(),
3106
- geminiQueryTaskType: z.string().optional(),
3107
- model: z.string().optional(),
3108
- dimensions: z.number().int().positive().optional(),
3109
- timeout: z.number().int().positive().optional(),
3110
- batchSize: z.number().int().positive().optional(),
3111
- }).optional(),
3112
- });
3113
- // ─── Plugin Entry ───────────────────────────────────────────────
3114
- const engine = createHyperMemEngine();
3115
- export default definePluginEntry({
3116
- id: 'hypercompositor',
3117
- name: 'HyperCompositor — context engine',
3118
- description: 'Four-layer memory architecture for OpenClaw agents: SQLite hot cache, message history, vector search, and structured library.',
3119
- kind: 'context-engine',
3120
- configSchema: buildPluginConfigSchema(hypercompositorConfigSchema),
3121
- register(api) {
3122
- // ── Resolve plugin config from openclaw.json ──
3123
- const pluginCfg = (api.pluginConfig ?? {});
3124
- _pluginConfig = pluginCfg;
3125
- // ── Resolve HYPERMEM_PATH: pluginConfig > ESM package resolve > dev fallback ──
3126
- if (pluginCfg.hyperMemPath) {
3127
- HYPERMEM_PATH = pluginCfg.hyperMemPath;
3128
- console.log(`[hypermem-plugin] Using configured hyperMemPath: ${HYPERMEM_PATH}`);
3129
- }
3130
- else {
3131
- try {
3132
- const resolvedUrl = import.meta.resolve('@psiclawops/hypermem');
3133
- HYPERMEM_PATH = resolvedUrl.startsWith('file:') ? fileURLToPath(resolvedUrl) : resolvedUrl;
3134
- }
3135
- catch {
3136
- // Dev fallback: resolve relative to plugin directory
3137
- const __pluginDir = path.dirname(fileURLToPath(import.meta.url));
3138
- HYPERMEM_PATH = path.resolve(__pluginDir, '../../dist/index.js');
3139
- console.log(`[hypermem-plugin] Falling back to dev path: ${HYPERMEM_PATH}`);
3140
- }
3141
- }
3142
- api.registerContextEngine('hypercompositor', () => engine);
3143
- // ── HyperForm config dir init ──
3144
- // Copy defaults and guide to ~/.openclaw/hypermem/config/ on every load.
3145
- // Defaults are overwritten on plugin update. Active config files are never touched.
3146
- void (async () => {
3147
- try {
3148
- const dataDir = _pluginConfig.dataDir ?? path.join(os.homedir(), '.openclaw/hypermem');
3149
- const configDir = path.join(dataDir, 'config');
3150
- await fs.mkdir(configDir, { recursive: true });
3151
- const __pluginDir = path.dirname(fileURLToPath(import.meta.url));
3152
- const defaultsSrc = path.resolve(__pluginDir, '../../../config-defaults');
3153
- const defaultFiles = [
3154
- 'hyperform-fos-defaults.json',
3155
- 'hyperform-mod-defaults.json',
3156
- 'HYPERFORM-GUIDE.md',
3157
- ];
3158
- for (const fname of defaultFiles) {
3159
- const src = path.join(defaultsSrc, fname);
3160
- const dest = path.join(configDir, fname);
3161
- try {
3162
- await fs.copyFile(src, dest);
3163
- }
3164
- catch {
3165
- // defaults may not exist in dev builds — non-fatal
3166
- }
3167
- }
3168
- // On first install, copy defaults as active config if active files don't exist
3169
- for (const [src, dest] of [
3170
- ['hyperform-fos-defaults.json', 'hyperform-fos.json'],
3171
- ['hyperform-mod-defaults.json', 'hyperform-mod.json'],
3172
- ]) {
3173
- const destPath = path.join(configDir, dest);
3174
- try {
3175
- await fs.access(destPath);
3176
- }
3177
- catch {
3178
- // Active config doesn't exist — copy defaults as starting point
3179
- try {
3180
- await fs.copyFile(path.join(configDir, src), destPath);
3181
- }
3182
- catch {
3183
- // non-fatal
3184
- }
3185
- }
3186
- }
3187
- }
3188
- catch {
3189
- // non-fatal — HyperForm config init is best-effort
3190
- }
3191
- })();
3192
- // P1.7: Bind TaskFlow runtime for task visibility — best-effort.
3193
- // Guard: api.runtime.taskFlow may not exist on older OpenClaw versions.
3194
- try {
3195
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3196
- const tf = api.runtime?.taskFlow;
3197
- if (tf && typeof tf.bindSession === 'function') {
3198
- _taskFlowRuntime = tf.bindSession({
3199
- sessionKey: 'hypermem-plugin',
3200
- requesterOrigin: 'hypermem-plugin',
3201
- });
3202
- }
3203
- }
3204
- catch {
3205
- // TaskFlow binding is best-effort — plugin remains fully functional without it
3206
- }
3207
- },
3208
- });
3209
- //# sourceMappingURL=index.js.map