import fs from "node:fs" ;
import path from "node:path" ;
import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope.js" ;
import { replaceConfigFile } from "../config/config.js" ;
import { logConfigUpdated } from "../config/logging.js" ;
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js" ;
import type { OpenClawConfig } from "../config/types.openclaw.js" ;
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js" ;
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js" ;
import { defaultRuntime } from "../runtime.js" ;
import { lowercasePreservingWhitespace } from "../shared/string-coerce.js" ;
import { createClackPrompter } from "../wizard/clack-prompter.js" ;
import {
createQuietRuntime,
purgeAgentSessionStoreEntries,
requireValidConfigFileSnapshot,
} from "./agents.command-shared.js" ;
import { findAgentEntryIndex, listAgentEntries, pruneAgentConfig } from "./agents.config.js" ;
import { moveToTrash } from "./onboard-helpers.js" ;
function normalizeWorkspacePathForComparison(input: string): string {
const resolved = path.resolve(input.replaceAll("\0" , "" ));
let normalized = resolved;
try {
normalized = fs.realpathSync.native (resolved);
} catch {
// Keep lexical path for non-existent directories.
}
if (process.platform === "win32" ) {
return lowercasePreservingWhitespace(normalized);
}
return normalized;
}
function isPathWithinRoot(candidatePath: string, rootPath: string): boolean {
const relative = path.relative(rootPath, candidatePath);
return relative === "" || (!relative.startsWith(".." ) && !path.isAbsolute(relative));
}
function workspacePathsOverlap(left: string, right: string): boolean {
const normalizedLeft = normalizeWorkspacePathForComparison(left);
const normalizedRight = normalizeWorkspacePathForComparison(right);
return (
isPathWithinRoot(normalizedLeft, normalizedRight) ||
isPathWithinRoot(normalizedRight, normalizedLeft)
);
}
function findOverlappingWorkspaceAgentIds(
cfg: OpenClawConfig,
agentId: string,
workspaceDir: string,
): string[] {
const entries = listAgentEntries(cfg);
const normalizedAgentId = normalizeAgentId(agentId);
const overlappingAgentIds: string[] = [];
for (const entry of entries) {
const otherAgentId = normalizeAgentId(entry.id);
if (otherAgentId === normalizedAgentId) {
continue ;
}
const otherWorkspace = resolveAgentWorkspaceDir(cfg, otherAgentId);
if (workspacePathsOverlap(workspaceDir, otherWorkspace)) {
overlappingAgentIds.push(otherAgentId);
}
}
return overlappingAgentIds;
}
type AgentsDeleteOptions = {
id: string;
force?: boolean ;
json?: boolean ;
};
export async function agentsDeleteCommand(
opts: AgentsDeleteOptions,
runtime: RuntimeEnv = defaultRuntime,
) {
const configSnapshot = await requireValidConfigFileSnapshot(runtime);
if (!configSnapshot) {
return ;
}
const cfg = configSnapshot.sourceConfig ?? configSnapshot.config;
const baseHash = configSnapshot.hash;
const input = opts.id?.trim();
if (!input) {
runtime.error("Agent id is required." );
runtime.exit(1 );
return ;
}
const agentId = normalizeAgentId(input);
if (agentId !== input) {
runtime.log(`Normalized agent id to "${agentId}" .`);
}
if (agentId === DEFAULT_AGENT_ID) {
runtime.error(`"${DEFAULT_AGENT_ID}" cannot be deleted.`);
runtime.exit(1 );
return ;
}
if (findAgentEntryIndex(listAgentEntries(cfg), agentId) < 0 ) {
runtime.error(`Agent "${agentId}" not found.`);
runtime.exit(1 );
return ;
}
if (!opts.force) {
if (!process.stdin.isTTY) {
runtime.error("Non-interactive session. Re-run with --force." );
runtime.exit(1 );
return ;
}
const prompter = createClackPrompter();
const confirmed = await prompter.confirm({
message: `Delete agent "${agentId}" and prune workspace/state?`,
initialValue: false ,
});
if (!confirmed) {
runtime.log("Cancelled." );
return ;
}
}
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
const agentDir = resolveAgentDir(cfg, agentId);
const sessionsDir = resolveSessionTranscriptsDirForAgent(agentId);
const result = pruneAgentConfig(cfg, agentId);
await replaceConfigFile({
nextConfig: result.config,
...(baseHash !== undefined ? { baseHash } : {}),
writeOptions: opts.json ? { skipOutputLogs: true } : undefined,
});
if (!opts.json) {
logConfigUpdated(runtime);
}
// Purge session store entries for this agent so orphaned sessions cannot be targeted (#65524).
await purgeAgentSessionStoreEntries(cfg, agentId);
const quietRuntime = opts.json ? createQuietRuntime(runtime) : runtime;
// Only trash the workspace if no other agent can depend on that path (#70890).
const workspaceSharedWith = findOverlappingWorkspaceAgentIds(cfg, agentId, workspaceDir);
const workspaceRetained = workspaceSharedWith.length > 0 ;
if (workspaceRetained) {
quietRuntime.log(
`Skipped workspace removal (shared with other agents: ${workspaceSharedWith.join(", " )}): ${workspaceDir}`,
);
} else {
await moveToTrash(workspaceDir, quietRuntime);
}
await moveToTrash(agentDir, quietRuntime);
await moveToTrash(sessionsDir, quietRuntime);
if (opts.json) {
writeRuntimeJson(runtime, {
agentId,
workspace: workspaceDir,
workspaceRetained: workspaceRetained || undefined,
workspaceRetainedReason: workspaceRetained ? "shared" : undefined,
workspaceSharedWith: workspaceRetained ? workspaceSharedWith : undefined,
agentDir,
sessionsDir,
removedBindings: result.removedBindings,
removedAllow: result.removedAllow,
});
} else {
runtime.log(`Deleted agent: ${agentId}`);
}
}
Messung V0.5 in Prozent C=100 H=99 G=99
¤ Dauer der Verarbeitung: 0.8 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland