From 21cff48d3f079f571573fcb553f13de9552f6df4 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 24 Jan 2026 17:46:11 +0700 Subject: [PATCH] refactor(server): switch agent registry to AgentStorage --- packages/server/README.md | 4 +- .../src/server/agent/agent-manager.test.ts | 58 ++-- .../server/src/server/agent/agent-manager.ts | 6 +- .../src/server/agent/agent-mcp.e2e.test.ts | 4 +- .../src/server/agent/agent-projections.ts | 2 +- .../agent-self-identification.e2e.test.ts | 34 +-- ...registry.test.ts => agent-storage.test.ts} | 93 ++---- .../{agent-registry.ts => agent-storage.ts} | 277 +++++++++++------- .../src/server/agent/mcp-server.test.ts | 24 +- .../server/src/server/agent/mcp-server.ts | 18 +- .../agent/providers/claude-agent.test.ts | 10 +- packages/server/src/server/bootstrap.ts | 24 +- packages/server/src/server/config.ts | 2 +- .../daemon-restart-resume.e2e.test.ts | 8 +- .../src/server/persistence-hooks.test.ts | 8 +- .../server/src/server/persistence-hooks.ts | 14 +- packages/server/src/server/session.ts | 24 +- .../src/server/test-utils/paseo-daemon.ts | 2 +- .../server/src/server/websocket-server.ts | 6 +- .../src/server/websocket-session-bridge.ts | 10 +- 20 files changed, 336 insertions(+), 292 deletions(-) rename packages/server/src/server/agent/{agent-registry.test.ts => agent-storage.test.ts} (76%) rename packages/server/src/server/agent/{agent-registry.ts => agent-storage.ts} (52%) diff --git a/packages/server/README.md b/packages/server/README.md index a8c53464a..ac7c35a18 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -75,13 +75,13 @@ DEEPGRAM_API_KEY=... # Streaming STT STT_MODEL=whisper-1 # Optional: override to gpt-4o-transcribe, etc. STT_CONFIDENCE_THRESHOLD=-3.0 # Optional: reject low-confidence clips STT_DEBUG_AUDIO_DIR=.stt-debug # Optional: persist raw dictation audio for debugging -PASEO_HOME=~/.paseo # Runtime state directory (agents.json, etc.) +PASEO_HOME=~/.paseo # Runtime state directory (agents/, etc.) PASEO_PORT=6767 # Voice server + MCP port (falls back to 6767) PORT=3000 # Server port NODE_ENV=development # Environment ``` -`PASEO_HOME` defaults to `~/.paseo` and isolates runtime artifacts like `agents.json`. `PASEO_PORT` takes precedence over `PORT` and controls both the HTTP server and MCP endpoint. For blue/green testing you can run a parallel server without touching production state: +`PASEO_HOME` defaults to `~/.paseo` and isolates runtime artifacts like `agents/`. `PASEO_PORT` takes precedence over `PORT` and controls both the HTTP server and MCP endpoint. For blue/green testing you can run a parallel server without touching production state: ```bash PASEO_HOME=~/.paseo-blue PASEO_PORT=7777 npm run dev diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index d6b51d5fc..dc4721be7 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -6,7 +6,7 @@ import { randomUUID } from "node:crypto"; import { createTestLogger } from "../../test-utils/test-logger.js"; import { AgentManager } from "./agent-manager.js"; -import { AgentRegistry } from "./agent-registry.js"; +import { AgentStorage } from "./agent-storage.js"; import type { AgentClient, AgentRunResult, @@ -106,13 +106,13 @@ describe("AgentManager", () => { test("normalizeConfig does not inject default model when omitted", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); - const registryPath = join(workdir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); const manager = new AgentManager({ clients: { codex: new TestAgentClient(), }, - registry, + registry: storage, logger, idFactory: () => "agent-without-model", }); @@ -127,13 +127,13 @@ describe("AgentManager", () => { test("createAgent persists provided title before returning", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); - const registryPath = join(workdir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); const manager = new AgentManager({ clients: { codex: new TestAgentClient(), }, - registry, + registry: storage, logger, idFactory: () => "agent-with-title", }); @@ -147,20 +147,20 @@ describe("AgentManager", () => { expect(snapshot.id).toBe("agent-with-title"); expect(snapshot.lifecycle).toBe("idle"); - const persisted = await registry.get("agent-with-title"); + const persisted = await storage.get("agent-with-title"); expect(persisted?.title).toBe("Fix Login Bug"); expect(persisted?.id).toBe("agent-with-title"); }); test("createAgent populates runtimeInfo after session creation", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); - const registryPath = join(workdir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); const manager = new AgentManager({ clients: { codex: new TestAgentClient(), }, - registry, + registry: storage, logger, idFactory: () => "agent-with-runtime-info", }); @@ -179,13 +179,13 @@ describe("AgentManager", () => { test("runAgent refreshes runtimeInfo after completion", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); - const registryPath = join(workdir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); const manager = new AgentManager({ clients: { codex: new TestAgentClient(), }, - registry, + registry: storage, logger, idFactory: () => "agent-with-run-runtime", }); @@ -205,14 +205,14 @@ describe("AgentManager", () => { test("listAgents excludes internal agents", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); - const registryPath = join(workdir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); let agentCounter = 0; const manager = new AgentManager({ clients: { codex: new TestAgentClient(), }, - registry, + registry: storage, logger, idFactory: () => `agent-${agentCounter++}`, }); @@ -239,13 +239,13 @@ describe("AgentManager", () => { test("getAgent returns internal agents by ID", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); - const registryPath = join(workdir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); const manager = new AgentManager({ clients: { codex: new TestAgentClient(), }, - registry, + registry: storage, logger, idFactory: () => "internal-agent", }); @@ -264,14 +264,14 @@ describe("AgentManager", () => { test("subscribe does not emit state events for internal agents to global subscribers", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); - const registryPath = join(workdir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); let agentCounter = 0; const manager = new AgentManager({ clients: { codex: new TestAgentClient(), }, - registry, + registry: storage, logger, idFactory: () => `agent-${agentCounter++}`, }); @@ -305,13 +305,13 @@ describe("AgentManager", () => { test("subscribe emits state events for internal agents when subscribed by agentId", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); - const registryPath = join(workdir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); const manager = new AgentManager({ clients: { codex: new TestAgentClient(), }, - registry, + registry: storage, logger, idFactory: () => "internal-agent", }); @@ -340,14 +340,14 @@ describe("AgentManager", () => { test("onAgentAttention is not called for internal agents", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); - const registryPath = join(workdir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); const attentionCalls: string[] = []; const manager = new AgentManager({ clients: { codex: new TestAgentClient(), }, - registry, + registry: storage, logger, idFactory: () => "internal-agent", onAgentAttention: ({ agentId }) => { diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 151db462f..e76d6f7b2 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -28,7 +28,7 @@ import type { ListPersistedAgentsOptions, PersistedAgentDescriptor, } from "./agent-sdk-types.js"; -import type { AgentRegistry } from "./agent-registry.js"; +import type { AgentStorage } from "./agent-storage.js"; export { AGENT_LIFECYCLE_STATUSES, type AgentLifecycleStatus }; @@ -57,7 +57,7 @@ export type AgentManagerOptions = { clients?: Partial>; maxTimelineItems?: number; idFactory?: () => string; - registry?: AgentRegistry; + registry?: AgentStorage; agentControlMcp?: AgentControlMcpConfig; onAgentAttention?: AgentAttentionCallback; logger: Logger; @@ -187,7 +187,7 @@ export class AgentManager { private readonly subscribers = new Set(); private readonly maxTimelineItems: number; private readonly idFactory: () => string; - private readonly registry?: AgentRegistry; + private readonly registry?: AgentStorage; private readonly previousStatuses = new Map(); private readonly agentControlMcp?: AgentControlMcpConfig; private onAgentAttention?: AgentAttentionCallback; diff --git a/packages/server/src/server/agent/agent-mcp.e2e.test.ts b/packages/server/src/server/agent/agent-mcp.e2e.test.ts index 4e916f922..d991b6a77 100644 --- a/packages/server/src/server/agent/agent-mcp.e2e.test.ts +++ b/packages/server/src/server/agent/agent-mcp.e2e.test.ts @@ -101,7 +101,7 @@ describe("agent MCP end-to-end", () => { staticDir, mcpDebug: false, agentClients: {}, - agentRegistryPath: path.join(paseoHome, "agents.json"), + agentStoragePath: path.join(paseoHome, "agents"), agentControlMcp: { url: `http://127.0.0.1:${port}/mcp/agents`, }, @@ -234,7 +234,7 @@ describe("agent MCP end-to-end", () => { staticDir, mcpDebug: false, agentClients: {}, - agentRegistryPath: path.join(paseoHome, "agents.json"), + agentStoragePath: path.join(paseoHome, "agents"), agentControlMcp: { url: `http://127.0.0.1:${port}/mcp/agents`, }, diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index bd4e5c3f1..f09680f06 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -2,7 +2,7 @@ import type { AgentSnapshotPayload } from "../messages.js"; import type { SerializableAgentConfig, StoredAgentRecord, -} from "./agent-registry.js"; +} from "./agent-storage.js"; import type { AgentCapabilityFlags, AgentMetadata, diff --git a/packages/server/src/server/agent/agent-self-identification.e2e.test.ts b/packages/server/src/server/agent/agent-self-identification.e2e.test.ts index 17054d0e8..1fe524437 100644 --- a/packages/server/src/server/agent/agent-self-identification.e2e.test.ts +++ b/packages/server/src/server/agent/agent-self-identification.e2e.test.ts @@ -7,7 +7,7 @@ import { randomUUID } from "node:crypto"; import { createTestLogger } from "../../test-utils/test-logger.js"; import { AgentManager } from "./agent-manager.js"; -import { AgentRegistry } from "./agent-registry.js"; +import { AgentStorage } from "./agent-storage.js"; import { createAgentMcpServer } from "./mcp-server.js"; import { createWorktree } from "../../utils/worktree.js"; import type { @@ -132,11 +132,11 @@ describe("self-identification MCP tools", () => { worktreeSlug: "self-ident", }); - const registryPath = path.join(repoDir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = path.join(repoDir, "agents"); + const storage = new AgentStorage(storagePath, logger); const manager = new AgentManager({ clients: { codex: new TestAgentClient() }, - registry, + registry: storage, logger, idFactory: () => "agent-self-ident", }); @@ -148,7 +148,7 @@ describe("self-identification MCP tools", () => { const server = await createAgentMcpServer({ agentManager: manager, - agentRegistry: registry, + agentStorage: storage, callerAgentId: agent.id, logger, }); @@ -183,11 +183,11 @@ describe("self-identification MCP tools", () => { const nestedDir = path.join(worktree.worktreePath, "nested"); execSync(`mkdir -p "${nestedDir}"`, { stdio: "ignore" }); - const registryPath = path.join(repoDir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = path.join(repoDir, "agents"); + const storage = new AgentStorage(storagePath, logger); const manager = new AgentManager({ clients: { codex: new TestAgentClient() }, - registry, + registry: storage, logger, idFactory: () => "agent-self-ident-subdir", }); @@ -199,7 +199,7 @@ describe("self-identification MCP tools", () => { const server = await createAgentMcpServer({ agentManager: manager, - agentRegistry: registry, + agentStorage: storage, callerAgentId: agent.id, logger, }); @@ -225,11 +225,11 @@ describe("self-identification MCP tools", () => { try { initGitRepo(repoDir); - const registryPath = path.join(repoDir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = path.join(repoDir, "agents"); + const storage = new AgentStorage(storagePath, logger); const manager = new AgentManager({ clients: { codex: new TestAgentClient() }, - registry, + registry: storage, logger, idFactory: () => "agent-non-worktree", }); @@ -241,7 +241,7 @@ describe("self-identification MCP tools", () => { const server = await createAgentMcpServer({ agentManager: manager, - agentRegistry: registry, + agentStorage: storage, callerAgentId: agent.id, logger, }); @@ -259,11 +259,11 @@ describe("self-identification MCP tools", () => { const repoDir = mkdtempSync(path.join(tmpdir(), "paseo-self-ident-")); try { - const registryPath = path.join(repoDir, "agents.json"); - const registry = new AgentRegistry(registryPath, logger); + const storagePath = path.join(repoDir, "agents"); + const storage = new AgentStorage(storagePath, logger); const manager = new AgentManager({ clients: { codex: new TestAgentClient() }, - registry, + registry: storage, logger, idFactory: () => "agent-non-git", }); @@ -275,7 +275,7 @@ describe("self-identification MCP tools", () => { const server = await createAgentMcpServer({ agentManager: manager, - agentRegistry: registry, + agentStorage: storage, callerAgentId: agent.id, logger, }); diff --git a/packages/server/src/server/agent/agent-registry.test.ts b/packages/server/src/server/agent/agent-storage.test.ts similarity index 76% rename from packages/server/src/server/agent/agent-registry.test.ts rename to packages/server/src/server/agent/agent-storage.test.ts index 436a665c1..2f0e92c7b 100644 --- a/packages/server/src/server/agent/agent-registry.test.ts +++ b/packages/server/src/server/agent/agent-storage.test.ts @@ -1,10 +1,10 @@ import { describe, expect, test, beforeEach, afterEach } from "vitest"; import os from "node:os"; import path from "node:path"; -import { mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs"; +import { mkdtempSync, rmSync } from "node:fs"; import { createTestLogger } from "../../test-utils/test-logger.js"; -import { AgentRegistry } from "./agent-registry.js"; +import { AgentStorage } from "./agent-storage.js"; import type { ManagedAgent } from "./agent-manager.js"; import type { AgentPermissionRequest, @@ -91,16 +91,16 @@ function createManagedAgent( return agent; } -describe("AgentRegistry", () => { +describe("AgentStorage", () => { let tmpDir: string; - let filePath: string; - let registry: AgentRegistry; + let storagePath: string; + let storage: AgentStorage; const logger = createTestLogger(); beforeEach(() => { tmpDir = mkdtempSync(path.join(os.tmpdir(), "agent-registry-")); - filePath = path.join(tmpDir, "agents.json"); - registry = new AgentRegistry(filePath, logger); + storagePath = path.join(tmpDir, "agents"); + storage = new AgentStorage(storagePath, logger); }); afterEach(() => { @@ -108,7 +108,7 @@ describe("AgentRegistry", () => { }); test("applySnapshot persists configs and snapshot metadata", async () => { - await registry.applySnapshot( + await storage.applySnapshot( createManagedAgent({ id: "agent-1", cwd: "/tmp/project", @@ -122,7 +122,7 @@ describe("AgentRegistry", () => { }) ); - const records = await registry.list(); + const records = await storage.list(); expect(records).toHaveLength(1); const [record] = records; expect(record.provider).toBe("claude"); @@ -131,7 +131,7 @@ describe("AgentRegistry", () => { expect(record.lastModeId).toBe("coding"); expect(record.lastStatus).toBe("idle"); - const reloaded = new AgentRegistry(filePath, logger); + const reloaded = new AgentStorage(storagePath, logger); const [persisted] = await reloaded.list(); expect(persisted.cwd).toBe("/tmp/project"); expect(persisted.config?.extra?.claude).toMatchObject({ maxThinkingTokens: 1024 }); @@ -140,14 +140,14 @@ describe("AgentRegistry", () => { test("applySnapshot preserves original createdAt timestamp", async () => { const agentId = "agent-created-at"; const firstTimestamp = new Date("2025-01-01T00:00:00.000Z"); - await registry.applySnapshot( + await storage.applySnapshot( createManagedAgent({ id: agentId, createdAt: firstTimestamp }) ); - const initialRecord = await registry.get(agentId); + const initialRecord = await storage.get(agentId); expect(initialRecord?.createdAt).toBe(firstTimestamp.toISOString()); - await registry.applySnapshot( + await storage.applySnapshot( createManagedAgent({ id: agentId, createdAt: new Date("2025-02-01T00:00:00.000Z"), @@ -156,57 +156,57 @@ describe("AgentRegistry", () => { }) ); - const updatedRecord = await registry.get(agentId); + const updatedRecord = await storage.get(agentId); expect(updatedRecord?.createdAt).toBe(firstTimestamp.toISOString()); expect(updatedRecord?.lastStatus).toBe("running"); }); test("stores titles independently of snapshots", async () => { - await registry.applySnapshot( + await storage.applySnapshot( createManagedAgent({ id: "agent-2", provider: "codex", cwd: "/tmp/second", }) ); - await registry.setTitle("agent-2", "Fix Login Bug"); + await storage.setTitle("agent-2", "Fix Login Bug"); - const current = await registry.get("agent-2"); + const current = await storage.get("agent-2"); expect(current?.title).toBe("Fix Login Bug"); - const reloaded = new AgentRegistry(filePath, logger); + const reloaded = new AgentStorage(storagePath, logger); const persisted = await reloaded.get("agent-2"); expect(persisted?.title).toBe("Fix Login Bug"); }); test("setTitle throws when the agent record does not exist", async () => { - await expect(registry.setTitle("missing-agent", "Impossible")) + await expect(storage.setTitle("missing-agent", "Impossible")) .rejects.toThrow("Agent missing-agent not found"); }); test("applySnapshot accepts explicit title overrides", async () => { const agentId = "agent-override"; - await registry.applySnapshot( + await storage.applySnapshot( createManagedAgent({ id: agentId }), { title: "Provided Title" } ); - const record = await registry.get(agentId); + const record = await storage.get(agentId); expect(record?.title).toBe("Provided Title"); }); test("applySnapshot preserves custom titles while updating metadata", async () => { const agentId = "agent-3"; - await registry.applySnapshot( + await storage.applySnapshot( createManagedAgent({ id: agentId, lifecycle: "idle", currentModeId: "plan", }) ); - await registry.setTitle(agentId, "Important Bug Fix"); + await storage.setTitle(agentId, "Important Bug Fix"); - await registry.applySnapshot( + await storage.applySnapshot( createManagedAgent({ id: agentId, lifecycle: "running", @@ -215,44 +215,15 @@ describe("AgentRegistry", () => { }) ); - const record = await registry.get(agentId); + const record = await storage.get(agentId); expect(record?.title).toBe("Important Bug Fix"); expect(record?.lastModeId).toBe("build"); expect(record?.lastStatus).toBe("running"); }); - test("recovers from trailing garbage in agents.json", async () => { - const payload = [ - { - id: "agent-4", - provider: "claude", - cwd: "/tmp/project", - createdAt: "2024-01-01T00:00:00.000Z", - updatedAt: "2024-01-01T00:00:00.000Z", - title: "Recovered agent", - lastStatus: "idle", - lastModeId: "plan", - config: null, - persistence: null, - }, - ]; - writeFileSync( - filePath, - `${JSON.stringify(payload, null, 2)}\nGARBAGE-TRAILING` - ); - - const reloaded = new AgentRegistry(filePath, logger); - const result = await reloaded.list(); - expect(result).toHaveLength(1); - expect(result[0]?.title).toBe("Recovered agent"); - - const sanitized = readFileSync(filePath, "utf8"); - expect(sanitized.includes("GARBAGE-TRAILING")).toBe(false); - }); - test("list returns all agents including internal ones", async () => { // Create a normal agent - await registry.applySnapshot( + await storage.applySnapshot( createManagedAgent({ id: "normal-agent", cwd: "/tmp/project", @@ -260,7 +231,7 @@ describe("AgentRegistry", () => { ); // Create an internal agent - await registry.applySnapshot( + await storage.applySnapshot( createManagedAgent({ id: "internal-agent", cwd: "/tmp/project", @@ -270,12 +241,12 @@ describe("AgentRegistry", () => { ); // Registry should return all agents - filtering is done at the manager level - const records = await registry.list(); + const records = await storage.list(); expect(records).toHaveLength(2); }); test("get returns internal agents by ID", async () => { - await registry.applySnapshot( + await storage.applySnapshot( createManagedAgent({ id: "internal-agent", cwd: "/tmp/project", @@ -284,13 +255,13 @@ describe("AgentRegistry", () => { { internal: true } ); - const record = await registry.get("internal-agent"); + const record = await storage.get("internal-agent"); expect(record).not.toBeNull(); expect(record?.internal).toBe(true); }); test("internal flag is persisted and reloaded", async () => { - await registry.applySnapshot( + await storage.applySnapshot( createManagedAgent({ id: "internal-agent", cwd: "/tmp/project", @@ -300,7 +271,7 @@ describe("AgentRegistry", () => { ); // Reload the registry from disk - const reloaded = new AgentRegistry(filePath, logger); + const reloaded = new AgentStorage(storagePath, logger); const record = await reloaded.get("internal-agent"); expect(record?.internal).toBe(true); diff --git a/packages/server/src/server/agent/agent-registry.ts b/packages/server/src/server/agent/agent-storage.ts similarity index 52% rename from packages/server/src/server/agent/agent-registry.ts rename to packages/server/src/server/agent/agent-storage.ts index f31607b63..5eb846c26 100644 --- a/packages/server/src/server/agent/agent-registry.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -64,19 +64,112 @@ export type SerializableAgentConfig = Pick< export type StoredAgentRecord = z.infer; -export class AgentRegistry { +export class AgentStorage { private cache: Map = new Map(); + private pathById: Map = new Map(); private loaded = false; - private filePath: string; + private baseDir: string; private loadPromise: Promise | null = null; private logger: Logger; - constructor(filePath: string, logger: Logger) { - this.filePath = filePath; - this.logger = logger.child({ module: "agent", component: "agent-registry" }); + constructor(baseDir: string, logger: Logger) { + this.baseDir = baseDir; + this.logger = logger.child({ module: "agent", component: "agent-storage" }); } - async load(): Promise { + async list(): Promise { + await this.load(); + return Array.from(this.cache.values()); + } + + async get(agentId: string): Promise { + await this.load(); + const cached = this.cache.get(agentId); + if (cached) { + return cached; + } + + const discoveredPath = await this.findAgentPathById(agentId); + if (!discoveredPath) { + return null; + } + + const record = await this.readRecordFile(discoveredPath); + if (!record) { + return null; + } + + this.cache.set(record.id, record); + this.pathById.set(record.id, discoveredPath); + return record; + } + + async upsert(record: StoredAgentRecord): Promise { + await this.load(); + const nextPath = this.buildRecordPath(record); + const previousPath = this.pathById.get(record.id); + + await fs.mkdir(path.dirname(nextPath), { recursive: true }); + await writeFileAtomically(nextPath, JSON.stringify(record, null, 2)); + + if (previousPath && previousPath !== nextPath) { + try { + await fs.unlink(previousPath); + } catch { + // ignore cleanup errors + } + } + + this.cache.set(record.id, record); + this.pathById.set(record.id, nextPath); + } + + async remove(agentId: string): Promise { + await this.load(); + const existingPath = + this.pathById.get(agentId) ?? (await this.findAgentPathById(agentId)); + if (existingPath) { + try { + await fs.unlink(existingPath); + } catch { + // ignore removal errors + } + } + + this.cache.delete(agentId); + this.pathById.delete(agentId); + } + + async applySnapshot( + agent: ManagedAgent, + options?: { title?: string | null; internal?: boolean } + ): Promise { + await this.load(); + const existing = (await this.get(agent.id)) ?? null; + const hasTitleOverride = + options !== undefined && Object.prototype.hasOwnProperty.call(options, "title"); + const hasInternalOverride = + options !== undefined && Object.prototype.hasOwnProperty.call(options, "internal"); + const record = toStoredAgentRecord(agent, { + title: hasTitleOverride ? options?.title ?? null : existing?.title ?? null, + createdAt: existing?.createdAt, + internal: hasInternalOverride + ? options?.internal + : (agent.internal ?? existing?.internal), + }); + await this.upsert(record); + } + + async setTitle(agentId: string, title: string): Promise { + await this.load(); + const record = await this.get(agentId); + if (!record) { + throw new Error(`Agent ${agentId} not found`); + } + await this.upsert({ ...record, title }); + } + + private async load(): Promise { if (this.loaded) { return Array.from(this.cache.values()); } @@ -89,140 +182,120 @@ export class AgentRegistry { } private async doLoad(): Promise { + this.cache.clear(); + this.pathById.clear(); + try { - const content = await fs.readFile(this.filePath, "utf8"); - const parsed = await this.parseContent(content); + const records = await this.scanDisk(); this.loaded = true; - return parsed; + return records; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { this.loaded = true; - this.cache.clear(); return []; } this.logger.error({ err: error }, "Failed to load agents"); this.loaded = true; - this.cache.clear(); return []; } } - async list(): Promise { - return this.load(); - } - - async get(agentId: string): Promise { - await this.load(); - return this.cache.get(agentId) ?? null; - } - - async upsert(record: StoredAgentRecord): Promise { - await this.load(); - this.cache.set(record.id, record); - await this.flush(); - } - - async remove(agentId: string): Promise { - await this.load(); - this.cache.delete(agentId); - await this.flush(); - } - - async applySnapshot( - agent: ManagedAgent, - options?: { title?: string | null; internal?: boolean } - ): Promise { - await this.load(); - const existing = this.cache.get(agent.id); - const hasTitleOverride = - options !== undefined && Object.prototype.hasOwnProperty.call(options, "title"); - const hasInternalOverride = - options !== undefined && Object.prototype.hasOwnProperty.call(options, "internal"); - const record = toStoredAgentRecord(agent, { - title: hasTitleOverride ? options?.title ?? null : existing?.title ?? null, - createdAt: existing?.createdAt, - internal: hasInternalOverride ? options?.internal : (agent.internal ?? existing?.internal), - }); - this.cache.set(agent.id, record); - await this.flush(); - } - - async setTitle(agentId: string, title: string): Promise { - await this.load(); - const record = this.cache.get(agentId); - if (!record) { - throw new Error(`Agent ${agentId} not found`); - } - this.cache.set(agentId, { ...record, title }); - await this.flush(); - } - - private async flush(): Promise { - const payload = JSON.stringify(Array.from(this.cache.values()), null, 2); - await fs.mkdir(path.dirname(this.filePath), { recursive: true }); - await writeFileAtomically(this.filePath, payload); - } - - private async parseContent(content: string): Promise { + private async scanDisk(): Promise { + const records: StoredAgentRecord[] = []; + let entries: Array = []; try { - return this.parseRecords(content); + entries = await fs.readdir(this.baseDir, { withFileTypes: true }); } catch (error) { - if (error instanceof SyntaxError) { - const recovered = await this.tryRecoverCorruptedContent(content); - if (recovered) { - return recovered; - } + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return []; } throw error; } - } - private parseRecords(content: string): StoredAgentRecord[] { - const parsed = JSON.parse(content); - if (!Array.isArray(parsed)) { - throw new Error("Invalid agents.json format"); - } - const records: StoredAgentRecord[] = []; - for (const entry of parsed) { + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + const projectDir = path.join(this.baseDir, entry.name); + let files: Array = []; try { - const record = STORED_AGENT_SCHEMA.parse(entry); + files = await fs.readdir(projectDir, { withFileTypes: true }); + } catch { + continue; + } + + for (const file of files) { + if (!file.isFile() || !file.name.endsWith(".json")) { + continue; + } + const filePath = path.join(projectDir, file.name); + const record = await this.readRecordFile(filePath); + if (!record) { + continue; + } records.push(record); this.cache.set(record.id, record); - } catch (error) { - this.logger.error({ err: error }, "Skipping invalid record"); + this.pathById.set(record.id, filePath); } } + return records; } - private async tryRecoverCorruptedContent( - content: string - ): Promise { - const start = content.indexOf("["); - const end = content.lastIndexOf("]"); - if (start === -1 || end === -1 || end <= start) { - return null; - } - const candidate = content.slice(start, end + 1); + private async readRecordFile(filePath: string): Promise { try { - this.cache.clear(); - const records = this.parseRecords(candidate); - this.logger.warn("Recovered corrupted agents.json payload; rewrote sanitized copy"); - const sanitizedPayload = JSON.stringify(records, null, 2); - await writeFileAtomically(this.filePath, sanitizedPayload); - return records; + const content = await fs.readFile(filePath, "utf8"); + const parsed = JSON.parse(content); + return STORED_AGENT_SCHEMA.parse(parsed); } catch (error) { - this.cache.clear(); + this.logger.error({ err: error, filePath }, "Skipping invalid agent record"); return null; } } + + private buildRecordPath(record: StoredAgentRecord): string { + const projectDir = projectDirNameFromCwd(record.cwd); + return path.join(this.baseDir, projectDir, `${record.id}.json`); + } + + private async findAgentPathById(agentId: string): Promise { + let projects: Array = []; + try { + projects = await fs.readdir(this.baseDir, { withFileTypes: true }); + } catch { + return null; + } + + for (const project of projects) { + if (!project.isDirectory()) { + continue; + } + const candidate = path.join(this.baseDir, project.name, `${agentId}.json`); + try { + await fs.access(candidate); + return candidate; + } catch { + // not here + } + } + + return null; + } +} + +function projectDirNameFromCwd(cwd: string): string { + const trimmed = cwd.replace(/^[\\/]+/, "").replace(/[\\/]+$/, ""); + if (!trimmed) { + return "root"; + } + return trimmed.replace(/[\\/]+/g, "-"); } async function writeFileAtomically(targetPath: string, payload: string) { const directory = path.dirname(targetPath); const tempPath = path.join( directory, - `.agents.json.tmp-${process.pid}-${Date.now()}-${randomUUID()}` + `.agent.tmp-${process.pid}-${Date.now()}-${randomUUID()}` ); await fs.writeFile(tempPath, payload, "utf8"); await fs.rename(tempPath, targetPath); diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index 3588a4817..24bb0a765 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -3,14 +3,14 @@ import { describe, expect, it, vi } from "vitest"; import { createTestLogger } from "../../test-utils/test-logger.js"; import { createAgentMcpServer } from "./mcp-server.js"; import type { AgentManager, ManagedAgent } from "./agent-manager.js"; -import type { AgentRegistry } from "./agent-registry.js"; +import type { AgentStorage } from "./agent-storage.js"; type TestDeps = { agentManager: AgentManager; - agentRegistry: AgentRegistry; + agentStorage: AgentStorage; spies: { agentManager: Record; - agentRegistry: Record; + agentStorage: Record; }; }; @@ -28,7 +28,7 @@ function createTestDeps(): TestDeps { getPendingPermissions: vi.fn(), }; - const agentRegistrySpies = { + const agentStorageSpies = { get: vi.fn().mockResolvedValue(null), setTitle: vi.fn().mockResolvedValue(undefined), applySnapshot: vi.fn(), @@ -38,10 +38,10 @@ function createTestDeps(): TestDeps { return { agentManager: agentManagerSpies as unknown as AgentManager, - agentRegistry: agentRegistrySpies as unknown as AgentRegistry, + agentStorage: agentStorageSpies as unknown as AgentStorage, spies: { agentManager: agentManagerSpies, - agentRegistry: agentRegistrySpies, + agentStorage: agentStorageSpies, }, }; } @@ -50,8 +50,8 @@ describe("create_agent MCP tool", () => { const logger = createTestLogger(); it("requires a concise title no longer than 60 characters", async () => { - const { agentManager, agentRegistry } = createTestDeps(); - const server = await createAgentMcpServer({ agentManager, agentRegistry, logger }); + const { agentManager, agentStorage } = createTestDeps(); + const server = await createAgentMcpServer({ agentManager, agentStorage, logger }); const tool = (server as any)._registeredTools["create_agent"]; expect(tool).toBeDefined(); @@ -79,7 +79,7 @@ describe("create_agent MCP tool", () => { }); it("passes caller-provided titles directly into createAgent", async () => { - const { agentManager, agentRegistry, spies } = createTestDeps(); + const { agentManager, agentStorage, spies } = createTestDeps(); spies.agentManager.createAgent.mockResolvedValue({ id: "agent-123", cwd: "/tmp/repo", @@ -88,7 +88,7 @@ describe("create_agent MCP tool", () => { availableModes: [], } as ManagedAgent); - const server = await createAgentMcpServer({ agentManager, agentRegistry, logger }); + const server = await createAgentMcpServer({ agentManager, agentStorage, logger }); const tool = (server as any)._registeredTools["create_agent"]; await tool.callback({ cwd: "/tmp/repo", @@ -104,14 +104,14 @@ describe("create_agent MCP tool", () => { }); it("set_title trims and persists titles for caller agent", async () => { - const { agentManager, agentRegistry, spies } = createTestDeps(); + const { agentManager, agentStorage, spies } = createTestDeps(); spies.agentManager.getAgent.mockReturnValue({ id: "agent-1", } as ManagedAgent); const server = await createAgentMcpServer({ agentManager, - agentRegistry, + agentStorage, logger, callerAgentId: "agent-1", }); diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index 79aff5c72..5a7aac81b 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -24,7 +24,7 @@ import { import { toAgentPayload } from "./agent-projections.js"; import { curateAgentActivity } from "./activity-curator.js"; import { AGENT_PROVIDER_DEFINITIONS } from "./provider-registry.js"; -import { AgentRegistry } from "./agent-registry.js"; +import { AgentStorage } from "./agent-storage.js"; import { createWorktree, isPaseoOwnedWorktreeCwd, @@ -36,7 +36,7 @@ import { injectLeadingPaseoInstructionTag } from "./paseo-instructions-tag.js"; export interface AgentMcpServerOptions { agentManager: AgentManager; - agentRegistry: AgentRegistry; + agentStorage: AgentStorage; /** * ID of the agent that is connecting to this MCP server. * When set, create_agent will auto-inject this as parentAgentId. @@ -240,12 +240,12 @@ function sanitizePermissionRequest( } async function resolveAgentTitle( - agentRegistry: AgentRegistry, + agentStorage: AgentStorage, agentId: string, logger: Logger ): Promise { try { - const record = await agentRegistry.get(agentId); + const record = await agentStorage.get(agentId); return record?.title ?? null; } catch (error) { logger.error( @@ -257,18 +257,18 @@ async function resolveAgentTitle( } async function serializeSnapshotWithMetadata( - agentRegistry: AgentRegistry, + agentStorage: AgentStorage, snapshot: ManagedAgent, logger: Logger ) { - const title = await resolveAgentTitle(agentRegistry, snapshot.id, logger); + const title = await resolveAgentTitle(agentStorage, snapshot.id, logger); return serializeAgentSnapshot(snapshot, { title }); } export async function createAgentMcpServer( options: AgentMcpServerOptions ): Promise { - const { agentManager, agentRegistry, callerAgentId, logger } = options; + const { agentManager, agentStorage, callerAgentId, logger } = options; const childLogger = logger.child({ module: "agent", component: "mcp-server" }); const waitTracker = new WaitForAgentTracker(logger); @@ -776,7 +776,7 @@ export async function createAgentMcpServer( } const structuredSnapshot = await serializeSnapshotWithMetadata( - agentRegistry, + agentStorage, snapshot, childLogger ); @@ -804,7 +804,7 @@ export async function createAgentMcpServer( const snapshots = agentManager.listAgents(); const agents = await Promise.all( snapshots.map((snapshot) => - serializeSnapshotWithMetadata(agentRegistry, snapshot, childLogger) + serializeSnapshotWithMetadata(agentStorage, snapshot, childLogger) ) ); return { diff --git a/packages/server/src/server/agent/providers/claude-agent.test.ts b/packages/server/src/server/agent/providers/claude-agent.test.ts index 39e8c4509..4f42f5453 100644 --- a/packages/server/src/server/agent/providers/claude-agent.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.test.ts @@ -33,7 +33,7 @@ import type { AgentTimelineItem, } from "../agent-sdk-types.js"; import { AgentManager } from "../agent-manager.js"; -import { AgentRegistry } from "../agent-registry.js"; +import { AgentStorage } from "../agent-storage.js"; import { createAgentMcpServer } from "../mcp-server.js"; const createHTTPServer = createServer; @@ -116,11 +116,11 @@ async function startAgentMcpServer(): Promise { const httpServer = createHTTPServer(app); const registryDir = mkdtempSync(path.join(os.tmpdir(), "agent-mcp-registry-")); - const registryPath = path.join(registryDir, "agents.json"); - const agentRegistry = new AgentRegistry(registryPath, testLogger); + const storagePath = path.join(registryDir, "agents"); + const agentStorage = new AgentStorage(storagePath, testLogger); const agentManager = new AgentManager({ clients: {}, - registry: agentRegistry, + registry: agentStorage, logger: testLogger, }); @@ -130,7 +130,7 @@ async function startAgentMcpServer(): Promise { const createAgentMcpTransport = async (callerAgentId?: string) => { const agentMcpServer = await createAgentMcpServer({ agentManager, - agentRegistry, + agentStorage, callerAgentId, logger: testLogger, }); diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index c9f476b27..d5d3fa643 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -42,8 +42,8 @@ import { DownloadTokenStore } from "./file-download/token-store.js"; import { OpenAISTT, type STTConfig } from "./agent/stt-openai.js"; import { OpenAITTS, type TTSConfig } from "./agent/tts-openai.js"; import { AgentManager } from "./agent/agent-manager.js"; -import { AgentRegistry } from "./agent/agent-registry.js"; -import { attachAgentRegistryPersistence } from "./persistence-hooks.js"; +import { AgentStorage } from "./agent/agent-storage.js"; +import { attachAgentStoragePersistence } from "./persistence-hooks.js"; import { createAgentMcpServer } from "./agent/mcp-server.js"; import { createAllClients, shutdownProviders } from "./agent/provider-registry.js"; import { createTerminalManager, type TerminalManager } from "../terminal/terminal-manager.js"; @@ -77,7 +77,7 @@ export type PaseoDaemonConfig = { staticDir: string; mcpDebug: boolean; agentClients: Partial>; - agentRegistryPath: string; + agentStoragePath: string; agentControlMcp: AgentControlMcpConfig; relayEnabled?: boolean; relayEndpoint?: string; @@ -89,7 +89,7 @@ export type PaseoDaemonConfig = { export interface PaseoDaemon { config: PaseoDaemonConfig; agentManager: AgentManager; - agentRegistry: AgentRegistry; + agentStorage: AgentStorage; terminalManager: TerminalManager; start(): Promise; stop(): Promise; @@ -98,7 +98,7 @@ export interface PaseoDaemon { export async function createPaseoDaemon( config: PaseoDaemonConfig, rootLogger: Logger -): Promise { + ): Promise { const logger = rootLogger.child({ module: "bootstrap" }); const connectionSessionId = randomUUID(); let relayTransport: RelayTransportController | null = null; @@ -204,21 +204,21 @@ export async function createPaseoDaemon( const httpServer = createHTTPServer(app); - const agentRegistry = new AgentRegistry(config.agentRegistryPath, logger); + const agentStorage = new AgentStorage(config.agentStoragePath, logger); const agentManager = new AgentManager({ clients: { ...createAllClients(logger), ...config.agentClients, }, - registry: agentRegistry, + registry: agentStorage, agentControlMcp: config.agentControlMcp, logger, }); const terminalManager = createTerminalManager(); - attachAgentRegistryPersistence(logger, agentManager, agentRegistry); - const persistedRecords = await agentRegistry.list(); + attachAgentStoragePersistence(logger, agentManager, agentStorage); + const persistedRecords = await agentStorage.list(); logger.info( `Agent registry loaded (${persistedRecords.length} record${persistedRecords.length === 1 ? "" : "s"}); agents will initialize on demand` ); @@ -229,7 +229,7 @@ export async function createPaseoDaemon( const createAgentMcpTransport = async (callerAgentId?: string) => { const agentMcpServer = await createAgentMcpServer({ agentManager, - agentRegistry, + agentStorage, callerAgentId, logger, }); @@ -374,7 +374,7 @@ export async function createPaseoDaemon( httpServer, logger, agentManager, - agentRegistry, + agentStorage, downloadTokenStore, config.paseoHome, { @@ -470,7 +470,7 @@ export async function createPaseoDaemon( return { config, agentManager, - agentRegistry, + agentStorage, terminalManager, start, stop, diff --git a/packages/server/src/server/config.ts b/packages/server/src/server/config.ts index c66b0070a..2077e34d6 100644 --- a/packages/server/src/server/config.ts +++ b/packages/server/src/server/config.ts @@ -83,7 +83,7 @@ export function loadConfig( agentControlMcp: { url: `http://${mcpListen}${DEFAULT_AGENT_MCP_ROUTE}`, }, - agentRegistryPath: path.join(paseoHome, "agents.json"), + agentStoragePath: path.join(paseoHome, "agents"), staticDir: "public", agentClients: {}, relayEnabled: true, diff --git a/packages/server/src/server/daemon-e2e/daemon-restart-resume.e2e.test.ts b/packages/server/src/server/daemon-e2e/daemon-restart-resume.e2e.test.ts index 13e063bbe..32768bf55 100644 --- a/packages/server/src/server/daemon-e2e/daemon-restart-resume.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/daemon-restart-resume.e2e.test.ts @@ -56,7 +56,7 @@ async function startDaemon(options: { staticDir, mcpDebug: false, agentClients: {}, - agentRegistryPath: path.join(options.paseoHome, "agents.json"), + agentStoragePath: path.join(options.paseoHome, "agents"), agentControlMcp: { url: `http://127.0.0.1:${port}/mcp/agents`, }, @@ -184,9 +184,9 @@ describe("daemon restart and agent resume", () => { await stopDaemon(currentDaemon); currentDaemon = null; - // Verify agents.json was persisted - const agentsJsonPath = path.join(paseoHome, "agents.json"); - expect(existsSync(agentsJsonPath)).toBe(true); + // Verify agent storage was persisted + const agentsDir = path.join(paseoHome, "agents"); + expect(existsSync(agentsDir)).toBe(true); // === PHASE 3: Start a NEW daemon with the SAME paseoHome === currentDaemon = await startDaemon({ paseoHome, staticDir }); diff --git a/packages/server/src/server/persistence-hooks.test.ts b/packages/server/src/server/persistence-hooks.test.ts index 4e0abd0ca..0a601eabd 100644 --- a/packages/server/src/server/persistence-hooks.test.ts +++ b/packages/server/src/server/persistence-hooks.test.ts @@ -1,9 +1,9 @@ import { describe, expect, test, vi } from "vitest"; import type { ManagedAgent } from "./agent/agent-manager.js"; -import type { StoredAgentRecord } from "./agent/agent-registry.js"; +import type { StoredAgentRecord } from "./agent/agent-storage.js"; import { - attachAgentRegistryPersistence, + attachAgentStoragePersistence, } from "./persistence-hooks.js"; import type { AgentPermissionRequest, @@ -108,7 +108,7 @@ function createRecord( } describe("persistence hooks", () => { - test("attachAgentRegistryPersistence forwards agent snapshots", async () => { + test("attachAgentStoragePersistence forwards agent snapshots", async () => { const applySnapshot = vi.fn().mockResolvedValue(undefined); let subscriber: (event: any) => void = () => { throw new Error("Agent manager subscriber was not registered"); @@ -123,7 +123,7 @@ describe("persistence hooks", () => { }; }), }; - attachAgentRegistryPersistence(testLogger, agentManager as any, { + attachAgentStoragePersistence(testLogger, agentManager as any, { applySnapshot, list: vi.fn(), } as any); diff --git a/packages/server/src/server/persistence-hooks.ts b/packages/server/src/server/persistence-hooks.ts index e3ebcfe10..46daa80dc 100644 --- a/packages/server/src/server/persistence-hooks.ts +++ b/packages/server/src/server/persistence-hooks.ts @@ -4,9 +4,9 @@ import type { AgentSessionConfig, } from "./agent/agent-sdk-types.js"; import type { - AgentRegistry, + AgentStorage, StoredAgentRecord, -} from "./agent/agent-registry.js"; +} from "./agent/agent-storage.js"; type LoggerLike = { child(bindings: Record): LoggerLike; @@ -17,7 +17,7 @@ function getLogger(logger: LoggerLike): LoggerLike { return logger.child({ module: "persistence" }); } -type AgentRegistryPersistence = Pick; +type AgentStoragePersistence = Pick; type AgentManagerStateSource = Pick; function isKnownProvider(provider: string): provider is AgentProvider { @@ -25,20 +25,20 @@ function isKnownProvider(provider: string): provider is AgentProvider { } /** - * Attach AgentRegistry persistence to an AgentManager instance so every + * Attach AgentStorage persistence to an AgentManager instance so every * agent_state snapshot is flushed to disk. */ -export function attachAgentRegistryPersistence( +export function attachAgentStoragePersistence( logger: LoggerLike, agentManager: AgentManagerStateSource, - registry: AgentRegistryPersistence + storage: AgentStoragePersistence ): () => void { const log = getLogger(logger); const unsubscribe = agentManager.subscribe((event) => { if (event.type !== "agent_state") { return; } - void registry.applySnapshot(event.agent).catch((error) => { + void storage.applySnapshot(event.agent).catch((error) => { log.error({ err: error, agentId: event.agent.id }, "Failed to persist agent snapshot"); }); }); diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 1336a9f05..dda044e00 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -63,7 +63,7 @@ import type { AgentPersistenceHandle, AgentTimelineItem, } from "./agent/agent-sdk-types.js"; -import { AgentRegistry, type StoredAgentRecord } from "./agent/agent-registry.js"; +import { AgentStorage, type StoredAgentRecord } from "./agent/agent-storage.js"; import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js"; import { listDirectoryEntries, @@ -307,7 +307,7 @@ export class Session { > | null = null; private agentTools: ToolSet | null = null; private agentManager: AgentManager; - private readonly agentRegistry: AgentRegistry; + private readonly agentStorage: AgentStorage; private readonly agentMcpConfig: AgentMcpClientConfig; private readonly downloadTokenStore: DownloadTokenStore; private readonly pushTokenStore: PushTokenStore; @@ -329,7 +329,7 @@ export class Session { downloadTokenStore: DownloadTokenStore, pushTokenStore: PushTokenStore, agentManager: AgentManager, - agentRegistry: AgentRegistry, + agentStorage: AgentStorage, agentMcpConfig: AgentMcpClientConfig, stt: OpenAISTT | null, tts: OpenAITTS | null, @@ -342,7 +342,7 @@ export class Session { this.downloadTokenStore = downloadTokenStore; this.pushTokenStore = pushTokenStore; this.agentManager = agentManager; - this.agentRegistry = agentRegistry; + this.agentStorage = agentStorage; this.agentMcpConfig = agentMcpConfig; this.terminalManager = terminalManager; this.voiceConversationStore = voiceConversationStore; @@ -653,7 +653,7 @@ export class Session { } const initPromise = (async () => { - const record = await this.agentRegistry.get(agentId); + const record = await this.agentStorage.get(agentId); if (!record) { throw new Error(`Agent not found: ${agentId}`); } @@ -704,7 +704,7 @@ export class Session { private async getStoredAgentTitle(agentId: string): Promise { try { - const record = await this.agentRegistry.get(agentId); + const record = await this.agentStorage.get(agentId); const title = record?.title ?? null; return title; } catch (error) { @@ -1105,7 +1105,7 @@ export class Session { } try { - await this.agentRegistry.remove(agentId); + await this.agentStorage.remove(agentId); } catch (error: any) { this.sessionLogger.error( { err: error, agentId }, @@ -1748,7 +1748,7 @@ export class Session { snapshot = existing; } } else { - const record = await this.agentRegistry.get(agentId); + const record = await this.agentStorage.get(agentId); if (!record) { throw new Error(`Agent not found: ${agentId}`); } @@ -3133,19 +3133,19 @@ export class Session { // ignore cleanup errors } try { - await this.agentRegistry.remove(agent.id); + await this.agentStorage.remove(agent.id); } catch { // ignore cleanup errors } } } - const registryRecords = await this.agentRegistry.list(); + const registryRecords = await this.agentStorage.list(); for (const record of registryRecords) { if (this.isPathWithinRoot(targetPath, record.cwd)) { removedAgents.add(record.id); try { - await this.agentRegistry.remove(record.id); + await this.agentStorage.remove(record.id); } catch { // ignore cleanup errors } @@ -3604,7 +3604,7 @@ export class Session { ); // Add persisted agents that have not been lazily initialized yet - const registryRecords = await this.agentRegistry.list(); + const registryRecords = await this.agentStorage.list(); const liveIds = new Set(agentSnapshots.map((a) => a.id)); const persistedAgents = registryRecords .filter((record) => !liveIds.has(record.id)) diff --git a/packages/server/src/server/test-utils/paseo-daemon.ts b/packages/server/src/server/test-utils/paseo-daemon.ts index ea69f1384..c6ffd937f 100644 --- a/packages/server/src/server/test-utils/paseo-daemon.ts +++ b/packages/server/src/server/test-utils/paseo-daemon.ts @@ -61,7 +61,7 @@ export async function createTestPaseoDaemon( staticDir, mcpDebug: false, agentClients: {}, - agentRegistryPath: path.join(paseoHome, "agents.json"), + agentStoragePath: path.join(paseoHome, "agents"), agentControlMcp: { url: `http://127.0.0.1:${port}/mcp/agents`, }, diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index 17afef7b0..437a8e738 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -1,7 +1,7 @@ import { WebSocketServer } from "ws"; import type { Server as HTTPServer } from "http"; import type { AgentManager } from "./agent/agent-manager.js"; -import type { AgentRegistry } from "./agent/agent-registry.js"; +import type { AgentStorage } from "./agent/agent-storage.js"; import type { DownloadTokenStore } from "./file-download/token-store.js"; import type { OpenAISTT } from "./agent/stt-openai.js"; import type { OpenAITTS } from "./agent/tts-openai.js"; @@ -31,7 +31,7 @@ export class VoiceAssistantWebSocketServer { server: HTTPServer, logger: pino.Logger, agentManager: AgentManager, - agentRegistry: AgentRegistry, + agentStorage: AgentStorage, downloadTokenStore: DownloadTokenStore, paseoHome: string, agentMcpConfig: AgentMcpClientConfig, @@ -43,7 +43,7 @@ export class VoiceAssistantWebSocketServer { this.bridge = new WebSocketSessionBridge( this.logger, agentManager, - agentRegistry, + agentStorage, downloadTokenStore, paseoHome, agentMcpConfig, diff --git a/packages/server/src/server/websocket-session-bridge.ts b/packages/server/src/server/websocket-session-bridge.ts index 189bb7f3a..a50e56491 100644 --- a/packages/server/src/server/websocket-session-bridge.ts +++ b/packages/server/src/server/websocket-session-bridge.ts @@ -8,7 +8,7 @@ import { } from "./messages.js"; import { Session } from "./session.js"; import { AgentManager } from "./agent/agent-manager.js"; -import { AgentRegistry } from "./agent/agent-registry.js"; +import { AgentStorage } from "./agent/agent-storage.js"; import type { AgentProvider } from "./agent/agent-sdk-types.js"; import { DownloadTokenStore } from "./file-download/token-store.js"; import { PushTokenStore } from "./push/token-store.js"; @@ -29,7 +29,7 @@ export class WebSocketSessionBridge { private readonly sessions: Map = new Map(); private clientIdCounter = 0; private readonly agentManager: AgentManager; - private readonly agentRegistry: AgentRegistry; + private readonly agentStorage: AgentStorage; private readonly downloadTokenStore: DownloadTokenStore; private readonly pushTokenStore: PushTokenStore; private readonly pushService: PushService; @@ -42,7 +42,7 @@ export class WebSocketSessionBridge { constructor( logger: pino.Logger, agentManager: AgentManager, - agentRegistry: AgentRegistry, + agentStorage: AgentStorage, downloadTokenStore: DownloadTokenStore, paseoHome: string, agentMcpConfig: AgentMcpClientConfig, @@ -51,7 +51,7 @@ export class WebSocketSessionBridge { ) { this.logger = logger.child({ module: "websocket-session-bridge" }); this.agentManager = agentManager; - this.agentRegistry = agentRegistry; + this.agentStorage = agentStorage; this.downloadTokenStore = downloadTokenStore; this.agentMcpConfig = agentMcpConfig; this.stt = speech?.stt ?? null; @@ -87,7 +87,7 @@ export class WebSocketSessionBridge { this.downloadTokenStore, this.pushTokenStore, this.agentManager, - this.agentRegistry, + this.agentStorage, this.agentMcpConfig, this.stt, this.tts,