fix(core): handle hub abort cleanup failures (#10918)

This commit is contained in:
Saoud Rizwan
2026-05-19 19:38:42 -07:00
committed by GitHub
parent e81c35d7c0
commit d238239a0f
4 changed files with 70 additions and 11 deletions
@@ -1329,6 +1329,27 @@ describe("HubRuntimeHost", () => {
);
});
it("serializes error abort reasons for hub abort commands", async () => {
commandMock.mockResolvedValue({ ok: true, payload: { applied: true } });
const { HubRuntimeHost } = await import("./hub-runtime-host");
const host = new HubRuntimeHost({ url: "ws://127.0.0.1:25463/hub" });
await host.abort(
"sess-1",
new Error("Interactive runtime abort requested"),
);
expect(commandMock).toHaveBeenCalledWith(
"run.abort",
{
sessionId: "sess-1",
reason: "Interactive runtime abort requested",
},
"sess-1",
);
});
it("reads messages through the hub instead of dereferencing client-local artifact paths", async () => {
const messages = [
{
@@ -308,12 +308,12 @@ function buildClientContributionRegistration(
return registration;
}
function abortReasonMessage(value: unknown): string {
function messageFromUnknown(value: unknown): string | undefined {
if (typeof value === "string" && value.trim()) {
return value.trim();
}
if (value instanceof Error) {
return value.message;
return value.message.trim() || undefined;
}
if (value && typeof value === "object" && "message" in value) {
const message = (value as { message?: unknown }).message;
@@ -321,7 +321,11 @@ function abortReasonMessage(value: unknown): string {
return message.trim();
}
}
return "Capability request was cancelled.";
return undefined;
}
function abortReasonMessage(value: unknown): string {
return messageFromUnknown(value) ?? "Capability request was cancelled.";
}
function parseApprovalInput(value: unknown): unknown {
@@ -1144,7 +1148,7 @@ export class HubRuntimeHost implements RuntimeHost {
async abort(sessionId: string, reason?: unknown): Promise<void> {
await this.client.command(
"run.abort",
{ sessionId, reason: typeof reason === "string" ? reason : undefined },
{ sessionId, reason: messageFromUnknown(reason) },
sessionId,
);
}
@@ -2,7 +2,7 @@ import type { HubEventEnvelope } from "@cline/shared";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { RuntimeHost } from "../../../runtime/host/runtime-host";
import { buildHubEvent, type HubTransportContext } from "./context";
import { handleSessionInput } from "./run-handlers";
import { handleRunAbort, handleSessionInput } from "./run-handlers";
function createContext(
overrides: Partial<RuntimeHost> = {},
@@ -160,4 +160,27 @@ describe("run handlers", () => {
resolveRun?.(undefined);
await expect(promise).resolves.toMatchObject({ ok: true });
});
it("treats abort as applied when the runtime abort hook rejects", async () => {
const abort = vi.fn().mockRejectedValue(new Error("Run aborted"));
const ctx = createContext({ abort });
const reply = await handleRunAbort(ctx, {
version: "v1",
command: "run.abort",
requestId: "req-abort",
clientId: "client-1",
sessionId: "session-1",
payload: {
sessionId: "session-1",
reason: "user cancelled",
},
});
expect(reply).toMatchObject({
ok: true,
payload: { applied: true },
});
expect(abort).toHaveBeenCalledWith("session-1", "user cancelled");
});
});
@@ -281,12 +281,23 @@ export async function handleRunAbort(
(approval) => approval.sessionId === sessionId,
reason,
);
await ctx.sessionHost.abort(sessionId, envelope.payload?.reason);
cancelPendingCapabilityRequests(
ctx,
(request) => request.sessionId === sessionId,
reason,
);
try {
await ctx.sessionHost.abort(sessionId, envelope.payload?.reason);
} catch (error) {
logHubMessage("warn", "run.abort_failed", {
command: envelope.command,
requestId: envelope.requestId,
clientId: envelope.clientId,
sessionId,
error,
});
} finally {
cancelPendingCapabilityRequests(
ctx,
(request) => request.sessionId === sessionId,
reason,
);
}
return okReply(envelope, { applied: true });
}