@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,626 @@
1
+ import { SqliteMemoryStore } from "./store/sqlite.js";
2
+ import { MemoryCapturePipeline } from "./capture.js";
3
+ import { MemoryRecallPipeline } from "./recall.js";
4
+ import { EmbeddingService } from "./store/embedding.js";
5
+ import { RerankerService } from "./store/reranker.js";
6
+ import { scanSkillsForHints } from "./skill-hints-loader.js";
7
+ import { distillFocusScenes } from "./pipeline/contextual-focus-builder.js";
8
+ import { distillCoreIdentity } from "./pipeline/identity-distiller.js";
9
+ import { spikeSkill as spikeSkillActivation, decayPotential } from "./pipeline/skill-prewarm.js";
10
+ import { NeuralSparkEngine } from "./pipeline/neural-spark.js";
11
+ import { fetchWithExternalRetry } from "./retry.js";
12
+ import { acquireLLMSlot } from "./llm-semaphore.js";
13
+ import "dotenv/config";
14
+ import path from "node:path";
15
+ import os from "node:os";
16
+ import fs from "node:fs";
17
+ import { randomBytes } from "node:crypto";
18
+ import { randomUUID } from "node:crypto";
19
+ import { hashPassword } from "../api/auth/crypto.js";
20
+ import { getMemoryTypeConfig } from "./memory-type-config.js";
21
+ import { redactSensitiveMemoryText } from "./redaction.js";
22
+ // Configure default path
23
+ const defaultDbPath = process.env.BRAINROUTER_MEMORY_DB || path.join(os.homedir(), ".brainrouter", "memory.db");
24
+ // Configurable LLM Runner — supports per-task model routing
25
+ class ModelLLMRunner {
26
+ modelOverride;
27
+ constructor(modelOverride) {
28
+ this.modelOverride = modelOverride?.trim() || undefined;
29
+ }
30
+ async run({ prompt, systemPrompt, timeoutMs = 120_000, taskId }) {
31
+ const endpoint = process.env.BRAINROUTER_LLM_ENDPOINT ?? "https://api.openai.com/v1/chat/completions";
32
+ const apiKey = process.env.BRAINROUTER_LLM_API_KEY;
33
+ if (!apiKey) {
34
+ // Typed sentinel so upstream pipelines can short-circuit cleanly without dumping a stack trace.
35
+ // Callers should check `error.code === "LLM_NOT_CONFIGURED"` and skip extraction silently.
36
+ const err = new Error(`[BrainRouter:${taskId}] BRAINROUTER_LLM_API_KEY is not set. Skipping LLM step.`);
37
+ err.code = "LLM_NOT_CONFIGURED";
38
+ throw err;
39
+ }
40
+ const model = this.modelOverride
41
+ ?? (process.env.BRAINROUTER_LLM_MODEL?.trim() || undefined)
42
+ ?? "gpt-4o-mini";
43
+ const messages = [];
44
+ if (systemPrompt) {
45
+ messages.push({ role: "system", content: systemPrompt });
46
+ }
47
+ messages.push({ role: "user", content: prompt });
48
+ const doFetch = () => fetchWithExternalRetry(endpoint, {
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ "Authorization": `Bearer ${apiKey}`,
53
+ },
54
+ body: JSON.stringify({ model, messages }),
55
+ signal: AbortSignal.timeout(timeoutMs),
56
+ }, {
57
+ label: `[BrainRouter:${taskId}] LLM API`,
58
+ });
59
+ // Acquire a slot from the global LLM semaphore BEFORE issuing the
60
+ // request. On consumer hardware (LM Studio with a single GPU) firing
61
+ // more than ~2 concurrent generations against the same backend causes
62
+ // the model to thrash or auto-unload — see llm-semaphore.ts for the
63
+ // full rationale. Cloud backends (OpenAI / OpenRouter) can lift the cap
64
+ // with BRAINROUTER_LLM_MAX_CONCURRENT=10 (or higher).
65
+ const release = await acquireLLMSlot();
66
+ try {
67
+ let res = await doFetch();
68
+ // LM Studio quirk: if the model has been idle long enough to auto-unload,
69
+ // it returns 400 with `{"error":"Model is unloaded."}` on the first call
70
+ // and then loads the model in the background. The next call usually
71
+ // succeeds. Detect that exact error and retry ONCE after a brief pause
72
+ // so background workers (contradiction check, graph extraction, focus
73
+ // shift detection) don't all fail when the user has been quiet for a bit.
74
+ if (res.status === 400) {
75
+ const errorBody = await res.text();
76
+ if (/model\s+(is\s+)?unloaded|model\s+not\s+loaded|no\s+models?\s+loaded/i.test(errorBody)) {
77
+ await new Promise((resolve) => setTimeout(resolve, 1500));
78
+ res = await doFetch();
79
+ if (!res.ok) {
80
+ const retryBody = await res.text();
81
+ throw new Error(`[BrainRouter:${taskId}] LLM model "${model}" was unloaded by the server; ` +
82
+ `retry also failed (${res.status} ${res.statusText}). ` +
83
+ `If you're using LM Studio, enable JIT model loading or pin the model as always-loaded. ` +
84
+ `Original error: ${errorBody}. Retry error: ${retryBody}`);
85
+ }
86
+ }
87
+ else {
88
+ throw new Error(`[BrainRouter:${taskId}] LLM Error (${model}): ${res.status} ${res.statusText} - ${errorBody}`);
89
+ }
90
+ }
91
+ else if (!res.ok) {
92
+ const errorBody = await res.text();
93
+ throw new Error(`[BrainRouter:${taskId}] LLM Error (${model}): ${res.status} ${res.statusText} - ${errorBody}`);
94
+ }
95
+ const data = await res.json();
96
+ // Defensive parsing — see brainrouter/src/agent/agent.ts callOpenAI for the
97
+ // full rationale. The short version: some endpoints return HTTP 200
98
+ // with an `error` envelope or a non-standard schema. Surface the
99
+ // actual response body in the error so a misconfigured model name
100
+ // doesn't crash with "Cannot read properties of undefined".
101
+ if (data && typeof data === "object" && data.error) {
102
+ const errMsg = typeof data.error === "string"
103
+ ? data.error
104
+ : (data.error.message ?? JSON.stringify(data.error).slice(0, 400));
105
+ throw new Error(`[BrainRouter:${taskId}] LLM endpoint returned an error envelope: ${errMsg}`);
106
+ }
107
+ if (!Array.isArray(data?.choices) || data.choices.length === 0) {
108
+ throw new Error(`[BrainRouter:${taskId}] LLM endpoint returned no choices for model "${model}". ` +
109
+ `Response body: ${JSON.stringify(data).slice(0, 600)}`);
110
+ }
111
+ const choice = data.choices[0];
112
+ // Tolerate both message (standard) and delta (streaming-style) shapes.
113
+ const content = choice?.message?.content ?? choice?.delta?.content;
114
+ if (typeof content !== "string") {
115
+ throw new Error(`[BrainRouter:${taskId}] LLM choice had no usable content. Choice: ${JSON.stringify(choice).slice(0, 600)}`);
116
+ }
117
+ return content;
118
+ }
119
+ finally {
120
+ // Always release, success or failure, so the queue keeps moving even
121
+ // if an upstream throw bubbles. The semaphore's release is idempotent.
122
+ release();
123
+ }
124
+ }
125
+ }
126
+ export class MemoryEngine {
127
+ store;
128
+ capturePipeline;
129
+ recallPipeline;
130
+ extractionRunner;
131
+ synthesisRunner;
132
+ sweeperTimer;
133
+ /**
134
+ * Reentrancy guard: setInterval doesn't wait for the previous callback to
135
+ * finish before firing the next tick. If a sweep takes longer than the
136
+ * configured interval (very common when LLM calls queue behind the
137
+ * concurrency semaphore), ticks pile up and each one tries to extract
138
+ * the SAME backlog rows. The guard ensures at most one sweep is in flight
139
+ * at any time; later ticks become no-ops while a previous one runs.
140
+ */
141
+ sweepInProgress = false;
142
+ personaCache = new Map();
143
+ PERSONA_CACHE_TTL_MS = parseInt(process.env.BRAINROUTER_PERSONA_CACHE_TTL_MS ?? String(60 * 60 * 1000), 10);
144
+ constructor(storeOrDbPath = defaultDbPath) {
145
+ if (typeof storeOrDbPath === "string") {
146
+ const dir = path.dirname(storeOrDbPath);
147
+ if (!fs.existsSync(dir)) {
148
+ fs.mkdirSync(dir, { recursive: true });
149
+ }
150
+ this.store = new SqliteMemoryStore(storeOrDbPath);
151
+ }
152
+ else {
153
+ this.store = storeOrDbPath;
154
+ }
155
+ this.store.init();
156
+ this.ensureSeedAdminUser().catch((err) => {
157
+ console.error("[BrainRouter] Failed to seed admin user:", err instanceof Error ? err.message : err);
158
+ });
159
+ this.extractionRunner = new ModelLLMRunner(process.env.BRAINROUTER_EXTRACTION_MODEL);
160
+ this.synthesisRunner = new ModelLLMRunner(process.env.BRAINROUTER_SYNTHESIS_MODEL);
161
+ const embeddingService = new EmbeddingService({
162
+ endpoint: process.env.BRAINROUTER_EMBEDDING_ENDPOINT,
163
+ apiKey: process.env.BRAINROUTER_EMBEDDING_API_KEY ?? process.env.BRAINROUTER_LLM_API_KEY,
164
+ model: process.env.BRAINROUTER_EMBEDDING_MODEL,
165
+ dimensions: process.env.BRAINROUTER_EMBEDDING_DIMENSIONS ? parseInt(process.env.BRAINROUTER_EMBEDDING_DIMENSIONS, 10) : undefined,
166
+ });
167
+ const rerankerService = new RerankerService({
168
+ endpoint: process.env.BRAINROUTER_RERANKER_ENDPOINT,
169
+ apiKey: process.env.BRAINROUTER_RERANKER_API_KEY,
170
+ model: process.env.BRAINROUTER_RERANKER_MODEL,
171
+ topN: process.env.BRAINROUTER_RERANKER_TOP_N
172
+ ? parseInt(process.env.BRAINROUTER_RERANKER_TOP_N, 10)
173
+ : undefined,
174
+ });
175
+ this.store.initVec(embeddingService.getDimensions());
176
+ if (embeddingService.isReady()) {
177
+ void this.store.reembedStaleRecords((text) => embeddingService.embed(text)).then((count) => {
178
+ if (count > 0) {
179
+ console.error(`[BrainRouter] Re-embedded ${count} stale cognitive vector records.`);
180
+ }
181
+ }).catch((err) => {
182
+ console.error("[BrainRouter] Failed to re-embed stale cognitive vector records:", err instanceof Error ? err.message : err);
183
+ });
184
+ }
185
+ this.capturePipeline = new MemoryCapturePipeline(this.store, this.extractionRunner, embeddingService, 1);
186
+ this.recallPipeline = new MemoryRecallPipeline(this.store, embeddingService, rerankerService);
187
+ this.startExtractionSweeper();
188
+ }
189
+ async ensureSeedAdminUser() {
190
+ const users = this.store.listUsers();
191
+ if (users.length > 0)
192
+ return;
193
+ const seededUserId = process.env.BRAINROUTER_DEFAULT_ADMIN_USER_ID ?? "admin";
194
+ const seededEmail = process.env.BRAINROUTER_ADMIN_EMAIL ?? "admin";
195
+ const seededPassword = process.env.BRAINROUTER_ADMIN_PASSWORD?.trim();
196
+ const apiKey = `br_${randomBytes(24).toString("hex")}`;
197
+ this.store.createUser(seededUserId, apiKey, "Default Admin", true);
198
+ this.store.updateUserEmail(seededUserId, seededEmail);
199
+ if (seededPassword) {
200
+ const passwordHash = await hashPassword(seededPassword);
201
+ this.store.updateUserPassword(seededUserId, passwordHash);
202
+ }
203
+ console.error(`[BrainRouter] Admin seeded. Email: ${seededEmail} API key (shown once): ${apiKey}`);
204
+ }
205
+ get capture() {
206
+ return this.capturePipeline.captureTurn.bind(this.capturePipeline);
207
+ }
208
+ capturePassiveL0(params) {
209
+ const now = new Date().toISOString();
210
+ const timestamp = params.timestamp ?? Date.now();
211
+ const record = {
212
+ id: `sensory_hook_${params.sessionKey}_${timestamp}_${randomUUID()}`,
213
+ userId: params.userId,
214
+ sessionKey: params.sessionKey,
215
+ sessionId: params.sessionId ?? "",
216
+ role: params.role,
217
+ messageText: redactSensitiveMemoryText(params.content),
218
+ recordedAt: now,
219
+ timestamp,
220
+ skillTag: params.skillTag ?? "",
221
+ };
222
+ this.store.upsertSensory(record);
223
+ return record;
224
+ }
225
+ async explainRecall(params) {
226
+ return this.recallPipeline.recall({ ...params, explain: true });
227
+ }
228
+ get recall() {
229
+ return async (params) => {
230
+ const result = await this.recallPipeline.recall(params);
231
+ const persona = this.getPersona(params.userId);
232
+ if (persona) {
233
+ const existing = result.appendSystemContext ?? "";
234
+ result.appendSystemContext = `<user-persona>\n${persona.personaMd}\n</user-persona>\n\n` + existing;
235
+ result.coreIdentitySummary = persona.personaMd;
236
+ }
237
+ return result;
238
+ };
239
+ }
240
+ getPendingContradictions(userId, pagination) {
241
+ return this.store.getPendingContradictions(userId, pagination);
242
+ }
243
+ resolveContradiction(id, userId, status) {
244
+ return this.store.resolveContradiction(id, userId, status);
245
+ }
246
+ registerSkillHints(skillName, hints, sourceFile = "") {
247
+ this.store.upsertSkillHints(skillName, hints, sourceFile);
248
+ }
249
+ listSkillHints() {
250
+ return this.store.listSkillHints();
251
+ }
252
+ spikeSkill(userId, skillName) {
253
+ return spikeSkillActivation({ userId, skillName, store: this.store });
254
+ }
255
+ getSkillActivations(userId) {
256
+ const raw = this.store.getSkillActivations(userId);
257
+ const now = new Date();
258
+ return raw.map(r => ({
259
+ skillName: r.skillName,
260
+ potential: decayPotential({
261
+ potential: r.potential,
262
+ lastDecayTime: r.lastDecayTime,
263
+ now,
264
+ }),
265
+ lastDecayTime: r.lastDecayTime,
266
+ })).sort((a, b) => b.potential - a.potential);
267
+ }
268
+ autoScanSkillHints(skillsDirs) {
269
+ let loaded = 0;
270
+ for (const dir of skillsDirs) {
271
+ if (!fs.existsSync(dir))
272
+ continue;
273
+ const found = scanSkillsForHints(dir);
274
+ for (const item of found) {
275
+ const skillName = item.name || path.basename(path.dirname(item.filePath));
276
+ this.store.upsertSkillHints(skillName, item.hints, item.filePath);
277
+ loaded++;
278
+ }
279
+ }
280
+ if (loaded > 0) {
281
+ console.error(`[BrainRouter] Auto-loaded memory_hints for ${loaded} skill(s).`);
282
+ }
283
+ }
284
+ /** On-demand Focus Scene distillation — groups cognitives by scene and summarizes via LLM. */
285
+ async distillScenes(userId) {
286
+ return distillFocusScenes({ userId, store: this.store, llmRunner: this.synthesisRunner });
287
+ }
288
+ /** On-demand Core Identity distillation — cross-session synthesis of persona+instruction cognitives. */
289
+ async distillPersona(userId) {
290
+ const result = await distillCoreIdentity({ userId, store: this.store, llmRunner: this.synthesisRunner });
291
+ if (result.success && result.personaMd) {
292
+ this.personaCache.set(userId, { personaMd: result.personaMd, cachedAt: Date.now() });
293
+ }
294
+ return result;
295
+ }
296
+ /** Get the current Core Identity for a user, using prompt-level in-memory cache. */
297
+ getPersona(userId) {
298
+ const cached = this.personaCache.get(userId);
299
+ if (cached && (Date.now() - cached.cachedAt) < this.PERSONA_CACHE_TTL_MS) {
300
+ return { personaMd: cached.personaMd };
301
+ }
302
+ const persona = this.store.getCoreIdentity(userId);
303
+ if (persona) {
304
+ this.personaCache.set(userId, { personaMd: persona.personaMd, cachedAt: Date.now() });
305
+ }
306
+ return persona;
307
+ }
308
+ /** Get the top N active focus scenes for a user (ordered by heat score). */
309
+ getTopScenes(userId, limit = 3, cursor) {
310
+ return this.store.getTopContextualFocus(userId, limit, cursor);
311
+ }
312
+ /** Expose the ability to query the knowledge graph for a user/entity. */
313
+ queryGraph(userId, entity, skillTag, maxHops = 2) {
314
+ const node = this.store.getGraphNodeByEntity(userId, entity);
315
+ if (!node)
316
+ return { nodes: [], edges: [] };
317
+ return this.store.getGraphNeighbors(userId, node.id, skillTag, maxHops);
318
+ }
319
+ createUser(userId, apiKey, displayName = "", isAdmin = false) {
320
+ return this.store.createUser(userId, apiKey, displayName, isAdmin);
321
+ }
322
+ getUserByApiKey(apiKey) {
323
+ return this.store.getUserByApiKey(apiKey);
324
+ }
325
+ getUserByEmail(email) {
326
+ return this.store.getUserByEmail(email);
327
+ }
328
+ getUserById(userId) {
329
+ return this.store.getUserById(userId);
330
+ }
331
+ updatePassword(userId, hash) {
332
+ this.store.updateUserPassword(userId, hash);
333
+ }
334
+ updateUserEmail(userId, email) {
335
+ this.store.updateUserEmail(userId, email);
336
+ }
337
+ updateUserDisplayName(userId, displayName) {
338
+ this.store.updateUserDisplayName(userId, displayName);
339
+ }
340
+ updateUserStatus(userId, status) {
341
+ this.store.updateUserStatus(userId, status);
342
+ }
343
+ updateUserApiKey(userId, apiKey) {
344
+ this.store.updateUserApiKey(userId, apiKey);
345
+ }
346
+ listUsers(pagination) {
347
+ return this.store.listUsers(pagination);
348
+ }
349
+ deleteUser(userId) {
350
+ this.store.deleteUser(userId);
351
+ }
352
+ listMemories(userId, filters, pagination) {
353
+ return this.store.listMemories(userId, filters, pagination);
354
+ }
355
+ deleteMemory(userId, recordId) {
356
+ this.store.archiveCognitiveRecord(userId, recordId);
357
+ }
358
+ getMemoryById(userId, recordId) {
359
+ const memory = this.store.getMemoryById(userId, recordId);
360
+ if (!memory)
361
+ return null;
362
+ return { memory, evidence: this.store.getEvidenceByRecord(userId, recordId) };
363
+ }
364
+ upsertEngineeringMemory(params) {
365
+ const now = new Date().toISOString();
366
+ const config = getMemoryTypeConfig(params.type);
367
+ const record = {
368
+ id: `cognitive_manual_${randomUUID()}`,
369
+ userId: params.userId,
370
+ sessionKey: params.sessionKey ?? "",
371
+ sessionId: params.sessionId ?? "",
372
+ content: params.content,
373
+ type: params.type,
374
+ priority: params.priority ?? 75,
375
+ sceneName: params.activeSkill ? `${params.activeSkill} engineering` : "Software engineering memory",
376
+ skillTag: params.activeSkill ?? "",
377
+ halfLifeDays: config.halfLifeDays,
378
+ supersededBy: null,
379
+ invalidAt: null,
380
+ timestampStr: now,
381
+ timestampStart: now,
382
+ timestampEnd: now,
383
+ createdTime: now,
384
+ updatedTime: now,
385
+ metadata: params.metadata ?? {},
386
+ confidence: params.confidence ?? config.defaultConfidence,
387
+ status: "active",
388
+ sourceKind: params.sourceKind ?? "user_instruction",
389
+ verificationStatus: params.verificationStatus ?? "unverified",
390
+ repoPaths: params.repoPaths ?? [],
391
+ filePaths: params.filePaths ?? [],
392
+ commands: params.commands ?? [],
393
+ citationCount: 0,
394
+ lastCitedAt: null,
395
+ neverCitedCount: 0,
396
+ archived: false,
397
+ };
398
+ this.store.upsertCognitive(record);
399
+ return record;
400
+ }
401
+ getMemoriesByFilePath(userId, filePath, limit = 20) {
402
+ return this.store.getMemoriesByFilePath(userId, filePath, limit);
403
+ }
404
+ searchMemoryRecords(userId, query, limit = 20) {
405
+ return this.store.searchCognitiveFts(userId, query, limit);
406
+ }
407
+ updateMemory(userId, recordId, updates) {
408
+ const existing = this.store.getMemoryById(userId, recordId);
409
+ if (!existing)
410
+ return null;
411
+ const now = new Date().toISOString();
412
+ const updated = {
413
+ ...existing,
414
+ content: updates.content ?? existing.content,
415
+ status: updates.status ?? existing.status,
416
+ confidence: updates.confidence ?? existing.confidence,
417
+ verificationStatus: updates.verificationStatus ?? existing.verificationStatus,
418
+ updatedTime: now,
419
+ archived: updates.status === "archived" ? true : existing.archived,
420
+ metadata: updates.note
421
+ ? { ...existing.metadata, governanceNote: updates.note, governanceNoteAt: now }
422
+ : existing.metadata,
423
+ };
424
+ this.store.upsertCognitive(updated, { skipAudit: true });
425
+ this.store.insertOperation({
426
+ id: randomUUID(),
427
+ userId,
428
+ recordId,
429
+ operation: "memory_update",
430
+ actor: "user",
431
+ sessionKey: existing.sessionKey,
432
+ reason: updates.note ?? "",
433
+ createdAt: now,
434
+ metadata: {
435
+ contentChanged: typeof updates.content === "string",
436
+ status: updates.status,
437
+ confidence: updates.confidence,
438
+ verificationStatus: updates.verificationStatus,
439
+ },
440
+ });
441
+ return this.getMemoryById(userId, recordId);
442
+ }
443
+ updateMemoryStatus(userId, recordId, confidence, status) {
444
+ this.store.updateCognitiveConfidence(userId, recordId, confidence, status);
445
+ return this.getMemoryById(userId, recordId);
446
+ }
447
+ addEvidence(userId, recordId, evidence) {
448
+ const ev = {
449
+ id: evidence.id ?? randomUUID(),
450
+ userId,
451
+ recordId,
452
+ kind: evidence.kind,
453
+ ref: evidence.ref,
454
+ excerpt: evidence.excerpt ?? "",
455
+ observedAt: evidence.observedAt ?? new Date().toISOString(),
456
+ metadata: evidence.metadata ?? {},
457
+ };
458
+ this.store.insertEvidence(ev);
459
+ return ev;
460
+ }
461
+ getEvidence(userId, recordId) {
462
+ return this.store.getEvidenceByRecord(userId, recordId);
463
+ }
464
+ listEvidence(userId, filters, pagination) {
465
+ return this.store.listEvidence(userId, filters, pagination);
466
+ }
467
+ exportMemories(userId) {
468
+ return this.store.exportMemories(userId);
469
+ }
470
+ importMemories(userId, data) {
471
+ return this.store.importMemories(userId, data);
472
+ }
473
+ governanceDelete(userId, recordId, reason) {
474
+ this.store.hardDeleteMemory(userId, recordId, reason);
475
+ }
476
+ getOperationLog(userId, pagination, filters) {
477
+ return this.store.getOperationLog(userId, pagination, filters);
478
+ }
479
+ getStats(userId) {
480
+ return this.store.getMemoryStats(userId);
481
+ }
482
+ getDiagnostics(userId) {
483
+ const envKeys = Object.keys(process.env)
484
+ .filter((key) => key.startsWith("BRAINROUTER_") || key.includes("API") || key.includes("SECRET"))
485
+ .sort();
486
+ const recentOperations = this.store.getOperationLog(userId, { limit: 50 });
487
+ const recentErrors = recentOperations
488
+ .filter((op) => /error|degrad|fail/i.test(`${op.operation} ${op.reason} ${JSON.stringify(op.metadata ?? {})}`))
489
+ .slice(0, 10);
490
+ return {
491
+ timestamp: new Date().toISOString(),
492
+ sqliteVersion: this.store.getSqliteVersion(),
493
+ nodeVersion: process.version,
494
+ databaseStats: {
495
+ userStats: this.store.getMemoryStats(userId),
496
+ },
497
+ envKeys,
498
+ recentErrors,
499
+ };
500
+ }
501
+ startExtractionSweeper() {
502
+ if (process.env.BRAINROUTER_DISABLE_EXTRACTION_SWEEPER === "true") {
503
+ return;
504
+ }
505
+ const DEFAULT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
506
+ // Floor at 30s — a user typo of `100` (intended seconds, actually ms)
507
+ // would otherwise fire the sweeper 10x/second, each tick hammering the
508
+ // LLM backend with extraction calls for the entire backlog. With a
509
+ // local LM Studio that's an instant model-unload + flood of 400s.
510
+ // 30s is a conservative floor that still feels responsive while keeping
511
+ // backend load sane on consumer hardware.
512
+ const MIN_INTERVAL_MS = 30 * 1000;
513
+ const raw = parseInt(process.env.BRAINROUTER_EXTRACTION_SWEEP_INTERVAL_MS ?? String(DEFAULT_INTERVAL_MS), 10);
514
+ if (!Number.isFinite(raw) || raw <= 0) {
515
+ return;
516
+ }
517
+ let intervalMs = raw;
518
+ if (intervalMs < MIN_INTERVAL_MS) {
519
+ console.error(`[BrainRouter] BRAINROUTER_EXTRACTION_SWEEP_INTERVAL_MS=${raw} is below the ${MIN_INTERVAL_MS}ms floor ` +
520
+ `(value is in MILLISECONDS, not seconds). Clamping to ${MIN_INTERVAL_MS}ms. ` +
521
+ `Use a value like 60000 (1 min) or 300000 (5 min) for local backends.`);
522
+ intervalMs = MIN_INTERVAL_MS;
523
+ }
524
+ this.sweeperTimer = setInterval(() => {
525
+ if (this.sweepInProgress) {
526
+ // Previous tick still running (likely waiting on the LLM semaphore).
527
+ // Skip this tick instead of stacking a second invocation.
528
+ return;
529
+ }
530
+ this.sweepInProgress = true;
531
+ this.sweepUnextractedBacklog()
532
+ .catch((err) => {
533
+ console.error("[BrainRouter] Extraction backlog sweeper failed:", err instanceof Error ? err.message : err);
534
+ })
535
+ .finally(() => {
536
+ this.sweepInProgress = false;
537
+ });
538
+ }, intervalMs);
539
+ this.sweeperTimer.unref?.();
540
+ }
541
+ async sweepUnextractedBacklog() {
542
+ const olderThanMs = parseInt(process.env.BRAINROUTER_EXTRACTION_SWEEP_MIN_AGE_MS ?? String(2 * 60 * 1000), 10);
543
+ const maxFailures = parseInt(process.env.BRAINROUTER_EXTRACTION_MAX_FAILURES ?? "5", 10);
544
+ const backlog = this.store.sweepUnextractedBacklog({
545
+ olderThanMs: Number.isFinite(olderThanMs) ? olderThanMs : 2 * 60 * 1000,
546
+ maxFailures: Number.isFinite(maxFailures) ? maxFailures : 5,
547
+ minUnextracted: 1,
548
+ limit: 20,
549
+ });
550
+ let processed = 0;
551
+ let extracted = 0;
552
+ for (const item of backlog) {
553
+ const result = await this.capturePipeline.processBacklog({
554
+ userId: item.userId,
555
+ sessionKey: item.sessionKey,
556
+ sessionId: item.sessionId,
557
+ });
558
+ if (result.triggered) {
559
+ processed += 1;
560
+ extracted += result.extractedCount;
561
+ }
562
+ }
563
+ return { candidates: backlog.length, processed, extracted };
564
+ }
565
+ // ============================
566
+ // ACE Feedback Loop
567
+ // ============================
568
+ ACE_ARCHIVE_THRESHOLD = (() => {
569
+ const v = parseInt(process.env.BRAINROUTER_ACE_ARCHIVE_THRESHOLD ?? "10", 10);
570
+ return isNaN(v) || v <= 0 ? 0 : v;
571
+ })();
572
+ markCited(userId, citedRecordIds, allRecalledRecordIds) {
573
+ if (citedRecordIds.length > 0) {
574
+ this.store.markCited(userId, citedRecordIds);
575
+ }
576
+ if (citedRecordIds.length >= 2) {
577
+ try {
578
+ const sparkEngine = new NeuralSparkEngine(this.store);
579
+ sparkEngine.strengthenSpines(userId, citedRecordIds);
580
+ }
581
+ catch (err) {
582
+ console.error("[BrainRouter] Failed to strengthen spines on citation:", err.message);
583
+ }
584
+ }
585
+ const citedSet = new Set(citedRecordIds);
586
+ const nonCited = allRecalledRecordIds.filter(id => !citedSet.has(id));
587
+ if (nonCited.length > 0) {
588
+ const updated = this.store.incrementNeverCited(userId, nonCited);
589
+ if (this.ACE_ARCHIVE_THRESHOLD > 0) {
590
+ for (const { recordId, neverCitedCount } of updated) {
591
+ if (neverCitedCount >= this.ACE_ARCHIVE_THRESHOLD) {
592
+ this.store.archiveCognitiveRecord(userId, recordId);
593
+ console.error(`[BrainRouter] ACE: Auto-archived memory ${recordId} (never_cited_count=${neverCitedCount})`);
594
+ }
595
+ }
596
+ }
597
+ }
598
+ return {
599
+ cited: citedRecordIds.length,
600
+ nonCited: nonCited.length,
601
+ archiveThreshold: this.ACE_ARCHIVE_THRESHOLD,
602
+ };
603
+ }
604
+ // ============================
605
+ // Point-in-Time Search (asOf)
606
+ // ============================
607
+ searchAsOf(userId, query, asOf, limit = 10) {
608
+ const ts = Date.parse(asOf);
609
+ if (isNaN(ts)) {
610
+ throw new Error(`Invalid asOf timestamp: "${asOf}". Must be a valid ISO 8601 date string.`);
611
+ }
612
+ const results = this.store.searchCognitiveFtsAsOf(userId, query, limit, asOf);
613
+ return {
614
+ memories: results.map(r => ({
615
+ recordId: r.record_id,
616
+ content: r.content,
617
+ type: r.type,
618
+ score: r.score,
619
+ })),
620
+ asOf,
621
+ count: results.length,
622
+ };
623
+ }
624
+ }
625
+ // Singleton export
626
+ export const memoryEngine = new MemoryEngine();
@@ -0,0 +1,41 @@
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
+ /**
24
+ * Acquire one slot. Returns a release function the caller must invoke when
25
+ * the LLM call finishes (success OR failure). Use it like:
26
+ *
27
+ * const release = await acquireLLMSlot();
28
+ * try { ...llm call... } finally { release(); }
29
+ */
30
+ export declare function acquireLLMSlot(): Promise<() => void>;
31
+ /** Exposed for tests / diagnostics. */
32
+ export declare function getSemaphoreState(): {
33
+ cap: number;
34
+ inFlight: number;
35
+ queued: number;
36
+ };
37
+ /**
38
+ * Allow tests (or a future /config tool) to reset the cap and clear waiters
39
+ * without restarting the process.
40
+ */
41
+ export declare function resetSemaphoreForTests(): void;