Download src/claws/lifecycle-state.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 26.4 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/claws/lifecycle-state.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/claws/lifecycle-state.ts
-
curl -L -o lifecycle-state.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/claws/lifecycle-state.ts
26.4 kB
| import { isDeepStrictEqual } from "node:util"; | |
| import { coerceErrorMessage } from "@openclaw/normalization-core"; | |
| import { | |
| AgentSharedStoreOwnerError, | |
| assertAgentSessionStoreDeletionSafe, | |
| isPathOwnedBySurvivingAgent, | |
| readAgentDeleteDatabaseRegistry, | |
| resolveSurvivingDatabaseFilePaths, | |
| } from "../agents/agent-delete-databases.js"; | |
| import { getRuntimeConfig } from "../config/config.js"; | |
| import { clawCronGatewayJobMatchesRef, deleteClawCronRef, markClawCronRefRemoved } from "./cron.js"; | |
| import { | |
| clawBootstrapStateBlocksRemove, | |
| planClawBootstrapRemoval, | |
| removeClawBootstrap, | |
| } from "./lifecycle-bootstrap-removal.js"; | |
| import { | |
| withClawAgentConfigRemoval, | |
| digestClawAgentRemovalSurface, | |
| } from "./lifecycle-config-removal.js"; | |
| import { | |
| clawRemoveQuietRuntime, | |
| ClawRemoveError, | |
| cleanupClawAgentFilesystem, | |
| deletionEffects, | |
| readClawRemoveCronInventory, | |
| releaseClawRemoveRows, | |
| removeClawWorkspaceFile, | |
| workspaceContainsUntrackedEntries, | |
| } from "./lifecycle-delete-support.js"; | |
| import { removeClawMcpServers } from "./lifecycle-mcp-removal.js"; | |
| import { | |
| CLAW_REMOVE_PLAN_SCHEMA_VERSION, | |
| CLAW_REMOVE_RESULT_SCHEMA_VERSION, | |
| type ClawRemoveApplyOptions, | |
| type ClawRemovePlanOptions, | |
| type ClawRemoveResult, | |
| type ClawRemovePlan, | |
| type ClawRemovePlanAction, | |
| } from "./lifecycle-remove-contract.js"; | |
| import { readClawStatus } from "./lifecycle-status.js"; | |
| import { clawMcpRemovalSelector, planClawMcpServerRemoval } from "./mcp.js"; | |
| import { clawMonitorSnapshotSchema } from "./monitor-cleanup-contract.js"; | |
| import { applyClawPackageRemovalPhase } from "./package-remove-phase.js"; | |
| import { | |
| filterReferencedCleanup, | |
| projectClawPackageRemovePlan, | |
| digestClawRemovalState, | |
| } from "./package-remove-plan.js"; | |
| import { planClawPackageRemovals } from "./package-remove.js"; | |
| import { CLAW_OUTPUT_STABILITY } from "./types.js"; | |
| export { ClawRemoveError } from "./lifecycle-delete-support.js"; | |
| export { | |
| CLAW_REMOVE_PLAN_SCHEMA_VERSION, | |
| CLAW_REMOVE_RESULT_SCHEMA_VERSION, | |
| } from "./lifecycle-remove-contract.js"; | |
| export { readClawStatus, type ClawStatusRecord } from "./lifecycle-status.js"; | |
| export async function buildClawRemovePlan( | |
| target: string, | |
| options: ClawRemovePlanOptions = {}, | |
| ): Promise<ClawRemovePlan> { | |
| const status = await readClawStatus(target, options); | |
| const blockers: ClawRemovePlan["blockers"] = []; | |
| if (status.records.length === 0) { | |
| blockers.push({ | |
| code: "claw_not_found", | |
| message: `No installed Claw matches ${JSON.stringify(target)}.`, | |
| }); | |
| } else if (status.records.length > 1) { | |
| blockers.push({ | |
| code: "claw_ambiguous", | |
| message: `Claw name ${JSON.stringify(target)} matches multiple agents; use an agent id.`, | |
| }); | |
| } | |
| const record = status.records.length === 1 ? status.records[0] : undefined; | |
| if (record?.agentState === "modified") { | |
| blockers.push({ | |
| code: "agent_modified", | |
| message: `Agent ${JSON.stringify(record.install.agentId)} changed after add.`, | |
| }); | |
| } | |
| for (const file of record?.workspaceFiles ?? []) { | |
| if (file.state === "unsafe") { | |
| blockers.push({ | |
| code: "workspace_file_unsafe", | |
| message: `${file.path}: ${file.message ?? "unsafe file"}`, | |
| }); | |
| } | |
| } | |
| if (record && clawBootstrapStateBlocksRemove(record)) { | |
| blockers.push({ | |
| code: "bootstrap_cleanup_uncertain", | |
| message: `BOOTSTRAP.md has ${record.bootstrap.state} ownership state and must be reconciled before removal.`, | |
| }); | |
| } | |
| for (const server of record?.mcpServers ?? []) { | |
| if (server.state === "pending") { | |
| blockers.push({ | |
| code: "mcp_cleanup_uncertain", | |
| message: `MCP server ${JSON.stringify(server.name)} has ${server.state} ownership state and must be reconciled before removal.`, | |
| }); | |
| } | |
| } | |
| for (const cron of record?.cronJobs ?? []) { | |
| if (cron.status !== "removed" && (cron.status !== "complete" || !cron.schedulerJobId)) { | |
| blockers.push({ | |
| code: "cron_cleanup_uncertain", | |
| message: `Cron declaration ${JSON.stringify(cron.manifestId)} has ${cron.status} ownership state and must be reconciled before removal.`, | |
| }); | |
| } | |
| } | |
| const actions: ClawRemovePlanAction[] = []; | |
| if (record) { | |
| const packageCleanup = filterReferencedCleanup(options.referencedCleanup, "package"); | |
| const mcpCleanup = filterReferencedCleanup(options.referencedCleanup, "mcp"); | |
| const packageDecisions = await planClawPackageRemovals(record.install, record.packages, { | |
| ...options, | |
| deps: options.packageDeps, | |
| referencedCleanup: packageCleanup, | |
| }); | |
| const packagePlan = projectClawPackageRemovePlan({ | |
| decisions: packageDecisions, | |
| inspections: record.packages, | |
| cleanup: packageCleanup, | |
| }); | |
| blockers.push(...packagePlan.blockers); | |
| const config = options.config ?? getRuntimeConfig(); | |
| try { | |
| assertAgentSessionStoreDeletionSafe(config, record.install.agentId, options); | |
| } catch (error) { | |
| if (!(error instanceof AgentSharedStoreOwnerError)) { | |
| throw error; | |
| } | |
| blockers.push({ code: "shared_session_store_owner", message: error.message }); | |
| } | |
| const effects = deletionEffects( | |
| config, | |
| record.install.agentId, | |
| record.install.workspace, | |
| options.env, | |
| ); | |
| const survivingDatabaseFilePaths = resolveSurvivingDatabaseFilePaths( | |
| readAgentDeleteDatabaseRegistry(options), | |
| record.install.agentId, | |
| options.env, | |
| ); | |
| const sharedWithSurvivor = (pathname: string) => | |
| isPathOwnedBySurvivingAgent( | |
| config, | |
| record.install.agentId, | |
| pathname, | |
| survivingDatabaseFilePaths, | |
| options.env, | |
| ); | |
| const sharedWorkspace = Boolean(effects.workspace) && sharedWithSurvivor(effects.workspace); | |
| const sharedAgentDir = sharedWithSurvivor(effects.agentDir); | |
| const sharedSessionsDir = sharedWithSurvivor(effects.sessionsDir); | |
| const workspaceHasModifiedFiles = | |
| record.workspaceFiles.some((file) => file.state === "modified") || | |
| record.bootstrap.state === "modified"; | |
| const trackedWorkspacePaths = [ | |
| ...record.workspaceFiles.map((file) => file.path), | |
| ...(record.install.bootstrap && record.bootstrap.state === "pending" | |
| ? [record.bootstrap.path] | |
| : []), | |
| ]; | |
| const workspaceHasUntrackedEntries = await workspaceContainsUntrackedEntries( | |
| record.install.workspace, | |
| trackedWorkspacePaths, | |
| ); | |
| const { attachedJobs, monitors, inspectionUnavailable } = await readClawRemoveCronInventory( | |
| record.install.agentId, | |
| options, | |
| ); | |
| const ownedSchedulerJobIds = new Set( | |
| record.cronJobs | |
| .filter((cron) => cron.status !== "removed" && cron.schedulerJobId) | |
| .map((cron) => cron.schedulerJobId), | |
| ); | |
| actions.push({ | |
| kind: "agent", | |
| id: record.install.agentId, | |
| action: "remove", | |
| target: `agents.entries[${JSON.stringify(record.install.agentId)}]`, | |
| blocked: record.agentState === "modified", | |
| details: { | |
| expectedState: record.agentState, | |
| configDigest: record.install.agentConfigDigest, | |
| removalSurfaceDigest: digestClawAgentRemovalSurface( | |
| options.config ?? getRuntimeConfig(), | |
| record.install.agentId, | |
| ), | |
| ownedPaths: record.install.agentOwnedPaths, | |
| }, | |
| ...(record.agentState === "modified" ? { reason: "Agent config digest changed." } : {}), | |
| }); | |
| if (effects.pruned.removedBindings > 0) { | |
| actions.push({ | |
| kind: "configBinding", | |
| id: record.install.agentId, | |
| action: "remove", | |
| target: `bindings[agentId=${record.install.agentId}]`, | |
| blocked: record.agentState === "modified", | |
| details: { count: effects.pruned.removedBindings }, | |
| }); | |
| } | |
| if (effects.pruned.removedAllow > 0) { | |
| actions.push({ | |
| kind: "agentAllow", | |
| id: record.install.agentId, | |
| action: "remove", | |
| target: `tools.agentToAgent.allow[${record.install.agentId}]`, | |
| blocked: record.agentState === "modified", | |
| details: { count: effects.pruned.removedAllow }, | |
| }); | |
| } | |
| if (effects.workspace) { | |
| actions.push({ | |
| kind: "workspace", | |
| id: record.install.agentId, | |
| action: | |
| sharedWorkspace || workspaceHasModifiedFiles || workspaceHasUntrackedEntries | |
| ? "retain" | |
| : "trash", | |
| target: effects.workspace, | |
| blocked: record.agentState === "modified", | |
| details: { | |
| retained: sharedWorkspace || workspaceHasModifiedFiles || workspaceHasUntrackedEntries, | |
| sharedWith: effects.workspaceSharedWith, | |
| }, | |
| ...(sharedWorkspace | |
| ? { reason: "Workspace contains state owned by another agent." } | |
| : workspaceHasModifiedFiles | |
| ? { reason: "Workspace contains locally modified Claw-managed files." } | |
| : workspaceHasUntrackedEntries | |
| ? { reason: "Workspace contains files or directories not managed by this Claw." } | |
| : {}), | |
| }); | |
| } | |
| if (effects.agentDir) { | |
| actions.push({ | |
| kind: "agentState", | |
| id: record.install.agentId, | |
| action: sharedAgentDir ? "retain" : "trash", | |
| target: effects.agentDir, | |
| blocked: record.agentState === "modified", | |
| ...(sharedAgentDir | |
| ? { reason: "Agent directory contains state owned by another agent." } | |
| : {}), | |
| }); | |
| } | |
| actions.push({ | |
| kind: "sessionIndex", | |
| id: record.install.agentId, | |
| action: "delete", | |
| target: `session store entries for agent:${record.install.agentId}`, | |
| blocked: record.agentState === "modified", | |
| }); | |
| actions.push({ | |
| kind: "sessionTranscripts", | |
| id: record.install.agentId, | |
| action: sharedSessionsDir ? "retain" : "trash", | |
| target: effects.sessionsDir, | |
| blocked: record.agentState === "modified", | |
| ...(sharedSessionsDir | |
| ? { reason: "Session directory contains state owned by another agent." } | |
| : {}), | |
| }); | |
| for (const job of attachedJobs.filter((candidate) => !ownedSchedulerJobIds.has(candidate.id))) { | |
| if (monitors.some((monitor) => isDeepStrictEqual(monitor, job))) { | |
| actions.push({ | |
| kind: "scheduledJob", | |
| id: job.id, | |
| action: "remove", | |
| target: `cron_jobs:${job.id}`, | |
| blocked: false, | |
| reason: "Config-owned monitor; Gateway cancellation and drainage precede local cleanup.", | |
| details: { ...job }, | |
| }); | |
| continue; | |
| } | |
| blockers.push({ | |
| code: "agent_job_attached", | |
| message: `Cron job ${JSON.stringify(job.id)} still references agent ${JSON.stringify(record.install.agentId)}; reassign or remove independent work, or reconnect to the serving Gateway to verify monitor ownership.`, | |
| }); | |
| actions.push({ | |
| kind: "scheduledJob", | |
| id: job.id, | |
| action: "retain", | |
| target: `cron_jobs:${job.id}`, | |
| blocked: true, | |
| reason: | |
| "Scheduled work without verified Claw or config ownership must be handled explicitly.", | |
| details: { | |
| ...(inspectionUnavailable ? { monitorInspection: "unavailable" } : {}), | |
| name: job.name, | |
| enabled: job.enabled, | |
| agentId: job.agentId, | |
| ownerAgentId: job.ownerAgentId, | |
| }, | |
| }); | |
| } | |
| for (const file of record.workspaceFiles) { | |
| actions.push({ | |
| kind: "workspaceFile", | |
| id: file.path, | |
| action: file.state === "unchanged" ? "delete" : "retain", | |
| target: `${file.workspace}:${file.path}`, | |
| blocked: file.state === "unsafe", | |
| details: { | |
| expectedState: file.state, | |
| contentDigest: file.contentDigest, | |
| workspace: file.workspace, | |
| }, | |
| ...(file.state === "modified" | |
| ? { reason: "Local content changed; preserve the file." } | |
| : {}), | |
| }); | |
| } | |
| const bootstrapAction = planClawBootstrapRemoval(record); | |
| if (bootstrapAction) { | |
| actions.push(bootstrapAction); | |
| } | |
| actions.push(...packagePlan.actions); | |
| const unmatchedMcpSelectors = new Set(mcpCleanup?.selected ?? []); | |
| for (const server of record.mcpServers) { | |
| const blocked = server.state === "pending"; | |
| const decision = planClawMcpServerRemoval(server, { | |
| ...options, | |
| referencedCleanup: mcpCleanup, | |
| }); | |
| unmatchedMcpSelectors.delete(clawMcpRemovalSelector(server)); | |
| if (decision.blocked) { | |
| blockers.push({ | |
| code: "referenced_cleanup_requires_override", | |
| message: `${clawMcpRemovalSelector(server)}: ${decision.reason ?? "explicit conflict override is required"}`, | |
| }); | |
| } | |
| actions.push({ | |
| kind: "mcpServer", | |
| id: server.name, | |
| action: blocked ? "retain" : decision.action, | |
| target: `mcp.servers.${server.name}`, | |
| blocked, | |
| details: { | |
| expectedState: server.state, | |
| configDigest: server.configDigest, | |
| relationship: server.relationship, | |
| origin: server.origin, | |
| independentOwner: server.independentOwner, | |
| affectedClawAgentIds: decision.affectedClawAgentIds, | |
| cleanupMode: mcpCleanup?.mode ?? "retain", | |
| availableCleanupModes: | |
| server.relationship === "referenced" | |
| ? ["retain", "remove-if-unused", "remove-selected"] | |
| : ["remove"], | |
| }, | |
| ...(blocked | |
| ? { reason: `MCP ownership state is ${server.state}.` } | |
| : decision.reason | |
| ? { reason: decision.reason } | |
| : {}), | |
| }); | |
| } | |
| for (const selector of unmatchedMcpSelectors) { | |
| blockers.push({ | |
| code: "referenced_cleanup_not_found", | |
| message: `Selected referenced resource ${JSON.stringify(selector)} is not owned by this Claw.`, | |
| }); | |
| } | |
| for (const cron of record.cronJobs) { | |
| const blocked = | |
| cron.status !== "removed" && (cron.status !== "complete" || !cron.schedulerJobId); | |
| actions.push({ | |
| kind: "cronJob", | |
| id: cron.manifestId, | |
| action: blocked ? "retain" : "remove", | |
| target: cron.schedulerJobId ?? cron.declarationKey, | |
| blocked, | |
| details: { | |
| expectedStatus: cron.status, | |
| declarationKey: cron.declarationKey, | |
| schedulerJobId: cron.schedulerJobId, | |
| job: cron.job, | |
| }, | |
| ...(blocked ? { reason: `Cron ownership state is ${cron.status}.` } : {}), | |
| }); | |
| } | |
| actions.push({ | |
| kind: "installRecord", | |
| id: record.install.agentId, | |
| action: "remove", | |
| target: `claw_installs:${record.install.agentId}`, | |
| blocked: false, | |
| details: { | |
| expectedStatus: record.install.status, | |
| planIntegrity: record.install.planIntegrity, | |
| sourceIntegrity: record.install.claw.integrity, | |
| }, | |
| }); | |
| } | |
| const planIdentity = { | |
| target, | |
| agentId: record?.install.agentId, | |
| actions, | |
| blockers, | |
| }; | |
| return { | |
| schemaVersion: CLAW_REMOVE_PLAN_SCHEMA_VERSION, | |
| stability: CLAW_OUTPUT_STABILITY, | |
| dryRun: true, | |
| mutationAllowed: false, | |
| planIntegrity: digestClawRemovalState(planIdentity), | |
| target, | |
| ...(record ? { agentId: record.install.agentId } : {}), | |
| actions, | |
| blockers, | |
| }; | |
| } | |
| export async function applyClawRemovePlan( | |
| plan: ClawRemovePlan, | |
| options: ClawRemoveApplyOptions = {}, | |
| ): Promise<ClawRemoveResult> { | |
| if (options.consentPlanIntegrity !== plan.planIntegrity) { | |
| throw new ClawRemoveError( | |
| "plan_integrity_mismatch", | |
| "Consent does not match the current Claw remove plan; run remove --dry-run again.", | |
| ); | |
| } | |
| if (plan.blockers.length > 0 || !plan.agentId) { | |
| throw new ClawRemoveError("remove_blocked", "The Claw remove plan contains blockers."); | |
| } | |
| const monitorGateway = options.monitorGateway; | |
| if (!monitorGateway) { | |
| throw new ClawRemoveError( | |
| "monitor_gateway_required", | |
| "Claw removal requires the serving Gateway to establish safe cancellation and drainage.", | |
| ); | |
| } | |
| const currentPlan = await buildClawRemovePlan(plan.target, options); | |
| if (currentPlan.planIntegrity !== plan.planIntegrity) { | |
| throw new ClawRemoveError("remove_changed", "Claw-owned state changed after remove planning."); | |
| } | |
| const agentId = plan.agentId; | |
| const plannedAgentAction = plan.actions.find( | |
| (action) => action.kind === "agent" && action.id === agentId, | |
| ); | |
| const expectedRemovalSurfaceDigest = plannedAgentAction?.details?.removalSurfaceDigest; | |
| if (typeof expectedRemovalSurfaceDigest !== "string") { | |
| throw new ClawRemoveError("remove_changed", "Claw remove plan is missing config state."); | |
| } | |
| const current = await readClawStatus(plan.agentId, options); | |
| const record = current.records[0]; | |
| if ( | |
| !record || | |
| record.agentState === "modified" || | |
| clawBootstrapStateBlocksRemove(record) || | |
| record.workspaceFiles.some((file) => file.state === "unsafe") || | |
| record.mcpServers.some((server) => server.state === "pending") | |
| ) { | |
| throw new ClawRemoveError("remove_changed", "Claw-owned state changed after remove planning."); | |
| } | |
| const packageDecisions = await planClawPackageRemovals(record.install, record.packages, { | |
| ...options, | |
| deps: options.packageDeps, | |
| referencedCleanup: filterReferencedCleanup(options.referencedCleanup, "package"), | |
| }); | |
| const plannedPackages = plan.actions | |
| .filter((action) => action.kind === "packageRef") | |
| .map((action) => `${action.id}:${action.action}`) | |
| .toSorted(); | |
| const currentPackages = packageDecisions | |
| .map( | |
| (decision) => | |
| `${decision.packageRef.kind}:${decision.packageRef.ref}@${decision.packageRef.version}:${decision.action === "uninstall" ? "uninstall" : "release"}`, | |
| ) | |
| .toSorted(); | |
| if (JSON.stringify(plannedPackages) !== JSON.stringify(currentPackages)) { | |
| throw new ClawRemoveError("remove_changed", "Package ownership changed after remove planning."); | |
| } | |
| const plannedMcpServers = plan.actions | |
| .filter((action) => action.kind === "mcpServer") | |
| .map((action) => `${action.id}:${action.action}`) | |
| .toSorted(); | |
| const currentMcpServers = record.mcpServers | |
| .map((server) => `${server.name}:${planClawMcpServerRemoval(server, options).action}`) | |
| .toSorted(); | |
| if (JSON.stringify(plannedMcpServers) !== JSON.stringify(currentMcpServers)) { | |
| throw new ClawRemoveError("remove_changed", "MCP ownership changed after remove planning."); | |
| } | |
| const result: ClawRemoveResult = { | |
| schemaVersion: CLAW_REMOVE_RESULT_SCHEMA_VERSION, | |
| stability: CLAW_OUTPUT_STABILITY, | |
| dryRun: false, | |
| status: "partial", | |
| agentId, | |
| agentRemoved: false, | |
| workspaceFiles: [], | |
| packages: [], | |
| mcpServers: [], | |
| cronJobs: [], | |
| packageRefsReleased: 0, | |
| }; | |
| const partial = (code: string, message: string): ClawRemoveResult => ({ | |
| ...result, | |
| error: { code, message }, | |
| }); | |
| const monitors = currentPlan.actions | |
| .filter((action) => action.kind === "scheduledJob" && action.action === "remove") | |
| .map((action) => clawMonitorSnapshotSchema.parse(action.details)); | |
| return await withClawAgentConfigRemoval<ClawRemoveResult>( | |
| { | |
| agentId, | |
| expectedDigest: record.install.agentConfigDigest, | |
| expectedInstall: record.orphaned ? null : record.install, | |
| expectedRemovalSurfaceDigest, | |
| expectedState: record.agentState, | |
| fallbackWorkspace: record.install.workspace, | |
| config: options.config, | |
| stateDatabase: options, | |
| onModified: () => | |
| new ClawRemoveError("agent_modified", "Agent config changed during remove."), | |
| quiesceMonitors: (operationId) => monitorGateway.quiesce(agentId, operationId, monitors), | |
| drainMonitors: async (operationId) => await monitorGateway.drain(agentId, operationId), | |
| }, | |
| async (commitRemoval, assertCurrent) => { | |
| assertCurrent(); | |
| const mcpRemoval = await removeClawMcpServers({ | |
| agentId, | |
| servers: record.mcpServers, | |
| options, | |
| assertCurrent, | |
| }); | |
| assertCurrent(); | |
| result.mcpServers = mcpRemoval.mcpServers; | |
| if (mcpRemoval.error) { | |
| return partial("mcp_cleanup_failed", mcpRemoval.error); | |
| } | |
| const cronJobs = result.cronJobs; | |
| for (const cron of record.cronJobs) { | |
| if (cron.status !== "removed" && (!cron.schedulerJobId || cron.status !== "complete")) { | |
| throw new ClawRemoveError( | |
| "cron_cleanup_uncertain", | |
| `Cron declaration ${JSON.stringify(cron.manifestId)} is not safely removable.`, | |
| ); | |
| } | |
| if ( | |
| cron.status !== "removed" && | |
| (!options.cronGateway?.get || !options.cronGateway.remove) | |
| ) { | |
| throw new ClawRemoveError( | |
| "cron_gateway_required", | |
| "Claw cron jobs require the gateway-owned cron.get and cron.remove APIs.", | |
| ); | |
| } | |
| try { | |
| if (cron.status !== "removed") { | |
| const live = await options.cronGateway!.get!(cron.schedulerJobId!); | |
| if (live != null && !clawCronGatewayJobMatchesRef(agentId, cron, live)) { | |
| throw new Error( | |
| `Cron declaration ${JSON.stringify(cron.manifestId)} changed after planning.`, | |
| ); | |
| } | |
| assertCurrent(); | |
| if (live != null) { | |
| try { | |
| await options.cronGateway!.remove(cron.schedulerJobId!); | |
| } catch (removeError) { | |
| // Re-read after transport loss; a durable removal may have succeeded. | |
| const afterRemove = await options.cronGateway!.get!(cron.schedulerJobId!); | |
| if (afterRemove != null) { | |
| throw removeError; | |
| } | |
| } | |
| } | |
| assertCurrent(); | |
| markClawCronRefRemoved(agentId, cron.manifestId, options); | |
| } | |
| deleteClawCronRef(agentId, cron.manifestId, options); | |
| cronJobs.push({ | |
| manifestId: cron.manifestId, | |
| schedulerJobId: cron.schedulerJobId, | |
| action: "removed", | |
| }); | |
| } catch (error) { | |
| const message = coerceErrorMessage(error); | |
| cronJobs.push({ | |
| manifestId: cron.manifestId, | |
| schedulerJobId: cron.schedulerJobId, | |
| action: "error", | |
| message, | |
| }); | |
| return partial("cron_cleanup_failed", message); | |
| } | |
| } | |
| const configRemoval = await commitRemoval(); | |
| const { cleanupTargets, configBeforeDelete, completeDeletion } = configRemoval; | |
| result.agentRemoved = configRemoval.agentRemoved; | |
| try { | |
| await configRemoval.drainMonitors(); | |
| configRemoval.assertCurrent(); | |
| } catch (error) { | |
| return partial("monitor_cleanup_failed", coerceErrorMessage(error)); | |
| } | |
| const purgeSessions = | |
| options.purgeSessions ?? | |
| (await import("../config/sessions/cleanup-service.js")).purgeAgentSessionStoreEntries; | |
| const purgeFailed = await purgeSessions(configBeforeDelete, agentId, { | |
| env: options.env, | |
| runDatabaseCleanup: configRemoval.runDatabaseCleanup, | |
| }); | |
| assertCurrent(); | |
| if (purgeFailed) { | |
| return partial( | |
| "session_cleanup_failed", | |
| "Session cleanup failed; correct the reported error and retry Claw removal.", | |
| ); | |
| } | |
| try { | |
| const removed = await applyClawPackageRemovalPhase(packageDecisions, { | |
| ...options, | |
| agentId, | |
| operationId: configRemoval.operationId, | |
| assertCurrent, | |
| }); | |
| result.packages = removed.packages; | |
| result.pluginRuntime = removed.application; | |
| result.warnings = removed.warnings; | |
| } catch (error) { | |
| return partial("package_cleanup_failed", coerceErrorMessage(error)); | |
| } | |
| assertCurrent(); | |
| const packageErrors = result.packages.filter((pkg) => pkg.action === "error"); | |
| if (packageErrors.length > 0) { | |
| return partial("package_cleanup_failed", packageErrors.map((pkg) => pkg.reason).join("; ")); | |
| } | |
| const workspaceFiles = result.workspaceFiles; | |
| for (const file of record.workspaceFiles) { | |
| assertCurrent(); | |
| workspaceFiles.push(await removeClawWorkspaceFile(file, assertCurrent)); | |
| } | |
| assertCurrent(); | |
| const bootstrap = await removeClawBootstrap(record, assertCurrent); | |
| const cleanupErrors = workspaceFiles | |
| .filter((file) => file.action === "error") | |
| .map((file) => file.message ?? `Could not remove ${file.path}.`); | |
| if (bootstrap?.action === "error") { | |
| cleanupErrors.push(bootstrap.message ?? `Could not remove ${bootstrap.path}.`); | |
| } | |
| if (cleanupErrors.length === 0) { | |
| const workspaceHasRemainingEntries = await workspaceContainsUntrackedEntries( | |
| cleanupTargets.workspaceDir, | |
| record.workspaceFiles.map((file) => file.path), | |
| ); | |
| configRemoval.assertCurrent(); | |
| cleanupErrors.push( | |
| ...(await cleanupClawAgentFilesystem({ | |
| agentId, | |
| nextConfig: configRemoval.nextConfig, | |
| targets: cleanupTargets, | |
| runtime: clawRemoveQuietRuntime, | |
| trashPath: options.trashPath, | |
| stateDatabase: options, | |
| assertCurrent, | |
| retainWorkspace: | |
| workspaceHasRemainingEntries || | |
| bootstrap?.action === "retainedModified" || | |
| workspaceFiles.some((file) => file.action === "retainedModified"), | |
| })), | |
| ); | |
| } | |
| const complete = releaseClawRemoveRows( | |
| agentId, | |
| workspaceFiles, | |
| cleanupErrors, | |
| configRemoval.assertCurrent, | |
| completeDeletion, | |
| options, | |
| ); | |
| return { | |
| ...result, | |
| status: complete ? "complete" : "partial", | |
| ...(bootstrap ? { bootstrap } : {}), | |
| packageRefsReleased: complete ? record.packages.length : 0, | |
| ...(complete | |
| ? {} | |
| : { | |
| error: { | |
| code: "workspace_cleanup_failed", | |
| message: cleanupErrors.join("; "), | |
| }, | |
| }), | |
| }; | |
| }, | |
| ).catch((error: unknown) => | |
| partial( | |
| error instanceof ClawRemoveError ? error.code : "monitor_cleanup_failed", | |
| coerceErrorMessage(error), | |
| ), | |
| ); | |
| } | |