refactor(workspace): remove chat-slot agent pool to prevent workspace pollution
Chat-slot agents were being persisted as durable entries in openclaw.json, causing spurious workspace directories (e.g. chat-slot-main-1) to appear. Only explicit workspace creation via init now creates durable agent entries. Workspace discovery and session routing ignore chat-slot internals.
This commit is contained in:
parent
3cd51759da
commit
11478c752e
@ -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",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@ -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 */ }
|
||||
|
||||
@ -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");
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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}`;
|
||||
}
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -241,6 +241,7 @@ export type SkillSyncResult = {
|
||||
*/
|
||||
export function discoverWorkspaceDirs(stateDir: string): string[] {
|
||||
const dirs = new Set<string>();
|
||||
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));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user