openrecall 0.2.1 → 0.3.0
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/package.json +1 -1
- package/src/agent.ts +97 -11
- package/src/index.ts +26 -2
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { storeMemory, searchByTag, getTagsForMemory } from "./memory"
|
|
1
|
+
import { storeMemory, searchByTag, getTagsForMemory, deleteMemory, updateMemory, setTags } from "./memory"
|
|
2
2
|
import { isDbAvailable } from "./db"
|
|
3
3
|
import * as fs from "fs"
|
|
4
4
|
import * as path from "path"
|
|
@@ -57,6 +57,92 @@ function buildFingerprintTags(filePath: string, directory?: string): string[] {
|
|
|
57
57
|
return tags
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Remove all existing memories tagged with a specific filepath.
|
|
62
|
+
*/
|
|
63
|
+
function purgeFileMemories(absPath: string, projectId: string): number {
|
|
64
|
+
const tag = `filepath:${absPath}`.toLowerCase()
|
|
65
|
+
const existing = searchByTag(tag, { projectId, limit: 50 })
|
|
66
|
+
for (const m of existing) {
|
|
67
|
+
deleteMemory(m.id)
|
|
68
|
+
}
|
|
69
|
+
return existing.length
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if a file already has a fresh memory. Returns true if fresh (skip store).
|
|
74
|
+
* If stale, purges old memories so caller can store fresh ones.
|
|
75
|
+
*/
|
|
76
|
+
function isFileFreshInMemory(absPath: string, projectId: string, directory?: string): boolean {
|
|
77
|
+
const tag = `filepath:${absPath}`.toLowerCase()
|
|
78
|
+
const existing = searchByTag(tag, { projectId, limit: 1 })
|
|
79
|
+
if (existing.length === 0) return false
|
|
80
|
+
|
|
81
|
+
const memory = existing[0]!
|
|
82
|
+
const memoryTags = getTagsForMemory(memory.id)
|
|
83
|
+
|
|
84
|
+
let storedGitHash: string | undefined
|
|
85
|
+
let storedMtime: number | undefined
|
|
86
|
+
for (const t of memoryTags) {
|
|
87
|
+
if (t.startsWith("git:")) storedGitHash = t.slice(4)
|
|
88
|
+
if (t.startsWith("mtime:")) storedMtime = parseFloat(t.slice(6))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const current = getFileFingerprint(absPath, directory)
|
|
92
|
+
|
|
93
|
+
// Compare git hash first
|
|
94
|
+
if (current.gitHash && storedGitHash && current.gitHash === storedGitHash) {
|
|
95
|
+
return true // fresh
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fall back to mtime
|
|
99
|
+
if (!current.gitHash && storedMtime !== undefined && current.mtime > 0) {
|
|
100
|
+
if (Math.abs(current.mtime - storedMtime) < 1000) return true // fresh
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Stale — purge old memories for this file
|
|
104
|
+
purgeFileMemories(absPath, projectId)
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Upsert a single file memory: update existing or create new.
|
|
110
|
+
* For read-tool tracking where we want exactly one memory per file.
|
|
111
|
+
*/
|
|
112
|
+
function upsertFileMemory(
|
|
113
|
+
content: string,
|
|
114
|
+
projectId: string,
|
|
115
|
+
filePath: string,
|
|
116
|
+
tags: string[],
|
|
117
|
+
source: string,
|
|
118
|
+
sessionId?: string,
|
|
119
|
+
): void {
|
|
120
|
+
const absPath = path.resolve(filePath)
|
|
121
|
+
const tag = `filepath:${absPath}`.toLowerCase()
|
|
122
|
+
const existing = searchByTag(tag, { projectId, limit: 1 })
|
|
123
|
+
|
|
124
|
+
if (existing.length > 0) {
|
|
125
|
+
const memory = existing[0]!
|
|
126
|
+
updateMemory(memory.id, { content, source })
|
|
127
|
+
// Refresh fingerprint tags
|
|
128
|
+
const fpTags = buildFingerprintTags(filePath)
|
|
129
|
+
const nonFpTags = getTagsForMemory(memory.id).filter(
|
|
130
|
+
(t) => !t.startsWith("git:") && !t.startsWith("mtime:"),
|
|
131
|
+
)
|
|
132
|
+
setTags(memory.id, [...nonFpTags, ...fpTags])
|
|
133
|
+
} else {
|
|
134
|
+
storeMemory({
|
|
135
|
+
content,
|
|
136
|
+
category: "discovery",
|
|
137
|
+
projectId,
|
|
138
|
+
sessionId,
|
|
139
|
+
source,
|
|
140
|
+
tags,
|
|
141
|
+
force: true,
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
60
146
|
/**
|
|
61
147
|
* Check if a stored file memory is still fresh by comparing fingerprints.
|
|
62
148
|
* Returns { fresh, memory, storedContent } or null if no memory found.
|
|
@@ -159,11 +245,14 @@ export function scanProjectFiles(directory: string, projectId: string): void {
|
|
|
159
245
|
// Skip large files (> 50KB)
|
|
160
246
|
if (stat.size > 50 * 1024) continue
|
|
161
247
|
|
|
162
|
-
//
|
|
248
|
+
// Skip if already scanned this process lifetime
|
|
163
249
|
const cacheKey = `${fullPath}:${stat.mtimeMs}`
|
|
164
250
|
if (scannedFiles.has(cacheKey)) continue
|
|
165
251
|
scannedFiles.add(cacheKey)
|
|
166
252
|
|
|
253
|
+
// Skip if memory already has fresh content for this file
|
|
254
|
+
if (isFileFreshInMemory(fullPath, projectId, directory)) continue
|
|
255
|
+
|
|
167
256
|
const content = fs.readFileSync(fullPath, "utf-8")
|
|
168
257
|
if (!content.trim()) continue
|
|
169
258
|
|
|
@@ -224,7 +313,6 @@ function storePackageJsonMemory(content: string, projectId: string, filePath: st
|
|
|
224
313
|
projectId,
|
|
225
314
|
source: `file-scan: ${filePath}`,
|
|
226
315
|
tags: ["project-config", "package.json", ...fpTags],
|
|
227
|
-
force: true,
|
|
228
316
|
})
|
|
229
317
|
}
|
|
230
318
|
} catch {
|
|
@@ -254,7 +342,6 @@ function storeFileChunks(content: string, projectId: string, filePath: string, d
|
|
|
254
342
|
projectId,
|
|
255
343
|
source: `file-scan: ${filePath} (section ${i + 1})`,
|
|
256
344
|
tags: ["file-content", path.basename(filePath).toLowerCase(), ...fpTags],
|
|
257
|
-
force: true,
|
|
258
345
|
})
|
|
259
346
|
stored++
|
|
260
347
|
} catch {
|
|
@@ -329,15 +416,14 @@ export function extractFileKnowledge(
|
|
|
329
416
|
const basename = path.basename(filePath)
|
|
330
417
|
const fpTags = buildFingerprintTags(filePath)
|
|
331
418
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
category: "discovery",
|
|
419
|
+
upsertFileMemory(
|
|
420
|
+
`File ${basename}: ${preview}`,
|
|
335
421
|
projectId,
|
|
422
|
+
filePath,
|
|
423
|
+
["file-content", basename.toLowerCase(), ...fpTags],
|
|
424
|
+
`tool-read: ${filePath}`,
|
|
336
425
|
sessionId,
|
|
337
|
-
|
|
338
|
-
tags: ["file-content", basename.toLowerCase(), ...fpTags],
|
|
339
|
-
force: true,
|
|
340
|
-
})
|
|
426
|
+
)
|
|
341
427
|
} else if (toolName === "edit" || toolName === "write") {
|
|
342
428
|
// Store what was edited/written
|
|
343
429
|
const basename = path.basename(filePath)
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { getDb } from "./db"
|
|
|
4
4
|
import { isDbAvailable } from "./db"
|
|
5
5
|
import { initConfig, getConfig, type OpenRecallConfig } from "./config"
|
|
6
6
|
import { initClient } from "./client"
|
|
7
|
-
import { searchMemories, listMemories, getStats, sanitizeQuery } from "./memory"
|
|
7
|
+
import { storeMemory, searchMemories, listMemories, getStats, sanitizeQuery } from "./memory"
|
|
8
8
|
import { maybeRunMaintenance } from "./maintenance"
|
|
9
9
|
import { extractFromToolOutput, clearSessionExtraction } from "./extract"
|
|
10
10
|
import { incrementCounter, clearCounter, scanProjectFiles, extractFileKnowledge } from "./agent"
|
|
@@ -185,7 +185,7 @@ export default async function OpenRecallPlugin(
|
|
|
185
185
|
"Calling read on a file after memory_file_check already returned its content is FORBIDDEN — it wastes tokens and time. " +
|
|
186
186
|
"This rule applies to ALL files, every time, with no exceptions.",
|
|
187
187
|
"When the user asks you to remember, save, store, or note something, you MUST call memory_store immediately.",
|
|
188
|
-
"Use memory_store to save important findings, decisions, user preferences, and patterns.",
|
|
188
|
+
"Use memory_store to save important findings, decisions, user preferences, and patterns. Your final responses are automatically distilled into memory.",
|
|
189
189
|
]
|
|
190
190
|
|
|
191
191
|
// Add dynamic summary if DB is available
|
|
@@ -229,6 +229,30 @@ export default async function OpenRecallPlugin(
|
|
|
229
229
|
}
|
|
230
230
|
},
|
|
231
231
|
|
|
232
|
+
// Auto-distill the LLM's final response text into memory
|
|
233
|
+
"experimental.text.complete": async (input, output) => {
|
|
234
|
+
if (!isDbAvailable()) return
|
|
235
|
+
const text = output.text
|
|
236
|
+
if (!text || text.length < 80) return
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
// Take a concise summary: first 400 chars of the response
|
|
240
|
+
const summary = text.length > 400
|
|
241
|
+
? text.slice(0, 397) + "..."
|
|
242
|
+
: text
|
|
243
|
+
storeMemory({
|
|
244
|
+
content: summary,
|
|
245
|
+
category: "discovery",
|
|
246
|
+
projectId,
|
|
247
|
+
sessionId: input.sessionID,
|
|
248
|
+
source: "auto-distill: assistant response",
|
|
249
|
+
tags: ["auto-distill", "assistant-response"],
|
|
250
|
+
})
|
|
251
|
+
} catch {
|
|
252
|
+
// Silent fail — never block the response
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
|
|
232
256
|
// During compaction, remind to preserve important context
|
|
233
257
|
"experimental.session.compacting": async (_input, output) => {
|
|
234
258
|
output.context.push(
|