@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.
- package/.env.example +144 -0
- package/README.md +56 -0
- package/agents/README.md +120 -0
- package/agents/code-reviewer.md +97 -0
- package/agents/security-auditor.md +101 -0
- package/agents/test-engineer.md +95 -0
- package/dist/__tests__/agent_mode.test.d.ts +1 -0
- package/dist/__tests__/api-routes.test.d.ts +1 -0
- package/dist/__tests__/api-routes.test.js +170 -0
- package/dist/__tests__/crypto.test.d.ts +1 -0
- package/dist/__tests__/crypto.test.js +28 -0
- package/dist/__tests__/host-integrations.test.d.ts +1 -0
- package/dist/__tests__/host-integrations.test.js +82 -0
- package/dist/__tests__/integration.test.d.ts +1 -0
- package/dist/__tests__/integration.test.js +50 -0
- package/dist/__tests__/loader.test.d.ts +1 -0
- package/dist/__tests__/loader.test.js +89 -0
- package/dist/__tests__/neural-spark.test.d.ts +1 -0
- package/dist/__tests__/neural-spark.test.js +112 -0
- package/dist/__tests__/pagination.test.d.ts +1 -0
- package/dist/__tests__/pagination.test.js +23 -0
- package/dist/__tests__/redaction.test.d.ts +1 -0
- package/dist/__tests__/redaction.test.js +17 -0
- package/dist/__tests__/registry.test.d.ts +1 -0
- package/dist/__tests__/registry.test.js +56 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +30 -0
- package/dist/__tests__/skill-activation.test.d.ts +1 -0
- package/dist/__tests__/skill-activation.test.js +112 -0
- package/dist/__tests__/working-memory.test.d.ts +1 -0
- package/dist/__tests__/working-memory.test.js +200 -0
- package/dist/__tests__/workspace-paths.test.d.ts +1 -0
- package/dist/__tests__/workspace-paths.test.js +56 -0
- package/dist/__tests__/writer.test.d.ts +1 -0
- package/dist/__tests__/writer.test.js +94 -0
- package/dist/api/auth/crypto.d.ts +4 -0
- package/dist/api/auth/crypto.js +54 -0
- package/dist/api/middleware/auth.d.ts +12 -0
- package/dist/api/middleware/auth.js +90 -0
- package/dist/api/pagination.d.ts +18 -0
- package/dist/api/pagination.js +32 -0
- package/dist/api/routes/auth.d.ts +1 -0
- package/dist/api/routes/auth.js +130 -0
- package/dist/api/routes/chat-completions.d.ts +7 -0
- package/dist/api/routes/chat-completions.js +474 -0
- package/dist/api/routes/contradictions.d.ts +1 -0
- package/dist/api/routes/contradictions.js +28 -0
- package/dist/api/routes/evidence.d.ts +1 -0
- package/dist/api/routes/evidence.js +59 -0
- package/dist/api/routes/governance.d.ts +1 -0
- package/dist/api/routes/governance.js +95 -0
- package/dist/api/routes/graph.d.ts +1 -0
- package/dist/api/routes/graph.js +25 -0
- package/dist/api/routes/hooks.d.ts +1 -0
- package/dist/api/routes/hooks.js +88 -0
- package/dist/api/routes/memories.d.ts +1 -0
- package/dist/api/routes/memories.js +92 -0
- package/dist/api/routes/persona.d.ts +1 -0
- package/dist/api/routes/persona.js +9 -0
- package/dist/api/routes/scenes.d.ts +1 -0
- package/dist/api/routes/scenes.js +35 -0
- package/dist/api/routes/skills.d.ts +1 -0
- package/dist/api/routes/skills.js +14 -0
- package/dist/api/routes/stats.d.ts +1 -0
- package/dist/api/routes/stats.js +8 -0
- package/dist/api/routes/users.d.ts +1 -0
- package/dist/api/routes/users.js +82 -0
- package/dist/api/routes/working.d.ts +1 -0
- package/dist/api/routes/working.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +492 -0
- package/dist/integrations/claude-code.d.ts +12 -0
- package/dist/integrations/claude-code.js +35 -0
- package/dist/integrations/codex.d.ts +12 -0
- package/dist/integrations/codex.js +34 -0
- package/dist/integrations/generic-mcp.d.ts +52 -0
- package/dist/integrations/generic-mcp.js +118 -0
- package/dist/loader.d.ts +29 -0
- package/dist/loader.js +200 -0
- package/dist/memory/capture.d.ts +35 -0
- package/dist/memory/capture.js +230 -0
- package/dist/memory/config.d.ts +2 -0
- package/dist/memory/config.js +3 -0
- package/dist/memory/engine.d.ts +203 -0
- package/dist/memory/engine.js +626 -0
- package/dist/memory/llm-semaphore.d.ts +41 -0
- package/dist/memory/llm-semaphore.js +81 -0
- package/dist/memory/memory-type-config.d.ts +11 -0
- package/dist/memory/memory-type-config.js +65 -0
- package/dist/memory/pipeline/cognitive-contradiction.d.ts +7 -0
- package/dist/memory/pipeline/cognitive-contradiction.js +59 -0
- package/dist/memory/pipeline/cognitive-dedup.d.ts +23 -0
- package/dist/memory/pipeline/cognitive-dedup.js +38 -0
- package/dist/memory/pipeline/cognitive-extractor.d.ts +21 -0
- package/dist/memory/pipeline/cognitive-extractor.js +183 -0
- package/dist/memory/pipeline/contextual-focus-builder.d.ts +13 -0
- package/dist/memory/pipeline/contextual-focus-builder.js +135 -0
- package/dist/memory/pipeline/focus-direction-shift.d.ts +10 -0
- package/dist/memory/pipeline/focus-direction-shift.js +27 -0
- package/dist/memory/pipeline/graph-builder.d.ts +11 -0
- package/dist/memory/pipeline/graph-builder.js +88 -0
- package/dist/memory/pipeline/graph-recall.d.ts +13 -0
- package/dist/memory/pipeline/graph-recall.js +55 -0
- package/dist/memory/pipeline/identity-distiller.d.ts +15 -0
- package/dist/memory/pipeline/identity-distiller.js +40 -0
- package/dist/memory/pipeline/l1-contradiction.d.ts +7 -0
- package/dist/memory/pipeline/l1-contradiction.js +66 -0
- package/dist/memory/pipeline/l1-dedup.d.ts +23 -0
- package/dist/memory/pipeline/l1-dedup.js +39 -0
- package/dist/memory/pipeline/l1-extractor.d.ts +21 -0
- package/dist/memory/pipeline/l1-extractor.js +180 -0
- package/dist/memory/pipeline/l2-direction-shift.d.ts +10 -0
- package/dist/memory/pipeline/l2-direction-shift.js +27 -0
- package/dist/memory/pipeline/l2-scene.d.ts +15 -0
- package/dist/memory/pipeline/l2-scene.js +140 -0
- package/dist/memory/pipeline/l3-distiller.d.ts +15 -0
- package/dist/memory/pipeline/l3-distiller.js +40 -0
- package/dist/memory/pipeline/neural-spark.d.ts +27 -0
- package/dist/memory/pipeline/neural-spark.js +78 -0
- package/dist/memory/pipeline/skill-prewarm.d.ts +63 -0
- package/dist/memory/pipeline/skill-prewarm.js +127 -0
- package/dist/memory/pipeline/task-queue.d.ts +54 -0
- package/dist/memory/pipeline/task-queue.js +117 -0
- package/dist/memory/prompts/cognitive-contradiction.d.ts +1 -0
- package/dist/memory/prompts/cognitive-contradiction.js +25 -0
- package/dist/memory/prompts/cognitive-extraction.d.ts +10 -0
- package/dist/memory/prompts/cognitive-extraction.js +114 -0
- package/dist/memory/prompts/core-identity.d.ts +6 -0
- package/dist/memory/prompts/core-identity.js +60 -0
- package/dist/memory/prompts/focus-direction-shift.d.ts +5 -0
- package/dist/memory/prompts/focus-direction-shift.js +32 -0
- package/dist/memory/prompts/focus-scene-cluster.d.ts +2 -0
- package/dist/memory/prompts/focus-scene-cluster.js +33 -0
- package/dist/memory/prompts/focus-scene.d.ts +7 -0
- package/dist/memory/prompts/focus-scene.js +40 -0
- package/dist/memory/prompts/graph-extraction-batch.d.ts +14 -0
- package/dist/memory/prompts/graph-extraction-batch.js +54 -0
- package/dist/memory/prompts/graph-extraction.d.ts +2 -0
- package/dist/memory/prompts/graph-extraction.js +53 -0
- package/dist/memory/prompts/l1-contradiction-batch.d.ts +16 -0
- package/dist/memory/prompts/l1-contradiction-batch.js +47 -0
- package/dist/memory/prompts/l1-contradiction.d.ts +1 -0
- package/dist/memory/prompts/l1-contradiction.js +25 -0
- package/dist/memory/prompts/l1-extraction.d.ts +10 -0
- package/dist/memory/prompts/l1-extraction.js +114 -0
- package/dist/memory/prompts/l2-direction-shift.d.ts +5 -0
- package/dist/memory/prompts/l2-direction-shift.js +32 -0
- package/dist/memory/prompts/l2-scene-cluster.d.ts +2 -0
- package/dist/memory/prompts/l2-scene-cluster.js +33 -0
- package/dist/memory/prompts/l2-scene.d.ts +7 -0
- package/dist/memory/prompts/l2-scene.js +40 -0
- package/dist/memory/prompts/l3-persona.d.ts +6 -0
- package/dist/memory/prompts/l3-persona.js +60 -0
- package/dist/memory/recall.d.ts +47 -0
- package/dist/memory/recall.js +427 -0
- package/dist/memory/redaction.d.ts +1 -0
- package/dist/memory/redaction.js +24 -0
- package/dist/memory/retry.d.ts +13 -0
- package/dist/memory/retry.js +53 -0
- package/dist/memory/scheduler.d.ts +9 -0
- package/dist/memory/scheduler.js +16 -0
- package/dist/memory/skill-hints-loader.d.ts +30 -0
- package/dist/memory/skill-hints-loader.js +100 -0
- package/dist/memory/store/embedding.d.ts +16 -0
- package/dist/memory/store/embedding.js +68 -0
- package/dist/memory/store/reranker.d.ts +24 -0
- package/dist/memory/store/reranker.js +83 -0
- package/dist/memory/store/sqlite.d.ts +167 -0
- package/dist/memory/store/sqlite.js +1816 -0
- package/dist/memory/store/types.d.ts +101 -0
- package/dist/memory/store/types.js +1 -0
- package/dist/memory/types.d.ts +207 -0
- package/dist/memory/types.js +7 -0
- package/dist/memory/validation.d.ts +441 -0
- package/dist/memory/validation.js +129 -0
- package/dist/memory/working/canvas.d.ts +5 -0
- package/dist/memory/working/canvas.js +43 -0
- package/dist/memory/working/offload.d.ts +71 -0
- package/dist/memory/working/offload.js +211 -0
- package/dist/memory/working/step-log.d.ts +16 -0
- package/dist/memory/working/step-log.js +35 -0
- package/dist/registry.d.ts +34 -0
- package/dist/registry.js +305 -0
- package/dist/resolver.d.ts +17 -0
- package/dist/resolver.js +126 -0
- package/dist/scripts/validate-foreign-workspace-path.d.ts +1 -0
- package/dist/scripts/validate-foreign-workspace-path.js +39 -0
- package/dist/tools/agent_memory_tools.d.ts +485 -0
- package/dist/tools/agent_memory_tools.js +793 -0
- package/dist/tools/create_skill.d.ts +46 -0
- package/dist/tools/create_skill.js +46 -0
- package/dist/tools/get_doc.d.ts +21 -0
- package/dist/tools/get_doc.js +24 -0
- package/dist/tools/get_persona.d.ts +15 -0
- package/dist/tools/get_persona.js +20 -0
- package/dist/tools/get_reference.d.ts +15 -0
- package/dist/tools/get_reference.js +20 -0
- package/dist/tools/get_skill.d.ts +34 -0
- package/dist/tools/get_skill.js +65 -0
- package/dist/tools/get_template_doc.d.ts +21 -0
- package/dist/tools/get_template_doc.js +24 -0
- package/dist/tools/list_docs.d.ts +15 -0
- package/dist/tools/list_docs.js +16 -0
- package/dist/tools/list_skills.d.ts +18 -0
- package/dist/tools/list_skills.js +17 -0
- package/dist/tools/list_template_docs.d.ts +15 -0
- package/dist/tools/list_template_docs.js +16 -0
- package/dist/tools/memory-engineering.d.ts +225 -0
- package/dist/tools/memory-engineering.js +284 -0
- package/dist/tools/memory-explain.d.ts +34 -0
- package/dist/tools/memory-explain.js +109 -0
- package/dist/tools/memory-governance.d.ts +171 -0
- package/dist/tools/memory-governance.js +224 -0
- package/dist/tools/memory-hooks.d.ts +67 -0
- package/dist/tools/memory-hooks.js +102 -0
- package/dist/tools/memory-working.d.ts +98 -0
- package/dist/tools/memory-working.js +101 -0
- package/dist/tools/memory_capture_turn.d.ts +66 -0
- package/dist/tools/memory_capture_turn.js +85 -0
- package/dist/tools/memory_consolidate.d.ts +55 -0
- package/dist/tools/memory_consolidate.js +176 -0
- package/dist/tools/memory_contradictions.d.ts +53 -0
- package/dist/tools/memory_contradictions.js +52 -0
- package/dist/tools/memory_graph_query.d.ts +51 -0
- package/dist/tools/memory_graph_query.js +35 -0
- package/dist/tools/memory_mark_cited.d.ts +43 -0
- package/dist/tools/memory_mark_cited.js +63 -0
- package/dist/tools/memory_recall.d.ts +77 -0
- package/dist/tools/memory_recall.js +81 -0
- package/dist/tools/memory_register_skill_hints.d.ts +49 -0
- package/dist/tools/memory_register_skill_hints.js +55 -0
- package/dist/tools/memory_resolve_session.d.ts +24 -0
- package/dist/tools/memory_resolve_session.js +133 -0
- package/dist/tools/memory_search.d.ts +146 -0
- package/dist/tools/memory_search.js +84 -0
- package/dist/tools/search_skills.d.ts +18 -0
- package/dist/tools/search_skills.js +17 -0
- package/dist/tools/update_doc.d.ts +24 -0
- package/dist/tools/update_doc.js +35 -0
- package/dist/tools/update_skill.d.ts +30 -0
- package/dist/tools/update_skill.js +80 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +4 -0
- package/dist/writer.d.ts +30 -0
- package/dist/writer.js +220 -0
- package/docs/TEMPLATE ONLY +1 -0
- package/docs/api/API.md +64 -0
- package/docs/api/security/SECURITY.md +58 -0
- package/docs/deployment/DockerDeployment.md +30 -0
- package/docs/design/Design.md +59 -0
- package/docs/design/themes/apple.md +101 -0
- package/docs/design/themes/dieter-grid.md +100 -0
- package/docs/design/themes/gallery-white.md +100 -0
- package/docs/design/themes/pinterest.md +101 -0
- package/docs/design/themes/realty-open-house.md +101 -0
- package/docs/design/themes/vodafone.md +101 -0
- package/docs/hooks/Hooks.md +30 -0
- package/docs/schema/Schema.md +35 -0
- package/docs/strategy/ScalingStrategy.md +19 -0
- package/package.json +88 -0
- package/references/accessibility-checklist.md +160 -0
- package/references/orchestration-patterns.md +370 -0
- package/references/performance-checklist.md +153 -0
- package/references/security-checklist.md +134 -0
- package/references/testing-patterns.md +236 -0
- package/skills/agent/adr-skill/SKILL.md +299 -0
- package/skills/agent/agentic-engineering-workflow/SKILL.md +95 -0
- package/skills/agent/bootstrap-skill/SKILL.md +103 -0
- package/skills/agent/context-engineering/SKILL.md +307 -0
- package/skills/agent/debugging-and-error-recovery/SKILL.md +308 -0
- package/skills/agent/developer-growth-analysis/SKILL.md +328 -0
- package/skills/agent/doubt-driven-skill/SKILL.md +249 -0
- package/skills/agent/handover-skill/SKILL.md +112 -0
- package/skills/agent/idea-refine-skill/SKILL.md +185 -0
- package/skills/agent/idea-refine-skill/examples.md +238 -0
- package/skills/agent/idea-refine-skill/frameworks.md +99 -0
- package/skills/agent/idea-refine-skill/refinement-criteria.md +113 -0
- package/skills/agent/interview-skill/SKILL.md +226 -0
- package/skills/agent/planning-skill/SKILL.md +270 -0
- package/skills/agent/skill-authoring/SKILL.md +189 -0
- package/skills/agent/source-driven-skill/SKILL.md +197 -0
- package/skills/agent/spec-driven-skill/SKILL.md +221 -0
- package/skills/agent/sync-skill/SKILL.md +92 -0
- package/skills/agent/using-agent-skills/SKILL.md +189 -0
- package/skills/api/a11y-skill/SKILL.md +88 -0
- package/skills/api/api-skill/SKILL.md +123 -0
- package/skills/api/auth-skill/SKILL.md +80 -0
- package/skills/api/debug-skill/SKILL.md +535 -0
- package/skills/api/performance-skill/SKILL.md +100 -0
- package/skills/api/testing-skill/SKILL.md +100 -0
- package/skills/codebase/code-review-and-quality/SKILL.md +228 -0
- package/skills/codebase/code-simplification/SKILL.md +352 -0
- package/skills/codebase/code-structure-cleanup/SKILL.md +142 -0
- package/skills/codebase/concerns-skill/SKILL.md +89 -0
- package/skills/codebase/conventions-skill/SKILL.md +95 -0
- package/skills/codebase/doc-management-skill/SKILL.md +47 -0
- package/skills/codebase/git-workflow-skill/SKILL.md +312 -0
- package/skills/communication/1-3-1-rule/SKILL.md +120 -0
- package/skills/design/brutalist-skill/SKILL.md +131 -0
- package/skills/design/concept-diagrams/SKILL.md +387 -0
- package/skills/design/concept-diagrams/examples/apartment-floor-plan-conversion.md +244 -0
- package/skills/design/concept-diagrams/examples/automated-password-reset-flow.md +276 -0
- package/skills/design/concept-diagrams/examples/autonomous-llm-research-agent-flow.md +240 -0
- package/skills/design/concept-diagrams/examples/banana-journey-tree-to-smoothie.md +161 -0
- package/skills/design/concept-diagrams/examples/commercial-aircraft-structure.md +209 -0
- package/skills/design/concept-diagrams/examples/cpu-ooo-microarchitecture.md +236 -0
- package/skills/design/concept-diagrams/examples/electricity-grid-flow.md +182 -0
- package/skills/design/concept-diagrams/examples/feature-film-production-pipeline.md +172 -0
- package/skills/design/concept-diagrams/examples/hospital-emergency-department-flow.md +165 -0
- package/skills/design/concept-diagrams/examples/ml-benchmark-grouped-bar-chart.md +114 -0
- package/skills/design/concept-diagrams/examples/place-order-uml-sequence.md +325 -0
- package/skills/design/concept-diagrams/examples/smart-city-infrastructure.md +173 -0
- package/skills/design/concept-diagrams/examples/smartphone-layer-anatomy.md +154 -0
- package/skills/design/concept-diagrams/examples/sn2-reaction-mechanism.md +247 -0
- package/skills/design/concept-diagrams/examples/wind-turbine-structure.md +338 -0
- package/skills/design/concept-diagrams/references/dashboard-patterns.md +43 -0
- package/skills/design/concept-diagrams/references/infrastructure-patterns.md +144 -0
- package/skills/design/concept-diagrams/references/physical-shape-cookbook.md +42 -0
- package/skills/design/concept-diagrams/templates/template.html +174 -0
- package/skills/design/gpt-tasteskill/SKILL.md +114 -0
- package/skills/design/minimalist-skill/SKILL.md +116 -0
- package/skills/design/output-skill/SKILL.md +87 -0
- package/skills/design/redesign-skill/SKILL.md +213 -0
- package/skills/design/soft-skill/SKILL.md +132 -0
- package/skills/design/stitch-skill/EXAMPLE.md +121 -0
- package/skills/design/stitch-skill/SKILL.md +222 -0
- package/skills/design/taste-skill/SKILL.md +269 -0
- package/skills/devops/ci-cd-skill/SKILL.md +402 -0
- package/skills/devops/docker-skill/SKILL.md +297 -0
- package/skills/devops/domain-skill/SKILL.md +234 -0
- package/skills/lifecycle/changelog-generator/SKILL.md +135 -0
- package/skills/lifecycle/incremental-skill/SKILL.md +257 -0
- package/skills/lifecycle/migration-skill/SKILL.md +218 -0
- package/skills/lifecycle/shipping-skill/SKILL.md +321 -0
- package/skills/memory/agent-memory/SKILL.md +122 -0
- package/skills/qa/browser-testing-skill/SKILL.md +314 -0
- 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;
|