fix(FN-000): process agent memory dreams

This commit is contained in:
gsxdsm
2026-04-17 10:03:46 -07:00
parent d5d5c13781
commit 0515882da1
6 changed files with 402 additions and 36 deletions

View File

@@ -484,15 +484,21 @@ export { MemoryBackendError } from "./memory-backend.js";
export type { MemoryBackendCapabilities, MemoryFileInfo, MemoryGetOptions, MemoryGetResult, MemorySearchOptions, MemorySearchResult } from "./memory-backend.js";
export {
agentDailyMemoryPath,
agentMemoryDreamsPath,
agentMemoryLongTermPath,
agentMemoryWorkspacePath,
buildDreamProcessingPrompt,
createMemoryDreamsAutomation,
DEFAULT_MEMORY_DREAMS_SCHEDULE,
ensureAgentMemoryFiles,
extractDreamProcessorResult,
MEMORY_DREAMS_SCHEDULE_NAME,
processAgentMemoryDreams,
processMemoryDreams,
syncMemoryDreamsAutomation,
} from "./memory-dreams.js";
export type { DreamProcessorResult, DreamPromptExecutor } from "./memory-dreams.js";
export type { AgentDreamProcessorResult, DreamProcessorResult, DreamPromptExecutor } from "./memory-dreams.js";
// ── Project Insights ──────────────────────────────────────────────────────

View File

@@ -1,8 +1,16 @@
import { describe, expect, it, vi } from "vitest";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
agentDailyMemoryPath,
agentMemoryDreamsPath,
agentMemoryLongTermPath,
createMemoryDreamsAutomation,
DEFAULT_MEMORY_DREAMS_SCHEDULE,
ensureAgentMemoryFiles,
MEMORY_DREAMS_SCHEDULE_NAME,
processAgentMemoryDreams,
syncMemoryDreamsAutomation,
} from "./memory-dreams.js";
@@ -16,6 +24,8 @@ describe("memory-dreams automation", () => {
expect(automation.steps![0].id).toBe("memory-dream-processor");
expect(automation.steps![0].prompt).toContain(".fusion/memory/DREAMS.md");
expect(automation.steps![0].prompt).toContain(".fusion/memory/MEMORY.md");
expect(automation.steps![0].prompt).toContain(".fusion/agent-memory/{agentId}/");
expect(automation.steps![0].prompt).toContain("Keep agent memory separate from workspace memory");
});
it("uses custom schedule and model when provided", () => {
@@ -54,4 +64,69 @@ describe("memory-dreams automation", () => {
);
expect(result?.id).toBe("dreams-1");
});
it("creates agent long-term, daily, and dreams memory files", async () => {
const rootDir = await mkdtemp(join(tmpdir(), "agent-dreams-"));
try {
const date = new Date("2026-04-17T12:00:00.000Z");
await ensureAgentMemoryFiles(rootDir, {
id: "ceo-agent",
name: "CEO",
memory: "Prioritize roadmap sequencing.",
} as any, date);
await expect(readFile(agentMemoryLongTermPath(rootDir, "ceo-agent"), "utf-8"))
.resolves.toContain("Prioritize roadmap sequencing");
await expect(readFile(agentMemoryDreamsPath(rootDir, "ceo-agent"), "utf-8"))
.resolves.toContain("Agent Memory Dreams");
await expect(readFile(agentDailyMemoryPath(rootDir, "ceo-agent", date), "utf-8"))
.resolves.toContain("Agent Daily Memory 2026-04-17");
} finally {
await rm(rootDir, { recursive: true, force: true });
}
});
it("processes agent daily memory into agent dreams and long-term updates", async () => {
const rootDir = await mkdtemp(join(tmpdir(), "agent-dreams-process-"));
try {
const date = new Date("2026-04-17T12:00:00.000Z");
const agent = {
id: "ceo-agent",
name: "CEO",
role: "executor",
state: "idle",
memory: "Existing CEO preference.",
metadata: {},
createdAt: date.toISOString(),
updatedAt: date.toISOString(),
} as any;
await ensureAgentMemoryFiles(rootDir, agent, date);
await writeFile(
agentDailyMemoryPath(rootDir, "ceo-agent", date),
"# Agent Daily Memory 2026-04-17\n\n- CEO should delegate implementation after sequencing.",
"utf-8",
);
const result = await processAgentMemoryDreams(rootDir, [agent], async (prompt) => {
expect(prompt).toContain("private memory for agent CEO");
expect(prompt).toContain("delegate implementation");
return "## DREAMS\n\nDelegation after sequencing is recurring.\n\n## LONG_TERM_UPDATES\n\n- Delegate implementation after roadmap sequencing.";
}, date);
expect(result).toEqual([{
agentId: "ceo-agent",
dreams: "Delegation after sequencing is recurring.",
longTermUpdates: "- Delegate implementation after roadmap sequencing.",
}]);
await expect(readFile(agentMemoryDreamsPath(rootDir, "ceo-agent"), "utf-8"))
.resolves.toContain("Delegation after sequencing is recurring");
await expect(readFile(agentMemoryLongTermPath(rootDir, "ceo-agent"), "utf-8"))
.resolves.toContain("Delegate implementation after roadmap sequencing");
await expect(readFile(agentDailyMemoryPath(rootDir, "ceo-agent", date), "utf-8"))
.resolves.toContain("Processed into dreams");
} finally {
await rm(rootDir, { recursive: true, force: true });
}
});
});

View File

@@ -1,5 +1,6 @@
import { appendFile, readFile, writeFile } from "node:fs/promises";
import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
import {
dailyMemoryPath,
ensureOpenClawMemoryFiles,
@@ -7,7 +8,8 @@ import {
memoryLongTermPath,
} from "./memory-backend.js";
import type { ScheduledTaskCreateInput } from "./automation.js";
import type { ProjectSettings } from "./types.js";
import type { Agent, ProjectSettings } from "./types.js";
import { isEphemeralAgent } from "./types.js";
export const MEMORY_DREAMS_SCHEDULE_NAME = "Memory Dreams";
export const DEFAULT_MEMORY_DREAMS_SCHEDULE = "0 4 * * *";
@@ -17,8 +19,59 @@ export interface DreamProcessorResult {
longTermUpdates: string;
}
export interface AgentDreamProcessorResult extends DreamProcessorResult {
agentId: string;
}
export type DreamPromptExecutor = (prompt: string) => Promise<string>;
const AGENT_MEMORY_ROOT = ".fusion/agent-memory";
const AGENT_MEMORY_FILENAME = "MEMORY.md";
const AGENT_DREAMS_FILENAME = "DREAMS.md";
const DAILY_AGENT_MEMORY_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
export function agentMemoryWorkspacePath(rootDir: string, agentId: string): string {
const safeAgentId = agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
return join(rootDir, AGENT_MEMORY_ROOT, safeAgentId);
}
export function agentMemoryLongTermPath(rootDir: string, agentId: string): string {
return join(agentMemoryWorkspacePath(rootDir, agentId), AGENT_MEMORY_FILENAME);
}
export function agentMemoryDreamsPath(rootDir: string, agentId: string): string {
return join(agentMemoryWorkspacePath(rootDir, agentId), AGENT_DREAMS_FILENAME);
}
export function agentDailyMemoryPath(rootDir: string, agentId: string, date = new Date()): string {
return join(agentMemoryWorkspacePath(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
}
export async function ensureAgentMemoryFiles(rootDir: string, agent: Pick<Agent, "id" | "name" | "memory">, date = new Date()): Promise<void> {
const workspacePath = agentMemoryWorkspacePath(rootDir, agent.id);
await mkdir(workspacePath, { recursive: true });
const longTermPath = agentMemoryLongTermPath(rootDir, agent.id);
if (!existsSync(longTermPath)) {
const title = agent.name?.trim() ? `# Agent Memory: ${agent.name.trim()}` : "# Agent Memory";
await writeFile(
longTermPath,
`${title}\n\n<!-- Per-agent memory. Keep separate from workspace Project Memory. -->\n\n${agent.memory?.trim() ?? ""}\n`,
"utf-8",
);
}
const dreamsPath = agentMemoryDreamsPath(rootDir, agent.id);
if (!existsSync(dreamsPath)) {
await writeFile(dreamsPath, "# Agent Memory Dreams\n\n<!-- Synthesized patterns from this agent's daily notes. -->\n", "utf-8");
}
const dailyPath = agentDailyMemoryPath(rootDir, agent.id, date);
if (!existsSync(dailyPath)) {
await writeFile(dailyPath, `# Agent Daily Memory ${date.toISOString().slice(0, 10)}\n\n<!-- Running observations for this agent. -->\n`, "utf-8");
}
}
export function buildDreamProcessingPrompt(input: {
date: string;
longTermMemory: string;
@@ -104,6 +157,70 @@ export async function processMemoryDreams(
return result;
}
async function readAgentDailyNotes(rootDir: string, agentId: string, date: Date): Promise<string> {
const workspacePath = agentMemoryWorkspacePath(rootDir, agentId);
const dateKey = date.toISOString().slice(0, 10);
const dailyPath = agentDailyMemoryPath(rootDir, agentId, date);
if (existsSync(dailyPath)) {
return readFile(dailyPath, "utf-8");
}
const files = await readdir(workspacePath).catch(() => [] as string[]);
const chunks: string[] = [];
for (const file of files) {
if (!DAILY_AGENT_MEMORY_RE.test(file)) continue;
if (!file.startsWith(dateKey)) continue;
const absPath = join(workspacePath, file);
if ((await stat(absPath)).isFile()) {
chunks.push(await readFile(absPath, "utf-8"));
}
}
return chunks.join("\n\n");
}
export async function processAgentMemoryDreams(
rootDir: string,
agents: Agent[],
executePrompt: DreamPromptExecutor,
date = new Date(),
): Promise<AgentDreamProcessorResult[]> {
const dateKey = date.toISOString().slice(0, 10);
const results: AgentDreamProcessorResult[] = [];
for (const agent of agents) {
if (isEphemeralAgent(agent)) {
continue;
}
await ensureAgentMemoryFiles(rootDir, agent, date);
const longTermPath = agentMemoryLongTermPath(rootDir, agent.id);
const dreamsPath = agentMemoryDreamsPath(rootDir, agent.id);
const dailyPath = agentDailyMemoryPath(rootDir, agent.id, date);
const prompt = buildDreamProcessingPrompt({
date: dateKey,
longTermMemory: await readIfExists(longTermPath),
previousDreams: await readIfExists(dreamsPath),
dailyMemory: await readAgentDailyNotes(rootDir, agent.id, date),
}).replace(
"You are processing project memory in an OpenClaw-style memory system.",
`You are processing private memory for agent ${agent.name} (${agent.id}) in an OpenClaw-style memory system.`,
);
const result = extractDreamProcessorResult(await executePrompt(prompt));
if (result.dreams) {
await appendFile(dreamsPath, `\n## ${dateKey}\n\n${result.dreams}\n`, "utf-8");
}
if (result.longTermUpdates) {
await appendFile(longTermPath, `\n## Dream Updates ${dateKey}\n\n${result.longTermUpdates}\n`, "utf-8");
}
await writeFile(dailyPath, `# Agent Daily Memory ${dateKey}\n\n<!-- Processed into dreams on ${new Date().toISOString()} -->\n`, "utf-8");
results.push({ agentId: agent.id, ...result });
}
return results;
}
export function createMemoryDreamsAutomation(
settings: Partial<ProjectSettings>,
modelProvider?: string,
@@ -120,13 +237,20 @@ export function createMemoryDreamsAutomation(
4. Append a dated synthesis to \`.fusion/memory/DREAMS.md\` with patterns, open loops, contradictions, and emerging themes.
5. Append only durable conventions, decisions, pitfalls, or constraints to \`.fusion/memory/MEMORY.md\`.
6. Reset today's daily note to a short processed marker after successful synthesis.
7. For every persisted non-ephemeral agent in \`.fusion/agents/*.json\`, repeat the same process for that agent's private memory workspace at \`.fusion/agent-memory/{agentId}/\`:
- Read \`MEMORY.md\`, \`DREAMS.md\`, and today's \`YYYY-MM-DD.md\`.
- If the agent workspace is missing, create it and seed \`MEMORY.md\` from the agent JSON \`memory\` field when present.
- Append agent-specific synthesis to that agent's \`DREAMS.md\`.
- Promote only durable agent-specific operating preferences, habits, or constraints to that agent's \`MEMORY.md\`.
- Reset that agent's daily note after successful synthesis.
## Rules
- Do not copy task logs or changelog entries into long-term memory.
- Do not invent facts.
- Keep dreams useful for future agents, not a transcript of the day.
- Preserve the three-layer model: daily notes are raw, DREAMS.md is synthesis, MEMORY.md is curated durable knowledge.`;
- Preserve the three-layer model for both workspace and agent memory: daily notes are raw, DREAMS.md is synthesis, MEMORY.md is curated durable knowledge.
- Keep agent memory separate from workspace memory. Do not promote private agent operating notes into project memory unless they are useful to every agent in the workspace.`;
return {
name: MEMORY_DREAMS_SCHEDULE_NAME,

View File

@@ -143,7 +143,7 @@ Documents persist across sessions and are visible in the dashboard's Documents t
## Memory Boundaries
You may receive an Agent Memory section and a Project Memory section.
- Agent Memory is specific to you, including imported and user-created agents such as CEO-style coordinator agents. Use it for your durable operating preferences and role context.
- Agent Memory is specific to you, including imported and user-created agents such as CEO-style coordinator agents. It has its own long-term memory, daily notes, dreams, and qmd-backed retrieval under .fusion/agent-memory/{agentId}/.
- Project Memory is the workspace memory system under .fusion/memory/ with long-term memory, daily notes, dreams, and qmd-backed retrieval.
- Keep these separate: do not copy personal agent operating notes into Project Memory unless they are genuinely useful to every future agent in this workspace.

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm } from "node:fs/promises";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
@@ -84,6 +84,74 @@ describe("createMemoryTools", () => {
expect(getResult.content[0]!.text).toContain("roadmap sequencing");
});
it("creates daily and dreams files for per-agent memory lookup", async () => {
const [searchTool] = createMemoryTools(tempDir, { memoryBackendType: "file" }, {
agentMemory: {
agentId: "ceo-agent",
agentName: "CEO",
memory: "The CEO agent should prioritize roadmap sequencing and delegation.",
},
});
await (searchTool as any).execute("call-1", {
query: "roadmap",
limit: 5,
}, undefined, undefined, undefined);
const today = new Date().toISOString().slice(0, 10);
await expect(readFile(join(tempDir, ".fusion", "agent-memory", "ceo-agent", "MEMORY.md"), "utf-8"))
.resolves.toContain("Agent Memory: CEO");
await expect(readFile(join(tempDir, ".fusion", "agent-memory", "ceo-agent", "DREAMS.md"), "utf-8"))
.resolves.toContain("Agent Memory Dreams");
await expect(readFile(join(tempDir, ".fusion", "agent-memory", "ceo-agent", `${today}.md`), "utf-8"))
.resolves.toContain("Agent Daily Memory");
});
it("appends to this agent's daily memory through memory_append", async () => {
const tools = createMemoryTools(tempDir, { memoryBackendType: "file" }, {
agentMemory: {
agentId: "ceo-agent",
agentName: "CEO",
memory: "The CEO agent should prioritize roadmap sequencing and delegation.",
},
});
const appendTool = tools.find((tool) => tool.name === "memory_append")!;
const result = await (appendTool as any).execute("call-1", {
scope: "agent",
layer: "daily",
content: "- Follow up with execution agents after roadmap planning.",
}, undefined, undefined, undefined);
const today = new Date().toISOString().slice(0, 10);
await expect(readFile(join(tempDir, ".fusion", "agent-memory", "ceo-agent", `${today}.md`), "utf-8"))
.resolves.toContain("Follow up with execution agents");
expect(result.details).toEqual({ scope: "agent", layer: "daily" });
});
it("memory_get reads agent dreams returned by memory_search", async () => {
const [, getTool, appendTool] = createMemoryTools(tempDir, { memoryBackendType: "file" }, {
agentMemory: {
agentId: "ceo-agent",
agentName: "CEO",
memory: "The CEO agent should prioritize roadmap sequencing and delegation.",
},
});
await (appendTool as any).execute("call-1", {
scope: "agent",
layer: "daily",
content: "- Daily note",
}, undefined, undefined, undefined);
const getResult = await (getTool as any).execute("call-2", {
path: ".fusion/agent-memory/ceo-agent/DREAMS.md",
startLine: 1,
lineCount: 10,
}, undefined, undefined, undefined);
expect(getResult.content[0]!.text).toContain("Agent Memory Dreams");
});
it("builds qmd collection and search args for separate agent memory", () => {
expect(buildQmdAgentMemoryCollectionAddArgs(tempDir, "ceo-agent")).toEqual([
"collection",

View File

@@ -7,7 +7,8 @@
* The parameter schemas are canonical here — executor.ts imports and reuses them.
*/
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { join } from "node:path";
import type { AgentStore, AgentState, AgentCapability, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message } from "@fusion/core";
@@ -96,6 +97,10 @@ export const memoryGetParams = Type.Object({
});
export const memoryAppendParams = Type.Object({
scope: Type.Optional(Type.Union([
Type.Literal("project"),
Type.Literal("agent"),
], { description: "project for workspace memory, agent for this agent's private memory" })),
layer: Type.Union([
Type.Literal("long-term"),
Type.Literal("daily"),
@@ -129,8 +134,10 @@ type MemorySearchHit = {
const AGENT_MEMORY_ROOT = ".fusion/agent-memory";
const AGENT_MEMORY_FILENAME = "MEMORY.md";
const AGENT_DREAMS_FILENAME = "DREAMS.md";
const agentQmdRefreshState = new Map<string, { lastStartedAt: number; inFlight?: Promise<void> }>();
const AGENT_QMD_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
const DAILY_AGENT_MEMORY_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
function sanitizeAgentMemoryId(agentId: string): string {
return agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
@@ -140,6 +147,14 @@ function agentMemoryDisplayPath(agentId: string): string {
return `${AGENT_MEMORY_ROOT}/${sanitizeAgentMemoryId(agentId)}/${AGENT_MEMORY_FILENAME}`;
}
function agentDreamsDisplayPath(agentId: string): string {
return `${AGENT_MEMORY_ROOT}/${sanitizeAgentMemoryId(agentId)}/${AGENT_DREAMS_FILENAME}`;
}
function agentDailyDisplayPath(agentId: string, date = new Date()): string {
return `${AGENT_MEMORY_ROOT}/${sanitizeAgentMemoryId(agentId)}/${date.toISOString().slice(0, 10)}.md`;
}
function agentMemoryDirectory(rootDir: string, agentId: string): string {
return join(rootDir, AGENT_MEMORY_ROOT, sanitizeAgentMemoryId(agentId));
}
@@ -148,6 +163,14 @@ function agentMemoryFilePath(rootDir: string, agentId: string): string {
return join(agentMemoryDirectory(rootDir, agentId), AGENT_MEMORY_FILENAME);
}
function agentDreamsFilePath(rootDir: string, agentId: string): string {
return join(agentMemoryDirectory(rootDir, agentId), AGENT_DREAMS_FILENAME);
}
function agentDailyFilePath(rootDir: string, agentId: string, date = new Date()): string {
return join(agentMemoryDirectory(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
}
export function qmdAgentMemoryCollectionName(rootDir: string, agentId: string): string {
const hash = createHash("sha1").update(`${rootDir}:${agentId}`).digest("hex").slice(0, 12);
return `fusion-agent-memory-${sanitizeAgentMemoryId(agentId).toLowerCase()}-${hash}`;
@@ -179,20 +202,52 @@ export function buildQmdAgentMemorySearchArgs(rootDir: string, agentId: string,
async function syncAgentMemoryFile(rootDir: string, agentMemory?: AgentMemoryContext): Promise<string | null> {
const content = agentMemory?.memory?.trim();
if (!agentMemory?.agentId || !content) {
if (!agentMemory?.agentId) {
return null;
}
const dir = agentMemoryDirectory(rootDir, agentMemory.agentId);
await mkdir(dir, { recursive: true });
const longTermPath = agentMemoryFilePath(rootDir, agentMemory.agentId);
if (!existsSync(longTermPath)) {
const title = agentMemory.agentName?.trim()
? `# Agent Memory: ${agentMemory.agentName.trim()}`
: "# Agent Memory";
const fileContent = `${title}\n\n<!-- Per-agent memory. Keep separate from workspace Project Memory. -->\n\n${content}\n`;
await writeFile(agentMemoryFilePath(rootDir, agentMemory.agentId), fileContent, "utf-8");
const fileContent = `${title}\n\n<!-- Per-agent memory. Keep separate from workspace Project Memory. -->\n\n${content || ""}\n`;
await writeFile(longTermPath, fileContent, "utf-8");
}
const dreamsPath = agentDreamsFilePath(rootDir, agentMemory.agentId);
if (!existsSync(dreamsPath)) {
await writeFile(dreamsPath, "# Agent Memory Dreams\n\n<!-- Synthesized patterns from this agent's daily notes. -->\n", "utf-8");
}
const dailyPath = agentDailyFilePath(rootDir, agentMemory.agentId);
if (!existsSync(dailyPath)) {
await writeFile(dailyPath, `# Agent Daily Memory ${new Date().toISOString().slice(0, 10)}\n\n<!-- Running observations for this agent. -->\n`, "utf-8");
}
return agentMemoryDisplayPath(agentMemory.agentId);
}
async function listAgentMemoryFiles(rootDir: string, agentMemory: AgentMemoryContext): Promise<Array<{ absPath: string; displayPath: string }>> {
await syncAgentMemoryFile(rootDir, agentMemory);
const dir = agentMemoryDirectory(rootDir, agentMemory.agentId);
const files = [
{ absPath: agentMemoryFilePath(rootDir, agentMemory.agentId), displayPath: agentMemoryDisplayPath(agentMemory.agentId) },
{ absPath: agentDreamsFilePath(rootDir, agentMemory.agentId), displayPath: agentDreamsDisplayPath(agentMemory.agentId) },
];
for (const entry of await readdir(dir).catch(() => [] as string[])) {
if (!DAILY_AGENT_MEMORY_RE.test(entry)) continue;
const absPath = join(dir, entry);
const fileStat = await stat(absPath);
if (fileStat.isFile()) {
files.push({
absPath,
displayPath: `${AGENT_MEMORY_ROOT}/${sanitizeAgentMemoryId(agentMemory.agentId)}/${entry}`,
});
}
}
return files;
}
function scoreAgentMemorySnippet(snippet: string, query: string): number {
const terms = query.toLowerCase().split(/[^a-z0-9_-]+/i).filter((term) => term.length >= 2);
const normalized = snippet.toLowerCase();
@@ -205,16 +260,17 @@ async function searchAgentMemoryFile(rootDir: string, agentMemory: AgentMemoryCo
return [];
}
const content = await readFile(agentMemoryFilePath(rootDir, agentMemory.agentId), "utf-8");
const lines = content.split("\n");
const results: MemorySearchHit[] = [];
for (const file of await listAgentMemoryFiles(rootDir, agentMemory)) {
const content = await readFile(file.absPath, "utf-8");
const lines = content.split("\n");
for (let index = 0; index < lines.length; index += 8) {
const chunk = lines.slice(index, index + 12).join("\n").trim();
if (!chunk) continue;
const score = scoreAgentMemorySnippet(chunk, query);
if (score === 0) continue;
results.push({
path: displayPath,
path: file.displayPath,
lineStart: index + 1,
lineEnd: Math.min(index + 12, lines.length),
snippet: chunk.slice(0, 1200),
@@ -222,6 +278,7 @@ async function searchAgentMemoryFile(rootDir: string, agentMemory: AgentMemoryCo
backend: "agent-memory",
});
}
}
return results.slice(0, limit);
}
@@ -298,19 +355,36 @@ async function searchAgentMemoryWithQmd(rootDir: string, agentMemory: AgentMemor
}
}
function resolveAgentMemoryPath(rootDir: string, agentId: string, path: string): { absPath: string; displayPath: string } | null {
const safeAgentId = sanitizeAgentMemoryId(agentId);
const prefix = `${AGENT_MEMORY_ROOT}/${safeAgentId}/`;
if (!path.startsWith(prefix)) {
return null;
}
const filename = path.slice(prefix.length);
if (filename !== AGENT_MEMORY_FILENAME && filename !== AGENT_DREAMS_FILENAME && !DAILY_AGENT_MEMORY_RE.test(filename)) {
return null;
}
return {
absPath: join(agentMemoryDirectory(rootDir, agentId), filename),
displayPath: `${prefix}${filename}`,
};
}
async function getAgentMemoryWindow(rootDir: string, agentMemory: AgentMemoryContext, path: string, startLine = 1, lineCount = 40) {
if (path !== agentMemoryDisplayPath(agentMemory.agentId) || !agentMemory.memory?.trim()) {
const resolved = resolveAgentMemoryPath(rootDir, agentMemory.agentId, path);
if (!resolved) {
return null;
}
await syncAgentMemoryFile(rootDir, agentMemory);
const content = await readFile(agentMemoryFilePath(rootDir, agentMemory.agentId), "utf-8");
const content = await readFile(resolved.absPath, "utf-8");
const lines = content.split("\n");
const start = Math.max(1, Math.floor(startLine));
const count = Math.max(1, Math.min(Math.floor(lineCount), 200));
const startIndex = Math.min(start - 1, lines.length);
const endIndex = Math.min(startIndex + count, lines.length);
return {
path: agentMemoryDisplayPath(agentMemory.agentId),
path: resolved.displayPath,
content: lines.slice(startIndex, endIndex).join("\n"),
startLine: start,
endLine: endIndex,
@@ -595,7 +669,7 @@ export function createMemoryGetTool(rootDir: string, settings?: MemoryToolSettin
};
}
export function createMemoryAppendTool(rootDir: string, settings?: MemoryToolSettings): ToolDefinition {
export function createMemoryAppendTool(rootDir: string, settings?: MemoryToolSettings, options?: MemoryToolOptions): ToolDefinition {
return {
name: "memory_append",
label: "Append Memory",
@@ -604,20 +678,39 @@ export function createMemoryAppendTool(rootDir: string, settings?: MemoryToolSet
"use daily for running observations and open loops. Skip this tool when there is no reusable memory.",
parameters: memoryAppendParams,
execute: async (_id: string, params: Static<typeof memoryAppendParams>) => {
await ensureOpenClawMemoryFiles(rootDir);
const targetPath = params.layer === "long-term" ? memoryLongTermPath(rootDir) : dailyMemoryPath(rootDir);
const content = params.content.trim();
if (!content) {
return { content: [{ type: "text" as const, text: "ERROR: memory content cannot be empty" }], details: {} };
}
const scope = params.scope ?? "project";
if (scope === "agent") {
if (!options?.agentMemory) {
return { content: [{ type: "text" as const, text: "ERROR: agent memory is not available in this session" }], details: {} };
}
await syncAgentMemoryFile(rootDir, options.agentMemory);
const targetPath = params.layer === "long-term"
? agentMemoryFilePath(rootDir, options.agentMemory.agentId)
: agentDailyFilePath(rootDir, options.agentMemory.agentId);
await appendFile(targetPath, `\n${content}\n`, "utf-8");
if (resolveMemoryBackend(settings).type === "qmd") {
void refreshAgentMemoryQmdIndex(rootDir, options.agentMemory).catch(() => {});
}
return {
content: [{ type: "text" as const, text: `Appended to agent ${params.layer} memory.` }],
details: { scope, layer: params.layer },
};
}
await ensureOpenClawMemoryFiles(rootDir);
const targetPath = params.layer === "long-term" ? memoryLongTermPath(rootDir) : dailyMemoryPath(rootDir);
await appendFile(targetPath, `\n${content}\n`, "utf-8");
if (resolveMemoryBackend(settings).type === "qmd") {
scheduleQmdProjectMemoryRefresh(rootDir);
}
return {
content: [{ type: "text" as const, text: `Appended to ${params.layer} memory.` }],
details: { layer: params.layer },
details: { scope, layer: params.layer },
};
},
};
@@ -632,7 +725,7 @@ export function createMemoryTools(rootDir: string, settings?: MemoryToolSettings
createMemoryGetTool(rootDir, settings, options),
];
if (getMemoryBackendCapabilities(settings).writable) {
tools.push(createMemoryAppendTool(rootDir, settings));
tools.push(createMemoryAppendTool(rootDir, settings, options));
}
return tools;
}