diff --git a/src/agents/agent-scope.ts b/src/agents/agent-scope.ts index e4a179ad80a..b8fc856a3f6 100644 --- a/src/agents/agent-scope.ts +++ b/src/agents/agent-scope.ts @@ -267,7 +267,10 @@ export function resolveAgentMultipleWorkspaces( if (!Array.isArray(mw) || mw.length === 0) { return undefined; } - return mw.map((p) => stripNullBytes(resolveUserPath(p))); + const resolved = mw + .filter((p) => typeof p === "string" && p.trim().length > 0) + .map((p) => stripNullBytes(resolveUserPath(p))); + return resolved.length > 0 ? resolved : undefined; } export function resolveAgentWorkspaceDir(cfg: OpenClawConfig, agentId: string) { @@ -332,16 +335,21 @@ export function resolveAgentIdsByWorkspacePath( matches.push({ id, workspaceDir, order: index }); continue; } - // Also match individual multipleWorkspaces entries. + // Also match individual multipleWorkspaces entries — pick the most specific (longest path). const multiWs = resolveAgentMultipleWorkspaces(cfg, id); if (multiWs) { + let bestMatch: string | null = null; for (const ws of multiWs) { const normalizedWs = normalizePathForComparison(ws); if (isPathWithinRoot(normalizedWorkspacePath, normalizedWs)) { - matches.push({ id, workspaceDir: normalizedWs, order: index }); - break; + if (!bestMatch || normalizedWs.length > bestMatch.length) { + bestMatch = normalizedWs; + } } } + if (bestMatch) { + matches.push({ id, workspaceDir: bestMatch, order: index }); + } } } diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index f922bb7cb7c..7d7ef4f67e7 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -668,6 +668,16 @@ export async function ensureCompositeWorkspace(params: { const base = path.basename(ws); nameCount.set(base, (nameCount.get(base) ?? 0) + 1); } + + // First pass: collect all non-collision names so collision resolution can avoid them. + const reservedNames = new Set(); + for (const ws of workspacePaths) { + const base = path.basename(ws); + if ((nameCount.get(base) ?? 0) === 1) { + reservedNames.add(base); + } + } + const nameUsed = new Map(); for (const ws of workspacePaths) { const base = path.basename(ws); @@ -675,9 +685,15 @@ export async function ensureCompositeWorkspace(params: { if ((nameCount.get(base) ?? 0) > 1) { const parentName = path.basename(path.dirname(ws)); const candidate = `${parentName}-${base}`; - const usedCount = nameUsed.get(candidate) ?? 0; - linkName = usedCount > 0 ? `${candidate}-${usedCount}` : candidate; - nameUsed.set(candidate, usedCount + 1); + let suffix = nameUsed.get(candidate) ?? 0; + linkName = suffix > 0 ? `${candidate}-${suffix}` : candidate; + // Ensure the resolved name doesn't collide with a non-collision entry or existing entry. + while (reservedNames.has(linkName)) { + suffix += 1; + linkName = `${candidate}-${suffix}`; + } + nameUsed.set(candidate, suffix + 1); + reservedNames.add(linkName); } else { linkName = base; } @@ -722,8 +738,13 @@ export async function ensureCompositeWorkspace(params: { // Target changed — remove old symlink. await fs.unlink(linkPath); } catch { - // Symlink doesn't exist or isn't a symlink — remove whatever is there. - await fs.rm(linkPath, { force: true, recursive: false }).catch(() => {}); + // Symlink doesn't exist or isn't a symlink — try to remove whatever is there. + try { + await fs.rm(linkPath, { force: true, recursive: false }); + } catch (rmErr) { + compositeLog.warn(`Cannot replace non-symlink at ${linkPath}: ${String(rmErr)}`); + continue; + } } await fs.symlink(target, linkPath);