diff --git a/apps/web/app/api/chat/chat.test.ts b/apps/web/app/api/chat/chat.test.ts index 48a23e7e030..5bd2ed0890e 100644 --- a/apps/web/app/api/chat/chat.test.ts +++ b/apps/web/app/api/chat/chat.test.ts @@ -215,8 +215,7 @@ describe("Chat API routes", () => { expect(persistUserMessage).toHaveBeenCalledWith("s1", expect.objectContaining({ id: "m1" })); }); - it("repairs managed workspace routing before starting a persisted session run", async () => { - const { ensureManagedWorkspaceRouting } = await import("@/lib/workspace"); + it("uses the persisted workspace agent id when available", async () => { const { getSessionMeta } = await import("@/app/api/web-sessions/shared"); const { startRun, hasActiveRun, subscribeToRun } = await import("@/lib/active-runs"); vi.mocked(hasActiveRun).mockReturnValue(false); @@ -230,7 +229,6 @@ describe("Chat API routes", () => { workspaceName: "default", workspaceRoot: "/home/testuser/.openclaw-dench/workspace", workspaceAgentId: "main", - chatAgentId: "chat-slot-main-2", } as never); const { POST } = await import("./route.js"); @@ -245,14 +243,9 @@ describe("Chat API routes", () => { }), }); await POST(req); - expect(ensureManagedWorkspaceRouting).toHaveBeenCalledWith( - "default", - "/home/testuser/.openclaw-dench/workspace", - { markDefault: false }, - ); expect(startRun).toHaveBeenCalledWith( expect.objectContaining({ - overrideAgentId: "chat-slot-main-2", + overrideAgentId: "main", }), ); }); diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts index e5443438ce3..5c612941d0e 100644 --- a/apps/web/app/api/chat/route.ts +++ b/apps/web/app/api/chat/route.ts @@ -3,10 +3,6 @@ import { resolveActiveAgentId, resolveAgentWorkspacePrefix, resolveOpenClawStateDir, - resolveWorkspaceDirForName, - resolveWorkspaceRoot, - getActiveWorkspaceName, - ensureManagedWorkspaceRouting, } from "@/lib/workspace"; import { startRun, @@ -145,18 +141,8 @@ export async function POST(req: Request) { }); const sessionMeta = getSessionMeta(sessionId); - const workspaceName = - sessionMeta?.workspaceName - ?? getActiveWorkspaceName() - ?? "default"; - const workspaceRoot = - sessionMeta?.workspaceRoot - ?? resolveWorkspaceRoot() - ?? resolveWorkspaceDirForName(workspaceName); - ensureManagedWorkspaceRouting(workspaceName, workspaceRoot, { markDefault: false }); const effectiveAgentId = - sessionMeta?.chatAgentId - ?? sessionMeta?.workspaceAgentId + sessionMeta?.workspaceAgentId ?? resolveActiveAgentId(); try { @@ -193,11 +179,11 @@ export async function POST(req: Request) { } catch { /* ignore enqueue errors on closed stream */ } }, 15_000); - unsubscribe = subscribeToRun( - runKey, - (event: SseEvent | null) => { - if (closed) {return;} - if (event === null) { + unsubscribe = subscribeToRun( + runKey, + (event: SseEvent | null) => { + if (closed) {return;} + if (event === null) { closed = true; if (keepalive) { clearInterval(keepalive); keepalive = null; } try { controller.close(); } catch { /* already closed */ } diff --git a/apps/web/app/api/profiles/route.test.ts b/apps/web/app/api/profiles/route.test.ts index 30b311c9dbe..f12658afbd0 100644 --- a/apps/web/app/api/profiles/route.test.ts +++ b/apps/web/app/api/profiles/route.test.ts @@ -2,7 +2,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; vi.mock("@/lib/workspace", () => ({ discoverWorkspaces: vi.fn(() => []), - ensureManagedWorkspaceRouting: vi.fn(), getActiveWorkspaceName: vi.fn(() => null), resolveOpenClawStateDir: vi.fn(() => "/home/testuser/.openclaw-dench"), resolveWorkspaceDirForName: vi.fn((name: string) => @@ -176,11 +175,6 @@ describe("profiles API", () => { expect(json.stateDir).toBe(STATE_DIR); expect(json.workspaceRoot).toBe(`${STATE_DIR}/workspace-work`); expect(json.workspace.name).toBe("work"); - expect(workspace.ensureManagedWorkspaceRouting).toHaveBeenCalledWith( - "work", - `${STATE_DIR}/workspace-work`, - { markDefault: false }, - ); expect(workspace.setUIActiveWorkspace).toHaveBeenCalledWith("work"); }); diff --git a/apps/web/app/api/profiles/switch/route.ts b/apps/web/app/api/profiles/switch/route.ts index b08be48cc07..8fbdda1a2a5 100644 --- a/apps/web/app/api/profiles/switch/route.ts +++ b/apps/web/app/api/profiles/switch/route.ts @@ -1,9 +1,7 @@ import { discoverWorkspaces, - ensureManagedWorkspaceRouting, getActiveWorkspaceName, resolveOpenClawStateDir, - resolveWorkspaceDirForName, resolveWorkspaceRoot, setUIActiveWorkspace, } from "@/lib/workspace"; @@ -46,10 +44,6 @@ export async function POST(req: Request) { ); } - const workspaceRoot = - discovered.find((workspace) => workspace.name === requestedWorkspace)?.workspaceDir - ?? resolveWorkspaceDirForName(requestedWorkspace); - ensureManagedWorkspaceRouting(requestedWorkspace, workspaceRoot, { markDefault: false }); setUIActiveWorkspace(requestedWorkspace); const activeWorkspace = getActiveWorkspaceName(); const selected = discoverWorkspaces().find((workspace) => workspace.name === activeWorkspace) ?? null; diff --git a/apps/web/app/api/web-sessions/route.ts b/apps/web/app/api/web-sessions/route.ts index 6373753baf5..d4240fcba85 100644 --- a/apps/web/app/api/web-sessions/route.ts +++ b/apps/web/app/api/web-sessions/route.ts @@ -3,13 +3,11 @@ import { randomUUID } from "node:crypto"; import { trackServer } from "@/lib/telemetry"; import { type WebSessionMeta, ensureDir, readIndex, writeIndex } from "./shared"; import { - ensureManagedWorkspaceRouting, getActiveWorkspaceName, resolveActiveAgentId, resolveWorkspaceDirForName, resolveWorkspaceRoot, } from "@/lib/workspace"; -import { allocateChatAgent } from "@/lib/chat-agent-registry"; export { type WebSessionMeta }; @@ -38,22 +36,8 @@ export async function POST(req: Request) { const workspaceName = getActiveWorkspaceName() ?? "default"; const workspaceRoot = resolveWorkspaceRoot() ?? resolveWorkspaceDirForName(workspaceName); - ensureManagedWorkspaceRouting(workspaceName, workspaceRoot, { markDefault: false }); const workspaceAgentId = resolveActiveAgentId(); - - // Assign a pool slot agent for concurrent chat support. - // Falls back to the workspace agent if no slots are available. - let chatAgentId: string | undefined; - let effectiveAgentId = workspaceAgentId; - try { - const slot = allocateChatAgent(id); - chatAgentId = slot.chatAgentId; - effectiveAgentId = slot.chatAgentId; - } catch { - // Fall back to workspace agent - } - - const gatewaySessionKey = `agent:${effectiveAgentId}:web:${id}`; + const gatewaySessionKey = `agent:${workspaceAgentId}:web:${id}`; const session: WebSessionMeta = { id, @@ -65,9 +49,8 @@ export async function POST(req: Request) { workspaceName: workspaceName || undefined, workspaceRoot, workspaceAgentId, - chatAgentId, gatewaySessionKey, - agentMode: chatAgentId ? "ephemeral" : "workspace", + agentMode: "workspace", lastActiveAt: now, }; diff --git a/apps/web/app/api/web-sessions/shared.ts b/apps/web/app/api/web-sessions/shared.ts index 28fb2db4d89..4d54c20321a 100644 --- a/apps/web/app/api/web-sessions/shared.ts +++ b/apps/web/app/api/web-sessions/shared.ts @@ -110,12 +110,16 @@ export function getSessionMeta(sessionId: string): WebSessionMeta | undefined { * Uses pinned metadata when available, falls back to workspace-global resolution. */ export function resolveSessionAgentId(sessionId: string, fallbackAgentId: string): string { const meta = getSessionMeta(sessionId); - return meta?.chatAgentId ?? meta?.workspaceAgentId ?? fallbackAgentId; + return meta?.workspaceAgentId ?? fallbackAgentId; } /** Resolve the gateway session key for a session. * Uses pinned metadata when available, constructs from the given agent ID otherwise. */ export function resolveSessionKey(sessionId: string, fallbackAgentId: string): string { const meta = getSessionMeta(sessionId); - return meta?.gatewaySessionKey ?? `agent:${fallbackAgentId}:web:${sessionId}`; + if (meta?.gatewaySessionKey && !meta.gatewaySessionKey.includes(":chat-slot-")) { + return meta.gatewaySessionKey; + } + const agentId = meta?.workspaceAgentId ?? fallbackAgentId; + return `agent:${agentId}:web:${sessionId}`; } diff --git a/apps/web/app/api/workspace/init/route.test.ts b/apps/web/app/api/workspace/init/route.test.ts index 05e426cae63..962baad030a 100644 --- a/apps/web/app/api/workspace/init/route.test.ts +++ b/apps/web/app/api/workspace/init/route.test.ts @@ -24,7 +24,6 @@ vi.mock("@/lib/workspace", () => ({ isValidWorkspaceName: vi.fn(() => true), resolveWorkspaceRoot: vi.fn(() => null), ensureAgentInConfig: vi.fn(), - ensureChatAgentPool: vi.fn(), })); describe("POST /api/workspace/init", () => { diff --git a/apps/web/app/api/workspace/init/route.ts b/apps/web/app/api/workspace/init/route.ts index 67e3cd17e10..f03024d8b84 100644 --- a/apps/web/app/api/workspace/init/route.ts +++ b/apps/web/app/api/workspace/init/route.ts @@ -14,7 +14,6 @@ import { isValidWorkspaceName, resolveWorkspaceRoot, ensureAgentInConfig, - ensureChatAgentPool, } from "@/lib/workspace"; import { BOOTSTRAP_TEMPLATE_CONTENT, @@ -208,9 +207,6 @@ export async function POST(req: Request) { // Register a per-workspace agent in openclaw.json and make it the default. ensureAgentInConfig(workspaceName, workspaceDir); - // Pre-create a pool of chat agent slots for concurrent web chat sessions. - ensureChatAgentPool(workspaceName, workspaceDir); - // Switch the UI to the new workspace. setUIActiveWorkspace(workspaceName); const activeWorkspace = getActiveWorkspaceName(); diff --git a/apps/web/app/api/workspace/switch/route.ts b/apps/web/app/api/workspace/switch/route.ts index 75e873d8d92..79119615cdd 100644 --- a/apps/web/app/api/workspace/switch/route.ts +++ b/apps/web/app/api/workspace/switch/route.ts @@ -1,9 +1,7 @@ import { discoverWorkspaces, - ensureManagedWorkspaceRouting, getActiveWorkspaceName, resolveOpenClawStateDir, - resolveWorkspaceDirForName, resolveWorkspaceRoot, setUIActiveWorkspace, setDefaultAgentInConfig, @@ -48,10 +46,6 @@ export async function POST(req: Request) { ); } - const workspaceRoot = - discovered.find((workspace) => workspace.name === requestedWorkspace)?.workspaceDir - ?? resolveWorkspaceDirForName(requestedWorkspace); - ensureManagedWorkspaceRouting(requestedWorkspace, workspaceRoot, { markDefault: false }); setUIActiveWorkspace(requestedWorkspace); setDefaultAgentInConfig(requestedWorkspace); trackServer("workspace_switched"); diff --git a/apps/web/lib/active-runs.ts b/apps/web/lib/active-runs.ts index 9a1e0798f95..cb98a039d41 100644 --- a/apps/web/lib/active-runs.ts +++ b/apps/web/lib/active-runs.ts @@ -23,7 +23,6 @@ import { writeFile, } from "node:fs/promises"; import { resolveWebChatDir, resolveOpenClawStateDir, resolveActiveAgentId } from "./workspace"; -import { markChatAgentIdle } from "./chat-agent-registry"; import { type AgentProcessHandle, type AgentEvent, @@ -1971,9 +1970,6 @@ function wireChildProcess(run: ActiveRun): void { // Normal completion path. run.status = exitedClean ? "completed" : "error"; - // Release the chat agent pool slot so it can be reused. - try { markChatAgentIdle(run.sessionId); } catch { /* best-effort */ } - // Final persistence flush (removes _streaming flag). flushPersistence(run).catch(() => {}); @@ -2106,8 +2102,6 @@ function finalizeWaitingRun(run: ActiveRun): void { stopSubscribeProcess(run); - try { markChatAgentIdle(run.sessionId); } catch { /* best-effort */ } - flushPersistence(run).catch(() => {}); for (const sub of run.subscribers) { diff --git a/apps/web/lib/workspace-profiles.test.ts b/apps/web/lib/workspace-profiles.test.ts index 6f9e07d2c63..fa8e5a8c038 100644 --- a/apps/web/lib/workspace-profiles.test.ts +++ b/apps/web/lib/workspace-profiles.test.ts @@ -379,6 +379,30 @@ describe("workspace (flat workspace model)", () => { expect(workspaces[0]?.isActive).toBe(true); }); + it("ignores internal workspace directories for main/chat-slot agent ids", async () => { + const { discoverWorkspaces, mockReaddir, mockExists, mockReadFile } = + await importWorkspace(); + mockReadFile.mockImplementation(() => { + throw new Error("ENOENT"); + }); + mockReaddir.mockReturnValue([ + makeDirent("workspace", true), + makeDirent("workspace-main", true), + makeDirent("workspace-chat-slot-main-1", true), + ] as unknown as Dirent[]); + mockExists.mockImplementation((p) => { + const s = String(p); + return ( + s === join(STATE_DIR, "workspace") || + s === join(STATE_DIR, "workspace-main") || + s === join(STATE_DIR, "workspace-chat-slot-main-1") + ); + }); + + const workspaces = discoverWorkspaces(); + expect(workspaces.map((workspace) => workspace.name)).toEqual(["default"]); + }); + it("keeps root default and workspace-dench as distinct workspaces", async () => { const { discoverWorkspaces, mockReaddir, mockExists, mockReadFile } = await importWorkspace(); @@ -614,7 +638,7 @@ describe("workspace (flat workspace model)", () => { }); describe("ensureManagedWorkspaceRouting", () => { - it("repairs the default workspace agent and chat slots without flipping the active default agent", async () => { + it("repairs the default workspace agent and prunes chat slots without flipping the active default agent", async () => { const { ensureManagedWorkspaceRouting, mockExists, mockReadFile, mockWriteFile } = await importWorkspace(); const configPath = join(STATE_DIR, "openclaw.json"); @@ -657,8 +681,8 @@ describe("workspace (flat workspace model)", () => { expect(written.agents.defaults.workspace).toBe(defaultWorkspaceDir); expect(written.agents.list.find((agent) => agent.id === "main")?.workspace).toBe(defaultWorkspaceDir); - expect(written.agents.list.find((agent) => agent.id === "chat-slot-main-1")?.workspace).toBe(defaultWorkspaceDir); - expect(written.agents.list.find((agent) => agent.id === "chat-slot-main-2")?.workspace).toBe(defaultWorkspaceDir); + expect(written.agents.list.find((agent) => agent.id === "chat-slot-main-1")).toBeUndefined(); + expect(written.agents.list.find((agent) => agent.id === "chat-slot-main-2")).toBeUndefined(); expect(written.agents.list.find((agent) => agent.id === "kumareth")?.default).toBe(true); expect(written.agents.list.find((agent) => agent.id === "main")?.default).toBeUndefined(); }); diff --git a/apps/web/lib/workspace.ts b/apps/web/lib/workspace.ts index a2ae32aea14..67057ba57ae 100644 --- a/apps/web/lib/workspace.ts +++ b/apps/web/lib/workspace.ts @@ -85,6 +85,11 @@ function workspaceNameFromDirName(dirName: string): string | null { return normalizeWorkspaceName(dirName.slice(WORKSPACE_PREFIX.length)); } +function isInternalWorkspaceNameForDiscovery(name: string): boolean { + const lowered = name.toLowerCase(); + return lowered === GATEWAY_MAIN_AGENT_ID || lowered.startsWith(CHAT_SLOT_PREFIX); +} + function stateDirPath(): string { return join(resolveOpenClawHomeDir(), FIXED_STATE_DIRNAME); } @@ -149,7 +154,7 @@ function scanWorkspaceNames(stateDir: string): string[] { const names = readdirSync(stateDir, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => workspaceNameFromDirName(entry.name)) - .filter((name): name is string => Boolean(name)); + .filter((name): name is string => Boolean(name && !isInternalWorkspaceNameForDiscovery(name))); return [...new Set(names)].toSorted((a, b) => a.localeCompare(b)); } catch { return []; @@ -412,18 +417,15 @@ function applyDefaultAgentMarker(list: OpenClawAgentEntry[], targetAgentId: stri return changed; } -function ensureChatSlotEntries( - list: OpenClawAgentEntry[], - baseId: string, - workspaceDir: string, - poolSize: number, -): boolean { - let changed = false; - for (let i = 1; i <= poolSize; i++) { - const slotId = `${CHAT_SLOT_PREFIX}${baseId}-${i}`; - changed = upsertAgentWorkspace(list, slotId, workspaceDir) || changed; +function removeChatSlotEntries(list: OpenClawAgentEntry[], baseId?: string): boolean { + const prefix = baseId ? `${CHAT_SLOT_PREFIX}${baseId}-` : CHAT_SLOT_PREFIX; + const next = list.filter((agent) => !agent.id.startsWith(prefix)); + if (next.length === list.length) { + return false; } - return changed; + list.length = 0; + list.push(...next); + return true; } /** @@ -448,6 +450,9 @@ export function ensureAgentInConfig( const list = ensureConfigAgentList(config); const resolvedId = workspaceNameToAgentId(normalized); + // Chat slots are internal, ephemeral session mechanics and should not be + // persisted as durable named agents in config. + changed = removeChatSlotEntries(list) || changed; changed = upsertAgentWorkspace(list, resolvedId, workspaceDir) || changed; if (options?.markDefault ?? true) { changed = applyDefaultAgentMarker(list, resolvedId) || changed; @@ -459,21 +464,23 @@ export function ensureAgentInConfig( } /** - * Pre-create a pool of chat agent slots in `agents.list[]` so the gateway - * knows about them at startup. Each slot shares the workspace directory - * of the parent workspace agent, enabling concurrent chat sessions. + * Legacy compatibility helper. + * + * Chat-slot agents are no longer persisted in openclaw.json. This function + * now prunes stale slot entries if present. */ export function ensureChatAgentPool(workspaceName: string, workspaceDir: string, poolSize = DEFAULT_CHAT_POOL_SIZE): void { + void workspaceDir; + void poolSize; const normalized = normalizeWorkspaceName(workspaceName); if (!normalized) { throw new Error("Invalid workspace name."); } const config = readOpenClawConfig(); - let changed = syncDefaultWorkspacePointer(config, normalized, workspaceDir); + let changed = false; const list = ensureConfigAgentList(config); const baseId = workspaceNameToAgentId(normalized); - - changed = ensureChatSlotEntries(list, baseId, workspaceDir, poolSize) || changed; + changed = removeChatSlotEntries(list, baseId) || changed; if (changed) { writeOpenClawConfig(config); @@ -481,15 +488,15 @@ export function ensureChatAgentPool(workspaceName: string, workspaceDir: string, } /** - * Repair the managed agent mapping for a workspace in a single config pass. - * This is used on chat creation/send so stale `main` or `chat-slot-main-*` - * entries are redirected back to the intended managed workspace. + * Repair the workspace mapping for an existing managed agent without creating + * new entries. This also prunes stale `chat-slot-*` agent entries. */ export function ensureManagedWorkspaceRouting( workspaceName: string, workspaceDir: string, options?: { markDefault?: boolean; poolSize?: number }, ): void { + void options; const normalized = normalizeWorkspaceName(workspaceName); if (!normalized) { throw new Error("Invalid workspace name."); @@ -498,15 +505,13 @@ export function ensureManagedWorkspaceRouting( let changed = syncDefaultWorkspacePointer(config, normalized, workspaceDir); const list = ensureConfigAgentList(config); const resolvedId = workspaceNameToAgentId(normalized); - - changed = upsertAgentWorkspace(list, resolvedId, workspaceDir) || changed; - changed = ensureChatSlotEntries( - list, - resolvedId, - workspaceDir, - options?.poolSize ?? DEFAULT_CHAT_POOL_SIZE, - ) || changed; - if (options?.markDefault) { + const existing = list.find((agent) => agent.id === resolvedId); + if (existing && existing.workspace !== workspaceDir) { + existing.workspace = workspaceDir; + changed = true; + } + changed = removeChatSlotEntries(list, resolvedId) || changed; + if (options?.markDefault && existing) { changed = applyDefaultAgentMarker(list, resolvedId) || changed; } @@ -519,13 +524,8 @@ export function ensureManagedWorkspaceRouting( * Return the list of chat slot agent IDs for a workspace. */ export function getChatSlotAgentIds(workspaceName?: string): string[] { - const config = readOpenClawConfig(); - const list = config.agents?.list; - if (!Array.isArray(list)) { return []; } - - const baseId = workspaceNameToAgentId(workspaceName ?? getActiveWorkspaceName() ?? DEFAULT_WORKSPACE_NAME); - const prefix = `${CHAT_SLOT_PREFIX}${baseId}-`; - return list.filter((a) => a.id.startsWith(prefix)).map((a) => a.id); + void workspaceName; + return []; } export { CHAT_SLOT_PREFIX, DEFAULT_CHAT_POOL_SIZE }; diff --git a/src/cli/workspace-seed.test.ts b/src/cli/workspace-seed.test.ts index ce5853c45cb..d23ef0c232c 100644 --- a/src/cli/workspace-seed.test.ts +++ b/src/cli/workspace-seed.test.ts @@ -244,6 +244,34 @@ describe("discoverWorkspaceDirs", () => { expect(dirs).toHaveLength(2); }); + it("ignores chat-slot agent workspace entries", () => { + const wsDefault = path.join(tempDir, "workspace"); + const wsUser = path.join(tempDir, "workspace-user"); + const wsSlot = path.join(tempDir, "workspace-chat-slot-main-1"); + mkdirSync(wsDefault, { recursive: true }); + mkdirSync(wsUser, { recursive: true }); + mkdirSync(wsSlot, { recursive: true }); + writeFileSync( + path.join(tempDir, "openclaw.json"), + JSON.stringify({ + agents: { + defaults: { workspace: wsDefault }, + list: [ + { id: "main", workspace: wsDefault }, + { id: "user", workspace: wsUser }, + { id: "chat-slot-main-1", workspace: wsSlot }, + ], + }, + }), + "utf-8", + ); + + const dirs = discoverWorkspaceDirs(tempDir); + expect(dirs).toContain(path.resolve(wsDefault)); + expect(dirs).toContain(path.resolve(wsUser)); + expect(dirs).not.toContain(path.resolve(wsSlot)); + }); + it("deduplicates workspace dirs", () => { const ws = path.join(tempDir, "workspace"); mkdirSync(ws, { recursive: true }); diff --git a/src/cli/workspace-seed.ts b/src/cli/workspace-seed.ts index 095d2778b9a..5a313461f4e 100644 --- a/src/cli/workspace-seed.ts +++ b/src/cli/workspace-seed.ts @@ -241,6 +241,7 @@ export type SkillSyncResult = { */ export function discoverWorkspaceDirs(stateDir: string): string[] { const dirs = new Set(); + const CHAT_SLOT_PREFIX = "chat-slot-"; for (const name of ["openclaw.json", "config.json"]) { const configPath = path.join(stateDir, name); if (!existsSync(configPath)) { @@ -250,7 +251,7 @@ export function discoverWorkspaceDirs(stateDir: string): string[] { const raw = JSON.parse(readFileSync(configPath, "utf-8")) as { agents?: { defaults?: { workspace?: string }; - list?: Array<{ workspace?: string }>; + list?: Array<{ id?: string; workspace?: string }>; }; }; const defaultWs = raw?.agents?.defaults?.workspace?.trim(); @@ -258,6 +259,10 @@ export function discoverWorkspaceDirs(stateDir: string): string[] { dirs.add(path.resolve(defaultWs)); } for (const agent of raw?.agents?.list ?? []) { + const id = agent.id?.trim().toLowerCase(); + if (id?.startsWith(CHAT_SLOT_PREFIX)) { + continue; + } const ws = agent.workspace?.trim(); if (ws && existsSync(ws)) { dirs.add(path.resolve(ws));