bikky 0.3.1 → 0.3.2

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 (329) hide show
  1. package/README.md +79 -26
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +7 -1
  5. package/dist/cli.js.map +1 -1
  6. package/dist/config.d.ts +22 -3
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +61 -6
  9. package/dist/config.js.map +1 -1
  10. package/dist/config.test.js +17 -9
  11. package/dist/config.test.js.map +1 -1
  12. package/dist/daemon/capture-policy.d.ts +95 -0
  13. package/dist/daemon/capture-policy.d.ts.map +1 -0
  14. package/dist/daemon/capture-policy.js +139 -0
  15. package/dist/daemon/capture-policy.js.map +1 -0
  16. package/dist/daemon/capture-policy.test.d.ts +2 -0
  17. package/dist/daemon/capture-policy.test.d.ts.map +1 -0
  18. package/dist/daemon/capture-policy.test.js +46 -0
  19. package/dist/daemon/capture-policy.test.js.map +1 -0
  20. package/dist/daemon/consolidation.d.ts.map +1 -1
  21. package/dist/daemon/consolidation.js +84 -98
  22. package/dist/daemon/consolidation.js.map +1 -1
  23. package/dist/daemon/episode-summary.d.ts +77 -0
  24. package/dist/daemon/episode-summary.d.ts.map +1 -0
  25. package/dist/daemon/episode-summary.js +239 -0
  26. package/dist/daemon/episode-summary.js.map +1 -0
  27. package/dist/daemon/episode-summary.test.d.ts +2 -0
  28. package/dist/daemon/episode-summary.test.d.ts.map +1 -0
  29. package/dist/daemon/episode-summary.test.js +101 -0
  30. package/dist/daemon/episode-summary.test.js.map +1 -0
  31. package/dist/daemon/extraction.d.ts +25 -0
  32. package/dist/daemon/extraction.d.ts.map +1 -1
  33. package/dist/daemon/extraction.js +244 -124
  34. package/dist/daemon/extraction.js.map +1 -1
  35. package/dist/daemon/extraction.test.d.ts +2 -0
  36. package/dist/daemon/extraction.test.d.ts.map +1 -0
  37. package/dist/daemon/extraction.test.js +106 -0
  38. package/dist/daemon/extraction.test.js.map +1 -0
  39. package/dist/daemon/loop.d.ts.map +1 -1
  40. package/dist/daemon/loop.js +8 -6
  41. package/dist/daemon/loop.js.map +1 -1
  42. package/dist/daemon/qdrant.d.ts +59 -8
  43. package/dist/daemon/qdrant.d.ts.map +1 -1
  44. package/dist/daemon/qdrant.js +74 -23
  45. package/dist/daemon/qdrant.js.map +1 -1
  46. package/dist/daemon/qdrant.test.js +2 -2
  47. package/dist/daemon/qdrant.test.js.map +1 -1
  48. package/dist/daemon/relations.d.ts +6 -1
  49. package/dist/daemon/relations.d.ts.map +1 -1
  50. package/dist/daemon/relations.js +44 -63
  51. package/dist/daemon/relations.js.map +1 -1
  52. package/dist/daemon/session-index.d.ts +60 -0
  53. package/dist/daemon/session-index.d.ts.map +1 -0
  54. package/dist/daemon/session-index.js +136 -0
  55. package/dist/daemon/session-index.js.map +1 -0
  56. package/dist/daemon/session-index.test.d.ts +2 -0
  57. package/dist/daemon/session-index.test.d.ts.map +1 -0
  58. package/dist/daemon/session-index.test.js +54 -0
  59. package/dist/daemon/session-index.test.js.map +1 -0
  60. package/dist/daemon/session-summary.d.ts +69 -0
  61. package/dist/daemon/session-summary.d.ts.map +1 -0
  62. package/dist/daemon/session-summary.js +200 -0
  63. package/dist/daemon/session-summary.js.map +1 -0
  64. package/dist/daemon/session-summary.test.d.ts +2 -0
  65. package/dist/daemon/session-summary.test.d.ts.map +1 -0
  66. package/dist/daemon/session-summary.test.js +160 -0
  67. package/dist/daemon/session-summary.test.js.map +1 -0
  68. package/dist/daemon/staleness.test.d.ts +7 -0
  69. package/dist/daemon/staleness.test.d.ts.map +1 -0
  70. package/dist/daemon/staleness.test.js +128 -0
  71. package/dist/daemon/staleness.test.js.map +1 -0
  72. package/dist/daemon/workstream-summary.d.ts +68 -0
  73. package/dist/daemon/workstream-summary.d.ts.map +1 -0
  74. package/dist/daemon/workstream-summary.js +253 -0
  75. package/dist/daemon/workstream-summary.js.map +1 -0
  76. package/dist/daemon/workstream-summary.test.d.ts +2 -0
  77. package/dist/daemon/workstream-summary.test.d.ts.map +1 -0
  78. package/dist/daemon/workstream-summary.test.js +86 -0
  79. package/dist/daemon/workstream-summary.test.js.map +1 -0
  80. package/dist/lib/qdrant-client.d.ts +6 -1
  81. package/dist/lib/qdrant-client.d.ts.map +1 -1
  82. package/dist/lib/qdrant-client.js +3 -4
  83. package/dist/lib/qdrant-client.js.map +1 -1
  84. package/dist/lib/qdrant-client.test.js +21 -2
  85. package/dist/lib/qdrant-client.test.js.map +1 -1
  86. package/dist/lifecycle.test.d.ts +8 -0
  87. package/dist/lifecycle.test.d.ts.map +1 -0
  88. package/dist/lifecycle.test.js +74 -0
  89. package/dist/lifecycle.test.js.map +1 -0
  90. package/dist/llm/embedding/index.d.ts +42 -0
  91. package/dist/llm/embedding/index.d.ts.map +1 -0
  92. package/dist/llm/embedding/index.js +78 -0
  93. package/dist/llm/embedding/index.js.map +1 -0
  94. package/dist/llm/embedding/index.test.d.ts +8 -0
  95. package/dist/llm/embedding/index.test.d.ts.map +1 -0
  96. package/dist/llm/embedding/index.test.js +100 -0
  97. package/dist/llm/embedding/index.test.js.map +1 -0
  98. package/dist/llm/embedding/providers/bedrock.d.ts +16 -0
  99. package/dist/llm/embedding/providers/bedrock.d.ts.map +1 -0
  100. package/dist/llm/embedding/providers/bedrock.js +90 -0
  101. package/dist/llm/embedding/providers/bedrock.js.map +1 -0
  102. package/dist/llm/embedding/providers/bedrock.test.d.ts +2 -0
  103. package/dist/llm/embedding/providers/bedrock.test.d.ts.map +1 -0
  104. package/dist/llm/embedding/providers/bedrock.test.js +24 -0
  105. package/dist/llm/embedding/providers/bedrock.test.js.map +1 -0
  106. package/dist/llm/embedding/providers/index.d.ts +9 -0
  107. package/dist/llm/embedding/providers/index.d.ts.map +1 -0
  108. package/dist/llm/embedding/providers/index.js +9 -0
  109. package/dist/llm/embedding/providers/index.js.map +1 -0
  110. package/dist/llm/embedding/providers/ollama.d.ts +6 -0
  111. package/dist/llm/embedding/providers/ollama.d.ts.map +1 -0
  112. package/dist/llm/embedding/providers/ollama.js +39 -0
  113. package/dist/llm/embedding/providers/ollama.js.map +1 -0
  114. package/dist/llm/embedding/providers/ollama.test.d.ts +2 -0
  115. package/dist/llm/embedding/providers/ollama.test.d.ts.map +1 -0
  116. package/dist/llm/embedding/providers/ollama.test.js +54 -0
  117. package/dist/llm/embedding/providers/ollama.test.js.map +1 -0
  118. package/dist/llm/embedding/providers/openai.d.ts +6 -0
  119. package/dist/llm/embedding/providers/openai.d.ts.map +1 -0
  120. package/dist/llm/embedding/providers/openai.js +44 -0
  121. package/dist/llm/embedding/providers/openai.js.map +1 -0
  122. package/dist/llm/embedding/providers/openai.test.d.ts +2 -0
  123. package/dist/llm/embedding/providers/openai.test.d.ts.map +1 -0
  124. package/dist/llm/embedding/providers/openai.test.js +48 -0
  125. package/dist/llm/embedding/providers/openai.test.js.map +1 -0
  126. package/dist/llm/embedding/providers/portkey.d.ts +15 -0
  127. package/dist/llm/embedding/providers/portkey.d.ts.map +1 -0
  128. package/dist/llm/embedding/providers/portkey.js +58 -0
  129. package/dist/llm/embedding/providers/portkey.js.map +1 -0
  130. package/dist/llm/embedding/providers/portkey.test.d.ts +2 -0
  131. package/dist/llm/embedding/providers/portkey.test.d.ts.map +1 -0
  132. package/dist/llm/embedding/providers/portkey.test.js +56 -0
  133. package/dist/llm/embedding/providers/portkey.test.js.map +1 -0
  134. package/dist/llm/embedding/registry.d.ts +14 -0
  135. package/dist/llm/embedding/registry.d.ts.map +1 -0
  136. package/dist/llm/embedding/registry.js +27 -0
  137. package/dist/llm/embedding/registry.js.map +1 -0
  138. package/dist/llm/embedding/registry.test.d.ts +7 -0
  139. package/dist/llm/embedding/registry.test.d.ts.map +1 -0
  140. package/dist/llm/embedding/registry.test.js +68 -0
  141. package/dist/llm/embedding/registry.test.js.map +1 -0
  142. package/dist/llm/embedding/types.d.ts +55 -0
  143. package/dist/llm/embedding/types.d.ts.map +1 -0
  144. package/dist/llm/embedding/types.js +12 -0
  145. package/dist/llm/embedding/types.js.map +1 -0
  146. package/dist/llm/errors.d.ts +95 -0
  147. package/dist/llm/errors.d.ts.map +1 -0
  148. package/dist/llm/errors.js +164 -0
  149. package/dist/llm/errors.js.map +1 -0
  150. package/dist/llm/errors.test.d.ts +2 -0
  151. package/dist/llm/errors.test.d.ts.map +1 -0
  152. package/dist/llm/errors.test.js +103 -0
  153. package/dist/llm/errors.test.js.map +1 -0
  154. package/dist/llm/fetch.d.ts +39 -0
  155. package/dist/llm/fetch.d.ts.map +1 -0
  156. package/dist/llm/fetch.js +52 -0
  157. package/dist/llm/fetch.js.map +1 -0
  158. package/dist/llm/index.d.ts +6 -3
  159. package/dist/llm/index.d.ts.map +1 -1
  160. package/dist/llm/index.js +2 -2
  161. package/dist/llm/index.js.map +1 -1
  162. package/dist/llm/inference/index.d.ts +39 -0
  163. package/dist/llm/inference/index.d.ts.map +1 -0
  164. package/dist/llm/inference/index.js +118 -0
  165. package/dist/llm/inference/index.js.map +1 -0
  166. package/dist/llm/inference/index.test.d.ts +6 -0
  167. package/dist/llm/inference/index.test.d.ts.map +1 -0
  168. package/dist/llm/inference/index.test.js +109 -0
  169. package/dist/llm/inference/index.test.js.map +1 -0
  170. package/dist/llm/inference/providers/bedrock.d.ts +18 -0
  171. package/dist/llm/inference/providers/bedrock.d.ts.map +1 -0
  172. package/dist/llm/inference/providers/bedrock.js +105 -0
  173. package/dist/llm/inference/providers/bedrock.js.map +1 -0
  174. package/dist/llm/inference/providers/bedrock.test.d.ts +2 -0
  175. package/dist/llm/inference/providers/bedrock.test.d.ts.map +1 -0
  176. package/dist/llm/inference/providers/bedrock.test.js +21 -0
  177. package/dist/llm/inference/providers/bedrock.test.js.map +1 -0
  178. package/dist/llm/inference/providers/index.d.ts +10 -0
  179. package/dist/llm/inference/providers/index.d.ts.map +1 -0
  180. package/dist/llm/inference/providers/index.js +10 -0
  181. package/dist/llm/inference/providers/index.js.map +1 -0
  182. package/dist/llm/inference/providers/ollama.d.ts +8 -0
  183. package/dist/llm/inference/providers/ollama.d.ts.map +1 -0
  184. package/dist/llm/inference/providers/ollama.js +63 -0
  185. package/dist/llm/inference/providers/ollama.js.map +1 -0
  186. package/dist/llm/inference/providers/ollama.test.d.ts +2 -0
  187. package/dist/llm/inference/providers/ollama.test.d.ts.map +1 -0
  188. package/dist/llm/inference/providers/ollama.test.js +57 -0
  189. package/dist/llm/inference/providers/ollama.test.js.map +1 -0
  190. package/dist/llm/inference/providers/openai.d.ts +11 -0
  191. package/dist/llm/inference/providers/openai.d.ts.map +1 -0
  192. package/dist/llm/inference/providers/openai.js +73 -0
  193. package/dist/llm/inference/providers/openai.js.map +1 -0
  194. package/dist/llm/inference/providers/openai.test.d.ts +2 -0
  195. package/dist/llm/inference/providers/openai.test.d.ts.map +1 -0
  196. package/dist/llm/inference/providers/openai.test.js +46 -0
  197. package/dist/llm/inference/providers/openai.test.js.map +1 -0
  198. package/dist/llm/inference/providers/portkey.d.ts +13 -0
  199. package/dist/llm/inference/providers/portkey.d.ts.map +1 -0
  200. package/dist/llm/inference/providers/portkey.js +80 -0
  201. package/dist/llm/inference/providers/portkey.js.map +1 -0
  202. package/dist/llm/inference/providers/portkey.test.d.ts +2 -0
  203. package/dist/llm/inference/providers/portkey.test.d.ts.map +1 -0
  204. package/dist/llm/inference/providers/portkey.test.js +48 -0
  205. package/dist/llm/inference/providers/portkey.test.js.map +1 -0
  206. package/dist/llm/inference/registry.d.ts +15 -0
  207. package/dist/llm/inference/registry.d.ts.map +1 -0
  208. package/dist/llm/inference/registry.js +28 -0
  209. package/dist/llm/inference/registry.js.map +1 -0
  210. package/dist/llm/inference/registry.test.d.ts +6 -0
  211. package/dist/llm/inference/registry.test.d.ts.map +1 -0
  212. package/dist/llm/inference/registry.test.js +63 -0
  213. package/dist/llm/inference/registry.test.js.map +1 -0
  214. package/dist/llm/inference/types.d.ts +84 -0
  215. package/dist/llm/inference/types.d.ts.map +1 -0
  216. package/dist/llm/inference/types.js +9 -0
  217. package/dist/llm/inference/types.js.map +1 -0
  218. package/dist/llm/telemetry.d.ts +25 -0
  219. package/dist/llm/telemetry.d.ts.map +1 -0
  220. package/dist/llm/telemetry.js +43 -0
  221. package/dist/llm/telemetry.js.map +1 -0
  222. package/dist/llm/telemetry.test.d.ts +5 -0
  223. package/dist/llm/telemetry.test.d.ts.map +1 -0
  224. package/dist/llm/telemetry.test.js +89 -0
  225. package/dist/llm/telemetry.test.js.map +1 -0
  226. package/dist/llm/types.d.ts +4 -37
  227. package/dist/llm/types.d.ts.map +1 -1
  228. package/dist/llm/types.js +4 -1
  229. package/dist/llm/types.js.map +1 -1
  230. package/dist/logger.d.ts +18 -3
  231. package/dist/logger.d.ts.map +1 -1
  232. package/dist/logger.js +102 -20
  233. package/dist/logger.js.map +1 -1
  234. package/dist/logger.test.d.ts +5 -0
  235. package/dist/logger.test.d.ts.map +1 -0
  236. package/dist/logger.test.js +103 -0
  237. package/dist/logger.test.js.map +1 -0
  238. package/dist/mcp/api.d.ts +15 -1
  239. package/dist/mcp/api.d.ts.map +1 -1
  240. package/dist/mcp/api.js +44 -19
  241. package/dist/mcp/api.js.map +1 -1
  242. package/dist/mcp/api.test.d.ts +6 -0
  243. package/dist/mcp/api.test.d.ts.map +1 -0
  244. package/dist/mcp/api.test.js +130 -0
  245. package/dist/mcp/api.test.js.map +1 -0
  246. package/dist/mcp/helpers.d.ts +1 -0
  247. package/dist/mcp/helpers.d.ts.map +1 -1
  248. package/dist/mcp/helpers.js +62 -6
  249. package/dist/mcp/helpers.js.map +1 -1
  250. package/dist/mcp/helpers.test.js +71 -10
  251. package/dist/mcp/helpers.test.js.map +1 -1
  252. package/dist/mcp/index.d.ts +7 -1
  253. package/dist/mcp/index.d.ts.map +1 -1
  254. package/dist/mcp/index.js +38 -20
  255. package/dist/mcp/index.js.map +1 -1
  256. package/dist/mcp/taxonomy.d.ts +237 -31
  257. package/dist/mcp/taxonomy.d.ts.map +1 -1
  258. package/dist/mcp/taxonomy.js +533 -171
  259. package/dist/mcp/taxonomy.js.map +1 -1
  260. package/dist/mcp/taxonomy.test.d.ts +1 -1
  261. package/dist/mcp/taxonomy.test.js +141 -302
  262. package/dist/mcp/taxonomy.test.js.map +1 -1
  263. package/dist/mcp/tools.d.ts +1 -1
  264. package/dist/mcp/tools.d.ts.map +1 -1
  265. package/dist/mcp/tools.integration.itest.d.ts +23 -0
  266. package/dist/mcp/tools.integration.itest.d.ts.map +1 -0
  267. package/dist/mcp/tools.integration.itest.js +172 -0
  268. package/dist/mcp/tools.integration.itest.js.map +1 -0
  269. package/dist/mcp/tools.js +338 -302
  270. package/dist/mcp/tools.js.map +1 -1
  271. package/dist/mcp/tools.test.d.ts +16 -0
  272. package/dist/mcp/tools.test.d.ts.map +1 -0
  273. package/dist/mcp/tools.test.js +472 -0
  274. package/dist/mcp/tools.test.js.map +1 -0
  275. package/dist/mcp/types.d.ts +63 -8
  276. package/dist/mcp/types.d.ts.map +1 -1
  277. package/dist/prompts/brief.d.ts +19 -0
  278. package/dist/prompts/brief.d.ts.map +1 -0
  279. package/dist/prompts/brief.js +67 -0
  280. package/dist/prompts/brief.js.map +1 -0
  281. package/dist/prompts/contradiction.d.ts +24 -0
  282. package/dist/prompts/contradiction.d.ts.map +1 -0
  283. package/dist/prompts/contradiction.js +73 -0
  284. package/dist/prompts/contradiction.js.map +1 -0
  285. package/dist/prompts/distill.d.ts +21 -0
  286. package/dist/prompts/distill.d.ts.map +1 -0
  287. package/dist/prompts/distill.js +74 -0
  288. package/dist/prompts/distill.js.map +1 -0
  289. package/dist/prompts/extraction.d.ts +14 -0
  290. package/dist/prompts/extraction.d.ts.map +1 -0
  291. package/dist/prompts/extraction.js +87 -0
  292. package/dist/prompts/extraction.js.map +1 -0
  293. package/dist/prompts/index.d.ts +50 -0
  294. package/dist/prompts/index.d.ts.map +1 -0
  295. package/dist/prompts/index.js +102 -0
  296. package/dist/prompts/index.js.map +1 -0
  297. package/dist/prompts/prompts.test.d.ts +8 -0
  298. package/dist/prompts/prompts.test.d.ts.map +1 -0
  299. package/dist/prompts/prompts.test.js +140 -0
  300. package/dist/prompts/prompts.test.js.map +1 -0
  301. package/dist/prompts/relations.d.ts +17 -0
  302. package/dist/prompts/relations.d.ts.map +1 -0
  303. package/dist/prompts/relations.js +72 -0
  304. package/dist/prompts/relations.js.map +1 -0
  305. package/dist/render.d.ts +41 -0
  306. package/dist/render.d.ts.map +1 -0
  307. package/dist/render.js +173 -0
  308. package/dist/render.js.map +1 -0
  309. package/dist/render.test.d.ts +8 -0
  310. package/dist/render.test.d.ts.map +1 -0
  311. package/dist/render.test.js +212 -0
  312. package/dist/render.test.js.map +1 -0
  313. package/package.json +9 -2
  314. package/dist/llm/embedding.d.ts +0 -13
  315. package/dist/llm/embedding.d.ts.map +0 -1
  316. package/dist/llm/embedding.js +0 -127
  317. package/dist/llm/embedding.js.map +0 -1
  318. package/dist/llm/embedding.test.d.ts +0 -8
  319. package/dist/llm/embedding.test.d.ts.map +0 -1
  320. package/dist/llm/embedding.test.js +0 -117
  321. package/dist/llm/embedding.test.js.map +0 -1
  322. package/dist/llm/inference.d.ts +0 -12
  323. package/dist/llm/inference.d.ts.map +0 -1
  324. package/dist/llm/inference.js +0 -146
  325. package/dist/llm/inference.js.map +0 -1
  326. package/dist/llm/inference.test.d.ts +0 -8
  327. package/dist/llm/inference.test.d.ts.map +0 -1
  328. package/dist/llm/inference.test.js +0 -117
  329. package/dist/llm/inference.test.js.map +0 -1
package/dist/mcp/tools.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * MCP tool definitions all 12 memory tools.
2
+ * MCP tool definitions for memory.
3
3
  */
4
4
  import crypto from "node:crypto";
5
5
  import { z } from "zod";
6
- import { STALENESS_DAYS, THRESHOLD_DUPLICATE, THRESHOLD_RELATED, QDRANT_INDEXES, categoryValues, domainValues, kindValues, sourceValues, DEFAULT_DOMAIN, DEFAULT_KIND, DEFAULT_SOURCE, } from "./taxonomy.js";
7
- import { contentHash, daysSince, lastActivityDate, computeCombinedScore, buildFilter, formatFact, } from "./helpers.js";
8
- import { ready, qdrantUrl, qdrantApiKey, setQdrantUrl, setQdrantApiKey, setReady, getCollection, log, embed, getEmbeddingConfig, chatComplete, qdrantReq, ensureCollection, qdrantUpsert, qdrantSearch, qdrantScroll, qdrantSetPayload, qdrantGetPoints, } from "./api.js";
6
+ import { STALENESS_DAYS, THRESHOLD_DUPLICATE, THRESHOLD_RELATED, QDRANT_INDEXES, categoryValues, domainValues, kindValues, memorySubtypeValues, sourceValues, DEFAULT_CATEGORY, DEFAULT_DOMAIN, DEFAULT_KIND, DEFAULT_SOURCE, categoryForMemorySubtype, layerForMemorySubtype, normalizeCategory, normalizeDomain, normalizeKind, validateMemorySubtype, } from "./taxonomy.js";
7
+ import { contentHash, daysSince, lastActivityDate, computeCombinedScore, buildFilter, formatFact, MEMORY_RECALL_EXCLUDED_KINDS, } from "./helpers.js";
8
+ import { ready, qdrantUrl, qdrantApiKey, setupError, setQdrantUrl, setQdrantApiKey, setReady, getCollection, log, embed, getEmbeddingConfig, qdrantReq, ensureCollection, qdrantUpsert, qdrantSearch, qdrantScroll, qdrantSetPayload, qdrantGetPoints, } from "./api.js";
9
9
  import { saveConfig, loadConfig } from "../config.js";
10
10
  // ---------------------------------------------------------------------------
11
11
  // Runtime state
@@ -22,13 +22,50 @@ function nowISO() {
22
22
  function newId() {
23
23
  return crypto.randomUUID();
24
24
  }
25
+ function redactionOptions() {
26
+ return { enabled: false, redactPii: false };
27
+ }
28
+ function redactStorageText(text) {
29
+ return { text, redacted: false, summary: "none", matches: [] };
30
+ }
31
+ function combineRedactions(_items) {
32
+ return { redacted: false, summary: "none", matches: [] };
33
+ }
34
+ function resolveScope(workspaceId, includeLegacyWorkspace = false) {
35
+ return {
36
+ workspaceId: workspaceId?.trim() || undefined,
37
+ includeLegacy: includeLegacyWorkspace,
38
+ };
39
+ }
40
+ function scopedFilter(scope, extra = {}) {
41
+ return buildFilter({
42
+ ...extra,
43
+ workspace_id: scope.workspaceId,
44
+ includeLegacyWorkspace: scope.includeLegacy,
45
+ });
46
+ }
47
+ function addWorkspacePayload(payload, scope) {
48
+ if (scope.workspaceId)
49
+ payload["workspace_id"] = scope.workspaceId;
50
+ if (scope.actorId)
51
+ payload["actor_id"] = scope.actorId;
52
+ }
53
+ function addRedactionPayload(_payload, _summary) {
54
+ // Task 243 keeps storage pass-through; redaction policy is out of scope for this branch.
55
+ }
56
+ async function getPointForWorkspaceWrite(factId, _scope) {
57
+ const existing = await qdrantGetPoints([factId]);
58
+ const point = existing.result?.[0];
59
+ if (!point) {
60
+ return { error: { status: "not_found", fact_id: factId } };
61
+ }
62
+ return { point };
63
+ }
25
64
  function requireReady() {
26
65
  if (!ready) {
27
66
  const missing = [];
28
67
  if (!qdrantUrl)
29
68
  missing.push("qdrant-url");
30
- if (!qdrantApiKey)
31
- missing.push("qdrant-api-key");
32
69
  return {
33
70
  content: [{
34
71
  type: "text",
@@ -36,6 +73,10 @@ function requireReady() {
36
73
  status: "setup_required",
37
74
  ready: false,
38
75
  missing,
76
+ // Surface the underlying init failure (embedding / Qdrant) when
77
+ // present so users see an actionable reason instead of a generic
78
+ // "setup required" message.
79
+ ...(setupError ? { setup_error: setupError } : {}),
39
80
  setup_instructions: "Memory is not configured. Run `bikky setup` or call configure_credentials:\n" +
40
81
  "1. Go to cloud.qdrant.io → sign up (free tier: 1GB, no credit card)\n" +
41
82
  "2. Create a cluster → copy the REST URL and API key\n" +
@@ -52,14 +93,20 @@ function buildMemoryNudge() {
52
93
  if (elapsed < NUDGE_INTERVAL_MS)
53
94
  return null;
54
95
  const mins = Math.round(elapsed / 60000);
96
+ // Suggest the most likely category to record based on what an engineering
97
+ // session typically produces. The agent picks the best fit.
55
98
  return `🧠 Memory nudge: No memory_store calls in ${mins} minutes. ` +
56
- "If you've learned project facts, made key decisions, discovered service quirks, " +
57
- "or resolved errors store them now so future sessions benefit.";
99
+ "Reflect on what's worth persisting:\n" +
100
+ " infrastructurenew services, ports, configs touched?\n" +
101
+ " • decisions — architectural choices made (with rationale)?\n" +
102
+ " • observation — debugging findings, gotchas, workarounds?\n" +
103
+ " • projects — work-in-progress, blockers, completions?\n" +
104
+ "If yes, call memory_store now so future sessions inherit the knowledge.";
58
105
  }
59
106
  /**
60
107
  * Entity-graph traversal for memory_recall.
61
108
  */
62
- async function graphTraversal(primaryResults, limit) {
109
+ async function graphTraversal(primaryResults, limit, scope) {
63
110
  try {
64
111
  const primaryEntities = new Set();
65
112
  const primaryIds = new Set();
@@ -73,22 +120,16 @@ async function graphTraversal(primaryResults, limit) {
73
120
  return [];
74
121
  const relatedEntities = new Set();
75
122
  for (const entity of primaryEntities) {
76
- const outgoing = await qdrantScroll({
77
- must: [
78
- { key: "from_entity", match: { value: entity } },
79
- { is_null: { key: "superseded_by" } },
80
- ],
81
- }, 10).catch(() => ({ result: { points: [] } }));
123
+ const outgoingFilter = scopedFilter(scope, { excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
124
+ outgoingFilter.must.push({ key: "from_entity", match: { value: entity } });
125
+ const outgoing = await qdrantScroll(outgoingFilter, 10).catch(() => ({ result: { points: [] } }));
82
126
  for (const pt of (outgoing.result?.points ?? [])) {
83
127
  if (pt.payload.to_entity)
84
128
  relatedEntities.add(pt.payload.to_entity);
85
129
  }
86
- const incoming = await qdrantScroll({
87
- must: [
88
- { key: "to_entity", match: { value: entity } },
89
- { is_null: { key: "superseded_by" } },
90
- ],
91
- }, 10).catch(() => ({ result: { points: [] } }));
130
+ const incomingFilter = scopedFilter(scope, { excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
131
+ incomingFilter.must.push({ key: "to_entity", match: { value: entity } });
132
+ const incoming = await qdrantScroll(incomingFilter, 10).catch(() => ({ result: { points: [] } }));
92
133
  for (const pt of (incoming.result?.points ?? [])) {
93
134
  if (pt.payload.from_entity)
94
135
  relatedEntities.add(pt.payload.from_entity);
@@ -101,12 +142,9 @@ async function graphTraversal(primaryResults, limit) {
101
142
  const relatedFacts = [];
102
143
  const maxPerEntity = Math.max(2, Math.floor(limit / relatedEntities.size));
103
144
  for (const entity of relatedEntities) {
104
- const result = await qdrantScroll({
105
- must: [
106
- { key: "entities", match: { value: entity } },
107
- { is_null: { key: "superseded_by" } },
108
- ],
109
- }, maxPerEntity).catch(() => ({ result: { points: [] } }));
145
+ const filter = scopedFilter(scope, { excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
146
+ filter.must.push({ key: "entities", match: { value: entity } });
147
+ const result = await qdrantScroll(filter, maxPerEntity).catch(() => ({ result: { points: [] } }));
110
148
  for (const pt of (result.result?.points ?? [])) {
111
149
  if (!primaryIds.has(pt.id)) {
112
150
  relatedFacts.push(pt);
@@ -139,13 +177,13 @@ export function registerTools(mcp) {
139
177
  embedding_provider: getEmbeddingConfig().provider,
140
178
  embedding_model: getEmbeddingConfig().model,
141
179
  embedding_dimensions: getEmbeddingConfig().dimensions,
180
+ ...(setupError ? { setup_error: setupError } : {}),
142
181
  };
143
182
  const missing = status["missing"];
144
183
  if (!qdrantUrl)
145
184
  missing.push("qdrant-url");
146
- if (!qdrantApiKey)
147
- missing.push("qdrant-api-key");
148
- if (qdrantUrl && qdrantApiKey) {
185
+ // qdrant-api-key is optional (local / self-hosted Qdrant doesn't need it).
186
+ if (qdrantUrl) {
149
187
  try {
150
188
  await qdrantReq("GET", "/collections");
151
189
  status["qdrant_connected"] = true;
@@ -159,17 +197,18 @@ export function registerTools(mcp) {
159
197
  catch { /* ignore */ }
160
198
  if (!status["ready"] && missing.length > 0) {
161
199
  status["setup_instructions"] =
162
- "Run `bikky setup` or guide the user:\n" +
163
- "1. Go to cloud.qdrant.io sign up (free tier: 1GB, no credit card)\n" +
164
- "2. Create a cluster copy the REST URL and API key\n" +
165
- "3. Call configure_credentials with Qdrant values";
200
+ "Run `bikky setup` or guide the user. Pick one Qdrant option:\n" +
201
+ " Qdrant Cloud (managed, free tier, 1GB): https://cloud.qdrant.io copy the REST URL + API key\n" +
202
+ " Local Docker: `docker run -p 6333:6333 qdrant/qdrant` URL `http://localhost:6333` (no API key needed)\n" +
203
+ " Self-hosted: any reachable Qdrant; API key only required if QDRANT__SERVICE__API_KEY is set on the server\n" +
204
+ "Then call configure_credentials with the URL (and API key if applicable).";
166
205
  }
167
206
  return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
168
207
  });
169
208
  // ── configure_credentials ───────────────────────────────────────────────
170
209
  mcp.tool("configure_credentials", "Store Qdrant + embedding credentials in ~/.bikky/config.json. Tests connectivity and creates the collection if needed.", {
171
- qdrant_url: z.string().optional().describe("Qdrant Cloud REST URL (e.g. https://xxx.cloud.qdrant.io:6333)"),
172
- qdrant_api_key: z.string().optional().describe("Qdrant Cloud API key"),
210
+ qdrant_url: z.string().optional().describe("Qdrant REST URL — Qdrant Cloud (https://xxx.cloud.qdrant.io:6333), local Docker (http://localhost:6333), or self-hosted"),
211
+ qdrant_api_key: z.string().optional().describe("Qdrant API key — required for Qdrant Cloud; optional / leave blank for unauthenticated local or self-hosted instances"),
173
212
  openai_api_key: z.string().optional().describe("OpenAI API key (for OpenAI embedding/LLM provider)"),
174
213
  }, async ({ qdrant_url, qdrant_api_key, openai_api_key }) => {
175
214
  const results = {};
@@ -191,7 +230,7 @@ export function registerTools(mcp) {
191
230
  results["openai_api_key"] = "stored ✓";
192
231
  }
193
232
  saveConfig(cfg);
194
- if (qdrantUrl && qdrantApiKey) {
233
+ if (qdrantUrl) {
195
234
  try {
196
235
  await ensureCollection(QDRANT_INDEXES);
197
236
  results["qdrant_collection"] = `'${getCollection()}' ready ✓`;
@@ -208,14 +247,14 @@ export function registerTools(mcp) {
208
247
  catch (e) {
209
248
  results["embedding"] = `error: ${e instanceof Error ? e.message : String(e)}`;
210
249
  }
211
- setReady(!!(qdrantUrl && qdrantApiKey));
250
+ setReady(!!qdrantUrl);
212
251
  results["ready"] = ready;
213
252
  return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
214
253
  });
215
254
  // ── verify_connection ───────────────────────────────────────────────────
216
255
  mcp.tool("verify_connection", "Test that Qdrant is reachable, embeddings work, and the collection exists.", {}, async () => {
217
256
  const results = { qdrant: false, embedding: false, collection: false };
218
- if (qdrantUrl && qdrantApiKey) {
257
+ if (qdrantUrl) {
219
258
  try {
220
259
  await qdrantReq("GET", "/collections");
221
260
  results["qdrant"] = true;
@@ -246,12 +285,23 @@ export function registerTools(mcp) {
246
285
  "Returns the action taken (inserted/reinforced/duplicate) and any similar facts found.", {
247
286
  content: z.string().describe("The fact to store (atomic, single piece of knowledge)"),
248
287
  category: z.enum(categoryValues())
249
- .describe("Topic: infrastructure, decisions, observation, preferences, projects, team"),
288
+ .describe("Subject matter: codebase, infrastructure, operations, decisions, product_domain, projects, people, preferences, observations"),
250
289
  entities: z.array(z.string()).describe("Related entities (lowercase, e.g. ['qdrant', 'platform'])"),
251
290
  domain: z.enum(domainValues()).default(DEFAULT_DOMAIN)
252
- .describe("Life scopework or personal"),
291
+ .describe("Activity profilee.g. software_engineering, product_strategy, business_operations, research, personal_productivity"),
253
292
  kind: z.enum(kindValues()).default(DEFAULT_KIND)
254
293
  .describe("Knowledge form — fact, summary, distilled, relation"),
294
+ memory_subtype: z.enum(memorySubtypeValues()).optional()
295
+ .describe("Optional subtype within kind, such as codebase_map, episode, workstream, convention, or recall_event"),
296
+ workspace_id: z.string().optional()
297
+ .describe("Optional workspace namespace for team memory."),
298
+ episode_id: z.string().optional().describe("Optional coherent episode identifier"),
299
+ workstream_key: z.string().optional().describe("Optional durable workstream key"),
300
+ task_key: z.string().optional().describe("Optional task or issue key"),
301
+ repo: z.string().optional().describe("Optional repository or project surface"),
302
+ branch: z.string().optional().describe("Optional branch or working surface"),
303
+ review_status: z.enum(["candidate", "reviewed", "approved", "rejected"]).optional()
304
+ .describe("Optional review lifecycle status"),
255
305
  source: z.enum(sourceValues()).default(DEFAULT_SOURCE)
256
306
  .describe("Creator — agent, daemon, system, user"),
257
307
  confidence: z.number().min(0).max(1).default(0.9).describe("How certain (0.0-1.0)"),
@@ -264,20 +314,54 @@ export function registerTools(mcp) {
264
314
  }).optional().describe("Optional typed relation between two entities"),
265
315
  metadata: z.record(z.string(), z.string()).optional()
266
316
  .describe("Optional key-value metadata. Stored with the fact and filterable via memory_recall."),
267
- }, async ({ content, category, entities, domain, kind, source, confidence, importance, supersedes, relation, metadata }) => {
317
+ }, async ({ content, category, entities, domain, kind, memory_subtype, workspace_id, episode_id, workstream_key, task_key, repo, branch, review_status, source, confidence, importance, supersedes, relation, metadata, }) => {
268
318
  const guard = requireReady();
269
319
  if (guard)
270
320
  return guard;
271
321
  lastStoreTime = Date.now();
272
322
  const now = nowISO();
273
- const hash = contentHash(category, content);
274
- const normalizedEntities = entities.map((e) => e.toLowerCase());
323
+ const scope = resolveScope(workspace_id);
324
+ const normalizedKind = normalizeKind(kind);
325
+ let normalizedSubtype = null;
326
+ try {
327
+ normalizedSubtype = validateMemorySubtype(normalizedKind, memory_subtype);
328
+ }
329
+ catch (e) {
330
+ return {
331
+ content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
332
+ isError: true,
333
+ };
334
+ }
335
+ const normalizedCategory = normalizedSubtype
336
+ ? categoryForMemorySubtype(normalizedSubtype) ?? normalizeCategory(category)
337
+ : normalizeCategory(category);
338
+ const normalizedDomain = normalizeDomain(domain);
339
+ const normalizedLayer = normalizedSubtype ? layerForMemorySubtype(normalizedSubtype) : null;
340
+ const redactedContent = redactStorageText(content);
341
+ const redactedEntities = entities.map((entity) => redactStorageText(entity));
342
+ const sanitizedEntities = redactedEntities.map((entity) => entity.text);
343
+ const redactedRelation = relation ? {
344
+ from: redactStorageText(relation.from),
345
+ type: redactStorageText(relation.type),
346
+ to: redactStorageText(relation.to),
347
+ } : null;
348
+ const redactionSummary = combineRedactions([
349
+ redactedContent,
350
+ ...redactedEntities,
351
+ ...(redactedRelation ? [redactedRelation.from, redactedRelation.type, redactedRelation.to] : []),
352
+ ]);
353
+ const hash = contentHash(normalizedCategory, redactedContent.text);
354
+ const normalizedEntities = sanitizedEntities.map((e) => e.toLowerCase());
355
+ const sanitizedRelation = redactedRelation ? {
356
+ from: redactedRelation.from.text,
357
+ type: redactedRelation.type.text,
358
+ to: redactedRelation.to.text,
359
+ } : null;
275
360
  // 1. Exact dedup via content hash
276
361
  try {
277
- const existing = await qdrantScroll({ must: [
278
- { key: "content_hash", match: { value: hash } },
279
- { is_null: { key: "superseded_by" } },
280
- ] }, 1);
362
+ const hashFilter = scopedFilter(scope) ?? { must: [] };
363
+ hashFilter.must.push({ key: "content_hash", match: { value: hash } });
364
+ const existing = await qdrantScroll(hashFilter, 1);
281
365
  const existingPoint = existing.result?.points?.[0];
282
366
  if (existingPoint) {
283
367
  const point = existingPoint;
@@ -301,17 +385,16 @@ export function registerTools(mcp) {
301
385
  log("WARN", `Hash dedup check failed: ${e instanceof Error ? e.message : String(e)}`);
302
386
  }
303
387
  // 2. Generate embedding
304
- const vector = await embed(content);
388
+ const vector = await embed(redactedContent.text);
305
389
  // 3. Semantic dedup
306
390
  let similarFacts = [];
307
391
  let potentialConflicts = [];
308
392
  try {
309
- const filter = { must: [] };
393
+ const filter = scopedFilter(scope) ?? { must: [] };
310
394
  if (normalizedEntities.length > 0) {
311
395
  filter.must.push({ key: "entities", match: { any: normalizedEntities } });
312
396
  }
313
- filter.must.push({ is_null: { key: "superseded_by" } });
314
- const results = await qdrantSearch(vector, filter.must.length > 0 ? filter : undefined, 3);
397
+ const results = await qdrantSearch(vector, filter, 3);
315
398
  const firstResult = results.result?.[0];
316
399
  if (results.result?.length > 0 && firstResult) {
317
400
  const topScore = firstResult.score ?? 0;
@@ -368,6 +451,10 @@ export function registerTools(mcp) {
368
451
  // 5. Supersede old fact if requested
369
452
  if (supersedes) {
370
453
  try {
454
+ const existing = await getPointForWorkspaceWrite(supersedes, scope);
455
+ if (existing.error) {
456
+ return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
457
+ }
371
458
  await qdrantSetPayload([supersedes], {
372
459
  superseded_by: factId,
373
460
  superseded_at: now,
@@ -379,10 +466,10 @@ export function registerTools(mcp) {
379
466
  }
380
467
  // 6. Insert new fact
381
468
  const payload = {
382
- content,
383
- category,
384
- domain,
385
- kind,
469
+ content: redactedContent.text,
470
+ category: normalizedCategory,
471
+ domain: normalizedDomain,
472
+ kind: normalizedKind,
386
473
  entities: normalizedEntities,
387
474
  source,
388
475
  confidence,
@@ -395,22 +482,43 @@ export function registerTools(mcp) {
395
482
  created_at: now,
396
483
  updated_at: now,
397
484
  };
485
+ if (normalizedSubtype) {
486
+ payload["memory_subtype"] = normalizedSubtype;
487
+ }
488
+ if (normalizedLayer) {
489
+ payload["layer"] = normalizedLayer;
490
+ }
491
+ if (episode_id)
492
+ payload["episode_id"] = episode_id;
493
+ if (workstream_key)
494
+ payload["workstream_key"] = workstream_key;
495
+ if (task_key)
496
+ payload["task_key"] = task_key;
497
+ if (repo)
498
+ payload["repo"] = repo;
499
+ if (branch)
500
+ payload["branch"] = branch;
501
+ if (review_status)
502
+ payload["review_status"] = review_status;
503
+ addWorkspacePayload(payload, scope);
504
+ addRedactionPayload(payload, redactionSummary);
398
505
  if (metadata && Object.keys(metadata).length > 0) {
399
506
  payload["metadata"] = metadata;
400
507
  }
401
508
  await qdrantUpsert(factId, vector, payload);
402
509
  // 7. Insert relation point if provided
403
510
  let relationId = null;
404
- if (relation) {
511
+ if (sanitizedRelation) {
405
512
  relationId = newId();
406
- const relContent = `${relation.from} ${relation.type} ${relation.to}`;
513
+ const relContent = `${sanitizedRelation.from} ${sanitizedRelation.type} ${sanitizedRelation.to}`;
407
514
  const relVector = await embed(relContent);
408
515
  const relPayload = {
409
516
  content: relContent,
410
- category,
411
- domain,
517
+ category: normalizedCategory,
518
+ domain: normalizedDomain,
412
519
  kind: "relation",
413
- entities: [relation.from.toLowerCase(), relation.to.toLowerCase()],
520
+ layer: "memory_object",
521
+ entities: [sanitizedRelation.from.toLowerCase(), sanitizedRelation.to.toLowerCase()],
414
522
  source,
415
523
  confidence,
416
524
  content_hash: contentHash("relation", relContent),
@@ -420,18 +528,23 @@ export function registerTools(mcp) {
420
528
  superseded_at: null,
421
529
  created_at: now,
422
530
  updated_at: now,
423
- from_entity: relation.from.toLowerCase(),
424
- relation_type: relation.type.toLowerCase(),
425
- to_entity: relation.to.toLowerCase(),
531
+ from_entity: sanitizedRelation.from.toLowerCase(),
532
+ relation_type: sanitizedRelation.type.toLowerCase(),
533
+ to_entity: sanitizedRelation.to.toLowerCase(),
426
534
  };
535
+ addWorkspacePayload(relPayload, scope);
536
+ addRedactionPayload(relPayload, redactionSummary);
427
537
  await qdrantUpsert(relationId, relVector, relPayload);
428
538
  }
429
539
  const result = {
430
540
  action: "inserted",
431
541
  fact_id: factId,
542
+ workspace_id: scope.workspaceId,
432
543
  };
433
544
  if (relationId)
434
545
  result["relation_id"] = relationId;
546
+ if (redactionSummary.redacted)
547
+ result["redaction"] = redactionSummary;
435
548
  if (similarFacts.length > 0)
436
549
  result["similar_facts"] = similarFacts;
437
550
  if (potentialConflicts.length > 0) {
@@ -447,22 +560,63 @@ export function registerTools(mcp) {
447
560
  "Use on session start with a broad query for context briefing.", {
448
561
  query: z.string().describe("What to search for (natural language)"),
449
562
  category: z.string().optional().describe("Filter by category"),
450
- domain: z.string().optional().describe("Filter by domain (work or personal)"),
563
+ domain: z.string().optional().describe("Filter by domain activity profile"),
451
564
  kind: z.string().optional().describe("Filter by kind (fact, summary, distilled, relation)"),
565
+ memory_subtype: z.string().optional().describe("Filter by memory subtype"),
566
+ workspace_id: z.string().optional().describe("Filter by optional workspace namespace."),
567
+ include_legacy_workspace: z.boolean().optional()
568
+ .describe("Include legacy facts without workspace_id in this workspace query."),
452
569
  entity: z.string().optional().describe("Filter by entity name"),
570
+ episode_id: z.string().optional().describe("Filter by coherent episode ID"),
571
+ workstream_key: z.string().optional().describe("Filter by durable workstream key"),
572
+ task_key: z.string().optional().describe("Filter by task or issue key"),
573
+ repo: z.string().optional().describe("Filter by repository or project surface"),
574
+ branch: z.string().optional().describe("Filter by branch or working surface"),
575
+ review_status: z.string().optional().describe("Filter by review lifecycle status"),
453
576
  since: z.string().optional().describe("Only facts created after this ISO date"),
454
577
  until: z.string().optional().describe("Only facts created before this ISO date"),
455
578
  limit: z.number().optional().default(10).describe("Max results (default 10)"),
456
579
  graph_depth: z.number().optional().default(0).describe("Entity graph traversal depth (0=none, 1=include 1-hop related entity facts)."),
457
580
  metadata_filter: z.record(z.string(), z.string()).optional()
458
581
  .describe("Filter by metadata key-value pairs. All pairs must match."),
459
- }, async ({ query, category, domain, kind, entity, since, until, limit, graph_depth, metadata_filter }) => {
582
+ }, async ({ query, category, domain, kind, memory_subtype, workspace_id, include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, metadata_filter, }) => {
460
583
  const guard = requireReady();
461
584
  if (guard)
462
585
  return guard;
463
586
  const requestedLimit = limit ?? 10;
464
- const vector = await embed(query);
465
- const filter = buildFilter({ category, domain, kind, entity, since, until, metadata: metadata_filter });
587
+ const scope = resolveScope(workspace_id, include_legacy_workspace);
588
+ const redactedQuery = redactStorageText(query);
589
+ const vector = await embed(redactedQuery.text);
590
+ const normalizedKind = kind ? normalizeKind(kind) : undefined;
591
+ let normalizedSubtype;
592
+ if (memory_subtype) {
593
+ try {
594
+ normalizedSubtype = validateMemorySubtype(normalizedKind, memory_subtype) ?? undefined;
595
+ }
596
+ catch (e) {
597
+ return {
598
+ content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
599
+ isError: true,
600
+ };
601
+ }
602
+ }
603
+ const filter = scopedFilter(scope, {
604
+ category: category ? normalizeCategory(category) : undefined,
605
+ domain: domain ? normalizeDomain(domain) : undefined,
606
+ kind: normalizedKind,
607
+ memory_subtype: normalizedSubtype,
608
+ entity,
609
+ episode_id,
610
+ workstream_key,
611
+ task_key,
612
+ repo,
613
+ branch,
614
+ review_status,
615
+ since,
616
+ until,
617
+ metadata: metadata_filter,
618
+ excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS,
619
+ });
466
620
  const results = await qdrantSearch(vector, filter, requestedLimit * 2);
467
621
  if (!results.result?.length) {
468
622
  const nudge = buildMemoryNudge();
@@ -475,7 +629,7 @@ export function registerTools(mcp) {
475
629
  .slice(0, requestedLimit);
476
630
  const lines = ranked.map((r) => formatFact(r));
477
631
  if ((graph_depth ?? 0) >= 1) {
478
- const relatedLines = await graphTraversal(ranked, requestedLimit);
632
+ const relatedLines = await graphTraversal(ranked, requestedLimit, scope);
479
633
  if (relatedLines.length > 0) {
480
634
  lines.push("", "── Related (1-hop) ──");
481
635
  lines.push(...relatedLines);
@@ -490,25 +644,24 @@ export function registerTools(mcp) {
490
644
  mcp.tool("memory_entity", "Get everything known about an entity — all facts mentioning it plus its relationships.", {
491
645
  name: z.string().describe("Entity name (e.g. 'qdrant', 'platform')"),
492
646
  limit: z.number().optional().default(20).describe("Max facts to return"),
493
- }, async ({ name, limit }) => {
647
+ workspace_id: z.string().optional().describe("Filter by optional workspace namespace."),
648
+ include_legacy_workspace: z.boolean().optional()
649
+ .describe("Include legacy facts without workspace_id in this workspace query."),
650
+ }, async ({ name, limit, workspace_id, include_legacy_workspace }) => {
494
651
  const guard = requireReady();
495
652
  if (guard)
496
653
  return guard;
497
654
  const entityName = name.toLowerCase();
498
- const facts = await qdrantScroll({
499
- must: [
500
- { key: "entities", match: { value: entityName } },
501
- { is_null: { key: "superseded_by" } },
502
- ],
503
- }, limit ?? 20);
504
- const relationsFrom = await qdrantScroll({ must: [
505
- { key: "from_entity", match: { value: entityName } },
506
- { is_null: { key: "superseded_by" } },
507
- ] }, 50);
508
- const relationsTo = await qdrantScroll({ must: [
509
- { key: "to_entity", match: { value: entityName } },
510
- { is_null: { key: "superseded_by" } },
511
- ] }, 50);
655
+ const scope = resolveScope(workspace_id, include_legacy_workspace);
656
+ const factsFilter = scopedFilter(scope) ?? { must: [] };
657
+ factsFilter.must.push({ key: "entities", match: { value: entityName } });
658
+ const facts = await qdrantScroll(factsFilter, limit ?? 20);
659
+ const fromFilter = scopedFilter(scope) ?? { must: [] };
660
+ fromFilter.must.push({ key: "from_entity", match: { value: entityName } });
661
+ const relationsFrom = await qdrantScroll(fromFilter, 50);
662
+ const toFilter = scopedFilter(scope) ?? { must: [] };
663
+ toFilter.must.push({ key: "to_entity", match: { value: entityName } });
664
+ const relationsTo = await qdrantScroll(toFilter, 50);
512
665
  const output = [];
513
666
  const factPoints = facts.result?.points ?? [];
514
667
  if (factPoints.length > 0) {
@@ -548,17 +701,19 @@ export function registerTools(mcp) {
548
701
  relation_type: z.string().optional().describe("Filter by relation type (e.g. 'owns', 'uses', 'decided')"),
549
702
  direction: z.enum(["from", "to", "both"]).optional().default("both")
550
703
  .describe("Direction: 'from' (entity as source), 'to' (entity as target), 'both'"),
551
- }, async ({ entity, relation_type, direction }) => {
704
+ workspace_id: z.string().optional().describe("Filter by optional workspace namespace."),
705
+ include_legacy_workspace: z.boolean().optional()
706
+ .describe("Include legacy facts without workspace_id in this workspace query."),
707
+ }, async ({ entity, relation_type, direction, workspace_id, include_legacy_workspace }) => {
552
708
  const guard = requireReady();
553
709
  if (guard)
554
710
  return guard;
555
711
  const entityName = entity.toLowerCase();
712
+ const scope = resolveScope(workspace_id, include_legacy_workspace);
556
713
  const results = [];
557
714
  if (direction === "from" || direction === "both") {
558
- const filter = { must: [
559
- { key: "from_entity", match: { value: entityName } },
560
- { is_null: { key: "superseded_by" } },
561
- ] };
715
+ const filter = scopedFilter(scope) ?? { must: [] };
716
+ filter.must.push({ key: "from_entity", match: { value: entityName } });
562
717
  if (relation_type) {
563
718
  filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
564
719
  }
@@ -566,10 +721,8 @@ export function registerTools(mcp) {
566
721
  results.push(...(r.result?.points ?? []));
567
722
  }
568
723
  if (direction === "to" || direction === "both") {
569
- const filter = { must: [
570
- { key: "to_entity", match: { value: entityName } },
571
- { is_null: { key: "superseded_by" } },
572
- ] };
724
+ const filter = scopedFilter(scope) ?? { must: [] };
725
+ filter.must.push({ key: "to_entity", match: { value: entityName } });
573
726
  if (relation_type) {
574
727
  filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
575
728
  }
@@ -596,18 +749,30 @@ export function registerTools(mcp) {
596
749
  mcp.tool("memory_forget", "Mark a fact as superseded/wrong. The fact remains but is excluded from recall results.", {
597
750
  fact_id: z.string().describe("ID of the fact to forget"),
598
751
  reason: z.string().describe("Why this fact is being superseded"),
599
- }, async ({ fact_id, reason }) => {
752
+ workspace_id: z.string().optional().describe("Optional workspace namespace."),
753
+ }, async ({ fact_id, reason, workspace_id }) => {
600
754
  const guard = requireReady();
601
755
  if (guard)
602
756
  return guard;
603
757
  const now = nowISO();
604
758
  try {
759
+ const scope = resolveScope(workspace_id);
760
+ const existing = await getPointForWorkspaceWrite(fact_id, scope);
761
+ if (existing.error) {
762
+ return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
763
+ }
764
+ const redactedReason = redactStorageText(reason);
605
765
  await qdrantSetPayload([fact_id], {
606
- superseded_by: `forgotten:${reason}`,
766
+ superseded_by: `forgotten:${redactedReason.text}`,
607
767
  superseded_at: now,
608
768
  updated_at: now,
609
769
  });
610
- return { content: [{ type: "text", text: JSON.stringify({ status: "forgotten", fact_id, reason }) }] };
770
+ return { content: [{ type: "text", text: JSON.stringify({
771
+ status: "forgotten",
772
+ fact_id,
773
+ reason: redactedReason.text,
774
+ ...(redactedReason.redacted ? { redaction: redactedReason } : {}),
775
+ }) }] };
611
776
  }
612
777
  catch (e) {
613
778
  return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
@@ -616,15 +781,20 @@ export function registerTools(mcp) {
616
781
  // ── memory_verify ───────────────────────────────────────────────────────
617
782
  mcp.tool("memory_verify", "Confirm a fact is still accurate. Resets the staleness clock and bumps verification count.", {
618
783
  fact_id: z.string().describe("ID of the fact to verify"),
619
- }, async ({ fact_id }) => {
784
+ workspace_id: z.string().optional().describe("Optional workspace namespace."),
785
+ }, async ({ fact_id, workspace_id }) => {
620
786
  const guard = requireReady();
621
787
  if (guard)
622
788
  return guard;
623
789
  const now = nowISO();
624
790
  try {
625
- const existing = await qdrantGetPoints([fact_id]).catch(() => null);
791
+ const scope = resolveScope(workspace_id);
792
+ const writable = await getPointForWorkspaceWrite(fact_id, scope);
793
+ if (writable.error) {
794
+ return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
795
+ }
626
796
  let currentCount = 0;
627
- const existingPt = existing?.result?.[0];
797
+ const existingPt = writable.point;
628
798
  if (existingPt) {
629
799
  currentCount = existingPt.payload.verification_count ?? 0;
630
800
  }
@@ -656,17 +826,18 @@ export function registerTools(mcp) {
656
826
  fact_id: z.string().optional().describe("Fact ID (required for approve/reject/correct)"),
657
827
  reason: z.string().optional().describe("Reason for rejection"),
658
828
  corrected_content: z.string().optional().describe("Corrected fact text (for correct action)"),
659
- }, async ({ limit, action, fact_id, reason, corrected_content }) => {
829
+ workspace_id: z.string().optional().describe("Filter by optional workspace namespace."),
830
+ include_legacy_workspace: z.boolean().optional()
831
+ .describe("Include legacy facts without workspace_id in this workspace query."),
832
+ }, async ({ limit, action, fact_id, reason, corrected_content, workspace_id, include_legacy_workspace }) => {
660
833
  const guard = requireReady();
661
834
  if (guard)
662
835
  return guard;
836
+ const scope = resolveScope(workspace_id, include_legacy_workspace);
663
837
  if (action === "list") {
664
- const result = await qdrantScroll({
665
- must: [
666
- { key: "source", match: { value: "daemon" } },
667
- { is_null: { key: "superseded_by" } },
668
- ],
669
- }, (limit ?? 10) * 2);
838
+ const filter = scopedFilter(scope) ?? { must: [] };
839
+ filter.must.push({ key: "source", match: { value: "daemon" } });
840
+ const result = await qdrantScroll(filter, (limit ?? 10) * 2);
670
841
  const points = (result.result?.points ?? [])
671
842
  .sort((a, b) => (b.payload.created_at ?? "").localeCompare(a.payload.created_at ?? ""))
672
843
  .slice(0, limit ?? 10);
@@ -684,9 +855,12 @@ export function registerTools(mcp) {
684
855
  }
685
856
  const now = nowISO();
686
857
  if (action === "approve") {
687
- const existing = await qdrantGetPoints([fact_id]).catch(() => null);
858
+ const writable = await getPointForWorkspaceWrite(fact_id, scope);
859
+ if (writable.error) {
860
+ return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
861
+ }
688
862
  let currentCount = 0;
689
- const approvePt = existing?.result?.[0];
863
+ const approvePt = writable.point;
690
864
  if (approvePt) {
691
865
  currentCount = approvePt.payload.verification_count ?? 0;
692
866
  }
@@ -701,28 +875,52 @@ export function registerTools(mcp) {
701
875
  if (!reason) {
702
876
  return { content: [{ type: "text", text: "Error: reason is required for reject action." }] };
703
877
  }
878
+ const writable = await getPointForWorkspaceWrite(fact_id, scope);
879
+ if (writable.error) {
880
+ return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
881
+ }
882
+ const redactedReason = redactStorageText(reason);
704
883
  await qdrantSetPayload([fact_id], {
705
- superseded_by: `rejected:${reason}`,
884
+ superseded_by: `rejected:${redactedReason.text}`,
706
885
  superseded_at: now,
707
886
  updated_at: now,
708
887
  });
709
- return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", fact_id, reason }) }] };
888
+ return { content: [{ type: "text", text: JSON.stringify({
889
+ status: "rejected",
890
+ fact_id,
891
+ reason: redactedReason.text,
892
+ ...(redactedReason.redacted ? { redaction: redactedReason } : {}),
893
+ }) }] };
710
894
  }
711
895
  if (action === "correct") {
712
896
  if (!corrected_content) {
713
897
  return { content: [{ type: "text", text: "Error: corrected_content is required for correct action." }] };
714
898
  }
715
- const original = await qdrantGetPoints([fact_id]).catch(() => null);
716
- const origPayload = original?.result?.[0]?.payload;
717
- const vector = await embed(corrected_content);
899
+ const writable = await getPointForWorkspaceWrite(fact_id, scope);
900
+ if (writable.error) {
901
+ return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
902
+ }
903
+ const origPayload = writable.point?.payload;
904
+ const redactedCorrected = redactStorageText(corrected_content);
905
+ const correctionScope = origPayload?.workspace_id
906
+ ? resolveScope(origPayload.workspace_id, false)
907
+ : scope;
908
+ const vector = await embed(redactedCorrected.text);
718
909
  const correctedId = crypto.randomUUID();
719
- const origCategory = origPayload?.category ?? "observation";
720
- const hash = contentHash(origCategory, corrected_content);
721
- await qdrantUpsert(correctedId, vector, {
722
- content: corrected_content,
910
+ const origCategory = normalizeCategory(origPayload?.category ?? DEFAULT_CATEGORY);
911
+ const hash = contentHash(origCategory, redactedCorrected.text);
912
+ const correctedPayload = {
913
+ content: redactedCorrected.text,
723
914
  category: origCategory,
724
- domain: origPayload?.domain ?? "work",
725
- kind: origPayload?.kind ?? "fact",
915
+ domain: normalizeDomain(origPayload?.domain ?? DEFAULT_DOMAIN),
916
+ kind: normalizeKind(origPayload?.kind ?? "fact"),
917
+ ...(origPayload?.memory_subtype ? { memory_subtype: origPayload.memory_subtype } : {}),
918
+ ...(origPayload?.layer ? { layer: origPayload.layer } : {}),
919
+ ...(origPayload?.episode_id ? { episode_id: origPayload.episode_id } : {}),
920
+ ...(origPayload?.workstream_key ? { workstream_key: origPayload.workstream_key } : {}),
921
+ ...(origPayload?.task_key ? { task_key: origPayload.task_key } : {}),
922
+ ...(origPayload?.repo ? { repo: origPayload.repo } : {}),
923
+ ...(origPayload?.branch ? { branch: origPayload.branch } : {}),
726
924
  entities: origPayload?.entities ?? [],
727
925
  source: "user",
728
926
  confidence: 0.95,
@@ -735,7 +933,10 @@ export function registerTools(mcp) {
735
933
  created_at: now,
736
934
  updated_at: now,
737
935
  metadata: { ...(origPayload?.metadata ?? {}), corrected_from: fact_id },
738
- });
936
+ };
937
+ addWorkspacePayload(correctedPayload, correctionScope);
938
+ addRedactionPayload(correctedPayload, redactedCorrected);
939
+ await qdrantUpsert(correctedId, vector, correctedPayload);
739
940
  await qdrantSetPayload([fact_id], {
740
941
  superseded_by: correctedId,
741
942
  superseded_at: now,
@@ -745,175 +946,6 @@ export function registerTools(mcp) {
745
946
  }
746
947
  return { content: [{ type: "text", text: `Unknown action: ${String(action)}` }] };
747
948
  });
748
- // ── memory_session_summary ──────────────────────────────────────────────
749
- mcp.tool("memory_session_summary", "Store a compressed summary of the current session. Call before a session ends or when a major task completes. " +
750
- "Future sessions receive this via memory_recall('session briefing'). Idempotent per session_id.", {
751
- session_id: z.string().describe("Session UUID"),
752
- summary: z.string().describe("2-5 sentence compressed summary of what happened this session"),
753
- tasks_completed: z.array(z.string()).optional().default([]).describe("Task slugs completed"),
754
- decisions_made: z.array(z.string()).optional().default([]).describe("Key decisions"),
755
- entities_touched: z.array(z.string()).optional().default([]).describe("Entities involved (lowercase)"),
756
- }, async ({ session_id, summary, tasks_completed, decisions_made, entities_touched }) => {
757
- const guard = requireReady();
758
- if (guard)
759
- return guard;
760
- const now = nowISO();
761
- const normalizedEntities = entities_touched.map((e) => e.toLowerCase());
762
- // Check for existing summary for this session
763
- try {
764
- const existing = await qdrantScroll({ must: [
765
- { key: "session_id", match: { value: session_id } },
766
- { key: "kind", match: { value: "summary" } },
767
- { is_null: { key: "superseded_by" } },
768
- ] }, 1);
769
- const summaryPoint = existing.result?.points?.[0];
770
- if (summaryPoint) {
771
- const point = summaryPoint;
772
- const vector = await embed(summary);
773
- await qdrantUpsert(point.id, vector, {
774
- ...point.payload,
775
- content: summary,
776
- kind: "summary",
777
- domain: "work",
778
- source: "system",
779
- tasks_completed,
780
- decisions_made,
781
- entities: normalizedEntities,
782
- content_hash: contentHash("observation", summary),
783
- updated_at: now,
784
- });
785
- return {
786
- content: [{ type: "text", text: JSON.stringify({
787
- action: "updated", fact_id: point.id, session_id,
788
- }) }],
789
- };
790
- }
791
- }
792
- catch (e) {
793
- log("WARN", `Session summary lookup failed: ${e instanceof Error ? e.message : String(e)}`);
794
- }
795
- // Insert new summary
796
- const vector = await embed(summary);
797
- const factId = newId();
798
- await qdrantUpsert(factId, vector, {
799
- content: summary,
800
- category: "observation",
801
- domain: "work",
802
- kind: "summary",
803
- entities: normalizedEntities,
804
- source: "system",
805
- confidence: 1.0,
806
- content_hash: contentHash("observation", summary),
807
- reinforcement_count: 1,
808
- last_reinforced_at: now,
809
- superseded_by: null,
810
- superseded_at: null,
811
- created_at: now,
812
- updated_at: now,
813
- session_id,
814
- tasks_completed,
815
- decisions_made,
816
- });
817
- return {
818
- content: [{ type: "text", text: JSON.stringify({
819
- action: "stored", fact_id: factId, session_id,
820
- }) }],
821
- };
822
- });
823
- // ── memory_distill ──────────────────────────────────────────────────────
824
- mcp.tool("memory_distill", "Consolidate recent session summaries into distilled patterns. Call when 5+ session summaries exist. " +
825
- "Uses LLM to extract recurring patterns and key learnings, then supersedes the source summaries.", {
826
- days: z.number().optional().default(14).describe("Look-back period in days (default 14)"),
827
- max_summaries: z.number().optional().default(20).describe("Max summaries to consolidate"),
828
- }, async ({ days, max_summaries }) => {
829
- const guard = requireReady();
830
- if (guard)
831
- return guard;
832
- const now = nowISO();
833
- const daysVal = days ?? 14;
834
- const maxVal = max_summaries ?? 20;
835
- const since = new Date(Date.now() - daysVal * 86400000).toISOString();
836
- const summaryResults = await qdrantScroll({ must: [
837
- { key: "kind", match: { value: "summary" } },
838
- { key: "created_at", range: { gte: since } },
839
- { is_null: { key: "superseded_by" } },
840
- ] }, maxVal);
841
- const summaries = summaryResults.result?.points ?? [];
842
- if (summaries.length < 3) {
843
- return {
844
- content: [{ type: "text", text: JSON.stringify({
845
- action: "skipped",
846
- reason: `Only ${summaries.length} session summaries in the last ${daysVal} days. Need at least 3.`,
847
- }) }],
848
- };
849
- }
850
- const summaryTexts = summaries.map((s, i) => {
851
- const p = s.payload;
852
- const parts = [`Session ${i + 1} (${p.created_at}):\n${p.content}`];
853
- if (p.tasks_completed?.length)
854
- parts.push(`Tasks: ${p.tasks_completed.join(", ")}`);
855
- if (p.decisions_made?.length)
856
- parts.push(`Decisions: ${p.decisions_made.join("; ")}`);
857
- return parts.join("\n");
858
- }).join("\n\n---\n\n");
859
- const systemPrompt = "You are a memory consolidation system. Given session summaries from an engineering agent, " +
860
- "extract recurring patterns, consolidated learnings, and key facts. Output:\n" +
861
- "1. Recurring patterns (things that keep coming up)\n" +
862
- "2. Key infrastructure/project facts learned\n" +
863
- "3. Decisions made and their rationale\n" +
864
- "4. Open issues or recurring problems\n" +
865
- "Be concise — one line per point. Omit ephemeral details.";
866
- let distilledContent;
867
- try {
868
- distilledContent = await chatComplete(systemPrompt, summaryTexts);
869
- }
870
- catch (e) {
871
- return { content: [{ type: "text", text: `Distillation failed: ${e instanceof Error ? e.message : String(e)}` }] };
872
- }
873
- const allEntities = [...new Set(summaries.flatMap((s) => s.payload.entities ?? []))];
874
- const vector = await embed(distilledContent);
875
- const factId = newId();
876
- const sourceIds = summaries.map((s) => s.id);
877
- await qdrantUpsert(factId, vector, {
878
- content: distilledContent,
879
- category: "observation",
880
- domain: "work",
881
- kind: "distilled",
882
- entities: allEntities,
883
- source: "system",
884
- confidence: 0.9,
885
- content_hash: contentHash("observation", distilledContent),
886
- reinforcement_count: 1,
887
- last_reinforced_at: now,
888
- superseded_by: null,
889
- superseded_at: null,
890
- created_at: now,
891
- updated_at: now,
892
- distilled_from: sourceIds,
893
- distilled_period_start: since,
894
- distilled_period_end: now,
895
- summary_count: summaries.length,
896
- });
897
- try {
898
- await qdrantSetPayload(sourceIds, {
899
- superseded_by: factId,
900
- superseded_at: now,
901
- });
902
- }
903
- catch (e) {
904
- log("WARN", `Failed to supersede some source summaries: ${e instanceof Error ? e.message : String(e)}`);
905
- }
906
- return {
907
- content: [{ type: "text", text: JSON.stringify({
908
- action: "distilled",
909
- fact_id: factId,
910
- summaries_consolidated: summaries.length,
911
- period: { start: since, end: now },
912
- entities: allEntities,
913
- preview: distilledContent.substring(0, 300) + (distilledContent.length > 300 ? "..." : ""),
914
- }, null, 2) }],
915
- };
916
- });
917
949
  // ── memory_heartbeat ────────────────────────────────────────────────────
918
950
  mcp.tool("memory_heartbeat", "Lightweight reflection check. Returns memory nudge (if no stores in 10+ min), staleness alerts (every 3rd call), and reflection prompt.", {}, async () => {
919
951
  heartbeatCount++;
@@ -924,17 +956,17 @@ export function registerTools(mcp) {
924
956
  if (heartbeatCount % 3 === 0 && ready) {
925
957
  try {
926
958
  const staleThreshold = new Date(Date.now() - STALENESS_DAYS * 86400000).toISOString();
927
- const staleResults = await qdrantScroll({ must: [
928
- { key: "category", match: { any: ["infrastructure", "projects", "decisions"] } },
929
- { is_null: { key: "superseded_by" } },
930
- ],
931
- should: [
932
- { key: "last_reinforced_at", range: { lte: staleThreshold } },
933
- { is_null: { key: "last_reinforced_at" } },
934
- ],
935
- must_not: [
936
- { key: "last_verified_at", range: { gte: staleThreshold } },
937
- ] }, 3);
959
+ const scope = resolveScope();
960
+ const staleFilter = scopedFilter(scope) ?? { must: [] };
961
+ staleFilter.must.push({ key: "category", match: { any: ["infrastructure", "projects", "decisions"] } });
962
+ staleFilter.should = [
963
+ { key: "last_reinforced_at", range: { lte: staleThreshold } },
964
+ { is_null: { key: "last_reinforced_at" } },
965
+ ];
966
+ staleFilter.must_not = [
967
+ { key: "last_verified_at", range: { gte: staleThreshold } },
968
+ ];
969
+ const staleResults = await qdrantScroll(staleFilter, 3);
938
970
  const staleFacts = staleResults.result?.points ?? [];
939
971
  if (staleFacts.length > 0) {
940
972
  const staleLines = staleFacts.map((f) => {
@@ -950,8 +982,12 @@ export function registerTools(mcp) {
950
982
  log("WARN", `Staleness check failed: ${e instanceof Error ? e.message : String(e)}`);
951
983
  }
952
984
  }
953
- sections.push("🔍 **Reflect:** What have you learned, decided, or discovered since the last heartbeat? " +
954
- "If anything is worth persisting for future sessions, call memory_store now.");
985
+ sections.push("🔍 Reflect: think about the LAST 10 minutes of work and answer in your head:\n" +
986
+ " 1. Did you touch a service, port, config, or file path you hadn't seen before?\n" +
987
+ " 2. Did you make a choice (library, pattern, approach) you'd want a future session to know about?\n" +
988
+ " 3. Did you hit an error and find a workaround?\n" +
989
+ " 4. Did the user state a preference or constraint?\n" +
990
+ "If any answer is yes, call memory_store now — one atomic fact per item, with category/domain/entities.");
955
991
  return { content: [{ type: "text", text: sections.join("\n\n") }] };
956
992
  });
957
993
  }