fix(agents): preserve survivor-owned databases offline (#129085)

Prepare surviving agent-directory ownership before filtering journaled SQLite paths, addressing the post-merge review of #129017.

Refs #129014
This commit is contained in:
Peter Steinberger
2026-08-25 01:03:38 -07:00
committed by GitHub
parent 876f2de46f
commit 203024eb33
2 changed files with 28 additions and 4 deletions
+11 -1
View File
@@ -5,12 +5,16 @@ import {
formatSharedAuthStoreOwnerDeleteError,
isSharedAuthStoreOwner,
} from "../agents/agent-delete-safety.js";
import { normalizeAgentDirRegistryPath } from "../agents/agent-dir-registry.js";
import {
isPathOwnedByAnotherRegisteredAgent,
normalizeAgentDirRegistryPath,
} from "../agents/agent-dir-registry.js";
import {
beginAgentDeletion,
claimCompletedAgentDeletion,
} from "../agents/agent-lifecycle-registry.js";
import {
listAgentIds,
resolveAgentDir,
resolveAgentWorkspaceDir,
tryResolveSoleAgentId,
@@ -358,6 +362,11 @@ export async function agentsDeleteCommand(
}
}
if (deleteFiles) {
// Directory ownership is process-local; prepare every survivor before filtering journal paths,
// or a deleted agent's database nested beneath a survivor's agentDir could be trashed.
for (const survivingAgentId of listAgentIds(result.config)) {
resolveAgentDir(result.config, survivingAgentId);
}
const canonicalAgentDir = normalizeAgentDirRegistryPath(agentDir);
const survivingDatabasePaths = new Set(
listOpenClawRegisteredAgentDatabases()
@@ -370,6 +379,7 @@ export async function agentsDeleteCommand(
return (
!isPathInside(canonicalAgentDir, canonicalPath) &&
!survivingDatabasePaths.has(canonicalPath) &&
!isPathOwnedByAnotherRegisteredAgent({ agentId, pathname }) &&
findOverlappingWorkspaceAgentIds(result.config, agentId, canonicalPath).length === 0
);
});
+17 -3
View File
@@ -675,10 +675,15 @@ describe("agents delete command", () => {
it("deregisters the agent database after offline deletion", async () => {
await withStateDirEnv("openclaw-agents-delete-registry-", async ({ tempRoot, stateDir }) => {
const mainAgentDir = path.join(tempRoot, "main-agent");
const cfg: OpenClawConfig = {
agents: {
list: [
{ id: "main", workspace: path.join(stateDir, "workspace-main") },
{
id: "main",
agentDir: mainAgentDir,
workspace: path.join(stateDir, "workspace-main"),
},
{ id: "ops", workspace: path.join(stateDir, "workspace-ops") },
],
},
@@ -687,8 +692,10 @@ describe("agents delete command", () => {
const databasePath = path.join(stateDir, "agents", "ops", "agent", "openclaw-agent.sqlite");
const externalDatabaseDir = path.join(tempRoot, "external-databases");
await fs.mkdir(externalDatabaseDir);
await fs.mkdir(mainAgentDir);
const externalDatabasePath = path.join(externalDatabaseDir, "ops.sqlite");
const sharedDatabasePath = path.join(externalDatabaseDir, "shared.sqlite");
const survivorOwnedDatabasePath = path.join(mainAgentDir, "ops.sqlite");
const externalDatabasePaths = [
externalDatabasePath,
`${externalDatabasePath}-wal`,
@@ -696,14 +703,16 @@ describe("agents delete command", () => {
`${externalDatabasePath}-journal`,
];
await Promise.all(
[...externalDatabasePaths, sharedDatabasePath].map((sqlitePath) =>
fs.writeFile(sqlitePath, ""),
[...externalDatabasePaths, sharedDatabasePath, survivorOwnedDatabasePath].map(
(sqlitePath) => fs.writeFile(sqlitePath, ""),
),
);
const canonicalExternalDatabaseDir = await fs.realpath(externalDatabaseDir);
const canonicalMainAgentDir = await fs.realpath(mainAgentDir);
registerOpenClawAgentDatabase({ agentId: "ops", path: databasePath });
registerOpenClawAgentDatabase({ agentId: "ops", path: externalDatabasePath });
registerOpenClawAgentDatabase({ agentId: "ops", path: sharedDatabasePath });
registerOpenClawAgentDatabase({ agentId: "ops", path: survivorOwnedDatabasePath });
registerOpenClawAgentDatabase({ agentId: "main", path: sharedDatabasePath });
recordAgentProvenance("ops", { createdVia: "operator" });
recordAgentProvenance("child", { createdVia: "agent", creatorAgentId: "ops" });
@@ -726,6 +735,11 @@ describe("agents delete command", () => {
path.join(canonicalExternalDatabaseDir, path.basename(sharedDatabasePath)),
expect.anything(),
);
expect(fsSafeMocks.movePathToTrash).not.toHaveBeenCalledWith(
path.join(canonicalMainAgentDir, path.basename(survivorOwnedDatabasePath)),
expect.anything(),
);
expect((await fs.stat(survivorOwnedDatabasePath)).isFile()).toBe(true);
const registeredDatabases = listOpenClawRegisteredAgentDatabases();
expect(registeredDatabases.map((entry) => entry.agentId)).not.toContain("ops");
expect(registeredDatabases).toEqual(