Merge 9f111c73658cd4763889ad1de0a7af61c12df2c2 into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
let5sne 2026-03-20 19:56:52 -07:00 committed by GitHub
commit 56ec706bd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 93 additions and 0 deletions

View File

@ -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}` });
}
},
};
}

View File

@ -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;

View File

@ -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 (