mirror of https://github.com/openclaw/openclaw.git
fix: clean up failed non-thread subagent spawns
This commit is contained in:
parent
0a04ef494d
commit
b72d0c8459
|
|
@ -685,6 +685,7 @@ export async function spawnSubagentDirect(
|
|||
// Best-effort cleanup only.
|
||||
}
|
||||
}
|
||||
let emitLifecycleHooks = false;
|
||||
if (threadBindingReady) {
|
||||
const hasEndedHook = hookRunner?.hasHooks("subagent_ended") === true;
|
||||
let endedHookEmitted = false;
|
||||
|
|
@ -712,21 +713,22 @@ export async function spawnSubagentDirect(
|
|||
// Spawn should still return an actionable error even if cleanup hooks fail.
|
||||
}
|
||||
}
|
||||
// Always delete the provisional child session after a failed spawn attempt.
|
||||
// If we already emitted subagent_ended above, suppress a duplicate lifecycle hook.
|
||||
try {
|
||||
await callGateway({
|
||||
method: "sessions.delete",
|
||||
params: {
|
||||
key: childSessionKey,
|
||||
deleteTranscript: true,
|
||||
emitLifecycleHooks: !endedHookEmitted,
|
||||
},
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
} catch {
|
||||
// Best-effort only.
|
||||
}
|
||||
emitLifecycleHooks = !endedHookEmitted;
|
||||
}
|
||||
// Always delete the provisional child session after a failed spawn attempt.
|
||||
// If we already emitted subagent_ended above, suppress a duplicate lifecycle hook.
|
||||
try {
|
||||
await callGateway({
|
||||
method: "sessions.delete",
|
||||
params: {
|
||||
key: childSessionKey,
|
||||
deleteTranscript: true,
|
||||
emitLifecycleHooks,
|
||||
},
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
} catch {
|
||||
// Best-effort only.
|
||||
}
|
||||
const messageText = summarizeError(err);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -231,4 +231,60 @@ describe("spawnSubagentDirect workspace inheritance", () => {
|
|||
expectedWorkspaceDir: "/tmp/requester-workspace",
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes the provisional child session when a non-thread subagent start fails", async () => {
|
||||
hoisted.callGatewayMock.mockImplementation(async (request: {
|
||||
method?: string;
|
||||
params?: { key?: string; deleteTranscript?: boolean; emitLifecycleHooks?: boolean };
|
||||
}) => {
|
||||
if (request.method === "sessions.patch") {
|
||||
return { ok: true };
|
||||
}
|
||||
if (request.method === "agent") {
|
||||
throw new Error("spawn startup failed");
|
||||
}
|
||||
if (request.method === "sessions.delete") {
|
||||
return { ok: true };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const result = await spawnSubagentDirect(
|
||||
{
|
||||
task: "fail after provisional session creation",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:main",
|
||||
agentChannel: "discord",
|
||||
agentAccountId: "acct-1",
|
||||
agentTo: "user-1",
|
||||
workspaceDir: "/tmp/requester-workspace",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: "error",
|
||||
error: "spawn startup failed",
|
||||
});
|
||||
expect(result.childSessionKey).toMatch(/^agent:main:subagent:/);
|
||||
expect(hoisted.registerSubagentRunMock).not.toHaveBeenCalled();
|
||||
|
||||
const deleteCall = hoisted.callGatewayMock.mock.calls.find(
|
||||
([request]) => (request as { method?: string }).method === "sessions.delete",
|
||||
)?.[0] as
|
||||
| {
|
||||
params?: {
|
||||
key?: string;
|
||||
deleteTranscript?: boolean;
|
||||
emitLifecycleHooks?: boolean;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
|
||||
expect(deleteCall?.params).toMatchObject({
|
||||
key: result.childSessionKey,
|
||||
deleteTranscript: true,
|
||||
emitLifecycleHooks: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue