mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): deliver failed steer retries as a new turn when the session is idle (#122919)
A steer rejected on the run/leaf fence parks as a failed steered queue row. Retry previously refused whenever the target run was gone, leaving the user's message permanently stuck with 'that run is no longer active'. Retry now converts the parked row into a plain queued send when the session is idle, so the text delivers as a fresh turn; active-run retries keep the original steer binding, and disconnected retries keep the existing error.
This commit is contained in:
committed by
GitHub
parent
c0b7ebd73e
commit
c98b841b41
@@ -168,7 +168,7 @@ export function moveQueuedChatMessage(host: ChatHost, id: string, toIndex: numbe
|
||||
}
|
||||
|
||||
export async function retryQueuedChatMessage(host: ChatHost, id: string) {
|
||||
const item = host.chatQueue.find((entry) => entry.id === id);
|
||||
let item = host.chatQueue.find((entry) => entry.id === id);
|
||||
if (
|
||||
!item ||
|
||||
item.pendingRunId ||
|
||||
@@ -180,23 +180,39 @@ export async function retryQueuedChatMessage(host: ChatHost, id: string) {
|
||||
return;
|
||||
}
|
||||
if (item.kind === "steered") {
|
||||
if (!host.connected || !host.client || !hasAbortableSessionRun(host)) {
|
||||
if (!host.connected || !host.client) {
|
||||
setChatError(host, t("chat.sendErrors.steerRunNoLongerActive"));
|
||||
return;
|
||||
}
|
||||
const retry = updateQueuedMessage(host, id, (entry) => ({
|
||||
...entry,
|
||||
sendAttempts: 0,
|
||||
sendError: undefined,
|
||||
sendRequestStartedAtMs: undefined,
|
||||
sendState: "waiting-idle",
|
||||
}));
|
||||
if (!retry) {
|
||||
if (hasAbortableSessionRun(host)) {
|
||||
const retry = updateQueuedMessage(host, id, (entry) => ({
|
||||
...entry,
|
||||
sendAttempts: 0,
|
||||
sendError: undefined,
|
||||
sendRequestStartedAtMs: undefined,
|
||||
sendState: "waiting-idle",
|
||||
}));
|
||||
if (!retry) {
|
||||
setChatError(host, OFFLINE_QUEUE_STORAGE_ERROR);
|
||||
return;
|
||||
}
|
||||
await steerQueuedChatMessageLifecycle(host, id, steerSendDependencies);
|
||||
return;
|
||||
}
|
||||
const converted = updateQueuedMessage(host, id, (entry) => {
|
||||
const {
|
||||
kind: _kind,
|
||||
pendingRunId: _pendingRunId,
|
||||
steerTargetRunId: _steerTargetRunId,
|
||||
...queued
|
||||
} = entry;
|
||||
return resetRetryState(queued, reconnectSafeQueuedSendState(host));
|
||||
});
|
||||
if (!converted) {
|
||||
setChatError(host, OFFLINE_QUEUE_STORAGE_ERROR);
|
||||
return;
|
||||
}
|
||||
await steerQueuedChatMessageLifecycle(host, id, steerSendDependencies);
|
||||
return;
|
||||
item = converted;
|
||||
}
|
||||
let outbox = findStoredOutbox(host, item.id);
|
||||
if (!outbox) {
|
||||
|
||||
@@ -8797,13 +8797,14 @@ describe("handleSendChat", () => {
|
||||
]);
|
||||
expect(host.lastError).toBe("no active turn to steer");
|
||||
|
||||
host.chatRunId = null;
|
||||
host.connected = false;
|
||||
await retryQueuedChatMessage(host, original.id);
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(host.lastError).toBe(
|
||||
"This steer still targets the previous run, but that run is no longer active.",
|
||||
);
|
||||
|
||||
host.connected = true;
|
||||
host.chatRunId = "active-run";
|
||||
host.chatDisplayedLeafEntryId = "leaf-advanced-during-tool-work";
|
||||
await retryQueuedChatMessage(host, original.id);
|
||||
@@ -8821,6 +8822,49 @@ describe("handleSendChat", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("retries a failed steer as a new turn when the session is idle", async () => {
|
||||
const payloads: Array<Record<string, unknown>> = [];
|
||||
const original = {
|
||||
id: "idle-failed-steer",
|
||||
text: "deliver this as a new turn",
|
||||
createdAt: 1,
|
||||
kind: "steered" as const,
|
||||
sendAttempts: 1,
|
||||
sendError: "The session switched branches — review and resend.",
|
||||
sendRequestStartedAtMs: 123,
|
||||
sendRunId: "previous-steer-request",
|
||||
sendState: "failed" as const,
|
||||
steerTargetRunId: "previous-run",
|
||||
sessionKey: "agent:main:main",
|
||||
agentId: "main",
|
||||
};
|
||||
const host = makeChatHost({
|
||||
requestHandlers: {
|
||||
"chat.history": idleChatHistory(original.sessionKey),
|
||||
"chat.send": (params: unknown) => {
|
||||
payloads.push(requireRecord(params, "idle steer retry payload"));
|
||||
return { status: "ok", runId: "new-turn" };
|
||||
},
|
||||
},
|
||||
chatError: "The session switched branches — review and resend.",
|
||||
chatQueue: [original],
|
||||
sessionKey: original.sessionKey,
|
||||
});
|
||||
expect(admitQueuedMessageForSession(host, host.sessionKey, original)).toBe(true);
|
||||
|
||||
await retryQueuedChatMessage(host, original.id);
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]).toMatchObject({ message: original.text });
|
||||
expect(payloads[0]).not.toHaveProperty("expectedRunId");
|
||||
expect(payloads[0]).not.toHaveProperty("expectedLeafEntryId");
|
||||
expect(payloads[0]).not.toHaveProperty("queueMode");
|
||||
expect(payloads[0]?.idempotencyKey).not.toBe(original.sendRunId);
|
||||
expect(host.chatQueue).toEqual([]);
|
||||
expect(host.chatError).toBeNull();
|
||||
expect(host.lastError).toBeNull();
|
||||
});
|
||||
|
||||
it("fails a restored steer that predates durable target identity", async () => {
|
||||
const original = {
|
||||
id: "legacy-targetless-steer",
|
||||
|
||||
Reference in New Issue
Block a user