mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-04 02:00:10 +00:00
fix(reply): keep the stall notice for Web UI turns without a queued follow-up
This commit is contained in:
parent
e0b05c52af
commit
0eca7a4454
3 changed files with 35 additions and 2 deletions
|
|
@ -214,7 +214,7 @@ The Control UI **System busyness** overlay and `diagnostics.lanes` report this w
|
|||
- `session.stuck` is reserved for recoverable stale session bookkeeping, including idle queued sessions with stale ownerless model/tool activity.
|
||||
- `session.stuck` always triggers recovery that can release the affected session lane. A `session.stalled` classification past the abort threshold (blocked tool call, stalled model call, or stalled embedded run) can also trigger active-abort recovery, so both classifications can unstick a queue, not only `session.stuck`.
|
||||
- Repeated model requests without semantic progress share one stagnation clock. Fresh transport bytes or another retry cannot renew it indefinitely. Recovery rechecks that evidence before aborting, honors owned tool and provider retry deadlines, and lets the existing run owner settle before the queue drains.
|
||||
- When recovery aborts an interactive turn before it replied and its request is already saved in the transcript, OpenClaw attempts one continuation instead of immediately asking the user to retry. When the next queued follow-up is from the same sender and route (for example a `chat.send` with `queueMode: "followup"`), it takes over the outstanding request; otherwise a recovery turn starts on the stalled turn's route. Both use the existing transcript and are instructed not to repeat completed actions. Other senders' queued messages and channel messages that arrived during the stalled turn run afterwards as their own turns. The "stopped making progress" notice is the last resort if that continuation also stalls or cannot be scheduled, including when the original request was not yet saved. Heartbeat and cron turns are unaffected.
|
||||
- When recovery aborts an interactive turn before it replied and its request is already saved in the transcript, OpenClaw attempts one continuation instead of immediately asking the user to retry. When the next queued follow-up is from the same sender and route (for example a `chat.send` with `queueMode: "followup"`), it takes over the outstanding request; otherwise a recovery turn starts on the stalled turn's route. Web UI (`chat.send`) turns without such a follow-up get the notice instead, because their reply only reaches the client that is still waiting on the stalled turn. Both use the existing transcript and are instructed not to repeat completed actions. Other senders' queued messages and channel messages that arrived during the stalled turn run afterwards as their own turns. The "stopped making progress" notice is the last resort if that continuation also stalls or cannot be scheduled, including when the original request was not yet saved. Heartbeat and cron turns are unaffected.
|
||||
- Repeated `session.stuck` and `session.long_running` warning log lines back off exponentially while the session remains unchanged; recovery attempts still run on every heartbeat tick regardless of that backoff.
|
||||
|
||||
## Related
|
||||
|
|
|
|||
|
|
@ -68,6 +68,12 @@ export function continueStalledReplyTurn({
|
|||
);
|
||||
return true;
|
||||
}
|
||||
// Source-bound reply owners (Web UI chat.send, group threads) deliver only the
|
||||
// follow-ups they queued themselves and would drop a recovery run's answer.
|
||||
// Leave the notice with the stalled turn's still-live dispatch.
|
||||
if (followupRun.queuedFollowupReplyDisposition) {
|
||||
return false;
|
||||
}
|
||||
const enqueued = enqueueFollowupRun(
|
||||
queueKey,
|
||||
buildStalledTurnRecoveryRun(followupRun),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
createAdmittedRunOperatorAuthority,
|
||||
type AdmittedRunOperatorAuthority,
|
||||
} from "../../agents/admitted-run-context.js";
|
||||
import { createChatSendLateFollowupDisposition } from "../../gateway/server-methods/chat-send-late-followup.js";
|
||||
import { createUserTurnTranscriptRecorder } from "../../sessions/user-turn-transcript.js";
|
||||
import {
|
||||
createSqliteTranscriptTarget,
|
||||
|
|
@ -88,7 +89,11 @@ const queueKey = "agent:main:telegram:direct:stalled";
|
|||
const settings: QueueSettings = { mode: "followup", debounceMs: 0 };
|
||||
|
||||
function createStalledRun(
|
||||
options: { isHeartbeat?: boolean; operatorAuthority?: AdmittedRunOperatorAuthority } = {},
|
||||
options: {
|
||||
isHeartbeat?: boolean;
|
||||
operatorAuthority?: AdmittedRunOperatorAuthority;
|
||||
queuedFollowupReplyDisposition?: FollowupRun["queuedFollowupReplyDisposition"];
|
||||
} = {},
|
||||
): StalledRun {
|
||||
const followupRun = createTestFollowupRun({
|
||||
sessionId: "stalled-session",
|
||||
|
|
@ -99,6 +104,7 @@ function createStalledRun(
|
|||
followupRun.originatingChannel = "telegram";
|
||||
followupRun.originatingTo = "12345";
|
||||
followupRun.operatorAuthority = options.operatorAuthority;
|
||||
followupRun.queuedFollowupReplyDisposition = options.queuedFollowupReplyDisposition;
|
||||
followupRun.images = [{ type: "image", data: "aW1n", mimeType: "image/png" }];
|
||||
followupRun.transcriptPrompt = "what is good at the hotel restaurant?";
|
||||
const transcriptTarget = createSqliteTranscriptTarget({
|
||||
|
|
@ -348,6 +354,27 @@ describe("runReplyAgent stalled turn continuation", () => {
|
|||
expect(drainedRuns).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("leaves the notice with a Web UI chat.send turn whose reply owner never queued", async () => {
|
||||
// The Gateway's chat.send reply owner delivers only follow-ups it deferred itself.
|
||||
const gatewayDeliver = vi.fn(async () => ({ kind: "delivered" as const }));
|
||||
const chatSendOwner = createChatSendLateFollowupDisposition({
|
||||
runId: "chat-send-run",
|
||||
originatingChannel: "webchat",
|
||||
logGateway: { info: vi.fn() } as never,
|
||||
deliver: gatewayDeliver,
|
||||
});
|
||||
const stalled = createStalledRun({
|
||||
queuedFollowupReplyDisposition: { kind: "deliver", deliver: chatSendOwner.deliver },
|
||||
});
|
||||
await stallBeforeOutput(stalled);
|
||||
|
||||
expect(stalled.runState.continueStalledTurn?.()).toBe(false);
|
||||
expect(getFollowupQueueDepth(queueKey)).toBe(0);
|
||||
|
||||
await settleStalledOwner(stalled);
|
||||
expect(drainedRuns).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not arm a continuation for heartbeat turns", async () => {
|
||||
const stalled = createStalledRun({ isHeartbeat: true });
|
||||
await stallBeforeOutput(stalled);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue