Skip to content

Commit 882d0f1

Browse files
committed
Add memory management include and delete actions
1 parent 0cc3623 commit 882d0f1

File tree

3 files changed

+183
-13
lines changed

3 files changed

+183
-13
lines changed

packages/server/src/index.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ import { dirname, join } from 'path'
77
import { fileURLToPath } from 'url'
88
import { learnFromDeletions, learnFromTrajectoryRevisions, getLearnedPreferences, clearPreferences, deletePreference } from './preference.js'
99
import { createSession, getSession, listSessions, completeSession, setSessionRewriting, updateSessionPayload } from './store.js'
10-
import { buildMemoryCatalog, buildMemoryReviewPayload, readMemoryFileContent } from './memory.js'
10+
import {
11+
buildMemoryCatalog,
12+
buildMemoryReviewPayload,
13+
deleteMemoryFile,
14+
includeMemoryFileInContext,
15+
readMemoryFileContent,
16+
removeMemoryFileFromContext,
17+
} from './memory.js'
1118

1219
const app = express()
1320
const DEFAULT_PORT = 38173
@@ -252,6 +259,33 @@ app.get('/api/memory/file', (req, res) => {
252259
res.json(file)
253260
})
254261

262+
app.post('/api/memory/include', (req, res) => {
263+
const projectRoot = join(__dirname, '../../..')
264+
const filePath = typeof req.body?.path === 'string' ? req.body.path : ''
265+
if (!filePath) return res.status(400).json({ error: 'Missing path in request body' })
266+
const result = includeMemoryFileInContext({ projectRoot, filePath })
267+
if (!result.ok) return res.status(404).json({ error: 'File not found in memory catalog' })
268+
res.json(result)
269+
})
270+
271+
app.post('/api/memory/exclude', (req, res) => {
272+
const projectRoot = join(__dirname, '../../..')
273+
const filePath = typeof req.body?.path === 'string' ? req.body.path : ''
274+
if (!filePath) return res.status(400).json({ error: 'Missing path in request body' })
275+
const result = removeMemoryFileFromContext({ projectRoot, filePath })
276+
if (!result.ok) return res.status(404).json({ error: 'File not found in memory catalog' })
277+
res.json(result)
278+
})
279+
280+
app.delete('/api/memory/file', (req, res) => {
281+
const projectRoot = join(__dirname, '../../..')
282+
const filePath = typeof req.query.path === 'string' ? req.query.path : ''
283+
if (!filePath) return res.status(400).json({ error: 'Missing path query' })
284+
const result = deleteMemoryFile({ projectRoot, filePath })
285+
if (!result.ok) return res.status(400).json({ error: result.reason || 'Failed to delete file' })
286+
res.json(result)
287+
})
288+
255289
app.post('/api/memory/review/create', async (req, res) => {
256290
const { sessionKey, noOpen, openHome, currentContextFiles, generatedContent } = req.body ?? {}
257291
const projectRoot = join(__dirname, '../../..')

packages/server/src/memory.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export interface MemoryCatalogPayload {
5151
defaultIncludedFileIds: string[]
5252
}
5353

54+
const MEMORY_INCLUDE_STATE_PATH = path.join(os.homedir(), '.openclaw', 'agentclick-memory-includes.json')
55+
5456
function walkMarkdownFiles(baseDir: string, options?: { maxFiles?: number }): string[] {
5557
const maxFiles = options?.maxFiles ?? 200
5658
const output: string[] = []
@@ -91,6 +93,23 @@ function safeRead(filePath: string, maxChars = 12000): string {
9193
}
9294
}
9395

96+
function readIncludedMemoryPaths(): string[] {
97+
try {
98+
if (!fs.existsSync(MEMORY_INCLUDE_STATE_PATH)) return []
99+
const raw = fs.readFileSync(MEMORY_INCLUDE_STATE_PATH, 'utf-8')
100+
const parsed = JSON.parse(raw) as { includedPaths?: string[] }
101+
return (parsed.includedPaths ?? []).filter(Boolean)
102+
} catch {
103+
return []
104+
}
105+
}
106+
107+
function writeIncludedMemoryPaths(paths: string[]): void {
108+
const dir = path.dirname(MEMORY_INCLUDE_STATE_PATH)
109+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
110+
fs.writeFileSync(MEMORY_INCLUDE_STATE_PATH, JSON.stringify({ includedPaths: paths }, null, 2), 'utf-8')
111+
}
112+
94113
function parseSections(markdown: string): Array<{ id: string; title: string }> {
95114
const out: Array<{ id: string; title: string }> = []
96115
const lines = markdown.split('\n')
@@ -134,7 +153,11 @@ export function buildMemoryCatalog(input: {
134153
currentContextFiles?: string[]
135154
}): MemoryCatalogPayload {
136155
const projectRoot = input.projectRoot
137-
const currentContextSet = new Set((input.currentContextFiles ?? []).map(p => path.resolve(projectRoot, p)))
156+
const persistedIncludes = readIncludedMemoryPaths().map(p => path.resolve(p))
157+
const currentContextSet = new Set([
158+
...persistedIncludes,
159+
...(input.currentContextFiles ?? []).map(p => path.resolve(projectRoot, p)),
160+
])
138161
const relatedMarkdown = walkMarkdownFiles(projectRoot, { maxFiles: 220 })
139162
const projectMemoryFiles = relatedMarkdown.filter(p => path.basename(p).toLowerCase().includes('memory'))
140163
const agentCacheFiles = collectAgentCacheMemoryFiles()
@@ -239,6 +262,43 @@ export function readMemoryFileContent(input: {
239262
}
240263
}
241264

265+
export function includeMemoryFileInContext(input: { projectRoot: string; filePath: string }): { ok: boolean; includedPaths: string[] } {
266+
const catalog = buildMemoryCatalog({ projectRoot: input.projectRoot })
267+
const target = catalog.files.find(f => path.resolve(f.path) === path.resolve(input.filePath))
268+
if (!target) return { ok: false, includedPaths: readIncludedMemoryPaths() }
269+
const current = new Set(readIncludedMemoryPaths().map(p => path.resolve(p)))
270+
current.add(path.resolve(target.path))
271+
const next = Array.from(current.values()).sort()
272+
writeIncludedMemoryPaths(next)
273+
return { ok: true, includedPaths: next }
274+
}
275+
276+
export function removeMemoryFileFromContext(input: { projectRoot: string; filePath: string }): { ok: boolean; includedPaths: string[] } {
277+
const catalog = buildMemoryCatalog({ projectRoot: input.projectRoot })
278+
const target = catalog.files.find(f => path.resolve(f.path) === path.resolve(input.filePath))
279+
if (!target) return { ok: false, includedPaths: readIncludedMemoryPaths() }
280+
const current = new Set(readIncludedMemoryPaths().map(p => path.resolve(p)))
281+
current.delete(path.resolve(target.path))
282+
const next = Array.from(current.values()).sort()
283+
writeIncludedMemoryPaths(next)
284+
return { ok: true, includedPaths: next }
285+
}
286+
287+
export function deleteMemoryFile(input: { projectRoot: string; filePath: string }): { ok: boolean; deletedPath?: string; reason?: string } {
288+
const catalog = buildMemoryCatalog({ projectRoot: input.projectRoot })
289+
const target = catalog.files.find(f => path.resolve(f.path) === path.resolve(input.filePath))
290+
if (!target) return { ok: false, reason: 'File not found in memory catalog' }
291+
try {
292+
fs.unlinkSync(target.path)
293+
} catch (err) {
294+
return { ok: false, reason: err instanceof Error ? err.message : String(err) }
295+
}
296+
const current = new Set(readIncludedMemoryPaths().map(p => path.resolve(p)))
297+
current.delete(path.resolve(target.path))
298+
writeIncludedMemoryPaths(Array.from(current.values()).sort())
299+
return { ok: true, deletedPath: target.path }
300+
}
301+
242302
export function buildMemoryReviewPayload(input: {
243303
projectRoot: string
244304
currentContextFiles?: string[]

packages/web/src/pages/MemoryManagementPage.tsx

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface MemoryFile {
66
relativePath: string
77
categories: string[]
88
preview: string
9+
inCurrentContent: boolean
910
}
1011

1112
interface MemoryGroup {
@@ -98,19 +99,26 @@ export default function MemoryManagementPage() {
9899
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set())
99100
const [loading, setLoading] = useState(true)
100101
const [fileLoading, setFileLoading] = useState(false)
102+
const [actionLoading, setActionLoading] = useState(false)
101103
const [error, setError] = useState('')
102104

105+
const loadCatalog = async () => {
106+
setLoading(true)
107+
try {
108+
const response = await fetch('/api/memory/files')
109+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
110+
const data = await response.json() as CatalogResponse
111+
setCatalog(data)
112+
setError('')
113+
} catch {
114+
setError('Failed to load memory catalog')
115+
} finally {
116+
setLoading(false)
117+
}
118+
}
119+
103120
useEffect(() => {
104-
fetch('/api/memory/files')
105-
.then(r => r.json())
106-
.then((data: CatalogResponse) => {
107-
setCatalog(data)
108-
setLoading(false)
109-
})
110-
.catch(() => {
111-
setError('Failed to load memory catalog')
112-
setLoading(false)
113-
})
121+
loadCatalog()
114122
}, [])
115123

116124
const fileMap = useMemo(() => {
@@ -144,6 +152,49 @@ export default function MemoryManagementPage() {
144152
})
145153
}
146154

155+
const selectedFile = useMemo(() => {
156+
if (!selectedPath) return null
157+
return catalog?.files.find(file => file.path === selectedPath) ?? null
158+
}, [catalog, selectedPath])
159+
160+
const setInContext = async (targetPath: string, include: boolean) => {
161+
setActionLoading(true)
162+
try {
163+
const response = await fetch(include ? '/api/memory/include' : '/api/memory/exclude', {
164+
method: 'POST',
165+
headers: { 'Content-Type': 'application/json' },
166+
body: JSON.stringify({ path: targetPath }),
167+
})
168+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
169+
await loadCatalog()
170+
setError('')
171+
} catch {
172+
setError(include ? 'Failed to include file in current content' : 'Failed to exclude file from current content')
173+
} finally {
174+
setActionLoading(false)
175+
}
176+
}
177+
178+
const handleDelete = async (targetPath: string) => {
179+
const confirmed = window.confirm('Delete this memory file permanently?')
180+
if (!confirmed) return
181+
setActionLoading(true)
182+
try {
183+
const response = await fetch(`/api/memory/file?path=${encodeURIComponent(targetPath)}`, { method: 'DELETE' })
184+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
185+
if (selectedPath === targetPath) {
186+
setSelectedPath('')
187+
setSelectedContent(null)
188+
}
189+
await loadCatalog()
190+
setError('')
191+
} catch {
192+
setError('Failed to delete memory file')
193+
} finally {
194+
setActionLoading(false)
195+
}
196+
}
197+
147198
return (
148199
<div className="min-h-screen bg-gray-50 dark:bg-zinc-950">
149200
<div className="max-w-7xl mx-auto py-8 px-4">
@@ -212,6 +263,32 @@ export default function MemoryManagementPage() {
212263
<>
213264
<p className="text-xs text-zinc-400 dark:text-slate-500 uppercase mb-1">Full File</p>
214265
<h2 className="text-sm font-mono text-zinc-800 dark:text-slate-200 break-all">{selectedContent.path}</h2>
266+
<div className="mt-3 flex flex-wrap gap-2">
267+
{selectedFile?.inCurrentContent ? (
268+
<button
269+
onClick={() => setInContext(selectedContent.path, false)}
270+
disabled={actionLoading}
271+
className="px-2.5 py-1.5 text-xs rounded border border-amber-300 text-amber-700 bg-amber-50 hover:bg-amber-100 disabled:opacity-60 disabled:cursor-not-allowed"
272+
>
273+
Remove From Context
274+
</button>
275+
) : (
276+
<button
277+
onClick={() => setInContext(selectedContent.path, true)}
278+
disabled={actionLoading}
279+
className="px-2.5 py-1.5 text-xs rounded border border-emerald-300 text-emerald-700 bg-emerald-50 hover:bg-emerald-100 disabled:opacity-60 disabled:cursor-not-allowed"
280+
>
281+
Include In Context
282+
</button>
283+
)}
284+
<button
285+
onClick={() => handleDelete(selectedContent.path)}
286+
disabled={actionLoading}
287+
className="px-2.5 py-1.5 text-xs rounded border border-red-300 text-red-700 bg-red-50 hover:bg-red-100 disabled:opacity-60 disabled:cursor-not-allowed"
288+
>
289+
Delete File
290+
</button>
291+
</div>
215292
<div className="mt-3 max-h-[70vh] overflow-y-auto pr-1">
216293
{renderMarkdownFriendly(selectedContent.content)}
217294
</div>
@@ -224,4 +301,3 @@ export default function MemoryManagementPage() {
224301
</div>
225302
)
226303
}
227-

0 commit comments

Comments
 (0)