refactor(server): switch agent registry to AgentStorage

This commit is contained in:
Mohamed Boudra
2026-01-24 17:46:11 +07:00
parent 9b82c02fd2
commit 21cff48d3f
20 changed files with 336 additions and 292 deletions
+2 -2
View File
@@ -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
@@ -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 }) => {
@@ -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<Record<AgentProvider, AgentClient>>;
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<SubscriptionRecord>();
private readonly maxTimelineItems: number;
private readonly idFactory: () => string;
private readonly registry?: AgentRegistry;
private readonly registry?: AgentStorage;
private readonly previousStatuses = new Map<string, AgentLifecycleStatus>();
private readonly agentControlMcp?: AgentControlMcpConfig;
private onAgentAttention?: AgentAttentionCallback;
@@ -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`,
},
@@ -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,
@@ -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,
});
@@ -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);
@@ -64,19 +64,112 @@ export type SerializableAgentConfig = Pick<
export type StoredAgentRecord = z.infer<typeof STORED_AGENT_SCHEMA>;
export class AgentRegistry {
export class AgentStorage {
private cache: Map<string, StoredAgentRecord> = new Map();
private pathById: Map<string, string> = new Map();
private loaded = false;
private filePath: string;
private baseDir: string;
private loadPromise: Promise<StoredAgentRecord[]> | 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<StoredAgentRecord[]> {
async list(): Promise<StoredAgentRecord[]> {
await this.load();
return Array.from(this.cache.values());
}
async get(agentId: string): Promise<StoredAgentRecord | null> {
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<void> {
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<void> {
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<void> {
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<void> {
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<StoredAgentRecord[]> {
if (this.loaded) {
return Array.from(this.cache.values());
}
@@ -89,140 +182,120 @@ export class AgentRegistry {
}
private async doLoad(): Promise<StoredAgentRecord[]> {
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<StoredAgentRecord[]> {
return this.load();
}
async get(agentId: string): Promise<StoredAgentRecord | null> {
await this.load();
return this.cache.get(agentId) ?? null;
}
async upsert(record: StoredAgentRecord): Promise<void> {
await this.load();
this.cache.set(record.id, record);
await this.flush();
}
async remove(agentId: string): Promise<void> {
await this.load();
this.cache.delete(agentId);
await this.flush();
}
async applySnapshot(
agent: ManagedAgent,
options?: { title?: string | null; internal?: boolean }
): Promise<void> {
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<void> {
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<void> {
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<StoredAgentRecord[]> {
private async scanDisk(): Promise<StoredAgentRecord[]> {
const records: StoredAgentRecord[] = [];
let entries: Array<import("node:fs").Dirent> = [];
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<import("node:fs").Dirent> = [];
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<StoredAgentRecord[] | null> {
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<StoredAgentRecord | null> {
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<string | null> {
let projects: Array<import("node:fs").Dirent> = [];
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);
@@ -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<string, any>;
agentRegistry: Record<string, any>;
agentStorage: Record<string, any>;
};
};
@@ -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",
});
@@ -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<string | null> {
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<McpServer> {
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 {
@@ -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<AgentMcpServerHandle> {
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<AgentMcpServerHandle> {
const createAgentMcpTransport = async (callerAgentId?: string) => {
const agentMcpServer = await createAgentMcpServer({
agentManager,
agentRegistry,
agentStorage,
callerAgentId,
logger: testLogger,
});
+12 -12
View File
@@ -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<Record<AgentProvider, AgentClient>>;
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<void>;
stop(): Promise<void>;
@@ -98,7 +98,7 @@ export interface PaseoDaemon {
export async function createPaseoDaemon(
config: PaseoDaemonConfig,
rootLogger: Logger
): Promise<PaseoDaemon> {
): Promise<PaseoDaemon> {
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,
+1 -1
View File
@@ -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,
@@ -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 });
@@ -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);
@@ -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<string, unknown>): LoggerLike;
@@ -17,7 +17,7 @@ function getLogger(logger: LoggerLike): LoggerLike {
return logger.child({ module: "persistence" });
}
type AgentRegistryPersistence = Pick<AgentRegistry, "applySnapshot" | "list">;
type AgentStoragePersistence = Pick<AgentStorage, "applySnapshot" | "list">;
type AgentManagerStateSource = Pick<AgentManager, "subscribe">;
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");
});
});
+12 -12
View File
@@ -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<string | null> {
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))
@@ -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`,
},
@@ -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,
@@ -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<WebSocket, Session> = 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,