Skip to content

Commit 1383373

Browse files
committed
test: Isolate REM pipeline test data to local workspace tmp directory to prevent mock pollution (#9746)
1 parent 320db45 commit 1383373

9 files changed

Lines changed: 170 additions & 84 deletions

File tree

ai/mcp/server/memory-core/config.template.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ const defaultConfig = {
130130
session: process.env.SESSION_COLLECTION_NAME || 'neo-agent-sessions',
131131
graph : process.env.GRAPH_COLLECTION_NAME || 'neo-native-graph'
132132
},
133+
/**
134+
* Target markdown file used for autonomous agent-to-user reporting (offline jobs).
135+
* @type {string}
136+
*/
137+
handoffFilePath: path.resolve(cwd, 'resources/content/sandman_handoff.md'),
133138
/**
134139
* Universal JSONL backup/export directory for all databases.
135140
* @type {string}

ai/mcp/server/memory-core/services/DreamService.mjs

Lines changed: 78 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
import fs from 'fs';
2-
import path from 'path';
3-
import yaml from 'js-yaml';
4-
import {fileURLToPath} from 'url';
5-
import crypto from 'crypto';
6-
import aiConfig from '../config.mjs';
7-
import Base from '../../../../../src/core/Base.mjs';
8-
import StorageRouter from '../managers/StorageRouter.mjs';
9-
import SQLiteVectorManager from '../managers/SQLiteVectorManager.mjs';
1+
import fs from 'fs';
2+
import path from 'path';
3+
import yaml from 'js-yaml';
4+
import { fileURLToPath } from 'url';
5+
import crypto from 'crypto';
6+
import aiConfig from '../config.mjs';
7+
import Base from '../../../../../src/core/Base.mjs';
8+
import StorageRouter from '../managers/StorageRouter.mjs';
9+
import SQLiteVectorManager from '../managers/SQLiteVectorManager.mjs';
1010
import TextEmbeddingService from './TextEmbeddingService.mjs';
11-
import GraphService from './GraphService.mjs';
12-
import Json from '../../../../../src/util/Json.mjs';
13-
import logger from '../logger.mjs';
14-
import Ollama from '../../../../provider/Ollama.mjs';
15-
import FileSystemIngestor from './FileSystemIngestor.mjs';
11+
import GraphService from './GraphService.mjs';
12+
import Json from '../../../../../src/util/Json.mjs';
13+
import logger from '../logger.mjs';
14+
import Ollama from '../../../../provider/Ollama.mjs';
15+
import FileSystemIngestor from './FileSystemIngestor.mjs';
16+
17+
const __filename = fileURLToPath(import.meta.url);
18+
const __dirname = path.dirname(__filename);
1619

1720
/**
1821
* @summary Service for offline GraphRAG extraction ("REM Sleep").
@@ -31,7 +34,7 @@ class DreamService extends Base {
3134
* @member {String} className='Neo.ai.mcp.server.memory-core.services.DreamService'
3235
* @protected
3336
*/
34-
className: 'Neo.ai.mcp.server.memory-core.services.DreamService',
37+
className: 'Neo.ai.mcp.server.memory-core.services.DreamService',
3538
/**
3639
* @member {Boolean} singleton=true
3740
* @protected
@@ -311,9 +314,7 @@ ${contextText}
311314
}
312315

313316
// Write to sandman_handoff.md
314-
const __filename = fileURLToPath(import.meta.url);
315-
const __dirname = path.dirname(__filename);
316-
const handoffFile = path.resolve(__dirname, '../../../../../resources/content/sandman_handoff.md');
317+
const handoffFile = aiConfig.handoffFilePath;
317318

318319
let handoffContent = '';
319320
if (fs.existsSync(handoffFile)) {
@@ -325,9 +326,14 @@ ${contextText}
325326
let newAlerts = false;
326327
for (const conflict of payload.conflicts) {
327328
const entry = `- **[${conflict.type}]** \`${conflict.issueId}\`: ${conflict.description} (Source Session: ${sessionId})\n`;
328-
if (!handoffContent.includes(entry)) {
329-
handoffContent += entry;
330-
newAlerts = true;
329+
const conflictIdentifier = `- **[${conflict.type}]** \`${conflict.issueId}\`:`;
330+
if (!handoffContent.includes(conflictIdentifier)) {
331+
// Check if *any* conflicting alert for this exact issue ID is already logged, to be safe.
332+
const anyConflictIdentifier = `\`${conflict.issueId}\`:`;
333+
if (!handoffContent.includes(anyConflictIdentifier)) {
334+
handoffContent += entry;
335+
newAlerts = true;
336+
}
331337
}
332338
}
333339

@@ -350,28 +356,27 @@ ${contextText}
350356
* @param {Object} session The wrapped session object
351357
* @param {Object} extractedPayload The parsed Tri-Vector schema
352358
*/
353-
async executeCapabilityGapInference(session, extractedPayload) {
354-
if (!extractedPayload || !extractedPayload.graph || !Array.isArray(extractedPayload.graph.nodes)) return;
355-
356-
// Find high-level architectural node outputs from this session
357-
const structuralNodes = extractedPayload.graph.nodes.filter(n =>
358-
n.type === 'FEATURE' || n.type === 'CONCEPT' || n.type === 'CLASS'
359+
async executeCapabilityGapInference(session, payload) {
360+
if (!payload || !payload.graph || !payload.graph.nodes) return;
361+
362+
const structuralNodes = payload.graph.nodes.filter(n =>
363+
n.type === 'FEATURE' || n.type === 'EPIC' || n.type === 'ISSUE' || n.type === 'CLASS'
359364
);
360365

361366
if (structuralNodes.length === 0) return;
362367

363-
logger.info(`[DreamService] Launching Capability Gap ReAct Loop for session ${session.meta.sessionId}...`);
364-
365-
// Retrieve the current native repository document tree for analysis contextualization
366-
const fsNodes = GraphService.db.nodes.items.filter(n =>
368+
logger.debug(`[DreamService] Launching Capability Gap Inference passes for ${structuralNodes.length} nodes...`);
369+
370+
// Resolve absolute root directory
371+
const neoRootDir = path.resolve(__dirname, '../../../../../');
372+
const handoffFile = aiConfig.handoffFilePath;
373+
const fsNodes = GraphService.db.nodes.items.filter(n =>
367374
n.label === 'FILE' || n.label === 'DIRECTORY'
368-
).map(n => n.properties?.path || '').filter(p =>
375+
).map(n => n.properties?.path || '').filter(p =>
369376
p && (p.startsWith('docs/') || p.startsWith('learn/') || p.startsWith('test/') || p.startsWith('src/'))
370377
).join('\n');
371378

372-
const __filename = fileURLToPath(import.meta.url);
373-
const neoRootDir = path.resolve(path.dirname(__filename), '../../../../../');
374-
const handoffFile = path.resolve(neoRootDir, 'resources/content/sandman_handoff.md');
379+
375380

376381
const provider = Neo.create(Ollama, {
377382
modelName: aiConfig.ollama.model
@@ -404,7 +409,7 @@ NEVER output raw markdown or conversational text. YOU MUST output EXACTLY ONE JS
404409
const result = await provider.generate(messages, {
405410
response_format: { type: 'json_object' }
406411
});
407-
412+
408413
const payload = Json.extract(result.content);
409414
if (!payload || !payload.action) break;
410415

@@ -422,24 +427,29 @@ NEVER output raw markdown or conversational text. YOU MUST output EXACTLY ONE JS
422427
const raw = fs.readFileSync(targetPath, 'utf8');
423428
messages.push({ role: 'assistant', content: result.content });
424429
messages.push({ role: 'user', content: `File contents of ${payload.path}:\n\n${raw}` });
425-
} catch(e) {
430+
} catch (e) {
426431
messages.push({ role: 'assistant', content: result.content });
427432
messages.push({ role: 'user', content: `Target path ${payload.path} does not physically exist. Cannot read.` });
428433
}
429434
} else if (payload.action === 'alert' && payload.message) {
430435
// We found a legitimate gap! Append to sandman_handoff.md!
431436
let handoffContent = fs.existsSync(handoffFile) ? fs.readFileSync(handoffFile, 'utf8') : '';
432437
const entry = `- **[Codebase Gap]** Node \`${node.name}\`: ${payload.message} (Source Session: ${session.meta.sessionId})\n`;
433-
if (!handoffContent.includes(entry)) {
438+
439+
// Check if a gap for this specific node already exists!
440+
const nodeIdentifier = `- **[Codebase Gap]** Node \`${node.name}\`:`;
441+
if (!handoffContent.includes(nodeIdentifier)) {
434442
fs.writeFileSync(handoffFile, handoffContent + entry, 'utf8');
435443
logger.info(`[DreamService] Gap Alert logged for ${node.name}.`);
444+
} else {
445+
logger.debug(`[DreamService] Gap Alert already exists for ${node.name}. Skipping duplication.`);
436446
}
437447
break;
438448
} else {
439449
logger.debug(`[DreamService] Gap Analyzer passed on ${node.name}.`);
440450
break; // action === 'pass' or fallback
441451
}
442-
} catch(e) {
452+
} catch (e) {
443453
if (e.message && e.message.includes('fetch failed')) {
444454
logger.debug('[DreamService] Skipping Gap Analysis (Ollama daemon offline).');
445455
} else {
@@ -459,8 +469,8 @@ NEVER output raw markdown or conversational text. YOU MUST output EXACTLY ONE JS
459469
*/
460470
async ingestIssueStates() {
461471
const __filename = fileURLToPath(import.meta.url);
462-
const __dirname = path.dirname(__filename);
463-
const issuesDir = path.resolve(__dirname, '../../../../../resources/content/issues');
472+
const __dirname = path.dirname(__filename);
473+
const issuesDir = path.resolve(__dirname, '../../../../../resources/content/issues');
464474

465475
if (!fs.existsSync(issuesDir)) {
466476
logger.warn(`[DreamService] Issues directory not found at ${issuesDir}`);
@@ -496,7 +506,7 @@ NEVER output raw markdown or conversational text. YOU MUST output EXACTLY ONE JS
496506

497507
parsedIssues.push({ issueId, meta, content, file });
498508
}
499-
} catch(e) {
509+
} catch (e) {
500510
logger.warn(`[DreamService] Failed to parse frontmatter for ${file}`, e);
501511
}
502512
}
@@ -585,7 +595,7 @@ NEVER output raw markdown or conversational text. YOU MUST output EXACTLY ONE JS
585595
body
586596
});
587597
}
588-
} catch(e) {
598+
} catch (e) {
589599
logger.warn(`[DreamService] Failed to link edges for ${file}`, e);
590600
}
591601
}
@@ -636,23 +646,23 @@ NEVER output raw markdown or conversational text. YOU MUST output EXACTLY ONE JS
636646
// Vector Apoptosis: Identify orphans and purge from Hybrid Store
637647
logger.info('[DreamService] Initializing Vector Apoptosis (Orphaned Node Cleanup)...');
638648
const orphaned = GraphService.getOrphanedNodes();
639-
649+
640650
if (orphaned.length > 0) {
641651
logger.info(`[DreamService] Apoptosis detected ${orphaned.length} orphaned nodes. Commencing eradication...`);
642652
GraphService.removeNodes(orphaned);
643653

644654
if (SQLiteVectorManager.db) {
645655
// Cross-layer purge from semantic embeddings
646656
logger.info(`[DreamService] Purging semantic vectors for ${orphaned.length} deleted nodes.`);
647-
657+
648658
['neo_graph_nodes', 'neo_agent_sessions_summary'].forEach(async collectionName => {
649659
try {
650660
const collection = await SQLiteVectorManager.getOrCreateCollection({ name: collectionName });
651661
if (collection) {
652662
await collection.delete({ ids: orphaned });
653663
}
654-
} catch(e) {
655-
logger.warn(`[DreamService] Apoptosis soft-failure on collection ${collectionName}: ${e.message}`);
664+
} catch (e) {
665+
logger.warn(`[DreamService] Apoptosis soft-failure on collection ${collectionName}: ${e.message}`);
656666
}
657667
});
658668
}
@@ -679,14 +689,14 @@ NEVER output raw markdown or conversational text. YOU MUST output EXACTLY ONE JS
679689
try {
680690
const sessionsVec = await SQLiteVectorManager.getSummaryCollection();
681691
const recent = await sessionsVec.get({ limit: 2, include: ['documents'] });
682-
692+
683693
let frontierText = "Neo.mjs Active Strategic Context: ";
684694
if (recent && recent.documents.length > 0) {
685695
frontierText += recent.documents.join("\n\n");
686696
} else {
687697
frontierText += "Initialization and Stabilization.";
688698
}
689-
699+
690700
logger.debug('[DreamService] Computing Frontier Baseline Vector...');
691701
frontierEmbedding = await TextEmbeddingService.embedText(frontierText, aiConfig.neoEmbeddingProvider);
692702
} catch (e) {
@@ -695,7 +705,7 @@ NEVER output raw markdown or conversational text. YOU MUST output EXACTLY ONE JS
695705
}
696706

697707
const f32 = new Float32Array(frontierEmbedding);
698-
708+
699709
// Execute the unified Hybrid SQL Query directly mapping native structural weights against active vectors!
700710
const stmt = SQLiteVectorManager.db.prepare(`
701711
SELECT
@@ -723,7 +733,7 @@ NEVER output raw markdown or conversational text. YOU MUST output EXACTLY ONE JS
723733

724734
for (const row of results) {
725735
const issueId = row.id;
726-
736+
727737
// Re-verify blocker topology natively using GraphService API
728738
const blockers = GraphService.db.edges.getByIndex('target', issueId).filter(e => e.type === 'BLOCKS');
729739
let isBlocked = false;
@@ -740,15 +750,15 @@ NEVER output raw markdown or conversational text. YOU MUST output EXACTLY ONE JS
740750

741751
const semantic_distance = parseFloat(row.semantic_distance) || 0.1;
742752
const struct_score = parseFloat(row.struct_score) || 0;
743-
753+
744754
// Lower distance = Higher significance. (Add 0.1 to avoid div by 0 and curb massive asymptotes)
745755
const semanticScore = 1.0 / (semantic_distance + 0.1);
746-
756+
747757
const priority = (semanticScore * SEMANTIC_WEIGHT) + (struct_score * STRUCTURAL_WEIGHT);
748758

749759
// Re-inflate node JSON locally
750760
let nodeData = null;
751-
try { nodeData = JSON.parse(row.data); } catch(e) {}
761+
try { nodeData = JSON.parse(row.data); } catch (e) { }
752762

753763
scoredNodes.push({
754764
node: nodeData || { id: issueId },
@@ -783,8 +793,20 @@ NEVER output raw markdown or conversational text. YOU MUST output EXACTLY ONE JS
783793

784794
const handoffFile = path.resolve(__dirname, '../../../../../resources/content/sandman_handoff.md');
785795
if (fs.existsSync(handoffFile)) {
786-
fs.appendFileSync(handoffFile, markdownAppend, 'utf-8');
787-
logger.info(`[DreamService] Golden Path recommendations exported to sandman_handoff.md`);
796+
let currentContent = fs.readFileSync(handoffFile, 'utf-8');
797+
const goldenPathHeader = `\n## Computed Golden Path (Strategic Recommendation)\n\n`;
798+
const headerIndex = currentContent.indexOf(goldenPathHeader.trim()); // trim() to handle potential newline variances
799+
800+
if (headerIndex !== -1) {
801+
// Replace everything from the header to the end of the file
802+
currentContent = currentContent.substring(0, headerIndex) + markdownAppend;
803+
} else {
804+
// Header not found, append safely
805+
currentContent += markdownAppend;
806+
}
807+
808+
fs.writeFileSync(handoffFile, currentContent, 'utf-8');
809+
logger.info(`[DreamService] Golden Path recommendations exported to sandman_handoff.md`);
788810
}
789811

790812
logger.info(`[DreamService] Mathematical Golden Path established. Anchored ${topNodes.length} strategic nodes to frontier.`);

resources/content/sandman_handoff.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,19 @@ This file tracks topological conflict alerts generated during overnight REM slee
55
## Active Conflicts
66

77
- **[SUPERSEDES]** `epic-9736`: The detailed architectural refinement path for Hebbian Memory Integration was superseded by the user-initiated and high-priority pivot to the entirely new task: 'Implement Agent Self-Discovery via Neural Link Introspection' (epic-9299). (Source Session: bfb21a96-8591-477d-a517-e8cd39eda19a)
8-
- **[OBSOLETES]** `Epic #9736`: The extensive work detailing Hebbian Memory Integration, Topological Ingestion, and Passive Artifact Ingestion is rendered obsolete by the final, complete pivot to a new primary objective: Epic #9299, 'Implement Agent Self-Discovery via Neural Link Introspection'. The topological focus has fundamentally shifted. (Source Session: bfb21a96-8591-477d-a517-e8cd39eda19a)
9-
- **[SUPERSEDES]** `issue-9736`: The core objective of Hebbian Memory Integration has been superseded by the user-initiated, high-level pivot to implement Agent Self-Discovery (#9299). (Source Session: bfb21a96-8591-477d-a517-e8cd39eda19a)
108
- **[SUPERSEDES]** `issue-9737`: This sub-task is structurally dependent on the now-superseded primary objective, Hebbian Memory Integration (#9736). (Source Session: bfb21a96-8591-477d-a517-e8cd39eda19a)
119
- **[SUPERSEDES]** `issue-9738`: This sub-task is structurally dependent on the now-superseded primary objective, Hebbian Memory Integration (#9736). (Source Session: bfb21a96-8591-477d-a517-e8cd39eda19a)
12-
- **[Codebase Gap]** Node `FileSystemIngestor`: [DOC_GAP] The active directory tree is empty, so there is no existing documentation, testing, or learning material found for the FileSystemIngestor feature. Comprehensive coverage needs to be established. (Source Session: mock-session-id)
10+
11+
- **[SUPERSEDES]** `issue-9743`: The complete implementation of the 'memory-core' REM/Dream extraction pipeline, including Graph-RAG and the automated handover protocol, renders the requirements addressed by this ticket obsolete and superseded. (Source Session: 41df81fb-54b0-41ae-a179-d3b0f12faa36)
12+
- **[SUPERSEDES]** `issue-9742`: The fully functional 'memory-core' and the implementation of robust path traversal security and configuration-driven ingestors supersede the original requirements of this ticket. (Source Session: 41df81fb-54b0-41ae-a179-d3b0f12faa36)
13+
- **[SUPERSEDES]** `issue-9741`: The resolution of all critical SQLite integration issues and the implementation of Vector Apoptosis ensure that the system state surpasses the scope and requirements of this ticket. (Source Session: 41df81fb-54b0-41ae-a179-d3b0f12faa36)
14+
- **[SUPERSEDES]** `issue-9740`: The successful creation of a configuration-driven FileSystemIngestor and the autonomous status of the 'memory-core' supersede the initial goals of this ticket. (Source Session: 41df81fb-54b0-41ae-a179-d3b0f12faa36)
15+
16+
17+
## Computed Golden Path (Strategic Recommendation)
18+
19+
Based on the latest Tri-Vector Synthesis and Topological Priorities, the following tasks are mathematically recommended as the next immediate focus:
20+
21+
1. **issue-9673**: Score 11.00 (Semantic: 5.00, Structural: 1.00)
22+
2. **issue-9637**: Score 11.00 (Semantic: 5.00, Structural: 1.00)
23+
3. **issue-9636**: Score 11.00 (Semantic: 5.00, Structural: 1.00)

test/playwright/unit/ai/graph/Database.spec.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@ import Database from '../../../../../ai/graph/Database.mjs';
2020
import SQLite from '../../../../../ai/graph/storage/SQLite.mjs';
2121
import fs from 'fs-extra';
2222
import path from 'path';
23-
import os from 'os';
2423

2524
test.describe('Neo.ai.graph.Database', () => {
2625
let db;
2726
let testRun = 0;
2827

2928
// Build an isolated tmp path for the database file tests
30-
const dbPath = path.join(os.tmpdir(), 'neo-graph-test.db');
29+
const tmpDir = path.resolve(process.cwd(), 'tmp');
30+
if (!fs.existsSync(tmpDir)) {
31+
fs.mkdirSync(tmpDir, { recursive: true });
32+
}
33+
const dbPath = path.join(tmpDir, 'neo-graph-test.db');
3134

3235
test.beforeEach(async () => {
3336
testRun++;

0 commit comments

Comments
 (0)