@kinqs/brainrouter-mcp-server 0.3.4

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 (337) hide show
  1. package/.env.example +144 -0
  2. package/README.md +56 -0
  3. package/agents/README.md +120 -0
  4. package/agents/code-reviewer.md +97 -0
  5. package/agents/security-auditor.md +101 -0
  6. package/agents/test-engineer.md +95 -0
  7. package/dist/__tests__/agent_mode.test.d.ts +1 -0
  8. package/dist/__tests__/api-routes.test.d.ts +1 -0
  9. package/dist/__tests__/api-routes.test.js +170 -0
  10. package/dist/__tests__/crypto.test.d.ts +1 -0
  11. package/dist/__tests__/crypto.test.js +28 -0
  12. package/dist/__tests__/host-integrations.test.d.ts +1 -0
  13. package/dist/__tests__/host-integrations.test.js +82 -0
  14. package/dist/__tests__/integration.test.d.ts +1 -0
  15. package/dist/__tests__/integration.test.js +50 -0
  16. package/dist/__tests__/loader.test.d.ts +1 -0
  17. package/dist/__tests__/loader.test.js +89 -0
  18. package/dist/__tests__/neural-spark.test.d.ts +1 -0
  19. package/dist/__tests__/neural-spark.test.js +112 -0
  20. package/dist/__tests__/pagination.test.d.ts +1 -0
  21. package/dist/__tests__/pagination.test.js +23 -0
  22. package/dist/__tests__/redaction.test.d.ts +1 -0
  23. package/dist/__tests__/redaction.test.js +17 -0
  24. package/dist/__tests__/registry.test.d.ts +1 -0
  25. package/dist/__tests__/registry.test.js +56 -0
  26. package/dist/__tests__/retry.test.d.ts +1 -0
  27. package/dist/__tests__/retry.test.js +30 -0
  28. package/dist/__tests__/skill-activation.test.d.ts +1 -0
  29. package/dist/__tests__/skill-activation.test.js +112 -0
  30. package/dist/__tests__/working-memory.test.d.ts +1 -0
  31. package/dist/__tests__/working-memory.test.js +200 -0
  32. package/dist/__tests__/workspace-paths.test.d.ts +1 -0
  33. package/dist/__tests__/workspace-paths.test.js +56 -0
  34. package/dist/__tests__/writer.test.d.ts +1 -0
  35. package/dist/__tests__/writer.test.js +94 -0
  36. package/dist/api/auth/crypto.d.ts +4 -0
  37. package/dist/api/auth/crypto.js +54 -0
  38. package/dist/api/middleware/auth.d.ts +12 -0
  39. package/dist/api/middleware/auth.js +90 -0
  40. package/dist/api/pagination.d.ts +18 -0
  41. package/dist/api/pagination.js +32 -0
  42. package/dist/api/routes/auth.d.ts +1 -0
  43. package/dist/api/routes/auth.js +130 -0
  44. package/dist/api/routes/chat-completions.d.ts +7 -0
  45. package/dist/api/routes/chat-completions.js +474 -0
  46. package/dist/api/routes/contradictions.d.ts +1 -0
  47. package/dist/api/routes/contradictions.js +28 -0
  48. package/dist/api/routes/evidence.d.ts +1 -0
  49. package/dist/api/routes/evidence.js +59 -0
  50. package/dist/api/routes/governance.d.ts +1 -0
  51. package/dist/api/routes/governance.js +95 -0
  52. package/dist/api/routes/graph.d.ts +1 -0
  53. package/dist/api/routes/graph.js +25 -0
  54. package/dist/api/routes/hooks.d.ts +1 -0
  55. package/dist/api/routes/hooks.js +88 -0
  56. package/dist/api/routes/memories.d.ts +1 -0
  57. package/dist/api/routes/memories.js +92 -0
  58. package/dist/api/routes/persona.d.ts +1 -0
  59. package/dist/api/routes/persona.js +9 -0
  60. package/dist/api/routes/scenes.d.ts +1 -0
  61. package/dist/api/routes/scenes.js +35 -0
  62. package/dist/api/routes/skills.d.ts +1 -0
  63. package/dist/api/routes/skills.js +14 -0
  64. package/dist/api/routes/stats.d.ts +1 -0
  65. package/dist/api/routes/stats.js +8 -0
  66. package/dist/api/routes/users.d.ts +1 -0
  67. package/dist/api/routes/users.js +82 -0
  68. package/dist/api/routes/working.d.ts +1 -0
  69. package/dist/api/routes/working.js +88 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +492 -0
  72. package/dist/integrations/claude-code.d.ts +12 -0
  73. package/dist/integrations/claude-code.js +35 -0
  74. package/dist/integrations/codex.d.ts +12 -0
  75. package/dist/integrations/codex.js +34 -0
  76. package/dist/integrations/generic-mcp.d.ts +52 -0
  77. package/dist/integrations/generic-mcp.js +118 -0
  78. package/dist/loader.d.ts +29 -0
  79. package/dist/loader.js +200 -0
  80. package/dist/memory/capture.d.ts +35 -0
  81. package/dist/memory/capture.js +230 -0
  82. package/dist/memory/config.d.ts +2 -0
  83. package/dist/memory/config.js +3 -0
  84. package/dist/memory/engine.d.ts +203 -0
  85. package/dist/memory/engine.js +626 -0
  86. package/dist/memory/llm-semaphore.d.ts +41 -0
  87. package/dist/memory/llm-semaphore.js +81 -0
  88. package/dist/memory/memory-type-config.d.ts +11 -0
  89. package/dist/memory/memory-type-config.js +65 -0
  90. package/dist/memory/pipeline/cognitive-contradiction.d.ts +7 -0
  91. package/dist/memory/pipeline/cognitive-contradiction.js +59 -0
  92. package/dist/memory/pipeline/cognitive-dedup.d.ts +23 -0
  93. package/dist/memory/pipeline/cognitive-dedup.js +38 -0
  94. package/dist/memory/pipeline/cognitive-extractor.d.ts +21 -0
  95. package/dist/memory/pipeline/cognitive-extractor.js +183 -0
  96. package/dist/memory/pipeline/contextual-focus-builder.d.ts +13 -0
  97. package/dist/memory/pipeline/contextual-focus-builder.js +135 -0
  98. package/dist/memory/pipeline/focus-direction-shift.d.ts +10 -0
  99. package/dist/memory/pipeline/focus-direction-shift.js +27 -0
  100. package/dist/memory/pipeline/graph-builder.d.ts +11 -0
  101. package/dist/memory/pipeline/graph-builder.js +88 -0
  102. package/dist/memory/pipeline/graph-recall.d.ts +13 -0
  103. package/dist/memory/pipeline/graph-recall.js +55 -0
  104. package/dist/memory/pipeline/identity-distiller.d.ts +15 -0
  105. package/dist/memory/pipeline/identity-distiller.js +40 -0
  106. package/dist/memory/pipeline/l1-contradiction.d.ts +7 -0
  107. package/dist/memory/pipeline/l1-contradiction.js +66 -0
  108. package/dist/memory/pipeline/l1-dedup.d.ts +23 -0
  109. package/dist/memory/pipeline/l1-dedup.js +39 -0
  110. package/dist/memory/pipeline/l1-extractor.d.ts +21 -0
  111. package/dist/memory/pipeline/l1-extractor.js +180 -0
  112. package/dist/memory/pipeline/l2-direction-shift.d.ts +10 -0
  113. package/dist/memory/pipeline/l2-direction-shift.js +27 -0
  114. package/dist/memory/pipeline/l2-scene.d.ts +15 -0
  115. package/dist/memory/pipeline/l2-scene.js +140 -0
  116. package/dist/memory/pipeline/l3-distiller.d.ts +15 -0
  117. package/dist/memory/pipeline/l3-distiller.js +40 -0
  118. package/dist/memory/pipeline/neural-spark.d.ts +27 -0
  119. package/dist/memory/pipeline/neural-spark.js +78 -0
  120. package/dist/memory/pipeline/skill-prewarm.d.ts +63 -0
  121. package/dist/memory/pipeline/skill-prewarm.js +127 -0
  122. package/dist/memory/pipeline/task-queue.d.ts +54 -0
  123. package/dist/memory/pipeline/task-queue.js +117 -0
  124. package/dist/memory/prompts/cognitive-contradiction.d.ts +1 -0
  125. package/dist/memory/prompts/cognitive-contradiction.js +25 -0
  126. package/dist/memory/prompts/cognitive-extraction.d.ts +10 -0
  127. package/dist/memory/prompts/cognitive-extraction.js +114 -0
  128. package/dist/memory/prompts/core-identity.d.ts +6 -0
  129. package/dist/memory/prompts/core-identity.js +60 -0
  130. package/dist/memory/prompts/focus-direction-shift.d.ts +5 -0
  131. package/dist/memory/prompts/focus-direction-shift.js +32 -0
  132. package/dist/memory/prompts/focus-scene-cluster.d.ts +2 -0
  133. package/dist/memory/prompts/focus-scene-cluster.js +33 -0
  134. package/dist/memory/prompts/focus-scene.d.ts +7 -0
  135. package/dist/memory/prompts/focus-scene.js +40 -0
  136. package/dist/memory/prompts/graph-extraction-batch.d.ts +14 -0
  137. package/dist/memory/prompts/graph-extraction-batch.js +54 -0
  138. package/dist/memory/prompts/graph-extraction.d.ts +2 -0
  139. package/dist/memory/prompts/graph-extraction.js +53 -0
  140. package/dist/memory/prompts/l1-contradiction-batch.d.ts +16 -0
  141. package/dist/memory/prompts/l1-contradiction-batch.js +47 -0
  142. package/dist/memory/prompts/l1-contradiction.d.ts +1 -0
  143. package/dist/memory/prompts/l1-contradiction.js +25 -0
  144. package/dist/memory/prompts/l1-extraction.d.ts +10 -0
  145. package/dist/memory/prompts/l1-extraction.js +114 -0
  146. package/dist/memory/prompts/l2-direction-shift.d.ts +5 -0
  147. package/dist/memory/prompts/l2-direction-shift.js +32 -0
  148. package/dist/memory/prompts/l2-scene-cluster.d.ts +2 -0
  149. package/dist/memory/prompts/l2-scene-cluster.js +33 -0
  150. package/dist/memory/prompts/l2-scene.d.ts +7 -0
  151. package/dist/memory/prompts/l2-scene.js +40 -0
  152. package/dist/memory/prompts/l3-persona.d.ts +6 -0
  153. package/dist/memory/prompts/l3-persona.js +60 -0
  154. package/dist/memory/recall.d.ts +47 -0
  155. package/dist/memory/recall.js +427 -0
  156. package/dist/memory/redaction.d.ts +1 -0
  157. package/dist/memory/redaction.js +24 -0
  158. package/dist/memory/retry.d.ts +13 -0
  159. package/dist/memory/retry.js +53 -0
  160. package/dist/memory/scheduler.d.ts +9 -0
  161. package/dist/memory/scheduler.js +16 -0
  162. package/dist/memory/skill-hints-loader.d.ts +30 -0
  163. package/dist/memory/skill-hints-loader.js +100 -0
  164. package/dist/memory/store/embedding.d.ts +16 -0
  165. package/dist/memory/store/embedding.js +68 -0
  166. package/dist/memory/store/reranker.d.ts +24 -0
  167. package/dist/memory/store/reranker.js +83 -0
  168. package/dist/memory/store/sqlite.d.ts +167 -0
  169. package/dist/memory/store/sqlite.js +1816 -0
  170. package/dist/memory/store/types.d.ts +101 -0
  171. package/dist/memory/store/types.js +1 -0
  172. package/dist/memory/types.d.ts +207 -0
  173. package/dist/memory/types.js +7 -0
  174. package/dist/memory/validation.d.ts +441 -0
  175. package/dist/memory/validation.js +129 -0
  176. package/dist/memory/working/canvas.d.ts +5 -0
  177. package/dist/memory/working/canvas.js +43 -0
  178. package/dist/memory/working/offload.d.ts +71 -0
  179. package/dist/memory/working/offload.js +211 -0
  180. package/dist/memory/working/step-log.d.ts +16 -0
  181. package/dist/memory/working/step-log.js +35 -0
  182. package/dist/registry.d.ts +34 -0
  183. package/dist/registry.js +305 -0
  184. package/dist/resolver.d.ts +17 -0
  185. package/dist/resolver.js +126 -0
  186. package/dist/scripts/validate-foreign-workspace-path.d.ts +1 -0
  187. package/dist/scripts/validate-foreign-workspace-path.js +39 -0
  188. package/dist/tools/agent_memory_tools.d.ts +485 -0
  189. package/dist/tools/agent_memory_tools.js +793 -0
  190. package/dist/tools/create_skill.d.ts +46 -0
  191. package/dist/tools/create_skill.js +46 -0
  192. package/dist/tools/get_doc.d.ts +21 -0
  193. package/dist/tools/get_doc.js +24 -0
  194. package/dist/tools/get_persona.d.ts +15 -0
  195. package/dist/tools/get_persona.js +20 -0
  196. package/dist/tools/get_reference.d.ts +15 -0
  197. package/dist/tools/get_reference.js +20 -0
  198. package/dist/tools/get_skill.d.ts +34 -0
  199. package/dist/tools/get_skill.js +65 -0
  200. package/dist/tools/get_template_doc.d.ts +21 -0
  201. package/dist/tools/get_template_doc.js +24 -0
  202. package/dist/tools/list_docs.d.ts +15 -0
  203. package/dist/tools/list_docs.js +16 -0
  204. package/dist/tools/list_skills.d.ts +18 -0
  205. package/dist/tools/list_skills.js +17 -0
  206. package/dist/tools/list_template_docs.d.ts +15 -0
  207. package/dist/tools/list_template_docs.js +16 -0
  208. package/dist/tools/memory-engineering.d.ts +225 -0
  209. package/dist/tools/memory-engineering.js +284 -0
  210. package/dist/tools/memory-explain.d.ts +34 -0
  211. package/dist/tools/memory-explain.js +109 -0
  212. package/dist/tools/memory-governance.d.ts +171 -0
  213. package/dist/tools/memory-governance.js +224 -0
  214. package/dist/tools/memory-hooks.d.ts +67 -0
  215. package/dist/tools/memory-hooks.js +102 -0
  216. package/dist/tools/memory-working.d.ts +98 -0
  217. package/dist/tools/memory-working.js +101 -0
  218. package/dist/tools/memory_capture_turn.d.ts +66 -0
  219. package/dist/tools/memory_capture_turn.js +85 -0
  220. package/dist/tools/memory_consolidate.d.ts +55 -0
  221. package/dist/tools/memory_consolidate.js +176 -0
  222. package/dist/tools/memory_contradictions.d.ts +53 -0
  223. package/dist/tools/memory_contradictions.js +52 -0
  224. package/dist/tools/memory_graph_query.d.ts +51 -0
  225. package/dist/tools/memory_graph_query.js +35 -0
  226. package/dist/tools/memory_mark_cited.d.ts +43 -0
  227. package/dist/tools/memory_mark_cited.js +63 -0
  228. package/dist/tools/memory_recall.d.ts +77 -0
  229. package/dist/tools/memory_recall.js +81 -0
  230. package/dist/tools/memory_register_skill_hints.d.ts +49 -0
  231. package/dist/tools/memory_register_skill_hints.js +55 -0
  232. package/dist/tools/memory_resolve_session.d.ts +24 -0
  233. package/dist/tools/memory_resolve_session.js +133 -0
  234. package/dist/tools/memory_search.d.ts +146 -0
  235. package/dist/tools/memory_search.js +84 -0
  236. package/dist/tools/search_skills.d.ts +18 -0
  237. package/dist/tools/search_skills.js +17 -0
  238. package/dist/tools/update_doc.d.ts +24 -0
  239. package/dist/tools/update_doc.js +35 -0
  240. package/dist/tools/update_skill.d.ts +30 -0
  241. package/dist/tools/update_skill.js +80 -0
  242. package/dist/types.d.ts +81 -0
  243. package/dist/types.js +4 -0
  244. package/dist/writer.d.ts +30 -0
  245. package/dist/writer.js +220 -0
  246. package/docs/TEMPLATE ONLY +1 -0
  247. package/docs/api/API.md +64 -0
  248. package/docs/api/security/SECURITY.md +58 -0
  249. package/docs/deployment/DockerDeployment.md +30 -0
  250. package/docs/design/Design.md +59 -0
  251. package/docs/design/themes/apple.md +101 -0
  252. package/docs/design/themes/dieter-grid.md +100 -0
  253. package/docs/design/themes/gallery-white.md +100 -0
  254. package/docs/design/themes/pinterest.md +101 -0
  255. package/docs/design/themes/realty-open-house.md +101 -0
  256. package/docs/design/themes/vodafone.md +101 -0
  257. package/docs/hooks/Hooks.md +30 -0
  258. package/docs/schema/Schema.md +35 -0
  259. package/docs/strategy/ScalingStrategy.md +19 -0
  260. package/package.json +88 -0
  261. package/references/accessibility-checklist.md +160 -0
  262. package/references/orchestration-patterns.md +370 -0
  263. package/references/performance-checklist.md +153 -0
  264. package/references/security-checklist.md +134 -0
  265. package/references/testing-patterns.md +236 -0
  266. package/skills/agent/adr-skill/SKILL.md +299 -0
  267. package/skills/agent/agentic-engineering-workflow/SKILL.md +95 -0
  268. package/skills/agent/bootstrap-skill/SKILL.md +103 -0
  269. package/skills/agent/context-engineering/SKILL.md +307 -0
  270. package/skills/agent/debugging-and-error-recovery/SKILL.md +308 -0
  271. package/skills/agent/developer-growth-analysis/SKILL.md +328 -0
  272. package/skills/agent/doubt-driven-skill/SKILL.md +249 -0
  273. package/skills/agent/handover-skill/SKILL.md +112 -0
  274. package/skills/agent/idea-refine-skill/SKILL.md +185 -0
  275. package/skills/agent/idea-refine-skill/examples.md +238 -0
  276. package/skills/agent/idea-refine-skill/frameworks.md +99 -0
  277. package/skills/agent/idea-refine-skill/refinement-criteria.md +113 -0
  278. package/skills/agent/interview-skill/SKILL.md +226 -0
  279. package/skills/agent/planning-skill/SKILL.md +270 -0
  280. package/skills/agent/skill-authoring/SKILL.md +189 -0
  281. package/skills/agent/source-driven-skill/SKILL.md +197 -0
  282. package/skills/agent/spec-driven-skill/SKILL.md +221 -0
  283. package/skills/agent/sync-skill/SKILL.md +92 -0
  284. package/skills/agent/using-agent-skills/SKILL.md +189 -0
  285. package/skills/api/a11y-skill/SKILL.md +88 -0
  286. package/skills/api/api-skill/SKILL.md +123 -0
  287. package/skills/api/auth-skill/SKILL.md +80 -0
  288. package/skills/api/debug-skill/SKILL.md +535 -0
  289. package/skills/api/performance-skill/SKILL.md +100 -0
  290. package/skills/api/testing-skill/SKILL.md +100 -0
  291. package/skills/codebase/code-review-and-quality/SKILL.md +228 -0
  292. package/skills/codebase/code-simplification/SKILL.md +352 -0
  293. package/skills/codebase/code-structure-cleanup/SKILL.md +142 -0
  294. package/skills/codebase/concerns-skill/SKILL.md +89 -0
  295. package/skills/codebase/conventions-skill/SKILL.md +95 -0
  296. package/skills/codebase/doc-management-skill/SKILL.md +47 -0
  297. package/skills/codebase/git-workflow-skill/SKILL.md +312 -0
  298. package/skills/communication/1-3-1-rule/SKILL.md +120 -0
  299. package/skills/design/brutalist-skill/SKILL.md +131 -0
  300. package/skills/design/concept-diagrams/SKILL.md +387 -0
  301. package/skills/design/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
  302. package/skills/design/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
  303. package/skills/design/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
  304. package/skills/design/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
  305. package/skills/design/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
  306. package/skills/design/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
  307. package/skills/design/concept-diagrams/examples/electricity-grid-flow.md +182 -0
  308. package/skills/design/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
  309. package/skills/design/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
  310. package/skills/design/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
  311. package/skills/design/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
  312. package/skills/design/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
  313. package/skills/design/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
  314. package/skills/design/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
  315. package/skills/design/concept-diagrams/examples/wind-turbine-structure.md +338 -0
  316. package/skills/design/concept-diagrams/references/dashboard-patterns.md +43 -0
  317. package/skills/design/concept-diagrams/references/infrastructure-patterns.md +144 -0
  318. package/skills/design/concept-diagrams/references/physical-shape-cookbook.md +42 -0
  319. package/skills/design/concept-diagrams/templates/template.html +174 -0
  320. package/skills/design/gpt-tasteskill/SKILL.md +114 -0
  321. package/skills/design/minimalist-skill/SKILL.md +116 -0
  322. package/skills/design/output-skill/SKILL.md +87 -0
  323. package/skills/design/redesign-skill/SKILL.md +213 -0
  324. package/skills/design/soft-skill/SKILL.md +132 -0
  325. package/skills/design/stitch-skill/EXAMPLE.md +121 -0
  326. package/skills/design/stitch-skill/SKILL.md +222 -0
  327. package/skills/design/taste-skill/SKILL.md +269 -0
  328. package/skills/devops/ci-cd-skill/SKILL.md +402 -0
  329. package/skills/devops/docker-skill/SKILL.md +297 -0
  330. package/skills/devops/domain-skill/SKILL.md +234 -0
  331. package/skills/lifecycle/changelog-generator/SKILL.md +135 -0
  332. package/skills/lifecycle/incremental-skill/SKILL.md +257 -0
  333. package/skills/lifecycle/migration-skill/SKILL.md +218 -0
  334. package/skills/lifecycle/shipping-skill/SKILL.md +321 -0
  335. package/skills/memory/agent-memory/SKILL.md +122 -0
  336. package/skills/qa/browser-testing-skill/SKILL.md +314 -0
  337. package/skills/ux/adversarial-ux-skill/SKILL.md +168 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Global semaphore that caps simultaneous LLM calls leaving this process.
3
+ *
4
+ * Why this exists: a single user turn can trigger an avalanche of LLM calls
5
+ * inside the MCP child — cognitive extraction, contradiction detection (one
6
+ * per existing record neighbour), graph extraction, focus-shift detection,
7
+ * plus the 5-min sweeper backfilling old sensory rows. Add the CLI's chat
8
+ * call hitting the SAME LM Studio endpoint and you can easily fire 10+
9
+ * concurrent requests at one local GPU. On consumer hardware that triggers
10
+ * either (a) LM Studio's auto-unload to free VRAM, (b) OOM, or (c) request
11
+ * queue overflow — all of which surface to BrainRouter as "Model is
12
+ * unloaded" or 500 errors.
13
+ *
14
+ * The fix is to serialize. This module exposes a simple promise-queue
15
+ * semaphore with a configurable cap. Default is 2: one slot for the
16
+ * user-facing extraction (foreground), one for opportunistic background
17
+ * work (graph / contradiction / sweeper). Cloud deployments with a real
18
+ * API backend (OpenAI, OpenRouter) can crank this up via the env var.
19
+ *
20
+ * Env knob:
21
+ * BRAINROUTER_LLM_MAX_CONCURRENT (default 2; values < 1 disable the cap)
22
+ */
23
+ const DEFAULT_CAP = 2;
24
+ function resolveCap() {
25
+ const raw = process.env.BRAINROUTER_LLM_MAX_CONCURRENT;
26
+ if (!raw)
27
+ return DEFAULT_CAP;
28
+ const parsed = parseInt(raw, 10);
29
+ if (!Number.isFinite(parsed) || parsed < 1)
30
+ return Number.POSITIVE_INFINITY;
31
+ return parsed;
32
+ }
33
+ let cap = resolveCap();
34
+ let inFlight = 0;
35
+ const waiters = [];
36
+ /**
37
+ * Acquire one slot. Returns a release function the caller must invoke when
38
+ * the LLM call finishes (success OR failure). Use it like:
39
+ *
40
+ * const release = await acquireLLMSlot();
41
+ * try { ...llm call... } finally { release(); }
42
+ */
43
+ export async function acquireLLMSlot() {
44
+ if (!Number.isFinite(cap)) {
45
+ // Cap disabled — passthrough.
46
+ return () => { };
47
+ }
48
+ if (inFlight < cap) {
49
+ inFlight++;
50
+ return makeRelease();
51
+ }
52
+ // Otherwise wait in line.
53
+ await new Promise((resolve) => waiters.push(resolve));
54
+ inFlight++;
55
+ return makeRelease();
56
+ }
57
+ function makeRelease() {
58
+ let released = false;
59
+ return () => {
60
+ if (released)
61
+ return;
62
+ released = true;
63
+ inFlight = Math.max(0, inFlight - 1);
64
+ const next = waiters.shift();
65
+ if (next)
66
+ next();
67
+ };
68
+ }
69
+ /** Exposed for tests / diagnostics. */
70
+ export function getSemaphoreState() {
71
+ return { cap, inFlight, queued: waiters.length };
72
+ }
73
+ /**
74
+ * Allow tests (or a future /config tool) to reset the cap and clear waiters
75
+ * without restarting the process.
76
+ */
77
+ export function resetSemaphoreForTests() {
78
+ cap = resolveCap();
79
+ inFlight = 0;
80
+ waiters.length = 0;
81
+ }
@@ -0,0 +1,11 @@
1
+ import type { MemoryTaskIntent, MemoryType } from "@kinqs/brainrouter-types";
2
+ export interface TypeConfig {
3
+ halfLifeDays: number | null;
4
+ defaultConfidence: number;
5
+ requiresEvidence: boolean;
6
+ intentAffinity: Partial<Record<MemoryTaskIntent, number>>;
7
+ }
8
+ export declare const TYPE_CONFIGS: Record<MemoryType, TypeConfig>;
9
+ export declare function getMemoryTypeConfig(type: string): TypeConfig;
10
+ export declare function detectTaskIntent(query: string): MemoryTaskIntent;
11
+ export declare function extractFilePathHints(text: string): string[];
@@ -0,0 +1,65 @@
1
+ const DEFAULT_TYPE_CONFIG = {
2
+ halfLifeDays: 30,
3
+ defaultConfidence: 0.65,
4
+ requiresEvidence: false,
5
+ intentAffinity: {},
6
+ };
7
+ export const TYPE_CONFIGS = {
8
+ persona: { halfLifeDays: 180, defaultConfidence: 0.7, requiresEvidence: false, intentAffinity: {} },
9
+ episodic: { halfLifeDays: 30, defaultConfidence: 0.65, requiresEvidence: false, intentAffinity: {} },
10
+ instruction: { halfLifeDays: null, defaultConfidence: 0.85, requiresEvidence: false, intentAffinity: {} },
11
+ skill_context: { halfLifeDays: 7, defaultConfidence: 0.65, requiresEvidence: false, intentAffinity: {} },
12
+ tool_preference: { halfLifeDays: 90, defaultConfidence: 0.7, requiresEvidence: false, intentAffinity: { build: 1.05 } },
13
+ codebase_fact: { halfLifeDays: 60, defaultConfidence: 0.65, requiresEvidence: false, intentAffinity: { build: 1.15, refactor: 1.1 } },
14
+ api_contract: { halfLifeDays: 90, defaultConfidence: 0.75, requiresEvidence: true, intentAffinity: { review: 1.2, security: 1.15, build: 1.1 } },
15
+ data_model: { halfLifeDays: 90, defaultConfidence: 0.75, requiresEvidence: true, intentAffinity: { review: 1.2, security: 1.15, build: 1.1 } },
16
+ dependency_constraint: { halfLifeDays: 120, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { build: 1.1, refactor: 1.1 } },
17
+ environment_constraint: { halfLifeDays: 120, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { build: 1.1, debug: 1.1 } },
18
+ architecture_decision: { halfLifeDays: 180, defaultConfidence: 0.8, requiresEvidence: false, intentAffinity: { build: 1.2, plan: 1.15, refactor: 1.1 } },
19
+ implementation_decision: { halfLifeDays: 90, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { build: 1.2, refactor: 1.1 } },
20
+ design_constraint: { halfLifeDays: 120, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { build: 1.1, review: 1.1 } },
21
+ security_policy: { halfLifeDays: 180, defaultConfidence: 0.8, requiresEvidence: true, intentAffinity: { security: 1.3, review: 1.2 } },
22
+ performance_baseline: { halfLifeDays: 45, defaultConfidence: 0.75, requiresEvidence: true, intentAffinity: { performance: 1.3, review: 1.1 } },
23
+ bug_finding: { halfLifeDays: 45, defaultConfidence: 0.7, requiresEvidence: false, intentAffinity: { debug: 1.3, review: 1.1 } },
24
+ debug_trace: { halfLifeDays: 30, defaultConfidence: 0.7, requiresEvidence: false, intentAffinity: { debug: 1.3 } },
25
+ fix_summary: { halfLifeDays: 60, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { debug: 1.15, build: 1.1 } },
26
+ verification_result: { halfLifeDays: 45, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { test: 1.25, performance: 1.15, debug: 1.1 } },
27
+ failed_attempt: { halfLifeDays: 45, defaultConfidence: 0.7, requiresEvidence: false, intentAffinity: { debug: 1.25 } },
28
+ regression_risk: { halfLifeDays: 90, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { review: 1.25, test: 1.15 } },
29
+ task_state: { halfLifeDays: 14, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { plan: 1.3, build: 1.1 } },
30
+ handover_note: { halfLifeDays: 21, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { plan: 1.25, build: 1.1 } },
31
+ blocked_reason: { halfLifeDays: 21, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { plan: 1.2, debug: 1.1 } },
32
+ review_comment: { halfLifeDays: 45, defaultConfidence: 0.7, requiresEvidence: false, intentAffinity: { review: 1.3 } },
33
+ release_note: { halfLifeDays: 90, defaultConfidence: 0.7, requiresEvidence: false, intentAffinity: { release: 1.3 } },
34
+ source_evidence: { halfLifeDays: 120, defaultConfidence: 0.8, requiresEvidence: false, intentAffinity: { review: 1.1, security: 1.1 } },
35
+ artifact_reference: { halfLifeDays: 90, defaultConfidence: 0.7, requiresEvidence: false, intentAffinity: { plan: 1.1, build: 1.1 } },
36
+ file_history: { halfLifeDays: 90, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { debug: 1.15, refactor: 1.2, review: 1.1 } },
37
+ command_knowledge: { halfLifeDays: 90, defaultConfidence: 0.75, requiresEvidence: false, intentAffinity: { debug: 1.2, performance: 1.15, test: 1.1 } },
38
+ };
39
+ export function getMemoryTypeConfig(type) {
40
+ return TYPE_CONFIGS[type] ?? DEFAULT_TYPE_CONFIG;
41
+ }
42
+ export function detectTaskIntent(query) {
43
+ const q = query.toLowerCase();
44
+ if (/\b(debug|bug|error|fail|failing|failure|repro|crash|stack|traceback|exception|panic|segfault|undefined|null)\b/.test(q))
45
+ return "debug";
46
+ if (/\b(review|audit|risk|regression|pr|pull request|code review|lgtm|approve)\b/.test(q))
47
+ return "review";
48
+ if (/\b(test|spec|coverage|vitest|jest|benchmark result|assertion|expect|describe|it\()\b/.test(q))
49
+ return "test";
50
+ if (/\b(plan|task|roadmap|todo|handover|blocked|next|sprint|milestone|backlog)\b/.test(q))
51
+ return "plan";
52
+ if (/\b(refactor|cleanup|simplify|migration|deprecate|rename|extract|move|reorganize|restructure)\b/.test(q))
53
+ return "refactor";
54
+ if (/\b(security|auth|permission|secret|token|vulnerability|xss|injection|csrf|rbac|oauth|jwt|sanitize|escape)\b/.test(q))
55
+ return "security";
56
+ if (/\b(performance|latency|throughput|slow|benchmark|r@5|token savings|optimize|cache|memory leak|profil|bottleneck)\b/.test(q))
57
+ return "performance";
58
+ if (/\b(release|ship|launch|deploy|changelog|version|tag|publish|hotfix)\b/.test(q))
59
+ return "release";
60
+ return "build";
61
+ }
62
+ export function extractFilePathHints(text) {
63
+ const matches = text.match(/(?:[\w.-]+\/)+[\w.-]+\.\w+|[\w.-]+\.(?:ts|tsx|js|jsx|json|md|sql|py|go|rs)/g) ?? [];
64
+ return [...new Set(matches.map((m) => m.replace(/^["'`]|["'`]$/g, "")))];
65
+ }
@@ -0,0 +1,7 @@
1
+ import type { IMemoryStore } from "@kinqs/brainrouter-types";
2
+ import type { LLMRunner, CognitiveRecord } from "@kinqs/brainrouter-types";
3
+ export declare function detectContradictions(params: {
4
+ newRecord: CognitiveRecord;
5
+ store: IMemoryStore;
6
+ llmRunner: LLMRunner;
7
+ }): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { COGNITIVE_CONTRADICTION_PROMPT } from "../prompts/cognitive-contradiction.js";
2
+ import crypto from "node:crypto";
3
+ export async function detectContradictions(params) {
4
+ const { newRecord, store, llmRunner } = params;
5
+ // 1. Search for potentially related memories
6
+ const candidates = store.searchCognitiveFts(newRecord.userId, newRecord.content, 5);
7
+ const evaluations = [];
8
+ const _parsedContradictionTimeout = parseInt(process.env.BRAINROUTER_CONTRADICTION_TIMEOUT_MS || "", 10);
9
+ const contradictionTimeoutMs = isNaN(_parsedContradictionTimeout) ? 60000 : _parsedContradictionTimeout;
10
+ for (const candidate of candidates) {
11
+ // Don't compare with self
12
+ if (candidate.record_id === newRecord.id)
13
+ continue;
14
+ const prompt = COGNITIVE_CONTRADICTION_PROMPT
15
+ .replace("{{newContent}}", newRecord.content)
16
+ .replace("{{existingContent}}", candidate.content);
17
+ try {
18
+ const response = await llmRunner.run({
19
+ prompt,
20
+ taskId: `contradiction-check-${newRecord.id}-${candidate.record_id}`,
21
+ timeoutMs: contradictionTimeoutMs
22
+ });
23
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
24
+ if (!jsonMatch)
25
+ continue;
26
+ const data = JSON.parse(jsonMatch[0]);
27
+ if (data.isContradiction && data.confidence > 0.7) {
28
+ evaluations.push({
29
+ candidate,
30
+ isContradiction: true,
31
+ confidence: data.confidence,
32
+ kind: data.kind || "genuine_conflict",
33
+ reason: data.reason
34
+ });
35
+ }
36
+ }
37
+ catch (e) {
38
+ console.error(`[BrainRouter] Contradiction check failed for ${newRecord.id} vs ${candidate.record_id}:`, e.message);
39
+ }
40
+ }
41
+ const hasTemporalUpdate = evaluations.some(ev => ev.kind === "temporal_update");
42
+ for (const ev of evaluations) {
43
+ if (hasTemporalUpdate) {
44
+ console.error(`[BrainRouter] TEMPORAL UPDATE DETECTED (transition): Superseding memory ${ev.candidate.record_id} with new memory ${newRecord.id}`);
45
+ store.invalidateCognitiveRecord(newRecord.userId, ev.candidate.record_id, newRecord.id);
46
+ }
47
+ else {
48
+ console.error(`[BrainRouter] CONTRADICTION DETECTED: ${newRecord.id} vs ${ev.candidate.record_id}`);
49
+ store.upsertContradiction({
50
+ id: `conflict_${crypto.randomBytes(4).toString("hex")}`,
51
+ userId: newRecord.userId,
52
+ recordIdA: ev.candidate.record_id,
53
+ recordIdB: newRecord.id,
54
+ reason: ev.reason,
55
+ confidence: ev.confidence
56
+ });
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,23 @@
1
+ import type { CognitiveRecord } from "@kinqs/brainrouter-types";
2
+ import type { IMemoryStore } from "@kinqs/brainrouter-types";
3
+ /**
4
+ * Result of the deduplication process
5
+ */
6
+ export interface DedupResult {
7
+ /** Memories that are unique and should be stored */
8
+ uniqueRecords: CognitiveRecord[];
9
+ /** Memories that were identified as exact duplicates and dropped */
10
+ droppedCount: number;
11
+ }
12
+ /**
13
+ * Proactively deduplicate extracted memories against the existing memory store
14
+ * before they are stored.
15
+ *
16
+ * Uses exact/near-exact string matching to prevent identical noisy facts
17
+ * from accumulating in the store.
18
+ */
19
+ export declare function deduplicateMemories(params: {
20
+ records: CognitiveRecord[];
21
+ store: IMemoryStore;
22
+ userId: string;
23
+ }): Promise<DedupResult>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Proactively deduplicate extracted memories against the existing memory store
3
+ * before they are stored.
4
+ *
5
+ * Uses exact/near-exact string matching to prevent identical noisy facts
6
+ * from accumulating in the store.
7
+ */
8
+ export async function deduplicateMemories(params) {
9
+ const { records, store, userId } = params;
10
+ if (records.length === 0) {
11
+ return { uniqueRecords: [], droppedCount: 0 };
12
+ }
13
+ const uniqueRecords = [];
14
+ let droppedCount = 0;
15
+ for (const newRecord of records) {
16
+ // 1. Keyword search to find potentially identical memories
17
+ // We only need top 3 to see if there is an exact match
18
+ const candidates = store.searchCognitiveFts(userId, newRecord.content, 3);
19
+ let isDuplicate = false;
20
+ for (const candidate of candidates) {
21
+ if (candidate.content.trim().toLowerCase() === newRecord.content.trim().toLowerCase()) {
22
+ isDuplicate = true;
23
+ break;
24
+ }
25
+ }
26
+ if (isDuplicate) {
27
+ console.log(`[BrainRouter] Dropped exact duplicate memory: "${newRecord.content}"`);
28
+ droppedCount++;
29
+ }
30
+ else {
31
+ uniqueRecords.push(newRecord);
32
+ }
33
+ }
34
+ return {
35
+ uniqueRecords,
36
+ droppedCount
37
+ };
38
+ }
@@ -0,0 +1,21 @@
1
+ import type { SensoryRecord, CognitiveRecord, LLMRunner } from "@kinqs/brainrouter-types";
2
+ export interface CognitiveExtractionResult {
3
+ success: boolean;
4
+ extractedCount: number;
5
+ records: CognitiveRecord[];
6
+ sceneNames: string[];
7
+ errorMessage?: string;
8
+ }
9
+ export declare function extractCognitiveMemories(params: {
10
+ messages: SensoryRecord[];
11
+ userId: string;
12
+ sessionKey: string;
13
+ sessionId: string;
14
+ llmRunner: LLMRunner;
15
+ maxMessagesPerExtraction?: number;
16
+ maxBackgroundMessages?: number;
17
+ previousSceneName?: string;
18
+ existingSceneNames?: string[];
19
+ activeSkill?: string;
20
+ skillHints?: string;
21
+ }): Promise<CognitiveExtractionResult>;
@@ -0,0 +1,183 @@
1
+ import { EXTRACT_MEMORIES_SYSTEM_PROMPT, formatExtractionPrompt } from "../prompts/cognitive-extraction.js";
2
+ import { getMemoryTypeConfig } from "../memory-type-config.js";
3
+ import crypto from "node:crypto";
4
+ const ALLOWED_MEMORY_TYPES = new Set([
5
+ "persona", "episodic", "instruction", "skill_context", "tool_preference",
6
+ "codebase_fact", "api_contract", "data_model", "dependency_constraint",
7
+ "environment_constraint", "architecture_decision", "implementation_decision",
8
+ "design_constraint", "security_policy", "performance_baseline", "bug_finding",
9
+ "debug_trace", "fix_summary", "verification_result", "failed_attempt",
10
+ "regression_risk", "task_state", "handover_note", "blocked_reason",
11
+ "review_comment", "release_note", "source_evidence", "artifact_reference",
12
+ "file_history", "command_knowledge",
13
+ ]);
14
+ const ALLOWED_SOURCE_KINDS = new Set([
15
+ "", "user_instruction", "source_file", "command_output", "test_result",
16
+ "model_inference", "prior_memory",
17
+ ]);
18
+ const ALLOWED_VERIFICATION_STATUSES = new Set([
19
+ "", "verified", "unverified", "stale",
20
+ ]);
21
+ function shouldExtractCognitive(text) {
22
+ if (!text)
23
+ return false;
24
+ const clean = text.trim();
25
+ if (clean.length < 3)
26
+ return false;
27
+ if (/^[^a-zA-Z\u4e00-\u9fa5]+$/.test(clean))
28
+ return false;
29
+ return true;
30
+ }
31
+ export async function extractCognitiveMemories(params) {
32
+ const { messages, userId, sessionKey, sessionId, llmRunner, maxMessagesPerExtraction = 10, maxBackgroundMessages = 5, previousSceneName, existingSceneNames, activeSkill, skillHints } = params;
33
+ if (messages.length === 0) {
34
+ return { success: true, extractedCount: 0, records: [], sceneNames: [] };
35
+ }
36
+ const qualifiedMessages = messages.filter((m) => shouldExtractCognitive(m.messageText));
37
+ if (qualifiedMessages.length === 0) {
38
+ return { success: true, extractedCount: 0, records: [], sceneNames: [] };
39
+ }
40
+ const newMessages = qualifiedMessages.slice(-maxMessagesPerExtraction);
41
+ const bgEndIdx = qualifiedMessages.length - newMessages.length;
42
+ const backgroundMessages = bgEndIdx > 0
43
+ ? qualifiedMessages.slice(Math.max(0, bgEndIdx - maxBackgroundMessages), bgEndIdx)
44
+ : [];
45
+ const userPrompt = formatExtractionPrompt({
46
+ newMessages,
47
+ backgroundMessages,
48
+ previousSceneName,
49
+ existingSceneNames,
50
+ activeSkill,
51
+ skillHints
52
+ });
53
+ let rawResult;
54
+ try {
55
+ rawResult = await llmRunner.run({
56
+ prompt: userPrompt,
57
+ systemPrompt: EXTRACT_MEMORIES_SYSTEM_PROMPT,
58
+ taskId: "cognitive-extraction",
59
+ timeoutMs: 120_000
60
+ });
61
+ }
62
+ catch (err) {
63
+ const errorMessage = err instanceof Error ? err.message : String(err);
64
+ const code = err?.code;
65
+ if (code === "LLM_NOT_CONFIGURED") {
66
+ // Expected, non-fatal: no LLM is configured server-side. Skip cognitive
67
+ // extraction silently; sensory records are still persisted by the caller.
68
+ return { success: false, extractedCount: 0, records: [], sceneNames: [], errorMessage: "LLM not configured; cognitive extraction skipped." };
69
+ }
70
+ console.error("[BrainRouter] LLM extraction failed:", err);
71
+ return { success: false, extractedCount: 0, records: [], sceneNames: [], errorMessage };
72
+ }
73
+ const parsedScenes = parseExtractionResult(rawResult);
74
+ const records = [];
75
+ const sceneNames = [];
76
+ const nowStr = new Date().toISOString();
77
+ for (const scene of parsedScenes) {
78
+ sceneNames.push(scene.scene_name);
79
+ for (const mem of scene.memories) {
80
+ const config = getMemoryTypeConfig(mem.type);
81
+ records.push({
82
+ id: `cognitive_${sessionKey}_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`,
83
+ userId,
84
+ sessionKey,
85
+ sessionId,
86
+ content: mem.content,
87
+ type: mem.type,
88
+ priority: mem.priority,
89
+ sceneName: scene.scene_name,
90
+ skillTag: mem.skill_tag || activeSkill || "",
91
+ halfLifeDays: config.halfLifeDays,
92
+ supersededBy: null,
93
+ timestampStr: "",
94
+ timestampStart: "",
95
+ timestampEnd: "",
96
+ createdTime: nowStr,
97
+ updatedTime: nowStr,
98
+ metadata: mem.metadata,
99
+ confidence: mem.confidence ?? config.defaultConfidence,
100
+ status: "active",
101
+ sourceKind: mem.sourceKind,
102
+ verificationStatus: mem.verificationStatus,
103
+ repoPaths: mem.repoPaths,
104
+ filePaths: mem.filePaths,
105
+ commands: mem.commands,
106
+ citationCount: 0,
107
+ lastCitedAt: null,
108
+ neverCitedCount: 0,
109
+ archived: false,
110
+ });
111
+ }
112
+ }
113
+ return {
114
+ success: true,
115
+ extractedCount: records.length,
116
+ records,
117
+ sceneNames
118
+ };
119
+ }
120
+ function parseExtractionResult(raw) {
121
+ try {
122
+ let cleaned = raw.trim();
123
+ if (cleaned.startsWith("\`\`\`")) {
124
+ cleaned = cleaned.replace(/^\`\`\`(?:json)?\s*\n?/, "").replace(/\n?\`\`\`\s*$/, "");
125
+ }
126
+ const match = cleaned.match(/\[[\s\S]*\]/);
127
+ if (!match)
128
+ return [];
129
+ const parsed = JSON.parse(match[0]);
130
+ if (!Array.isArray(parsed))
131
+ return [];
132
+ const scenes = [];
133
+ for (const item of parsed) {
134
+ if (!item || typeof item !== "object")
135
+ continue;
136
+ const s = item;
137
+ const memories = Array.isArray(s.memories) ? s.memories.map((m) => ({
138
+ content: String(m.content || ""),
139
+ type: parseMemoryType(m.type),
140
+ priority: clampNumber(m.priority, 0, 100, 50),
141
+ skill_tag: m.skill_tag ? String(m.skill_tag) : undefined,
142
+ confidence: typeof m.confidence === "number" ? clampNumber(m.confidence, 0, 1, 0.65) : undefined,
143
+ sourceKind: parseSourceKind(m.sourceKind ?? m.source_kind),
144
+ verificationStatus: parseVerificationStatus(m.verificationStatus ?? m.verification_status),
145
+ repoPaths: parseStringArray(m.repoPaths ?? m.repo_paths),
146
+ filePaths: parseStringArray(m.filePaths ?? m.file_paths),
147
+ commands: parseStringArray(m.commands),
148
+ metadata: m.metadata && typeof m.metadata === "object" ? m.metadata : {}
149
+ })).filter((m) => m.content.length > 0) : [];
150
+ scenes.push({
151
+ scene_name: String(s.scene_name || "Unknown Focus Scene"),
152
+ memories
153
+ });
154
+ }
155
+ return scenes;
156
+ }
157
+ catch (err) {
158
+ console.error("[BrainRouter] Failed to parse extraction result", err);
159
+ return [];
160
+ }
161
+ }
162
+ function parseMemoryType(value) {
163
+ const candidate = String(value || "");
164
+ return ALLOWED_MEMORY_TYPES.has(candidate) ? candidate : "episodic";
165
+ }
166
+ function parseSourceKind(value) {
167
+ const candidate = String(value || "");
168
+ return ALLOWED_SOURCE_KINDS.has(candidate) ? candidate : "model_inference";
169
+ }
170
+ function parseVerificationStatus(value) {
171
+ const candidate = String(value || "");
172
+ return ALLOWED_VERIFICATION_STATUSES.has(candidate) ? candidate : "unverified";
173
+ }
174
+ function parseStringArray(value) {
175
+ if (!Array.isArray(value))
176
+ return [];
177
+ return [...new Set(value.map((item) => String(item).trim()).filter(Boolean))];
178
+ }
179
+ function clampNumber(value, min, max, fallback) {
180
+ return typeof value === "number" && Number.isFinite(value)
181
+ ? Math.min(max, Math.max(min, value))
182
+ : fallback;
183
+ }
@@ -0,0 +1,13 @@
1
+ import type { IMemoryStore } from "@kinqs/brainrouter-types";
2
+ import type { LLMRunner } from "@kinqs/brainrouter-types";
3
+ /**
4
+ * Distills and updates Focus Scenes by grouping CognitiveRecords.
5
+ */
6
+ export declare function distillFocusScenes(params: {
7
+ userId: string;
8
+ store: IMemoryStore;
9
+ llmRunner: LLMRunner;
10
+ }): Promise<{
11
+ focusDistilled: number;
12
+ sceneNames: string[];
13
+ }>;
@@ -0,0 +1,135 @@
1
+ import { FOCUS_SCENE_SYSTEM_PROMPT, formatFocusScenePrompt } from "../prompts/focus-scene.js";
2
+ import { FOCUS_SCENE_CLUSTER_SYSTEM_PROMPT, formatFocusSceneClusterPrompt } from "../prompts/focus-scene-cluster.js";
3
+ import { MAX_FOCUS_SCENES } from "../scheduler.js";
4
+ import crypto from "node:crypto";
5
+ async function canonicalizeFocusNames(params) {
6
+ const { userId, store, llmRunner } = params;
7
+ const sceneNames = store.getDistinctSceneNames(userId);
8
+ if (sceneNames.length < 2)
9
+ return;
10
+ try {
11
+ const rawCluster = await llmRunner.run({
12
+ prompt: formatFocusSceneClusterPrompt(sceneNames),
13
+ systemPrompt: FOCUS_SCENE_CLUSTER_SYSTEM_PROMPT,
14
+ taskId: "focus-scene-clustering",
15
+ timeoutMs: 45_000,
16
+ });
17
+ const jsonMatch = rawCluster.match(/\[[\s\S]*\]/);
18
+ if (!jsonMatch)
19
+ return;
20
+ const clusters = JSON.parse(jsonMatch[0]);
21
+ if (!Array.isArray(clusters))
22
+ return;
23
+ for (const cluster of clusters) {
24
+ const canonical = String(cluster.canonical || "").trim();
25
+ const aliases = Array.isArray(cluster.aliases) ? cluster.aliases.map((a) => String(a).trim()) : [];
26
+ if (!canonical || aliases.length === 0)
27
+ continue;
28
+ for (const alias of aliases) {
29
+ if (alias === canonical)
30
+ continue;
31
+ store.renameFocusInCognitiveRecords(userId, alias, canonical);
32
+ }
33
+ }
34
+ }
35
+ catch (err) {
36
+ console.error(`[BrainRouter] Focus scene canonicalization failed for "${userId}":`, err.message);
37
+ }
38
+ }
39
+ /**
40
+ * Distills and updates Focus Scenes by grouping CognitiveRecords.
41
+ */
42
+ export async function distillFocusScenes(params) {
43
+ const { userId, store, llmRunner } = params;
44
+ // Run scene canonicalization/clustering pass to prevent cold-start fragmentation
45
+ await canonicalizeFocusNames({ userId, store, llmRunner });
46
+ // Decay all existing heat scores (each distillation cycle = time passing)
47
+ store.decayContextualFocusHeatScores(userId);
48
+ const sceneNames = store.getDistinctSceneNames(userId);
49
+ if (sceneNames.length === 0) {
50
+ return { focusDistilled: 0, sceneNames: [] };
51
+ }
52
+ const now = new Date().toISOString();
53
+ const distilled = [];
54
+ // Fetch existing focus scenes up-front so the prompt can avoid near-duplicates
55
+ const existingFocusNames = store.getTopContextualFocus(userId, 50).map(s => s.sceneName);
56
+ for (const sceneName of sceneNames) {
57
+ const cognitives = store.getCognitivesByFocus(userId, sceneName, 30);
58
+ if (cognitives.length === 0)
59
+ continue;
60
+ let summaryMd;
61
+ try {
62
+ summaryMd = await llmRunner.run({
63
+ prompt: formatFocusScenePrompt(sceneName, cognitives, existingFocusNames.filter(n => n !== sceneName)),
64
+ systemPrompt: FOCUS_SCENE_SYSTEM_PROMPT,
65
+ taskId: "focus-scene-distillation",
66
+ timeoutMs: 60_000,
67
+ });
68
+ }
69
+ catch (err) {
70
+ console.error(`[BrainRouter] Focus scene distillation failed for "${sceneName}":`, err.message);
71
+ continue;
72
+ }
73
+ const existing = store.getContextualFocusByName(userId, sceneName);
74
+ const record = {
75
+ id: existing?.id ?? `focus_${crypto.randomBytes(6).toString("hex")}`,
76
+ userId,
77
+ sceneName,
78
+ summaryMd: summaryMd.trim(),
79
+ heatScore: existing ? Math.min(100, existing.heatScore + 30) : 100,
80
+ lastActiveTime: now,
81
+ createdTime: existing?.createdTime ?? now,
82
+ updatedTime: now,
83
+ };
84
+ store.upsertContextualFocus(record);
85
+ distilled.push(sceneName);
86
+ }
87
+ // Auto-merge cold scenes if we exceed the max threshold
88
+ await mergeFocusScenes({ userId, store, llmRunner });
89
+ console.error(`[BrainRouter] Distilled ${distilled.length} focus scene(s) for user "${userId}".`);
90
+ return { focusDistilled: distilled.length, sceneNames: distilled };
91
+ }
92
+ async function mergeFocusScenes(params) {
93
+ const { userId, store, llmRunner } = params;
94
+ const focusCount = store.getContextualFocusCount(userId);
95
+ if (focusCount < MAX_FOCUS_SCENES)
96
+ return;
97
+ const overflow = focusCount - MAX_FOCUS_SCENES + 1;
98
+ const coldScenes = store.getColdContextualFocus(userId, overflow + 3);
99
+ if (coldScenes.length < 2)
100
+ return;
101
+ const archiveSceneName = "[Archived]";
102
+ const scenesToMerge = coldScenes.filter(s => s.sceneName !== archiveSceneName);
103
+ if (scenesToMerge.length < 2)
104
+ return;
105
+ const sceneSummaries = scenesToMerge.map(s => `## ${s.sceneName}\n${s.summaryMd}`).join("\n\n");
106
+ let unifiedSummary;
107
+ try {
108
+ unifiedSummary = await llmRunner.run({
109
+ prompt: `Merge the following old focus scene summaries into a single concise "Archived Focus Scenes" overview.\n\n${sceneSummaries}\n\nOutput only the Markdown summary. Be concise — no preamble.`,
110
+ systemPrompt: FOCUS_SCENE_SYSTEM_PROMPT,
111
+ taskId: "focus-scene-merge",
112
+ timeoutMs: 60_000,
113
+ });
114
+ }
115
+ catch (err) {
116
+ console.error(`[BrainRouter] Focus scene merge failed for "${userId}":`, err.message);
117
+ return;
118
+ }
119
+ const now = new Date().toISOString();
120
+ const existingArchive = store.getContextualFocusByName(userId, archiveSceneName);
121
+ const record = {
122
+ id: existingArchive?.id ?? `focus_${crypto.randomBytes(6).toString("hex")}`,
123
+ userId,
124
+ sceneName: archiveSceneName,
125
+ summaryMd: unifiedSummary.trim(),
126
+ heatScore: 10,
127
+ lastActiveTime: now,
128
+ createdTime: existingArchive?.createdTime ?? now,
129
+ updatedTime: now,
130
+ };
131
+ store.upsertContextualFocus(record);
132
+ const idsToDelete = scenesToMerge.map(s => s.id);
133
+ store.deleteContextualFocus(userId, idsToDelete);
134
+ console.error(`[BrainRouter] Focus auto-merge: merged ${idsToDelete.length} cold focus scenes into "${archiveSceneName}".`);
135
+ }
@@ -0,0 +1,10 @@
1
+ import type { CognitiveRecord, ContextualFocusRecord, LLMRunner } from "@kinqs/brainrouter-types";
2
+ export declare function detectFocusShift(params: {
3
+ activeScene: ContextualFocusRecord;
4
+ newCognitiveRecords: CognitiveRecord[];
5
+ llmRunner: LLMRunner;
6
+ }): Promise<{
7
+ shift: boolean;
8
+ confidence: number;
9
+ reason: string;
10
+ }>;