diff --git a/src/agents/tools/memory-tool.ts b/src/agents/tools/memory-tool.ts index bb5086bdb15..67c47f1692b 100644 --- a/src/agents/tools/memory-tool.ts +++ b/src/agents/tools/memory-tool.ts @@ -269,3 +269,75 @@ function deriveChatTypeFromSessionKey(sessionKey?: string): "direct" | "group" | } return "direct"; } + +export function createMemoryAuditTool(options: { + cfg: OpenClawConfig; + workspaceDir: string; + sessionKey?: string; +}): AnyAgentTool { + return { + name: "memory_audit", + description: + "Query change history for memory files (MEMORY.md, memory/*.md). Returns recent hash changes recorded when files were indexed. Use to detect unexpected modifications or verify when a file was last updated by the agent.", + parameters: Type.Object({ + path: Type.Optional( + Type.String({ + description: "Relative path to filter (e.g. 'MEMORY.md'). Omit to list all recent changes.", + }), + ), + limit: Type.Optional( + Type.Number({ + description: "Maximum number of changes to return (default 20).", + minimum: 1, + maximum: 200, + }), + ), + }), + execute: async (_, params) => { + const { manager, error } = await getMemorySearchManager({ + cfg: options.cfg, + workspaceDir: options.workspaceDir, + agentId: resolveSessionAgentId(options.sessionKey), + }); + if (error || !manager) { + return jsonResult({ error: error ?? "Memory manager unavailable" }); + } + const dbPath = manager.dbPath; + if (!dbPath) { + return jsonResult({ error: "Memory index path unavailable" }); + } + try { + const { DatabaseSync } = await import("node:sqlite"); + const db = new DatabaseSync(dbPath, { readOnly: true }); + const limit = readNumberParam(params, "limit") ?? 20; + const filterPath = readStringParam(params, "path"); + let rows: unknown[]; + if (filterPath) { + rows = db + .prepare( + `SELECT path, old_hash, new_hash, changed_at, source + FROM file_changes + WHERE path = ? + ORDER BY changed_at DESC + LIMIT ?`, + ) + .all(filterPath, limit); + } else { + rows = db + .prepare( + `SELECT path, old_hash, new_hash, changed_at, source + FROM file_changes + ORDER BY changed_at DESC + LIMIT ?`, + ) + .all(limit); + } + db.close(); + return jsonResult({ changes: rows, count: rows.length }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return jsonResult({ error: `Failed to query file_changes: ${message}` }); + } + }, + }; +} diff --git a/src/memory/manager-sync-ops.ts b/src/memory/manager-sync-ops.ts index 6babe931707..f46488cf34d 100644 --- a/src/memory/manager-sync-ops.ts +++ b/src/memory/manager-sync-ops.ts @@ -747,6 +747,13 @@ export abstract class MemoryManagerSyncOps { } return; } + // Audit: record the hash change before indexing + this.db + .prepare( + `INSERT INTO file_changes (path, old_hash, new_hash, changed_at, source) + VALUES (?, ?, ?, ?, ?)`, + ) + .run(entry.path, record?.hash ?? null, entry.hash, Date.now(), "agent"); await this.indexFile(entry, { source: "memory" }); if (params.progress) { params.progress.completed += 1; diff --git a/src/memory/memory-schema.ts b/src/memory/memory-schema.ts index a537c35f171..5433bbf6ffb 100644 --- a/src/memory/memory-schema.ts +++ b/src/memory/memory-schema.ts @@ -20,6 +20,20 @@ export function ensureMemoryIndexSchema(params: { mtime INTEGER NOT NULL, size INTEGER NOT NULL ); + \`); + params.db.exec(\` + CREATE TABLE IF NOT EXISTS file_changes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + path TEXT NOT NULL, + old_hash TEXT, + new_hash TEXT NOT NULL, + changed_at INTEGER NOT NULL, + source TEXT NOT NULL DEFAULT 'agent' + ); + \`); + params.db.exec(\` + CREATE INDEX IF NOT EXISTS file_changes_path_idx + ON file_changes (path, changed_at DESC); `); params.db.exec(` CREATE TABLE IF NOT EXISTS chunks (