fix: Create memory directory and symlink identity files during workspace setup

The memory search system expects files in ~/clawd/memory/ but workspace setup
only created identity files (SOUL.md, USER.md, AGENTS.md, IDENTITY.md) at the
workspace root. This caused the memory index to report "no memory files found"
even though identity files existed.

Changes:
- Create memory/ directory during ensureAgentWorkspace when ensureBootstrapFiles is true
- Symlink identity files (SOUL.md, USER.md, AGENTS.md, IDENTITY.md) into memory/
  so they are discoverable by the memory search indexer
- Add memoryDir to the return type of ensureAgentWorkspace
- Gracefully handle symlink creation failures (e.g., Windows without privileges)
- Skip symlink creation if target file already exists (preserves user customizations)

Fixes issue where clawdbot would claim "I have no memories" despite having
identity files, because the memory search index was empty.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
webdevtodayjason 2026-01-27 11:25:59 -06:00
parent f662039c47
commit 2f9bcb6a71
2 changed files with 98 additions and 0 deletions

View File

@ -1,8 +1,16 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
DEFAULT_AGENTS_FILENAME,
DEFAULT_IDENTITY_FILENAME,
DEFAULT_MEMORY_ALT_FILENAME,
DEFAULT_MEMORY_DIR,
DEFAULT_MEMORY_FILENAME,
DEFAULT_SOUL_FILENAME,
DEFAULT_USER_FILENAME,
ensureAgentWorkspace,
loadWorkspaceBootstrapFiles,
} from "./workspace.js";
import { makeTempWorkspace, writeWorkspaceFile } from "../test-helpers/workspace.js";
@ -47,3 +55,62 @@ describe("loadWorkspaceBootstrapFiles", () => {
expect(memoryEntries).toHaveLength(0);
});
});
describe("ensureAgentWorkspace", () => {
it("creates memory directory with symlinks to identity files", async () => {
const tempDir = await makeTempWorkspace("moltbot-workspace-");
const result = await ensureAgentWorkspace({
dir: tempDir,
ensureBootstrapFiles: true,
});
// Check memory directory was created
expect(result.memoryDir).toBe(path.join(tempDir, DEFAULT_MEMORY_DIR));
const memoryDirStat = await fs.stat(result.memoryDir!);
expect(memoryDirStat.isDirectory()).toBe(true);
// Check symlinks exist in memory directory
const memoryFiles = await fs.readdir(result.memoryDir!);
expect(memoryFiles).toContain(DEFAULT_SOUL_FILENAME);
expect(memoryFiles).toContain(DEFAULT_USER_FILENAME);
expect(memoryFiles).toContain(DEFAULT_AGENTS_FILENAME);
expect(memoryFiles).toContain(DEFAULT_IDENTITY_FILENAME);
// Check symlinks point to correct files
const soulLink = await fs.readlink(path.join(result.memoryDir!, DEFAULT_SOUL_FILENAME));
expect(soulLink).toBe(`../${DEFAULT_SOUL_FILENAME}`);
});
it("does not create memory directory when ensureBootstrapFiles is false", async () => {
const tempDir = await makeTempWorkspace("moltbot-workspace-");
const result = await ensureAgentWorkspace({
dir: tempDir,
ensureBootstrapFiles: false,
});
expect(result.memoryDir).toBeUndefined();
const memoryDirPath = path.join(tempDir, DEFAULT_MEMORY_DIR);
await expect(fs.access(memoryDirPath)).rejects.toThrow();
});
it("does not overwrite existing files in memory directory", async () => {
const tempDir = await makeTempWorkspace("moltbot-workspace-");
const memoryDir = path.join(tempDir, DEFAULT_MEMORY_DIR);
await fs.mkdir(memoryDir, { recursive: true });
// Create an existing file in memory directory
const existingContent = "existing content";
await fs.writeFile(path.join(memoryDir, DEFAULT_SOUL_FILENAME), existingContent);
await ensureAgentWorkspace({
dir: tempDir,
ensureBootstrapFiles: true,
});
// Check existing file was not overwritten
const content = await fs.readFile(path.join(memoryDir, DEFAULT_SOUL_FILENAME), "utf-8");
expect(content).toBe(existingContent);
});
});

View File

@ -28,6 +28,7 @@ export const DEFAULT_HEARTBEAT_FILENAME = "HEARTBEAT.md";
export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
export const DEFAULT_MEMORY_FILENAME = "MEMORY.md";
export const DEFAULT_MEMORY_ALT_FILENAME = "memory.md";
export const DEFAULT_MEMORY_DIR = "memory";
const TEMPLATE_DIR = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
@ -120,6 +121,7 @@ export async function ensureAgentWorkspace(params?: {
ensureBootstrapFiles?: boolean;
}): Promise<{
dir: string;
memoryDir?: string;
agentsPath?: string;
soulPath?: string;
toolsPath?: string;
@ -174,10 +176,39 @@ export async function ensureAgentWorkspace(params?: {
if (isBrandNewWorkspace) {
await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
}
// Ensure memory/ directory exists and symlink identity files for memory search indexing
const memoryDir = path.join(dir, DEFAULT_MEMORY_DIR);
await fs.mkdir(memoryDir, { recursive: true });
// Symlink identity files into memory/ so they are indexed by memory search
const identityFilesToSymlink = [
{ source: soulPath, target: path.join(memoryDir, DEFAULT_SOUL_FILENAME) },
{ source: userPath, target: path.join(memoryDir, DEFAULT_USER_FILENAME) },
{ source: agentsPath, target: path.join(memoryDir, DEFAULT_AGENTS_FILENAME) },
{ source: identityPath, target: path.join(memoryDir, DEFAULT_IDENTITY_FILENAME) },
];
for (const { source, target } of identityFilesToSymlink) {
try {
await fs.access(target);
// Target already exists, skip
} catch {
// Target doesn't exist, create symlink
try {
const relativePath = path.relative(memoryDir, source);
await fs.symlink(relativePath, target);
} catch {
// Symlink creation failed (e.g., on Windows without privileges), skip silently
}
}
}
await ensureGitRepo(dir, isBrandNewWorkspace);
return {
dir,
memoryDir,
agentsPath,
soulPath,
toolsPath,