mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 00:48:41 +00:00
fix(gateway): preserve stop reason for deferred agent aborts
This commit is contained in:
@@ -14,6 +14,7 @@ export type ChatAbortControllerEntry = {
|
||||
ownerDeviceId?: string;
|
||||
providerId?: string;
|
||||
authProviderId?: string;
|
||||
abortStopReason?: string;
|
||||
/**
|
||||
* Which RPC owns this registration. Absent (undefined) is treated as
|
||||
* `"chat-send"` so pre-existing callers that constructed entries without
|
||||
@@ -186,6 +187,9 @@ export function abortChatRunById(
|
||||
const bufferedText = ops.chatRunBuffers.get(runId);
|
||||
const partialText = bufferedText && bufferedText.trim() ? bufferedText : undefined;
|
||||
ops.chatAbortedRuns.set(runId, Date.now());
|
||||
if (stopReason) {
|
||||
active.abortStopReason = stopReason;
|
||||
}
|
||||
active.controller.abort();
|
||||
ops.chatAbortControllers.delete(runId);
|
||||
ops.chatRunBuffers.delete(runId);
|
||||
|
||||
@@ -3696,6 +3696,69 @@ describe("gateway agent handler chat.abort integration", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves stop-command reason when /stop lands during the accepted ack yield", async () => {
|
||||
prime();
|
||||
mocks.agentCommand.mockReturnValueOnce(new Promise(() => {}));
|
||||
|
||||
const context = makeContext();
|
||||
const respond = vi.fn();
|
||||
const runId = "idem-stop-before-dispatch";
|
||||
await invokeAgent(
|
||||
{
|
||||
message: "hi",
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
idempotencyKey: runId,
|
||||
},
|
||||
{ context, respond, reqId: runId, flushDispatch: false },
|
||||
);
|
||||
|
||||
expectRecordFields(mockCallArg(respond, 0, 1), {
|
||||
runId,
|
||||
sessionKey: "agent:main:main",
|
||||
status: "accepted",
|
||||
});
|
||||
expect(context.chatAbortControllers.has(runId)).toBe(true);
|
||||
|
||||
const stopRespond = vi.fn();
|
||||
await chatHandlers["chat.send"]({
|
||||
params: {
|
||||
sessionKey: "agent:main:main",
|
||||
message: "/stop",
|
||||
idempotencyKey: "idem-stop-command-before-dispatch",
|
||||
},
|
||||
respond: stopRespond as never,
|
||||
context,
|
||||
req: { type: "req", id: "stop-req", method: "chat.send" },
|
||||
client: null,
|
||||
isWebchatConnect: () => false,
|
||||
});
|
||||
|
||||
expectRecordFields(mockCallArg(stopRespond, 0, 1), {
|
||||
aborted: true,
|
||||
runIds: [runId],
|
||||
});
|
||||
expect(context.chatAbortControllers.has(runId)).toBe(false);
|
||||
|
||||
await flushScheduledDispatchStep();
|
||||
|
||||
expect(mocks.agentCommand).not.toHaveBeenCalled();
|
||||
expectRecordFields(context.dedupe.get(`agent:${runId}`)?.payload, {
|
||||
runId,
|
||||
status: "timeout",
|
||||
summary: "aborted",
|
||||
stopReason: "stop",
|
||||
});
|
||||
const finalResponse = respond.mock.calls.find(
|
||||
(call: unknown[]) => (call[1] as { status?: unknown } | undefined)?.status === "timeout",
|
||||
);
|
||||
expectRecordFields(requireValue(finalResponse, "terminal response missing")[1], {
|
||||
runId,
|
||||
status: "timeout",
|
||||
stopReason: "stop",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not dispatch when chat.abort lands during pre-accept setup", async () => {
|
||||
prime();
|
||||
const requestedSessionKey = "agent:main:legacy-main";
|
||||
|
||||
@@ -100,6 +100,7 @@ import {
|
||||
} from "../../utils/message-channel.js";
|
||||
import { resolveAssistantIdentity } from "../assistant-identity.js";
|
||||
import {
|
||||
type ChatAbortControllerEntry,
|
||||
registerChatAbortController,
|
||||
resolveAgentRunExpiresAtMs,
|
||||
updateChatRunProvider,
|
||||
@@ -547,6 +548,10 @@ function setAbortedAgentDedupeEntries(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveAbortedAgentStopReason(entry?: ChatAbortControllerEntry): string {
|
||||
return entry?.abortStopReason?.trim() || "rpc";
|
||||
}
|
||||
|
||||
function deleteGatewayDedupeEntries(params: {
|
||||
dedupe: GatewayRequestContext["dedupe"];
|
||||
keys: readonly string[];
|
||||
@@ -1669,11 +1674,12 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
let dispatched = false;
|
||||
try {
|
||||
if (activeRunAbort.controller.signal.aborted) {
|
||||
const stopReason = resolveAbortedAgentStopReason(activeRunAbort.entry);
|
||||
setAbortedAgentDedupeEntries({
|
||||
dedupe: context.dedupe,
|
||||
keys: agentDedupeKeys,
|
||||
runId,
|
||||
stopReason: "rpc",
|
||||
stopReason,
|
||||
});
|
||||
respond(
|
||||
true,
|
||||
@@ -1681,7 +1687,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
runId,
|
||||
status: "timeout" as const,
|
||||
summary: "aborted",
|
||||
stopReason: "rpc",
|
||||
stopReason,
|
||||
},
|
||||
undefined,
|
||||
{ runId },
|
||||
|
||||
Reference in New Issue
Block a user