diff --git a/src/agents/workspace.test.ts b/src/agents/workspace.test.ts index 507788f3c..921be8bea 100644 --- a/src/agents/workspace.test.ts +++ b/src/agents/workspace.test.ts @@ -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); + }); +}); diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index 0cef8e5f0..c98a8152c 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -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,