@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,427 @@
|
|
|
1
|
+
import { expandRecallWithGraph } from "./pipeline/graph-recall.js";
|
|
2
|
+
import { detectPrewarmSkills, buildPrewarmBlock } from "./pipeline/skill-prewarm.js";
|
|
3
|
+
import { detectTaskIntent, extractFilePathHints, getMemoryTypeConfig } from "./memory-type-config.js";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { NeuralSparkEngine } from "./pipeline/neural-spark.js";
|
|
6
|
+
function effectivePriority(memory) {
|
|
7
|
+
const halfLife = getMemoryTypeConfig(memory.type).halfLifeDays;
|
|
8
|
+
const ageMs = Date.now() - new Date(memory.created_time).getTime();
|
|
9
|
+
const ageDays = ageMs / 86_400_000;
|
|
10
|
+
let base = memory.priority;
|
|
11
|
+
if (halfLife) {
|
|
12
|
+
const decayFactor = Math.pow(0.5, ageDays / halfLife);
|
|
13
|
+
base = memory.priority * decayFactor;
|
|
14
|
+
}
|
|
15
|
+
const citationBoost = Math.min((memory.citation_count ?? 0) * 0.05, 0.30);
|
|
16
|
+
// Freshness boost: anything captured in the last 24h gets a small lift so
|
|
17
|
+
// brand-new facts surface even before they've been cited. Linear ramp from
|
|
18
|
+
// 1.15× at age 0 to 1.0× at age 1d.
|
|
19
|
+
const freshness = ageDays <= 1 ? 1 + 0.15 * (1 - ageDays) : 1;
|
|
20
|
+
return base * (1 + citationBoost) * freshness;
|
|
21
|
+
}
|
|
22
|
+
function applyFilters(records, filters) {
|
|
23
|
+
if (!filters)
|
|
24
|
+
return records;
|
|
25
|
+
const afterMs = filters.capturedAfter ? new Date(filters.capturedAfter).getTime() : undefined;
|
|
26
|
+
const beforeMs = filters.capturedBefore ? new Date(filters.capturedBefore).getTime() : undefined;
|
|
27
|
+
const types = filters.types && filters.types.length > 0 ? new Set(filters.types) : undefined;
|
|
28
|
+
const scenes = filters.scenes && filters.scenes.length > 0 ? new Set(filters.scenes) : undefined;
|
|
29
|
+
return records.filter((r) => {
|
|
30
|
+
if (types && !types.has(r.type))
|
|
31
|
+
return false;
|
|
32
|
+
if (scenes && (!r.scene_name || !scenes.has(r.scene_name)))
|
|
33
|
+
return false;
|
|
34
|
+
if (filters.skillTag && r.skill_tag !== filters.skillTag)
|
|
35
|
+
return false;
|
|
36
|
+
if (filters.minPriority !== undefined && r.priority < filters.minPriority)
|
|
37
|
+
return false;
|
|
38
|
+
if (afterMs !== undefined || beforeMs !== undefined) {
|
|
39
|
+
const created = r.created_time ? new Date(r.created_time).getTime() : NaN;
|
|
40
|
+
if (Number.isNaN(created))
|
|
41
|
+
return false;
|
|
42
|
+
if (afterMs !== undefined && created < afterMs)
|
|
43
|
+
return false;
|
|
44
|
+
if (beforeMs !== undefined && created > beforeMs)
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export class MemoryRecallPipeline {
|
|
51
|
+
store;
|
|
52
|
+
embeddingService;
|
|
53
|
+
rerankerService;
|
|
54
|
+
constructor(store, embeddingService, rerankerService) {
|
|
55
|
+
this.store = store;
|
|
56
|
+
this.embeddingService = embeddingService;
|
|
57
|
+
this.rerankerService = rerankerService;
|
|
58
|
+
}
|
|
59
|
+
async recall(params) {
|
|
60
|
+
const startTime = Date.now();
|
|
61
|
+
const { userId, sessionKey, query, activeSkill, filters } = params;
|
|
62
|
+
const intent = detectTaskIntent(query);
|
|
63
|
+
// 1. FTS5 BM25 search (Top 15)
|
|
64
|
+
const ftsResultsRaw = this.store.searchCognitiveFts(userId, query, 15);
|
|
65
|
+
const filePathResultsRaw = this.expandWithFilePathMatches(userId, query);
|
|
66
|
+
// 2. Vector search (Top 15, if enabled)
|
|
67
|
+
let vecResultsRaw = [];
|
|
68
|
+
if (this.embeddingService.isReady()) {
|
|
69
|
+
try {
|
|
70
|
+
const queryVec = await this.embeddingService.embed(query);
|
|
71
|
+
vecResultsRaw = this.store.searchCognitiveVec(userId, queryVec, 15);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
console.error("[BrainRouter] Vector search skipped during recall:", e.message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Filter the three candidate streams BEFORE RRF so the rank is computed
|
|
78
|
+
// on the actually-relevant pool, not a filtered subset of an unfiltered
|
|
79
|
+
// rank (which would bias scores toward records that happen to be in the
|
|
80
|
+
// top-15 globally even if irrelevant to the filter).
|
|
81
|
+
const ftsResults = applyFilters(ftsResultsRaw, filters);
|
|
82
|
+
const vecResults = applyFilters(vecResultsRaw, filters);
|
|
83
|
+
const filePathResults = applyFilters(filePathResultsRaw, filters);
|
|
84
|
+
if (ftsResults.length === 0 && vecResults.length === 0 && filePathResults.length === 0) {
|
|
85
|
+
const emptyStrategy = this.embeddingService.isReady() ? "hybrid-empty" : "keyword-empty";
|
|
86
|
+
const durationMs = Date.now() - startTime;
|
|
87
|
+
const recallExplanation = {
|
|
88
|
+
ftsHits: 0,
|
|
89
|
+
vecHits: 0,
|
|
90
|
+
filePathHits: 0,
|
|
91
|
+
rrfTopScore: 0,
|
|
92
|
+
intentDetected: intent,
|
|
93
|
+
typeBoosts: {},
|
|
94
|
+
skillBoostApplied: false,
|
|
95
|
+
rerankerUsed: false,
|
|
96
|
+
graphExpansion: false,
|
|
97
|
+
citationBoosts: {},
|
|
98
|
+
durationMs,
|
|
99
|
+
rerankerCandidates: 0,
|
|
100
|
+
scoredRecords: [],
|
|
101
|
+
};
|
|
102
|
+
if (!params.explain) {
|
|
103
|
+
this.writeRecallOp(userId, sessionKey, query, emptyStrategy, 0, durationMs, recallExplanation);
|
|
104
|
+
}
|
|
105
|
+
return { recallStrategy: emptyStrategy, recallExplanation };
|
|
106
|
+
}
|
|
107
|
+
// 3. RRF Merge (Reciprocal Rank Fusion)
|
|
108
|
+
const rrfMap = new Map();
|
|
109
|
+
ftsResults.forEach((r, idx) => {
|
|
110
|
+
const rank = idx + 1;
|
|
111
|
+
rrfMap.set(r.record_id, { record: r, rrfScore: 1 / (60 + rank) });
|
|
112
|
+
});
|
|
113
|
+
vecResults.forEach((r, idx) => {
|
|
114
|
+
const rank = idx + 1;
|
|
115
|
+
const existing = rrfMap.get(r.record_id);
|
|
116
|
+
if (existing) {
|
|
117
|
+
existing.rrfScore += 1 / (60 + rank);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
rrfMap.set(r.record_id, { record: r, rrfScore: 1 / (60 + rank) });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
filePathResults.forEach((r, idx) => {
|
|
124
|
+
const existing = rrfMap.get(r.record_id);
|
|
125
|
+
const filePathScore = 1 / (45 + idx + 1);
|
|
126
|
+
if (existing) {
|
|
127
|
+
existing.rrfScore += filePathScore;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
rrfMap.set(r.record_id, { record: r, rrfScore: filePathScore });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const rrfValues = Array.from(rrfMap.values()).map(v => v.rrfScore);
|
|
134
|
+
const rrfTopScore = rrfValues.length > 0 ? Math.max(...rrfValues) : 0;
|
|
135
|
+
// 4. Combine RRF with Decay + Skill boost
|
|
136
|
+
const typeBoosts = {};
|
|
137
|
+
const citationBoosts = {};
|
|
138
|
+
let skillBoostApplied = false;
|
|
139
|
+
const scoredResults = Array.from(rrfMap.values()).map(({ record, rrfScore }) => {
|
|
140
|
+
const baseScore = rrfScore * 30;
|
|
141
|
+
const priorityScore = (effectivePriority(record) / 100);
|
|
142
|
+
let finalScore = (baseScore * 0.7) + (priorityScore * 0.3);
|
|
143
|
+
if (activeSkill && record.skill_tag === activeSkill) {
|
|
144
|
+
finalScore *= 1.2;
|
|
145
|
+
skillBoostApplied = true;
|
|
146
|
+
}
|
|
147
|
+
const intentMultiplier = getMemoryTypeConfig(record.type).intentAffinity[intent] ?? 1;
|
|
148
|
+
if (intentMultiplier !== 1) {
|
|
149
|
+
typeBoosts[record.type] = intentMultiplier;
|
|
150
|
+
}
|
|
151
|
+
finalScore *= intentMultiplier;
|
|
152
|
+
const citationCount = record.citation_count ?? 0;
|
|
153
|
+
const citBoost = Math.min(citationCount * 0.05, 0.30);
|
|
154
|
+
if (citBoost > 0) {
|
|
155
|
+
citationBoosts[record.record_id] = citBoost;
|
|
156
|
+
}
|
|
157
|
+
return { record, score: finalScore };
|
|
158
|
+
});
|
|
159
|
+
// --- Neural Sparks & Spreading Activation ---
|
|
160
|
+
const maxScore = scoredResults.length > 0 ? Math.max(...scoredResults.map(r => r.score)) : 1.0;
|
|
161
|
+
const initialNodes = scoredResults.map(r => ({
|
|
162
|
+
id: r.record.record_id,
|
|
163
|
+
potential: maxScore > 0 ? r.score / maxScore : 0.0,
|
|
164
|
+
fired: false
|
|
165
|
+
}));
|
|
166
|
+
const sparkEngine = new NeuralSparkEngine(this.store);
|
|
167
|
+
const propagatedNodes = sparkEngine.propagateSparks(userId, initialNodes);
|
|
168
|
+
const propagatedMap = new Map(propagatedNodes.map(n => [n.id, n]));
|
|
169
|
+
const existingIds = new Set(scoredResults.map(r => r.record.record_id));
|
|
170
|
+
// Carry the full {id, potential, fired, type, preview, sceneName} so the
|
|
171
|
+
// UI can render a human-friendly label instead of the opaque record id.
|
|
172
|
+
// Track seen ids so we don't double-list a node that appears as both a
|
|
173
|
+
// seed and a propagation target.
|
|
174
|
+
const sparkedNodes = [];
|
|
175
|
+
const sparkedSeen = new Set();
|
|
176
|
+
const previewFromContent = (content) => {
|
|
177
|
+
const text = (content ?? "").toString().trim();
|
|
178
|
+
if (!text)
|
|
179
|
+
return undefined;
|
|
180
|
+
const oneLine = text.replace(/\s+/g, " ");
|
|
181
|
+
// Keep the preview short — the UI renders a compact pill, anything
|
|
182
|
+
// longer than ~70 chars wraps awkwardly even with ellipsis fallback.
|
|
183
|
+
return oneLine.length > 70 ? `${oneLine.slice(0, 67)}…` : oneLine;
|
|
184
|
+
};
|
|
185
|
+
const pushNode = (node, meta) => {
|
|
186
|
+
if (!node.id || sparkedSeen.has(node.id))
|
|
187
|
+
return;
|
|
188
|
+
sparkedSeen.add(node.id);
|
|
189
|
+
sparkedNodes.push({
|
|
190
|
+
id: node.id,
|
|
191
|
+
potential: Math.max(0, Math.min(1, Number(node.potential) || 0)),
|
|
192
|
+
fired: Boolean(node.fired),
|
|
193
|
+
type: meta?.type,
|
|
194
|
+
preview: meta?.preview,
|
|
195
|
+
sceneName: meta?.sceneName,
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
const sparkScoredResults = [];
|
|
199
|
+
for (const scored of scoredResults) {
|
|
200
|
+
const propNode = propagatedMap.get(scored.record.record_id);
|
|
201
|
+
if (propNode) {
|
|
202
|
+
const newScore = Math.max(scored.score, propNode.potential * maxScore);
|
|
203
|
+
// Every initial-seed node belongs in the trace, fired or not — the
|
|
204
|
+
// sub-threshold pills carry useful "we considered this but it didn't
|
|
205
|
+
// spread" signal.
|
|
206
|
+
pushNode(propNode, {
|
|
207
|
+
type: scored.record.type,
|
|
208
|
+
preview: previewFromContent(scored.record.content),
|
|
209
|
+
sceneName: scored.record.scene_name,
|
|
210
|
+
});
|
|
211
|
+
sparkScoredResults.push({
|
|
212
|
+
record: scored.record,
|
|
213
|
+
score: propNode.fired ? newScore * 1.5 : newScore,
|
|
214
|
+
fired: propNode.fired
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
sparkScoredResults.push(scored);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Pull in connected memories that were excited above the firing threshold
|
|
222
|
+
for (const propNode of propagatedNodes) {
|
|
223
|
+
if (propNode.fired && !existingIds.has(propNode.id)) {
|
|
224
|
+
const record = this.store.getMemoryById(userId, propNode.id);
|
|
225
|
+
if (record) {
|
|
226
|
+
pushNode(propNode, {
|
|
227
|
+
type: record.type,
|
|
228
|
+
preview: previewFromContent(record.content),
|
|
229
|
+
sceneName: record.sceneName,
|
|
230
|
+
});
|
|
231
|
+
const formattedRecord = {
|
|
232
|
+
record_id: record.id,
|
|
233
|
+
user_id: record.userId,
|
|
234
|
+
content: record.content,
|
|
235
|
+
type: record.type,
|
|
236
|
+
priority: record.priority,
|
|
237
|
+
scene_name: record.sceneName,
|
|
238
|
+
skill_tag: record.skillTag,
|
|
239
|
+
session_key: record.sessionKey,
|
|
240
|
+
timestamp_str: record.timestampStr,
|
|
241
|
+
created_time: record.createdTime,
|
|
242
|
+
citation_count: record.citationCount
|
|
243
|
+
};
|
|
244
|
+
const baseScore = propNode.potential * maxScore;
|
|
245
|
+
sparkScoredResults.push({
|
|
246
|
+
record: formattedRecord,
|
|
247
|
+
score: baseScore * 1.5,
|
|
248
|
+
fired: true
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
sparkScoredResults.sort((a, b) => b.score - a.score);
|
|
254
|
+
let topResults = sparkScoredResults.slice(0, 5);
|
|
255
|
+
// Stage 3 — Reranker (Top 20 from RRF + Sparks)
|
|
256
|
+
const rerankCandidates = sparkScoredResults.slice(0, 20);
|
|
257
|
+
let usedReranker = false;
|
|
258
|
+
if (this.rerankerService.isReady()) {
|
|
259
|
+
try {
|
|
260
|
+
const documents = rerankCandidates.map(r => r.record.content);
|
|
261
|
+
const ranked = await this.rerankerService.rerank({
|
|
262
|
+
query,
|
|
263
|
+
documents,
|
|
264
|
+
topN: this.rerankerService.getTopN()
|
|
265
|
+
});
|
|
266
|
+
topResults = ranked.map(r => rerankCandidates[r.index]);
|
|
267
|
+
usedReranker = true;
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
console.error("[BrainRouter] Reranker failed during recall, falling back to RRF:", e.message);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// 5. Format for context
|
|
274
|
+
const memoryLines = topResults.map(({ record }) => {
|
|
275
|
+
const tag = record.scene_name ? `${record.type}|${record.scene_name}` : record.type;
|
|
276
|
+
let line = `- [${tag}] ${record.content}`;
|
|
277
|
+
if (record.skill_tag) {
|
|
278
|
+
line += ` (skill: ${record.skill_tag})`;
|
|
279
|
+
}
|
|
280
|
+
return line;
|
|
281
|
+
});
|
|
282
|
+
const prependContext = `<relevant-memories>\n The following memories are relevant to this query. Reference only if helpful:\n\n ${memoryLines.join("\n ")}\n</relevant-memories>`;
|
|
283
|
+
// Build appendSystemContext with Contextual Focus Navigation + tools guide
|
|
284
|
+
const topScenes = this.store.getTopContextualFocus(userId, 3);
|
|
285
|
+
let appendSystemContext = "";
|
|
286
|
+
if (topScenes.length > 0) {
|
|
287
|
+
const sceneNav = topScenes
|
|
288
|
+
.map(s => ` - ${s.sceneName} (heat: ${s.heatScore.toFixed(0)})`)
|
|
289
|
+
.join("\n");
|
|
290
|
+
appendSystemContext += `<scene-navigation>\n Recent focus scenes:\n${sceneNav}\n</scene-navigation>\n\n`;
|
|
291
|
+
}
|
|
292
|
+
appendSystemContext += `<memory-tools-guide>
|
|
293
|
+
Use memory_search to retrieve more specific memories.
|
|
294
|
+
Use memory_contradictions to review unresolved conflicts.
|
|
295
|
+
Max 3 memory tool calls per turn.
|
|
296
|
+
</memory-tools-guide>`;
|
|
297
|
+
// Graph context expansion (2-hop BFS from matched entities)
|
|
298
|
+
const graphContext = expandRecallWithGraph({
|
|
299
|
+
topCognitiveResults: topResults.map(r => r.record),
|
|
300
|
+
query,
|
|
301
|
+
userId,
|
|
302
|
+
activeSkill,
|
|
303
|
+
store: this.store
|
|
304
|
+
});
|
|
305
|
+
const hasGraphExpansion = !!graphContext;
|
|
306
|
+
if (graphContext) {
|
|
307
|
+
appendSystemContext += `\n${graphContext}`;
|
|
308
|
+
}
|
|
309
|
+
if (process.env.BRAINROUTER_PREWARM_ENABLED === "true") {
|
|
310
|
+
try {
|
|
311
|
+
const prewarmResults = detectPrewarmSkills({
|
|
312
|
+
userId,
|
|
313
|
+
store: this.store,
|
|
314
|
+
excludeSkill: activeSkill,
|
|
315
|
+
});
|
|
316
|
+
const prewarmBlock = buildPrewarmBlock(prewarmResults);
|
|
317
|
+
if (prewarmBlock) {
|
|
318
|
+
appendSystemContext += `\n${prewarmBlock}`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
console.error("[BrainRouter] Skill pre-warming skipped:", e.message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const recalledCognitiveMemories = topResults.map(r => ({
|
|
326
|
+
content: r.record.content,
|
|
327
|
+
score: r.score,
|
|
328
|
+
type: r.record.type,
|
|
329
|
+
recordId: r.record.record_id,
|
|
330
|
+
skillTag: r.record.skill_tag
|
|
331
|
+
}));
|
|
332
|
+
const recallStrategy = vecResults.length > 0
|
|
333
|
+
? (usedReranker ? "hybrid+rerank" : "hybrid")
|
|
334
|
+
: (usedReranker ? "keyword+rerank" : (filePathResults.length > 0 ? "keyword+file" : "keyword"));
|
|
335
|
+
const durationMs = Date.now() - startTime;
|
|
336
|
+
const recallExplanation = {
|
|
337
|
+
ftsHits: ftsResults.length,
|
|
338
|
+
vecHits: vecResults.length,
|
|
339
|
+
filePathHits: filePathResults.length,
|
|
340
|
+
rrfTopScore,
|
|
341
|
+
intentDetected: intent,
|
|
342
|
+
typeBoosts,
|
|
343
|
+
skillBoostApplied,
|
|
344
|
+
rerankerUsed: usedReranker,
|
|
345
|
+
graphExpansion: hasGraphExpansion,
|
|
346
|
+
citationBoosts,
|
|
347
|
+
durationMs,
|
|
348
|
+
rerankerCandidates: rerankCandidates.length,
|
|
349
|
+
scoredRecords: topResults.map(r => ({
|
|
350
|
+
recordId: r.record.record_id,
|
|
351
|
+
finalScore: r.score,
|
|
352
|
+
type: r.record.type,
|
|
353
|
+
})),
|
|
354
|
+
sparkedNodes,
|
|
355
|
+
};
|
|
356
|
+
if (!params.explain) {
|
|
357
|
+
this.writeRecallOp(userId, sessionKey, query, recallStrategy, topResults.length, durationMs, recallExplanation);
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
prependContext,
|
|
361
|
+
appendSystemContext,
|
|
362
|
+
recalledCognitiveMemories,
|
|
363
|
+
recallStrategy,
|
|
364
|
+
activeFocusName: topScenes[0]?.sceneName,
|
|
365
|
+
recallExplanation,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
async explainRecall(params) {
|
|
369
|
+
return this.recall({ ...params, explain: true });
|
|
370
|
+
}
|
|
371
|
+
writeRecallOp(userId, sessionKey, query, strategy, hitCount, durationMs, explanation) {
|
|
372
|
+
try {
|
|
373
|
+
this.store.insertOperation({
|
|
374
|
+
id: randomUUID(),
|
|
375
|
+
userId,
|
|
376
|
+
recordId: null,
|
|
377
|
+
operation: "recall",
|
|
378
|
+
actor: "agent",
|
|
379
|
+
sessionKey,
|
|
380
|
+
reason: "",
|
|
381
|
+
createdAt: new Date().toISOString(),
|
|
382
|
+
metadata: {
|
|
383
|
+
query: query.slice(0, 500),
|
|
384
|
+
strategy,
|
|
385
|
+
hitCount,
|
|
386
|
+
durationMs,
|
|
387
|
+
ftsHits: explanation?.ftsHits ?? 0,
|
|
388
|
+
vecHits: explanation?.vecHits ?? 0,
|
|
389
|
+
intentDetected: explanation?.intentDetected ?? "none",
|
|
390
|
+
rerankerUsed: explanation?.rerankerUsed ?? false,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
// Audit writes are best-effort
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
expandWithFilePathMatches(userId, query) {
|
|
399
|
+
const filePaths = extractFilePathHints(query);
|
|
400
|
+
if (filePaths.length === 0)
|
|
401
|
+
return [];
|
|
402
|
+
const records = new Map();
|
|
403
|
+
for (const filePath of filePaths) {
|
|
404
|
+
for (const record of this.store.getMemoriesByFilePath(userId, filePath, 10)) {
|
|
405
|
+
records.set(record.id, record);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return Array.from(records.values()).map((record) => ({
|
|
409
|
+
record_id: record.id,
|
|
410
|
+
user_id: record.userId,
|
|
411
|
+
content: record.content,
|
|
412
|
+
type: record.type,
|
|
413
|
+
priority: record.priority,
|
|
414
|
+
scene_name: record.sceneName,
|
|
415
|
+
skill_tag: record.skillTag,
|
|
416
|
+
score: 1,
|
|
417
|
+
timestamp_str: record.timestampStr,
|
|
418
|
+
timestamp_start: record.timestampStart,
|
|
419
|
+
timestamp_end: record.timestampEnd,
|
|
420
|
+
session_key: record.sessionKey,
|
|
421
|
+
session_id: record.sessionId,
|
|
422
|
+
metadata_json: JSON.stringify(record.metadata),
|
|
423
|
+
created_time: record.createdTime,
|
|
424
|
+
citation_count: record.citationCount,
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function redactSensitiveMemoryText(text: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Each pattern targets a specific secret format.
|
|
2
|
+
// All patterns use the /g flag so String.replace replaces all occurrences per call.
|
|
3
|
+
// The array is module-level (not re-created per call) — safe because .replace() does
|
|
4
|
+
// not mutate lastIndex on string arguments.
|
|
5
|
+
const REDACTION_PATTERNS = [
|
|
6
|
+
// HTTP Authorization: Bearer <token>
|
|
7
|
+
[/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "[REDACTED]"],
|
|
8
|
+
// OpenAI-style secret keys (sk-...)
|
|
9
|
+
[/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]"],
|
|
10
|
+
// GitHub personal access tokens (ghp_...)
|
|
11
|
+
[/\bghp_[A-Za-z0-9_]{8,}\b/g, "[REDACTED]"],
|
|
12
|
+
// PEM private key blocks
|
|
13
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED]"],
|
|
14
|
+
// Database connection strings (Postgres, MongoDB, MySQL, Redis, SQLite).
|
|
15
|
+
[/\b(?:postgres|postgresql|mongodb|mysql|mongodb\+srv|redis|sqlite):\/\/[^:\s]+:[^@\s]+@[^\s]+\b/gi, "[REDACTED_CONN_STR]"],
|
|
16
|
+
// IPv4 addresses can expose infrastructure details.
|
|
17
|
+
[/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "[REDACTED_IP]"],
|
|
18
|
+
// .env-style assignments: API_KEY=... SECRET=... — require ≥6 chars in value to
|
|
19
|
+
// avoid over-redacting innocuous env vars like RETRY_COUNT=3 or LOG_LEVEL=info.
|
|
20
|
+
[/^[ \t]*[A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Z0-9_]*[ \t]*=[ \t]*\S{6,}.*$/gim, "[REDACTED]"],
|
|
21
|
+
];
|
|
22
|
+
export function redactSensitiveMemoryText(text) {
|
|
23
|
+
return REDACTION_PATTERNS.reduce((value, [pattern, replacement]) => value.replace(pattern, replacement), text);
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ExternalApiRetryOptions {
|
|
2
|
+
label: string;
|
|
3
|
+
maxRetries?: number;
|
|
4
|
+
baseDelayMs?: number;
|
|
5
|
+
sleep?: (ms: number) => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare class ExternalApiError extends Error {
|
|
8
|
+
readonly status?: number | undefined;
|
|
9
|
+
constructor(message: string, status?: number | undefined);
|
|
10
|
+
}
|
|
11
|
+
export declare function isRetryableExternalError(error: unknown): boolean;
|
|
12
|
+
export declare function retryExternalCall<T>(operation: () => Promise<T>, options: ExternalApiRetryOptions): Promise<T>;
|
|
13
|
+
export declare function fetchWithExternalRetry(input: string | URL | Request, init: RequestInit, options: ExternalApiRetryOptions): Promise<Response>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export class ExternalApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
constructor(message, status) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.name = "ExternalApiError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
10
|
+
const DEFAULT_BASE_DELAY_MS = 2_000;
|
|
11
|
+
const RETRYABLE_HTTP_STATUSES = new Set([429, 503]);
|
|
12
|
+
function defaultSleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
export function isRetryableExternalError(error) {
|
|
16
|
+
if (error instanceof ExternalApiError) {
|
|
17
|
+
return error.status !== undefined && RETRYABLE_HTTP_STATUSES.has(error.status);
|
|
18
|
+
}
|
|
19
|
+
if (error instanceof TypeError) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
export async function retryExternalCall(operation, options) {
|
|
25
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
26
|
+
const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
27
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
28
|
+
let attempt = 0;
|
|
29
|
+
while (true) {
|
|
30
|
+
try {
|
|
31
|
+
return await operation();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (attempt >= maxRetries || !isRetryableExternalError(error)) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const delayMs = baseDelayMs * (2 ** attempt);
|
|
38
|
+
attempt += 1;
|
|
39
|
+
console.error(`[BrainRouter] ${options.label} failed with a retryable error. Retrying in ${delayMs}ms (attempt ${attempt}/${maxRetries}).`);
|
|
40
|
+
await sleep(delayMs);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function fetchWithExternalRetry(input, init, options) {
|
|
45
|
+
return retryExternalCall(async () => {
|
|
46
|
+
const response = await fetch(input, init);
|
|
47
|
+
if (RETRYABLE_HTTP_STATUSES.has(response.status)) {
|
|
48
|
+
response.body?.cancel().catch(() => undefined);
|
|
49
|
+
throw new ExternalApiError(`${options.label} failed with HTTP ${response.status} ${response.statusText}`, response.status);
|
|
50
|
+
}
|
|
51
|
+
return response;
|
|
52
|
+
}, options);
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Focus distillation fires every N new Cognitive extractions per user (default: 10). */
|
|
2
|
+
export declare const FOCUS_TRIGGER_EVERY_N: number;
|
|
3
|
+
/** Identity distillation fires every N new Cognitive extractions per user (default: 50). */
|
|
4
|
+
export declare const IDENTITY_TRIGGER_EVERY_N: number;
|
|
5
|
+
/** Focus auto-merge max scenes threshold (default: 20). */
|
|
6
|
+
export declare const MAX_FOCUS_SCENES: number;
|
|
7
|
+
import type { SchedulerState } from "@kinqs/brainrouter-types";
|
|
8
|
+
export declare function shouldRunFocusDistill(state: SchedulerState): boolean;
|
|
9
|
+
export declare function shouldRunIdentityDistill(state: SchedulerState): boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// ============================
|
|
2
|
+
// Scheduler — N-turn trigger evaluator
|
|
3
|
+
// ============================
|
|
4
|
+
// Stateless logic only. State is persisted in the DB via SqliteMemoryStore.
|
|
5
|
+
/** Focus distillation fires every N new Cognitive extractions per user (default: 10). */
|
|
6
|
+
export const FOCUS_TRIGGER_EVERY_N = parseInt(process.env.BRAINROUTER_FOCUS_TRIGGER_N ?? "10", 10);
|
|
7
|
+
/** Identity distillation fires every N new Cognitive extractions per user (default: 50). */
|
|
8
|
+
export const IDENTITY_TRIGGER_EVERY_N = parseInt(process.env.BRAINROUTER_IDENTITY_TRIGGER_N ?? "50", 10);
|
|
9
|
+
/** Focus auto-merge max scenes threshold (default: 20). */
|
|
10
|
+
export const MAX_FOCUS_SCENES = parseInt(process.env.BRAINROUTER_MAX_FOCUS_SCENES ?? "20", 10);
|
|
11
|
+
export function shouldRunFocusDistill(state) {
|
|
12
|
+
return state.cognitiveCountSinceLastFocus >= FOCUS_TRIGGER_EVERY_N;
|
|
13
|
+
}
|
|
14
|
+
export function shouldRunIdentityDistill(state) {
|
|
15
|
+
return state.cognitiveCountSinceLastIdentity >= IDENTITY_TRIGGER_EVERY_N;
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface SkillFrontmatter {
|
|
2
|
+
name?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
memory_hints?: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse YAML frontmatter from a SKILL.md file.
|
|
9
|
+
* Uses a simple, zero-dependency regex parser that handles the common
|
|
10
|
+
* key: value and key: | (block scalar) patterns used in BrainRouter skills.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseSkillFrontmatter(content: string): SkillFrontmatter;
|
|
13
|
+
/**
|
|
14
|
+
* Load memory_hints from a SKILL.md file.
|
|
15
|
+
* Returns null if the file doesn't exist or has no memory_hints field.
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadSkillHints(skillMdPath: string): {
|
|
18
|
+
name: string;
|
|
19
|
+
hints: string;
|
|
20
|
+
} | null;
|
|
21
|
+
/**
|
|
22
|
+
* Scan a directory tree for SKILL.md files that contain memory_hints.
|
|
23
|
+
* Returns an array of { skillDir, name, hints } for each skill found.
|
|
24
|
+
*/
|
|
25
|
+
export declare function scanSkillsForHints(rootDir: string): Array<{
|
|
26
|
+
skillDir: string;
|
|
27
|
+
name: string;
|
|
28
|
+
hints: string;
|
|
29
|
+
filePath: string;
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Parse YAML frontmatter from a SKILL.md file.
|
|
4
|
+
* Uses a simple, zero-dependency regex parser that handles the common
|
|
5
|
+
* key: value and key: | (block scalar) patterns used in BrainRouter skills.
|
|
6
|
+
*/
|
|
7
|
+
export function parseSkillFrontmatter(content) {
|
|
8
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
9
|
+
if (!fmMatch)
|
|
10
|
+
return {};
|
|
11
|
+
const yaml = fmMatch[1];
|
|
12
|
+
const result = {};
|
|
13
|
+
// Parse block scalars (key: |\n line1\n line2) and simple key: value lines
|
|
14
|
+
const lines = yaml.split(/\r?\n/);
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < lines.length) {
|
|
17
|
+
const line = lines[i];
|
|
18
|
+
// Detect block scalar: "key: |"
|
|
19
|
+
const blockMatch = line.match(/^(\w[\w_-]*):\s*\|(.*)$/);
|
|
20
|
+
if (blockMatch) {
|
|
21
|
+
const key = blockMatch[0].split(":")[0].trim();
|
|
22
|
+
const blockLines = [];
|
|
23
|
+
i++;
|
|
24
|
+
// Collect indented lines
|
|
25
|
+
while (i < lines.length && (lines[i].startsWith(" ") || lines[i].startsWith("\t") || lines[i] === "")) {
|
|
26
|
+
blockLines.push(lines[i].replace(/^ /, "").replace(/^\t/, ""));
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
result[key] = blockLines.join("\n").trimEnd();
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// Simple key: value
|
|
33
|
+
const kvMatch = line.match(/^(\w[\w_-]*):\s*(.*)$/);
|
|
34
|
+
if (kvMatch) {
|
|
35
|
+
const key = kvMatch[1];
|
|
36
|
+
const val = kvMatch[2].trim().replace(/^["']|["']$/g, ""); // strip optional quotes
|
|
37
|
+
result[key] = val;
|
|
38
|
+
}
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Load memory_hints from a SKILL.md file.
|
|
45
|
+
* Returns null if the file doesn't exist or has no memory_hints field.
|
|
46
|
+
*/
|
|
47
|
+
export function loadSkillHints(skillMdPath) {
|
|
48
|
+
if (!existsSync(skillMdPath))
|
|
49
|
+
return null;
|
|
50
|
+
let content;
|
|
51
|
+
try {
|
|
52
|
+
content = readFileSync(skillMdPath, "utf-8");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const fm = parseSkillFrontmatter(content);
|
|
58
|
+
if (!fm.memory_hints || typeof fm.memory_hints !== "string" || !fm.memory_hints.trim()) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const skillName = typeof fm.name === "string" ? fm.name : "";
|
|
62
|
+
return {
|
|
63
|
+
name: skillName,
|
|
64
|
+
hints: fm.memory_hints.trim()
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Scan a directory tree for SKILL.md files that contain memory_hints.
|
|
69
|
+
* Returns an array of { skillDir, name, hints } for each skill found.
|
|
70
|
+
*/
|
|
71
|
+
export function scanSkillsForHints(rootDir) {
|
|
72
|
+
const results = [];
|
|
73
|
+
function walk(dir) {
|
|
74
|
+
try {
|
|
75
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
walk(`${dir}/${entry.name}`);
|
|
79
|
+
}
|
|
80
|
+
else if (entry.name === "SKILL.md") {
|
|
81
|
+
const filePath = `${dir}/${entry.name}`;
|
|
82
|
+
const loaded = loadSkillHints(filePath);
|
|
83
|
+
if (loaded) {
|
|
84
|
+
results.push({
|
|
85
|
+
skillDir: dir,
|
|
86
|
+
name: loaded.name,
|
|
87
|
+
hints: loaded.hints,
|
|
88
|
+
filePath
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
walk(rootDir);
|
|
99
|
+
return results;
|
|
100
|
+
}
|