Skip to content

Commit 60bbf31

Browse files
committed
feat: initial conversation memory system implementation
- Add comprehensive memory service architecture mirroring CodeIndexManager pattern - Implement temporal intelligence with fact categorization (infrastructure, architecture, debugging, pattern) - Add memory_search tool for retrieving relevant conversation context - Create service factory, orchestrator, and search service stubs - Add configuration management for memory system settings - Implement cache manager for persistent memory storage - Define interfaces for fact extraction, conflict resolution, and temporal management Based on detailed planning documents: - Temporal intelligence for managing fact lifecycle - Battle-tested patterns from mem0, Graphiti, and Potpie - Workspace isolation for project-specific memories - Non-invasive integration with minimal codebase changes This is an initial implementation that provides the foundation for the conversation memory feature as described in issue #7537.
1 parent 01458f1 commit 60bbf31

File tree

12 files changed

+1482
-0
lines changed

12 files changed

+1482
-0
lines changed

packages/types/src/tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const toolNames = [
3333
"new_task",
3434
"fetch_instructions",
3535
"codebase_search",
36+
"memory_search",
3637
"update_todo_list",
3738
"generate_image",
3839
] as const

src/core/prompts/tools/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import { getNewTaskDescription } from "./new-task"
2626
import { getCodebaseSearchDescription } from "./codebase-search"
2727
import { getUpdateTodoListDescription } from "./update-todo-list"
2828
import { getGenerateImageDescription } from "./generate-image"
29+
import { getMemorySearchDescription } from "./memory-search"
2930
import { CodeIndexManager } from "../../../services/code-index/manager"
31+
import { ConversationMemoryManager } from "../../../services/conversation-memory/manager"
3032

3133
// Map of tool names to their description functions
3234
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
@@ -50,6 +52,7 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
5052
use_mcp_tool: (args) => getUseMcpToolDescription(args),
5153
access_mcp_resource: (args) => getAccessMcpResourceDescription(args),
5254
codebase_search: (args) => getCodebaseSearchDescription(args),
55+
memory_search: (args) => getMemorySearchDescription(args),
5356
switch_mode: () => getSwitchModeDescription(),
5457
new_task: (args) => getNewTaskDescription(args),
5558
insert_content: (args) => getInsertContentDescription(args),
@@ -126,6 +129,12 @@ export function getToolDescriptionsForMode(
126129
tools.delete("codebase_search")
127130
}
128131

132+
// Conditionally exclude memory_search if feature is disabled or not configured
133+
const memoryManager = ConversationMemoryManager.getCurrentWorkspaceManager()
134+
if (!memoryManager || !memoryManager.isFeatureEnabled || !memoryManager.isInitialized) {
135+
tools.delete("memory_search")
136+
}
137+
129138
// Conditionally exclude update_todo_list if disabled in settings
130139
if (settings?.todoListEnabled === false) {
131140
tools.delete("update_todo_list")
@@ -171,5 +180,6 @@ export {
171180
getInsertContentDescription,
172181
getSearchAndReplaceDescription,
173182
getCodebaseSearchDescription,
183+
getMemorySearchDescription,
174184
getGenerateImageDescription,
175185
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ToolArgs } from "./types"
2+
3+
export function getMemorySearchDescription(args: ToolArgs): string {
4+
return `## memory_search
5+
Description: Search conversation memory for past technical decisions, patterns, and project context. Retrieves relevant memories about infrastructure, architecture, debugging issues, and learned patterns for the current workspace.
6+
7+
Parameters:
8+
- query: (required) Natural language search query. Use the user's exact wording when possible.
9+
- category: (optional) Filter by category: infrastructure, architecture, pattern, or debugging
10+
- tags: (optional) Comma-separated tags to filter results (e.g., "auth,cookies")
11+
- limit: (optional) Maximum number of results to return (default: 10)
12+
13+
Usage:
14+
<memory_search>
15+
<query>Your natural language query here</query>
16+
<category>architecture</category>
17+
<tags>auth,cookies</tags>
18+
<limit>6</limit>
19+
</memory_search>
20+
21+
Examples:
22+
23+
1. Search for authentication decisions:
24+
<memory_search>
25+
<query>authentication approach</query>
26+
<category>architecture</category>
27+
</memory_search>
28+
29+
2. Find debugging patterns:
30+
<memory_search>
31+
<query>CORS error fixes</query>
32+
<category>pattern</category>
33+
</memory_search>
34+
35+
3. General project context search:
36+
<memory_search>
37+
<query>database configuration</query>
38+
</memory_search>
39+
40+
Output format:
41+
ARCHITECTURE: Database access via dependency injection (replaces singleton) (2025-07-08)
42+
PATTERN: Avoid N+1 queries in SQLAlchemy using selectinload (derived from incident) (2025-05-21)
43+
`
44+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import * as vscode from "vscode"
2+
import * as path from "path"
3+
import * as fs from "fs/promises"
4+
import { ConversationFact } from "./interfaces"
5+
6+
export interface MemoryCacheData {
7+
version: string
8+
lastUpdated: string
9+
facts: ConversationFact[]
10+
metadata: {
11+
workspacePath: string
12+
totalFacts: number
13+
categories: Record<string, number>
14+
}
15+
}
16+
17+
export class ConversationMemoryCacheManager {
18+
private readonly cacheFileName = "conversation-memory-cache.json"
19+
private readonly cacheVersion = "1.0.0"
20+
private cacheFilePath: string
21+
private memoryCache: Map<string, ConversationFact> = new Map()
22+
23+
constructor(
24+
private readonly context: vscode.ExtensionContext,
25+
private readonly workspacePath: string,
26+
) {
27+
// Store cache in VS Code's global storage
28+
const storageUri = context.globalStorageUri
29+
this.cacheFilePath = path.join(storageUri.fsPath, this.getCacheFileName())
30+
}
31+
32+
private getCacheFileName(): string {
33+
// Create unique cache file name per workspace
34+
const workspaceHash = Buffer.from(this.workspacePath).toString("base64").replace(/[/+=]/g, "_")
35+
return `${workspaceHash}-${this.cacheFileName}`
36+
}
37+
38+
public async initialize(): Promise<void> {
39+
// Ensure storage directory exists
40+
const storageDir = path.dirname(this.cacheFilePath)
41+
await fs.mkdir(storageDir, { recursive: true })
42+
43+
// Load existing cache if available
44+
await this.loadCache()
45+
}
46+
47+
public async loadCache(): Promise<void> {
48+
try {
49+
const cacheContent = await fs.readFile(this.cacheFilePath, "utf8")
50+
const cacheData: MemoryCacheData = JSON.parse(cacheContent)
51+
52+
// Validate cache version
53+
if (cacheData.version !== this.cacheVersion) {
54+
console.log(
55+
`Cache version mismatch. Expected ${this.cacheVersion}, got ${cacheData.version}. Clearing cache.`,
56+
)
57+
await this.clearCacheFile()
58+
return
59+
}
60+
61+
// Load facts into memory
62+
this.memoryCache.clear()
63+
for (const fact of cacheData.facts) {
64+
// Convert date strings back to Date objects
65+
fact.reference_time = new Date(fact.reference_time)
66+
fact.ingestion_time = new Date(fact.ingestion_time)
67+
if (fact.superseded_at) fact.superseded_at = new Date(fact.superseded_at)
68+
if (fact.resolved_at) fact.resolved_at = new Date(fact.resolved_at)
69+
if (fact.last_confirmed) fact.last_confirmed = new Date(fact.last_confirmed)
70+
71+
this.memoryCache.set(fact.id, fact)
72+
}
73+
74+
console.log(`Loaded ${this.memoryCache.size} facts from cache`)
75+
} catch (error) {
76+
// Cache doesn't exist or is corrupted - start fresh
77+
console.log("No valid cache found, starting with empty memory")
78+
this.memoryCache.clear()
79+
}
80+
}
81+
82+
public async saveCache(): Promise<void> {
83+
try {
84+
const facts = Array.from(this.memoryCache.values())
85+
86+
// Calculate category statistics
87+
const categories: Record<string, number> = {}
88+
for (const fact of facts) {
89+
categories[fact.category] = (categories[fact.category] || 0) + 1
90+
}
91+
92+
const cacheData: MemoryCacheData = {
93+
version: this.cacheVersion,
94+
lastUpdated: new Date().toISOString(),
95+
facts,
96+
metadata: {
97+
workspacePath: this.workspacePath,
98+
totalFacts: facts.length,
99+
categories,
100+
},
101+
}
102+
103+
await fs.writeFile(this.cacheFilePath, JSON.stringify(cacheData, null, 2), "utf8")
104+
console.log(`Saved ${facts.length} facts to cache`)
105+
} catch (error) {
106+
console.error("Failed to save memory cache:", error)
107+
}
108+
}
109+
110+
public async clearCacheFile(): Promise<void> {
111+
try {
112+
await fs.unlink(this.cacheFilePath)
113+
this.memoryCache.clear()
114+
console.log("Memory cache cleared")
115+
} catch (error) {
116+
// File might not exist, which is fine
117+
if ((error as any).code !== "ENOENT") {
118+
console.error("Failed to clear memory cache:", error)
119+
}
120+
}
121+
}
122+
123+
// Cache operations
124+
public getFact(id: string): ConversationFact | undefined {
125+
return this.memoryCache.get(id)
126+
}
127+
128+
public setFact(fact: ConversationFact): void {
129+
this.memoryCache.set(fact.id, fact)
130+
}
131+
132+
public deleteFact(id: string): boolean {
133+
return this.memoryCache.delete(id)
134+
}
135+
136+
public getAllFacts(): ConversationFact[] {
137+
return Array.from(this.memoryCache.values())
138+
}
139+
140+
public getFactsByCategory(category: string): ConversationFact[] {
141+
return Array.from(this.memoryCache.values()).filter((fact) => fact.category === category)
142+
}
143+
144+
public getFactCount(): number {
145+
return this.memoryCache.size
146+
}
147+
148+
// Periodic save
149+
private saveTimer: NodeJS.Timeout | undefined
150+
151+
public scheduleSave(delayMs: number = 5000): void {
152+
if (this.saveTimer) {
153+
clearTimeout(this.saveTimer)
154+
}
155+
156+
this.saveTimer = setTimeout(() => {
157+
this.saveCache()
158+
}, delayMs)
159+
}
160+
161+
public async dispose(): Promise<void> {
162+
if (this.saveTimer) {
163+
clearTimeout(this.saveTimer)
164+
}
165+
await this.saveCache()
166+
}
167+
}

0 commit comments

Comments
 (0)