@kinqs/brainrouter-mcp-server 0.3.5 → 0.3.7
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 +121 -71
- package/README.md +1 -1
- package/dist/__tests__/cognitive-extractor.test.js +112 -0
- package/dist/__tests__/crypto.test.js +8 -1
- package/dist/__tests__/working-memory.test.js +67 -0
- package/dist/api/auth/crypto.js +8 -3
- package/dist/index.js +1 -1
- package/dist/memory/engine.js +21 -1
- package/dist/memory/pipeline/cognitive-extractor.js +19 -1
- package/dist/memory/recall.d.ts +3 -1
- package/dist/memory/recall.js +48 -3
- package/dist/memory/store/relevance-judge.d.ts +51 -0
- package/dist/memory/store/relevance-judge.js +196 -0
- package/dist/memory/working/canvas.js +11 -0
- package/docs/specs/0.3.7-terminal-ui-redesign.md +259 -0
- package/package.json +2 -2
- package/dist/memory/config.d.ts +0 -2
- package/dist/memory/config.js +0 -3
- package/dist/memory/pipeline/l1-contradiction.d.ts +0 -7
- package/dist/memory/pipeline/l1-contradiction.js +0 -66
- package/dist/memory/pipeline/l1-dedup.d.ts +0 -23
- package/dist/memory/pipeline/l1-dedup.js +0 -39
- package/dist/memory/pipeline/l1-extractor.d.ts +0 -21
- package/dist/memory/pipeline/l1-extractor.js +0 -180
- package/dist/memory/pipeline/l2-direction-shift.d.ts +0 -10
- package/dist/memory/pipeline/l2-direction-shift.js +0 -27
- package/dist/memory/pipeline/l2-scene.d.ts +0 -15
- package/dist/memory/pipeline/l2-scene.js +0 -140
- package/dist/memory/pipeline/l3-distiller.d.ts +0 -15
- package/dist/memory/pipeline/l3-distiller.js +0 -40
- package/dist/memory/pipeline/task-queue.d.ts +0 -54
- package/dist/memory/pipeline/task-queue.js +0 -117
- package/dist/memory/prompts/graph-extraction-batch.d.ts +0 -14
- package/dist/memory/prompts/graph-extraction-batch.js +0 -54
- package/dist/memory/prompts/l1-contradiction-batch.d.ts +0 -16
- package/dist/memory/prompts/l1-contradiction-batch.js +0 -47
- package/dist/memory/prompts/l1-contradiction.d.ts +0 -1
- package/dist/memory/prompts/l1-contradiction.js +0 -25
- package/dist/memory/prompts/l1-extraction.d.ts +0 -10
- package/dist/memory/prompts/l1-extraction.js +0 -114
- package/dist/memory/prompts/l2-direction-shift.d.ts +0 -5
- package/dist/memory/prompts/l2-direction-shift.js +0 -32
- package/dist/memory/prompts/l2-scene-cluster.d.ts +0 -2
- package/dist/memory/prompts/l2-scene-cluster.js +0 -33
- package/dist/memory/prompts/l2-scene.d.ts +0 -7
- package/dist/memory/prompts/l2-scene.js +0 -40
- package/dist/memory/prompts/l3-persona.d.ts +0 -6
- package/dist/memory/prompts/l3-persona.js +0 -60
- package/dist/memory/store/types.d.ts +0 -101
- package/dist/memory/store/types.js +0 -1
- package/dist/memory/types.d.ts +0 -207
- package/dist/memory/types.js +0 -7
- package/dist/memory/validation.d.ts +0 -441
- package/dist/memory/validation.js +0 -129
- package/dist/tools/agent_memory_tools.d.ts +0 -485
- package/dist/tools/agent_memory_tools.js +0 -793
- package/dist/tools/get_doc.d.ts +0 -21
- package/dist/tools/get_doc.js +0 -24
- package/dist/tools/list_docs.d.ts +0 -15
- package/dist/tools/list_docs.js +0 -16
- package/dist/tools/update_doc.d.ts +0 -24
- package/dist/tools/update_doc.js +0 -35
- /package/dist/__tests__/{agent_mode.test.d.ts → cognitive-extractor.test.d.ts} +0 -0
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { L1_CONTRADICTION_PROMPT } from "../prompts/l1-contradiction.js";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
|
-
export async function detectContradictions(params) {
|
|
4
|
-
const { newRecord, store, llmRunner } = params;
|
|
5
|
-
// 1. Search for potentially related memories
|
|
6
|
-
// We use keyword search on the content of the new record to find similar existing ones
|
|
7
|
-
const candidates = store.searchL1Fts(newRecord.userId, newRecord.content, 5);
|
|
8
|
-
const evaluations = [];
|
|
9
|
-
const _parsedContradictionTimeout = parseInt(process.env.BRAINROUTER_CONTRADICTION_TIMEOUT_MS || "", 10);
|
|
10
|
-
const contradictionTimeoutMs = isNaN(_parsedContradictionTimeout) ? 60000 : _parsedContradictionTimeout;
|
|
11
|
-
for (const candidate of candidates) {
|
|
12
|
-
// Don't compare with self
|
|
13
|
-
if (candidate.record_id === newRecord.id)
|
|
14
|
-
continue;
|
|
15
|
-
// Only compare if they are of the same type or both are episodic/persona
|
|
16
|
-
// (instructions don't usually contradict episodic facts)
|
|
17
|
-
const prompt = L1_CONTRADICTION_PROMPT
|
|
18
|
-
.replace("{{newContent}}", newRecord.content)
|
|
19
|
-
.replace("{{existingContent}}", candidate.content);
|
|
20
|
-
try {
|
|
21
|
-
const response = await llmRunner.run({
|
|
22
|
-
prompt,
|
|
23
|
-
taskId: `contradiction-check-${newRecord.id}-${candidate.record_id}`,
|
|
24
|
-
timeoutMs: contradictionTimeoutMs
|
|
25
|
-
});
|
|
26
|
-
// Simple JSON extraction (flexible for local models)
|
|
27
|
-
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
28
|
-
if (!jsonMatch)
|
|
29
|
-
continue;
|
|
30
|
-
const data = JSON.parse(jsonMatch[0]);
|
|
31
|
-
if (data.isContradiction && data.confidence > 0.7) {
|
|
32
|
-
evaluations.push({
|
|
33
|
-
candidate,
|
|
34
|
-
isContradiction: true,
|
|
35
|
-
confidence: data.confidence,
|
|
36
|
-
kind: data.kind || "genuine_conflict",
|
|
37
|
-
reason: data.reason
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
catch (e) {
|
|
42
|
-
console.error(`[BrainRouter] Contradiction check failed for ${newRecord.id} vs ${candidate.record_id}:`, e.message);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
// If ANY evaluation is a temporal_update, then the entire batch of contradictions represents a temporal transition!
|
|
46
|
-
const hasTemporalUpdate = evaluations.some(ev => ev.kind === "temporal_update");
|
|
47
|
-
for (const ev of evaluations) {
|
|
48
|
-
if (hasTemporalUpdate) {
|
|
49
|
-
// Treat all conflicting old records as superseded by the new record
|
|
50
|
-
console.error(`[BrainRouter] TEMPORAL UPDATE DETECTED (transition): Superseding memory ${ev.candidate.record_id} with new memory ${newRecord.id}`);
|
|
51
|
-
store.invalidateL1Record(newRecord.userId, ev.candidate.record_id, newRecord.id);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
// Genuine conflict
|
|
55
|
-
console.error(`[BrainRouter] CONTRADICTION DETECTED: ${newRecord.id} vs ${ev.candidate.record_id}`);
|
|
56
|
-
store.upsertContradiction({
|
|
57
|
-
id: `conflict_${crypto.randomBytes(4).toString("hex")}`,
|
|
58
|
-
userId: newRecord.userId,
|
|
59
|
-
recordIdA: ev.candidate.record_id,
|
|
60
|
-
recordIdB: newRecord.id,
|
|
61
|
-
reason: ev.reason,
|
|
62
|
-
confidence: ev.confidence
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { L1Record } from "@brainrouter/types";
|
|
2
|
-
import type { IMemoryStore } from "@brainrouter/types";
|
|
3
|
-
/**
|
|
4
|
-
* Result of the deduplication process
|
|
5
|
-
*/
|
|
6
|
-
export interface DedupResult {
|
|
7
|
-
/** Memories that are unique and should be stored */
|
|
8
|
-
uniqueRecords: L1Record[];
|
|
9
|
-
/** Memories that were identified as exact duplicates and dropped */
|
|
10
|
-
droppedCount: number;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Proactively deduplicate extracted memories against the existing memory store
|
|
14
|
-
* before they are stored.
|
|
15
|
-
*
|
|
16
|
-
* Uses exact/near-exact string matching to prevent identical noisy facts
|
|
17
|
-
* from accumulating in the L1 store.
|
|
18
|
-
*/
|
|
19
|
-
export declare function deduplicateMemories(params: {
|
|
20
|
-
records: L1Record[];
|
|
21
|
-
store: IMemoryStore;
|
|
22
|
-
userId: string;
|
|
23
|
-
}): Promise<DedupResult>;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Proactively deduplicate extracted memories against the existing memory store
|
|
3
|
-
* before they are stored.
|
|
4
|
-
*
|
|
5
|
-
* Uses exact/near-exact string matching to prevent identical noisy facts
|
|
6
|
-
* from accumulating in the L1 store.
|
|
7
|
-
*/
|
|
8
|
-
export async function deduplicateMemories(params) {
|
|
9
|
-
const { records, store, userId } = params;
|
|
10
|
-
if (records.length === 0) {
|
|
11
|
-
return { uniqueRecords: [], droppedCount: 0 };
|
|
12
|
-
}
|
|
13
|
-
const uniqueRecords = [];
|
|
14
|
-
let droppedCount = 0;
|
|
15
|
-
for (const newRecord of records) {
|
|
16
|
-
// 1. Keyword search to find potentially identical memories
|
|
17
|
-
// We only need top 3 to see if there is an exact match
|
|
18
|
-
const candidates = store.searchL1Fts(userId, newRecord.content, 3);
|
|
19
|
-
let isDuplicate = false;
|
|
20
|
-
for (const candidate of candidates) {
|
|
21
|
-
// Direct string comparison (case-insensitive, trimmed)
|
|
22
|
-
if (candidate.content.trim().toLowerCase() === newRecord.content.trim().toLowerCase()) {
|
|
23
|
-
isDuplicate = true;
|
|
24
|
-
break;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
if (isDuplicate) {
|
|
28
|
-
console.log(`[BrainRouter] Dropped exact duplicate memory: "${newRecord.content}"`);
|
|
29
|
-
droppedCount++;
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
uniqueRecords.push(newRecord);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
uniqueRecords,
|
|
37
|
-
droppedCount
|
|
38
|
-
};
|
|
39
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { L0Record, L1Record, LLMRunner } from "@brainrouter/types";
|
|
2
|
-
export interface L1ExtractionResult {
|
|
3
|
-
success: boolean;
|
|
4
|
-
extractedCount: number;
|
|
5
|
-
records: L1Record[];
|
|
6
|
-
sceneNames: string[];
|
|
7
|
-
errorMessage?: string;
|
|
8
|
-
}
|
|
9
|
-
export declare function extractL1Memories(params: {
|
|
10
|
-
messages: L0Record[];
|
|
11
|
-
userId: string;
|
|
12
|
-
sessionKey: string;
|
|
13
|
-
sessionId: string;
|
|
14
|
-
llmRunner: LLMRunner;
|
|
15
|
-
maxMessagesPerExtraction?: number;
|
|
16
|
-
maxBackgroundMessages?: number;
|
|
17
|
-
previousSceneName?: string;
|
|
18
|
-
existingSceneNames?: string[];
|
|
19
|
-
activeSkill?: string;
|
|
20
|
-
skillHints?: string;
|
|
21
|
-
}): Promise<L1ExtractionResult>;
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { EXTRACT_MEMORIES_SYSTEM_PROMPT, formatExtractionPrompt } from "../prompts/l1-extraction.js";
|
|
2
|
-
import { getMemoryTypeConfig } from "../memory-type-config.js";
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
|
-
const ALLOWED_MEMORY_TYPES = new Set([
|
|
5
|
-
"persona", "episodic", "instruction", "skill_context", "tool_preference",
|
|
6
|
-
"codebase_fact", "api_contract", "data_model", "dependency_constraint",
|
|
7
|
-
"environment_constraint", "architecture_decision", "implementation_decision",
|
|
8
|
-
"design_constraint", "security_policy", "performance_baseline", "bug_finding",
|
|
9
|
-
"debug_trace", "fix_summary", "verification_result", "failed_attempt",
|
|
10
|
-
"regression_risk", "task_state", "handover_note", "blocked_reason",
|
|
11
|
-
"review_comment", "release_note", "source_evidence", "artifact_reference",
|
|
12
|
-
"file_history", "command_knowledge",
|
|
13
|
-
]);
|
|
14
|
-
const ALLOWED_SOURCE_KINDS = new Set([
|
|
15
|
-
"", "user_instruction", "source_file", "command_output", "test_result",
|
|
16
|
-
"model_inference", "prior_memory",
|
|
17
|
-
]);
|
|
18
|
-
const ALLOWED_VERIFICATION_STATUSES = new Set([
|
|
19
|
-
"", "verified", "unverified", "stale",
|
|
20
|
-
]);
|
|
21
|
-
// Ensure the message has actual words to extract from, not just symbols or single letters.
|
|
22
|
-
function shouldExtractL1(text) {
|
|
23
|
-
if (!text)
|
|
24
|
-
return false;
|
|
25
|
-
const clean = text.trim();
|
|
26
|
-
if (clean.length < 3)
|
|
27
|
-
return false;
|
|
28
|
-
// If it's pure symbols/numbers, ignore
|
|
29
|
-
if (/^[^a-zA-Z\u4e00-\u9fa5]+$/.test(clean))
|
|
30
|
-
return false;
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
export async function extractL1Memories(params) {
|
|
34
|
-
const { messages, userId, sessionKey, sessionId, llmRunner, maxMessagesPerExtraction = 10, maxBackgroundMessages = 5, previousSceneName, existingSceneNames, activeSkill, skillHints } = params;
|
|
35
|
-
if (messages.length === 0) {
|
|
36
|
-
return { success: true, extractedCount: 0, records: [], sceneNames: [] };
|
|
37
|
-
}
|
|
38
|
-
const qualifiedMessages = messages.filter((m) => shouldExtractL1(m.messageText));
|
|
39
|
-
if (qualifiedMessages.length === 0) {
|
|
40
|
-
return { success: true, extractedCount: 0, records: [], sceneNames: [] };
|
|
41
|
-
}
|
|
42
|
-
const newMessages = qualifiedMessages.slice(-maxMessagesPerExtraction);
|
|
43
|
-
const bgEndIdx = qualifiedMessages.length - newMessages.length;
|
|
44
|
-
const backgroundMessages = bgEndIdx > 0
|
|
45
|
-
? qualifiedMessages.slice(Math.max(0, bgEndIdx - maxBackgroundMessages), bgEndIdx)
|
|
46
|
-
: [];
|
|
47
|
-
const userPrompt = formatExtractionPrompt({
|
|
48
|
-
newMessages,
|
|
49
|
-
backgroundMessages,
|
|
50
|
-
previousSceneName,
|
|
51
|
-
existingSceneNames,
|
|
52
|
-
activeSkill,
|
|
53
|
-
skillHints
|
|
54
|
-
});
|
|
55
|
-
let rawResult;
|
|
56
|
-
try {
|
|
57
|
-
rawResult = await llmRunner.run({
|
|
58
|
-
prompt: userPrompt,
|
|
59
|
-
systemPrompt: EXTRACT_MEMORIES_SYSTEM_PROMPT,
|
|
60
|
-
taskId: "l1-extraction",
|
|
61
|
-
timeoutMs: 120_000
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
66
|
-
console.error("[BrainRouter] LLM extraction failed:", err);
|
|
67
|
-
return { success: false, extractedCount: 0, records: [], sceneNames: [], errorMessage };
|
|
68
|
-
}
|
|
69
|
-
const parsedScenes = parseExtractionResult(rawResult);
|
|
70
|
-
const records = [];
|
|
71
|
-
const sceneNames = [];
|
|
72
|
-
const nowStr = new Date().toISOString();
|
|
73
|
-
for (const scene of parsedScenes) {
|
|
74
|
-
sceneNames.push(scene.scene_name);
|
|
75
|
-
for (const mem of scene.memories) {
|
|
76
|
-
const config = getMemoryTypeConfig(mem.type);
|
|
77
|
-
records.push({
|
|
78
|
-
id: `l1_${sessionKey}_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`,
|
|
79
|
-
userId,
|
|
80
|
-
sessionKey,
|
|
81
|
-
sessionId,
|
|
82
|
-
content: mem.content,
|
|
83
|
-
type: mem.type,
|
|
84
|
-
priority: mem.priority,
|
|
85
|
-
sceneName: scene.scene_name,
|
|
86
|
-
skillTag: mem.skill_tag || activeSkill || "",
|
|
87
|
-
halfLifeDays: config.halfLifeDays,
|
|
88
|
-
supersededBy: null,
|
|
89
|
-
timestampStr: "", // Phase 1: Not strictly tracking time from LLM, just raw extraction
|
|
90
|
-
timestampStart: "",
|
|
91
|
-
timestampEnd: "",
|
|
92
|
-
createdTime: nowStr,
|
|
93
|
-
updatedTime: nowStr,
|
|
94
|
-
metadata: mem.metadata,
|
|
95
|
-
confidence: mem.confidence ?? config.defaultConfidence,
|
|
96
|
-
status: "active",
|
|
97
|
-
sourceKind: mem.sourceKind,
|
|
98
|
-
verificationStatus: mem.verificationStatus,
|
|
99
|
-
repoPaths: mem.repoPaths,
|
|
100
|
-
filePaths: mem.filePaths,
|
|
101
|
-
commands: mem.commands,
|
|
102
|
-
// ACE fields — zero on creation, updated by citation tracking
|
|
103
|
-
citationCount: 0,
|
|
104
|
-
lastCitedAt: null,
|
|
105
|
-
neverCitedCount: 0,
|
|
106
|
-
archived: false,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
success: true,
|
|
112
|
-
extractedCount: records.length,
|
|
113
|
-
records,
|
|
114
|
-
sceneNames
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
function parseExtractionResult(raw) {
|
|
118
|
-
try {
|
|
119
|
-
let cleaned = raw.trim();
|
|
120
|
-
if (cleaned.startsWith("\`\`\`")) {
|
|
121
|
-
cleaned = cleaned.replace(/^\`\`\`(?:json)?\s*\n?/, "").replace(/\n?\`\`\`\s*$/, "");
|
|
122
|
-
}
|
|
123
|
-
const match = cleaned.match(/\[[\s\S]*\]/);
|
|
124
|
-
if (!match)
|
|
125
|
-
return [];
|
|
126
|
-
const parsed = JSON.parse(match[0]);
|
|
127
|
-
if (!Array.isArray(parsed))
|
|
128
|
-
return [];
|
|
129
|
-
const scenes = [];
|
|
130
|
-
for (const item of parsed) {
|
|
131
|
-
if (!item || typeof item !== "object")
|
|
132
|
-
continue;
|
|
133
|
-
const s = item;
|
|
134
|
-
const memories = Array.isArray(s.memories) ? s.memories.map((m) => ({
|
|
135
|
-
content: String(m.content || ""),
|
|
136
|
-
type: parseMemoryType(m.type),
|
|
137
|
-
priority: clampNumber(m.priority, 0, 100, 50),
|
|
138
|
-
skill_tag: m.skill_tag ? String(m.skill_tag) : undefined,
|
|
139
|
-
confidence: typeof m.confidence === "number" ? clampNumber(m.confidence, 0, 1, 0.65) : undefined,
|
|
140
|
-
sourceKind: parseSourceKind(m.sourceKind ?? m.source_kind),
|
|
141
|
-
verificationStatus: parseVerificationStatus(m.verificationStatus ?? m.verification_status),
|
|
142
|
-
repoPaths: parseStringArray(m.repoPaths ?? m.repo_paths),
|
|
143
|
-
filePaths: parseStringArray(m.filePaths ?? m.file_paths),
|
|
144
|
-
commands: parseStringArray(m.commands),
|
|
145
|
-
metadata: m.metadata && typeof m.metadata === "object" ? m.metadata : {}
|
|
146
|
-
})).filter((m) => m.content.length > 0) : [];
|
|
147
|
-
scenes.push({
|
|
148
|
-
scene_name: String(s.scene_name || "Unknown Scene"),
|
|
149
|
-
memories
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
return scenes;
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
console.error("[BrainRouter] Failed to parse extraction result", err);
|
|
156
|
-
return [];
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
function parseMemoryType(value) {
|
|
160
|
-
const candidate = String(value || "");
|
|
161
|
-
return ALLOWED_MEMORY_TYPES.has(candidate) ? candidate : "episodic";
|
|
162
|
-
}
|
|
163
|
-
function parseSourceKind(value) {
|
|
164
|
-
const candidate = String(value || "");
|
|
165
|
-
return ALLOWED_SOURCE_KINDS.has(candidate) ? candidate : "model_inference";
|
|
166
|
-
}
|
|
167
|
-
function parseVerificationStatus(value) {
|
|
168
|
-
const candidate = String(value || "");
|
|
169
|
-
return ALLOWED_VERIFICATION_STATUSES.has(candidate) ? candidate : "unverified";
|
|
170
|
-
}
|
|
171
|
-
function parseStringArray(value) {
|
|
172
|
-
if (!Array.isArray(value))
|
|
173
|
-
return [];
|
|
174
|
-
return [...new Set(value.map((item) => String(item).trim()).filter(Boolean))];
|
|
175
|
-
}
|
|
176
|
-
function clampNumber(value, min, max, fallback) {
|
|
177
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
178
|
-
? Math.min(max, Math.max(min, value))
|
|
179
|
-
: fallback;
|
|
180
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { L1Record, L2SceneRecord, LLMRunner } from "@brainrouter/types";
|
|
2
|
-
export declare function detectDirectionShift(params: {
|
|
3
|
-
activeScene: L2SceneRecord;
|
|
4
|
-
newL1Records: L1Record[];
|
|
5
|
-
llmRunner: LLMRunner;
|
|
6
|
-
}): Promise<{
|
|
7
|
-
shift: boolean;
|
|
8
|
-
confidence: number;
|
|
9
|
-
reason: string;
|
|
10
|
-
}>;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { L2_DIRECTION_SHIFT_SYSTEM_PROMPT, formatL2DirectionShiftPrompt } from "../prompts/l2-direction-shift.js";
|
|
2
|
-
export async function detectDirectionShift(params) {
|
|
3
|
-
const { activeScene, newL1Records, llmRunner } = params;
|
|
4
|
-
try {
|
|
5
|
-
const prompt = formatL2DirectionShiftPrompt(activeScene.sceneName, activeScene.summaryMd, newL1Records.map(r => ({ content: r.content, type: r.type })));
|
|
6
|
-
const response = await llmRunner.run({
|
|
7
|
-
prompt,
|
|
8
|
-
systemPrompt: L2_DIRECTION_SHIFT_SYSTEM_PROMPT,
|
|
9
|
-
taskId: "l2-direction-shift",
|
|
10
|
-
timeoutMs: 30_000,
|
|
11
|
-
});
|
|
12
|
-
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
13
|
-
if (!jsonMatch) {
|
|
14
|
-
throw new Error("No JSON object found in LLM response");
|
|
15
|
-
}
|
|
16
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
17
|
-
return {
|
|
18
|
-
shift: Boolean(parsed.shift),
|
|
19
|
-
confidence: Number(parsed.confidence) || 0,
|
|
20
|
-
reason: String(parsed.reason) || "",
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
catch (err) {
|
|
24
|
-
console.error(`[BrainRouter] L2 direction shift detection failed:`, err.message);
|
|
25
|
-
return { shift: false, confidence: 0, reason: "Error" };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { IMemoryStore } from "@brainrouter/types";
|
|
2
|
-
import type { LLMRunner } from "@brainrouter/types";
|
|
3
|
-
/**
|
|
4
|
-
* L2 Scene Pipeline
|
|
5
|
-
* Groups L1 memories by scene_name and asks the LLM to produce
|
|
6
|
-
* a Markdown summary for each scene. Updates heat scores.
|
|
7
|
-
*/
|
|
8
|
-
export declare function distillScenes(params: {
|
|
9
|
-
userId: string;
|
|
10
|
-
store: IMemoryStore;
|
|
11
|
-
llmRunner: LLMRunner;
|
|
12
|
-
}): Promise<{
|
|
13
|
-
scenesDistilled: number;
|
|
14
|
-
sceneNames: string[];
|
|
15
|
-
}>;
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { L2_SCENE_SYSTEM_PROMPT, formatL2ScenePrompt } from "../prompts/l2-scene.js";
|
|
2
|
-
import { L2_SCENE_CLUSTER_SYSTEM_PROMPT, formatSceneClusterPrompt } from "../prompts/l2-scene-cluster.js";
|
|
3
|
-
import { L2_MAX_SCENES } from "../scheduler.js";
|
|
4
|
-
import crypto from "node:crypto";
|
|
5
|
-
async function canonicalizeSceneNames(params) {
|
|
6
|
-
const { userId, store, llmRunner } = params;
|
|
7
|
-
const sceneNames = store.getDistinctSceneNames(userId);
|
|
8
|
-
if (sceneNames.length < 2)
|
|
9
|
-
return;
|
|
10
|
-
try {
|
|
11
|
-
const rawCluster = await llmRunner.run({
|
|
12
|
-
prompt: formatSceneClusterPrompt(sceneNames),
|
|
13
|
-
systemPrompt: L2_SCENE_CLUSTER_SYSTEM_PROMPT,
|
|
14
|
-
taskId: "l2-scene-clustering",
|
|
15
|
-
timeoutMs: 45_000,
|
|
16
|
-
});
|
|
17
|
-
const jsonMatch = rawCluster.match(/\[[\s\S]*\]/);
|
|
18
|
-
if (!jsonMatch)
|
|
19
|
-
return;
|
|
20
|
-
const clusters = JSON.parse(jsonMatch[0]);
|
|
21
|
-
if (!Array.isArray(clusters))
|
|
22
|
-
return;
|
|
23
|
-
for (const cluster of clusters) {
|
|
24
|
-
const canonical = String(cluster.canonical || "").trim();
|
|
25
|
-
const aliases = Array.isArray(cluster.aliases) ? cluster.aliases.map((a) => String(a).trim()) : [];
|
|
26
|
-
if (!canonical || aliases.length === 0)
|
|
27
|
-
continue;
|
|
28
|
-
for (const alias of aliases) {
|
|
29
|
-
if (alias === canonical)
|
|
30
|
-
continue;
|
|
31
|
-
store.renameSceneInL1Records(userId, alias, canonical);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
console.error(`[BrainRouter] Scene canonicalization failed for "${userId}":`, err.message);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* L2 Scene Pipeline
|
|
41
|
-
* Groups L1 memories by scene_name and asks the LLM to produce
|
|
42
|
-
* a Markdown summary for each scene. Updates heat scores.
|
|
43
|
-
*/
|
|
44
|
-
export async function distillScenes(params) {
|
|
45
|
-
const { userId, store, llmRunner } = params;
|
|
46
|
-
// Run scene canonicalization/clustering pass to prevent cold-start fragmentation
|
|
47
|
-
await canonicalizeSceneNames({ userId, store, llmRunner });
|
|
48
|
-
// Decay all existing heat scores (each distillation cycle = time passing)
|
|
49
|
-
store.decayL2HeatScores(userId);
|
|
50
|
-
const sceneNames = store.getDistinctSceneNames(userId);
|
|
51
|
-
if (sceneNames.length === 0) {
|
|
52
|
-
return { scenesDistilled: 0, sceneNames: [] };
|
|
53
|
-
}
|
|
54
|
-
const now = new Date().toISOString();
|
|
55
|
-
const distilled = [];
|
|
56
|
-
// Fetch existing L2 scene names up-front so the prompt can avoid near-duplicates
|
|
57
|
-
const existingL2SceneNames = store.getTopL2Scenes(userId, 50).map(s => s.sceneName);
|
|
58
|
-
for (const sceneName of sceneNames) {
|
|
59
|
-
const l1s = store.getL1sByScene(userId, sceneName, 30);
|
|
60
|
-
if (l1s.length === 0)
|
|
61
|
-
continue;
|
|
62
|
-
let summaryMd;
|
|
63
|
-
try {
|
|
64
|
-
summaryMd = await llmRunner.run({
|
|
65
|
-
prompt: formatL2ScenePrompt(sceneName, l1s, existingL2SceneNames.filter(n => n !== sceneName)),
|
|
66
|
-
systemPrompt: L2_SCENE_SYSTEM_PROMPT,
|
|
67
|
-
taskId: "l2-scene-distillation",
|
|
68
|
-
timeoutMs: 60_000,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
console.error(`[BrainRouter] L2 scene distillation failed for "${sceneName}":`, err.message);
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
const existing = store.getL2SceneByName(userId, sceneName);
|
|
76
|
-
const record = {
|
|
77
|
-
id: existing?.id ?? `l2_${crypto.randomBytes(6).toString("hex")}`,
|
|
78
|
-
userId,
|
|
79
|
-
sceneName,
|
|
80
|
-
summaryMd: summaryMd.trim(),
|
|
81
|
-
heatScore: existing ? Math.min(100, existing.heatScore + 30) : 100,
|
|
82
|
-
lastActiveTime: now,
|
|
83
|
-
createdTime: existing?.createdTime ?? now,
|
|
84
|
-
updatedTime: now,
|
|
85
|
-
};
|
|
86
|
-
store.upsertL2Scene(record);
|
|
87
|
-
distilled.push(sceneName);
|
|
88
|
-
}
|
|
89
|
-
// Auto-merge cold scenes if we exceed the max threshold
|
|
90
|
-
await mergeScenes({ userId, store, llmRunner });
|
|
91
|
-
console.error(`[BrainRouter] L2 distilled ${distilled.length} scene(s) for user "${userId}".`);
|
|
92
|
-
return { scenesDistilled: distilled.length, sceneNames: distilled };
|
|
93
|
-
}
|
|
94
|
-
async function mergeScenes(params) {
|
|
95
|
-
const { userId, store, llmRunner } = params;
|
|
96
|
-
const sceneCount = store.getL2SceneCount(userId);
|
|
97
|
-
if (sceneCount < L2_MAX_SCENES)
|
|
98
|
-
return;
|
|
99
|
-
const overflow = sceneCount - L2_MAX_SCENES + 1;
|
|
100
|
-
const coldScenes = store.getColdL2Scenes(userId, overflow + 3);
|
|
101
|
-
// Need at least 2 cold scenes to meaningfully merge
|
|
102
|
-
if (coldScenes.length < 2)
|
|
103
|
-
return;
|
|
104
|
-
// Skip any existing [Archived] scene from the merge input — we'll update it separately
|
|
105
|
-
const archiveSceneName = "[Archived]";
|
|
106
|
-
const scenesToMerge = coldScenes.filter(s => s.sceneName !== archiveSceneName);
|
|
107
|
-
if (scenesToMerge.length < 2)
|
|
108
|
-
return;
|
|
109
|
-
const sceneSummaries = scenesToMerge.map(s => `## ${s.sceneName}\n${s.summaryMd}`).join("\n\n");
|
|
110
|
-
let unifiedSummary;
|
|
111
|
-
try {
|
|
112
|
-
unifiedSummary = await llmRunner.run({
|
|
113
|
-
prompt: `Merge the following old scene summaries into a single concise "Archived Scenes" overview.\n\n${sceneSummaries}\n\nOutput only the Markdown summary. Be concise — no preamble.`,
|
|
114
|
-
systemPrompt: L2_SCENE_SYSTEM_PROMPT,
|
|
115
|
-
taskId: "l2-scene-merge",
|
|
116
|
-
timeoutMs: 60_000,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
catch (err) {
|
|
120
|
-
console.error(`[BrainRouter] L2 scene merge failed for "${userId}":`, err.message);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const now = new Date().toISOString();
|
|
124
|
-
// Look up any pre-existing [Archived] scene
|
|
125
|
-
const existingArchive = store.getL2SceneByName(userId, archiveSceneName);
|
|
126
|
-
const record = {
|
|
127
|
-
id: existingArchive?.id ?? `l2_${crypto.randomBytes(6).toString("hex")}`,
|
|
128
|
-
userId,
|
|
129
|
-
sceneName: archiveSceneName,
|
|
130
|
-
summaryMd: unifiedSummary.trim(),
|
|
131
|
-
heatScore: 10,
|
|
132
|
-
lastActiveTime: now,
|
|
133
|
-
createdTime: existingArchive?.createdTime ?? now,
|
|
134
|
-
updatedTime: now,
|
|
135
|
-
};
|
|
136
|
-
store.upsertL2Scene(record);
|
|
137
|
-
const idsToDelete = scenesToMerge.map(s => s.id);
|
|
138
|
-
store.deleteL2Scenes(userId, idsToDelete);
|
|
139
|
-
console.error(`[BrainRouter] L2 auto-merge: merged ${idsToDelete.length} cold scenes into "${archiveSceneName}".`);
|
|
140
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { IMemoryStore } from "@brainrouter/types";
|
|
2
|
-
import type { LLMRunner } from "@brainrouter/types";
|
|
3
|
-
/**
|
|
4
|
-
* L3 Persona Distillation Pipeline
|
|
5
|
-
* Scans ALL persona + instruction L1 memories across all sessions
|
|
6
|
-
* for a user and synthesizes a durable Narrative Profile.
|
|
7
|
-
*/
|
|
8
|
-
export declare function distillPersona(params: {
|
|
9
|
-
userId: string;
|
|
10
|
-
store: IMemoryStore;
|
|
11
|
-
llmRunner: LLMRunner;
|
|
12
|
-
}): Promise<{
|
|
13
|
-
success: boolean;
|
|
14
|
-
personaMd?: string;
|
|
15
|
-
}>;
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { L3_PERSONA_SYSTEM_PROMPT, formatL3PersonaPrompt } from "../prompts/l3-persona.js";
|
|
2
|
-
/**
|
|
3
|
-
* L3 Persona Distillation Pipeline
|
|
4
|
-
* Scans ALL persona + instruction L1 memories across all sessions
|
|
5
|
-
* for a user and synthesizes a durable Narrative Profile.
|
|
6
|
-
*/
|
|
7
|
-
export async function distillPersona(params) {
|
|
8
|
-
const { userId, store, llmRunner } = params;
|
|
9
|
-
// Cross-session: fetch all persona + instruction L1s for this user
|
|
10
|
-
const memories = store.getPersonaAndInstructionL1s(userId, 100);
|
|
11
|
-
if (memories.length === 0) {
|
|
12
|
-
console.error(`[BrainRouter] L3 skipped for "${userId}" — no persona/instruction memories yet.`);
|
|
13
|
-
return { success: false };
|
|
14
|
-
}
|
|
15
|
-
let personaMd;
|
|
16
|
-
try {
|
|
17
|
-
personaMd = await llmRunner.run({
|
|
18
|
-
prompt: formatL3PersonaPrompt(memories),
|
|
19
|
-
systemPrompt: L3_PERSONA_SYSTEM_PROMPT,
|
|
20
|
-
taskId: "l3-persona-distillation",
|
|
21
|
-
timeoutMs: 90_000,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
console.error(`[BrainRouter] L3 persona distillation failed for "${userId}":`, err.message);
|
|
26
|
-
return { success: false };
|
|
27
|
-
}
|
|
28
|
-
const now = new Date().toISOString();
|
|
29
|
-
const existing = store.getL3Persona(userId);
|
|
30
|
-
const record = {
|
|
31
|
-
userId,
|
|
32
|
-
personaMd: personaMd.trim(),
|
|
33
|
-
l1CountAtGeneration: memories.length,
|
|
34
|
-
createdTime: existing?.createdTime ?? now,
|
|
35
|
-
updatedTime: now,
|
|
36
|
-
};
|
|
37
|
-
store.upsertL3Persona(record);
|
|
38
|
-
console.error(`[BrainRouter] L3 persona updated for "${userId}" (${memories.length} L1s).`);
|
|
39
|
-
return { success: true, personaMd: personaMd.trim() };
|
|
40
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BrainRouter — Background Task Queue (Optimization C)
|
|
3
|
-
*
|
|
4
|
-
* Decouples L2 scene distillation and L3 persona distillation from the hot
|
|
5
|
-
* path of capture(). These are the most expensive, non-latency-critical steps.
|
|
6
|
-
*
|
|
7
|
-
* Graph extraction (Opt B) fires immediately as fire-and-forget — it is NOT queued.
|
|
8
|
-
* Contradiction detection (Opt A) also fires immediately as fire-and-forget.
|
|
9
|
-
*
|
|
10
|
-
* Flush triggers:
|
|
11
|
-
* 1. Lazy — called by recall() when the user is idle (lastCapture > idleThresholdMs)
|
|
12
|
-
* 2. Explicit — engine.flush(userId) called directly
|
|
13
|
-
* 3. Disabled — BRAINROUTER_ASYNC_PIPELINE=false keeps old fire-and-forget behaviour
|
|
14
|
-
*/
|
|
15
|
-
import type { SqliteMemoryStore } from "../store/sqlite.js";
|
|
16
|
-
import type { LLMRunner } from "../types.js";
|
|
17
|
-
export type TaskType = "l2_scene" | "l3_persona";
|
|
18
|
-
export interface PendingTask {
|
|
19
|
-
id: string;
|
|
20
|
-
userId: string;
|
|
21
|
-
taskType: TaskType;
|
|
22
|
-
/** JSON-encoded payload specific to the task type */
|
|
23
|
-
payload: string;
|
|
24
|
-
createdAt: string;
|
|
25
|
-
}
|
|
26
|
-
/** Is the async pipeline enabled? Default: true. Set to "false" to use old fire-and-forget. */
|
|
27
|
-
export declare function isAsyncPipelineEnabled(): boolean;
|
|
28
|
-
/**
|
|
29
|
-
* Enqueue a task in the persistent SQLite queue instead of firing it immediately.
|
|
30
|
-
* Safe to call from the capture hot path — synchronous SQLite insert, no I/O wait.
|
|
31
|
-
*/
|
|
32
|
-
export declare function enqueueTask(store: SqliteMemoryStore, task: {
|
|
33
|
-
userId: string;
|
|
34
|
-
taskType: TaskType;
|
|
35
|
-
payload: Record<string, unknown>;
|
|
36
|
-
}): void;
|
|
37
|
-
export interface FlushResult {
|
|
38
|
-
userId: string;
|
|
39
|
-
tasksProcessed: number;
|
|
40
|
-
tasksFailed: number;
|
|
41
|
-
durationMs: number;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Process all pending L2/L3 tasks for a user in FIFO order.
|
|
45
|
-
*
|
|
46
|
-
* Re-entrant safe: if a flush is already in progress for this userId, the existing
|
|
47
|
-
* promise is returned instead of starting a second flush.
|
|
48
|
-
*/
|
|
49
|
-
export declare function flushPendingTasks(userId: string, store: SqliteMemoryStore, synthesisRunner: LLMRunner): Promise<FlushResult>;
|
|
50
|
-
/**
|
|
51
|
-
* Returns true if the user has been idle long enough that pre-recall flushing
|
|
52
|
-
* won't noticeably delay the response.
|
|
53
|
-
*/
|
|
54
|
-
export declare function isUserIdle(store: SqliteMemoryStore, userId: string): boolean;
|