feat(server): add institutional memory recall
- File-based memory under .boocode/memory/ (project/user/reference topics) - Hierarchical 4-scope scan: global → home → project → session - Keyword/tag relevance matching for query-based recall - Injected as <boocode-memory> block in system prompt at assembly - v1 recall-only (extract/dream deferred to v2)
This commit is contained in:
72
apps/server/src/services/memory/scan.ts
Normal file
72
apps/server/src/services/memory/scan.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import type { MemoryEntry } from './entries.js';
|
||||
import { parseMemoryEntries } from './entries.js';
|
||||
import { getMemoryRoot } from './paths.js';
|
||||
|
||||
export interface MemoryScope {
|
||||
projectRoot: string;
|
||||
sessionDir?: string;
|
||||
homeDir?: string;
|
||||
}
|
||||
|
||||
async function scanDirectory(dir: string): Promise<MemoryEntry[]> {
|
||||
const entries: MemoryEntry[] = [];
|
||||
try {
|
||||
const files = await readdir(dir, { withFileTypes: true });
|
||||
for (const file of files) {
|
||||
if (file.isFile() && file.name.endsWith('.md')) {
|
||||
const content = await readFile(join(dir, file.name), 'utf8');
|
||||
entries.push(...parseMemoryEntries(file.name, content));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory doesn't exist
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
const MEMORY_TOPICS = ['project', 'user', 'reference'] as const;
|
||||
|
||||
async function scanTopicDirs(root: string): Promise<MemoryEntry[]> {
|
||||
const entries: MemoryEntry[] = [];
|
||||
for (const topic of MEMORY_TOPICS) {
|
||||
entries.push(...(await scanDirectory(join(root, topic))));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
export async function scanMemoryScopes(scope: MemoryScope): Promise<MemoryEntry[]> {
|
||||
const allEntries: MemoryEntry[] = [];
|
||||
|
||||
// 1. Global (~/.boocode/memory/) - lowest priority
|
||||
allEntries.push(...(await scanTopicDirs(getMemoryRoot(homedir()))));
|
||||
|
||||
// 2. Home ($HOME/.boocode/memory)
|
||||
const homeDir = scope.homeDir ?? homedir();
|
||||
const homeRoot = getMemoryRoot(homeDir);
|
||||
if (homeRoot !== getMemoryRoot(homedir())) {
|
||||
allEntries.push(...(await scanTopicDirs(homeRoot)));
|
||||
}
|
||||
|
||||
// 3. Project (.boocode/memory/ under project root)
|
||||
allEntries.push(...(await scanTopicDirs(getMemoryRoot(scope.projectRoot))));
|
||||
|
||||
// 4. Session (.boocode/sessions/<id>/memory.md) - highest priority
|
||||
if (scope.sessionDir) {
|
||||
try {
|
||||
const sessionFile = join(scope.sessionDir, 'memory.md');
|
||||
const content = await readFile(sessionFile, 'utf8');
|
||||
allEntries.push(...parseMemoryEntries('session-memory', content));
|
||||
} catch {
|
||||
// No session memory file
|
||||
}
|
||||
}
|
||||
|
||||
return allEntries;
|
||||
}
|
||||
|
||||
export async function scanProjectMemory(projectRoot: string): Promise<MemoryEntry[]> {
|
||||
return scanMemoryScopes({ projectRoot });
|
||||
}
|
||||
Reference in New Issue
Block a user