mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 08:31:24 +00:00
fix: harden queue retry debounce and add regression tests
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { ImageContent } from "@mariozechner/pi-ai";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { injectHistoryImagesIntoMessages, resolvePromptBuildHookResult } from "./attempt.js";
|
||||
import {
|
||||
injectHistoryImagesIntoMessages,
|
||||
resolvePromptBuildHookResult,
|
||||
resolvePromptModeForSession,
|
||||
} from "./attempt.js";
|
||||
|
||||
describe("injectHistoryImagesIntoMessages", () => {
|
||||
const image: ImageContent = { type: "image", data: "abc", mimeType: "image/png" };
|
||||
@@ -103,3 +107,14 @@ describe("resolvePromptBuildHookResult", () => {
|
||||
expect(result.prependContext).toBe("from-hook");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolvePromptModeForSession", () => {
|
||||
it("uses minimal mode for subagent sessions", () => {
|
||||
expect(resolvePromptModeForSession("agent:main:subagent:child")).toBe("minimal");
|
||||
});
|
||||
|
||||
it("uses full mode for cron sessions", () => {
|
||||
expect(resolvePromptModeForSession("agent:main:cron:job-1")).toBe("full");
|
||||
expect(resolvePromptModeForSession("agent:main:cron:job-1:run:run-abc")).toBe("full");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -221,6 +221,13 @@ export async function resolvePromptBuildHookResult(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePromptModeForSession(sessionKey?: string): "minimal" | "full" {
|
||||
if (!sessionKey) {
|
||||
return "full";
|
||||
}
|
||||
return isSubagentSessionKey(sessionKey) ? "minimal" : "full";
|
||||
}
|
||||
|
||||
function summarizeMessagePayload(msg: AgentMessage): { textChars: number; imageBlocks: number } {
|
||||
const content = (msg as { content?: unknown }).content;
|
||||
if (typeof content === "string") {
|
||||
@@ -494,7 +501,7 @@ export async function runEmbeddedAttempt(
|
||||
},
|
||||
});
|
||||
const isDefaultAgent = sessionAgentId === defaultAgentId;
|
||||
const promptMode = isSubagentSessionKey(params.sessionKey) ? "minimal" : "full";
|
||||
const promptMode = resolvePromptModeForSession(params.sessionKey);
|
||||
const docsPath = await resolveOpenClawDocsPath({
|
||||
workspaceDir: effectiveWorkspace,
|
||||
argv1: process.argv[1],
|
||||
|
||||
@@ -27,6 +27,7 @@ function createRetryingSend() {
|
||||
|
||||
describe("subagent-announce-queue", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
resetAnnounceQueuesForTests();
|
||||
});
|
||||
|
||||
@@ -116,4 +117,52 @@ describe("subagent-announce-queue", () => {
|
||||
expect(sender.prompts[1]).toContain("Queued #2");
|
||||
expect(sender.prompts[1]).toContain("queued item two");
|
||||
});
|
||||
|
||||
it("uses debounce floor for retries when debounce exceeds backoff", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
||||
const previousFast = process.env.OPENCLAW_TEST_FAST;
|
||||
delete process.env.OPENCLAW_TEST_FAST;
|
||||
|
||||
try {
|
||||
const attempts: number[] = [];
|
||||
const send = vi.fn(async () => {
|
||||
attempts.push(Date.now());
|
||||
if (attempts.length === 1) {
|
||||
throw new Error("transient timeout");
|
||||
}
|
||||
});
|
||||
|
||||
enqueueAnnounce({
|
||||
key: "announce:test:retry-debounce-floor",
|
||||
item: {
|
||||
prompt: "subagent completed",
|
||||
enqueuedAt: Date.now(),
|
||||
sessionKey: "agent:main:telegram:dm:u1",
|
||||
},
|
||||
settings: { mode: "followup", debounceMs: 5_000 },
|
||||
send,
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
expect(send).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(4_999);
|
||||
expect(send).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(send).toHaveBeenCalledTimes(2);
|
||||
const [firstAttempt, secondAttempt] = attempts;
|
||||
if (firstAttempt === undefined || secondAttempt === undefined) {
|
||||
throw new Error("expected two retry attempts");
|
||||
}
|
||||
expect(secondAttempt - firstAttempt).toBeGreaterThanOrEqual(5_000);
|
||||
} finally {
|
||||
if (previousFast === undefined) {
|
||||
delete process.env.OPENCLAW_TEST_FAST;
|
||||
} else {
|
||||
process.env.OPENCLAW_TEST_FAST = previousFast;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,9 +183,10 @@ function scheduleAnnounceDrain(key: string) {
|
||||
queue.consecutiveFailures++;
|
||||
// Exponential backoff on consecutive failures: 2s, 4s, 8s, ... capped at 60s.
|
||||
const errorBackoffMs = Math.min(1000 * Math.pow(2, queue.consecutiveFailures), 60_000);
|
||||
queue.lastEnqueuedAt = Date.now() + errorBackoffMs - queue.debounceMs;
|
||||
const retryDelayMs = Math.max(errorBackoffMs, queue.debounceMs);
|
||||
queue.lastEnqueuedAt = Date.now() + retryDelayMs - queue.debounceMs;
|
||||
defaultRuntime.error?.(
|
||||
`announce queue drain failed for ${key} (attempt ${queue.consecutiveFailures}, retry in ${Math.round(errorBackoffMs / 1000)}s): ${String(err)}`,
|
||||
`announce queue drain failed for ${key} (attempt ${queue.consecutiveFailures}, retry in ${Math.round(retryDelayMs / 1000)}s): ${String(err)}`,
|
||||
);
|
||||
} finally {
|
||||
queue.draining = false;
|
||||
@@ -205,7 +206,8 @@ export function enqueueAnnounce(params: {
|
||||
send: (item: AnnounceQueueItem) => Promise<void>;
|
||||
}): boolean {
|
||||
const queue = getAnnounceQueue(params.key, params.settings, params.send);
|
||||
queue.lastEnqueuedAt = Date.now();
|
||||
// Preserve any retry backoff marker already encoded in lastEnqueuedAt.
|
||||
queue.lastEnqueuedAt = Math.max(queue.lastEnqueuedAt, Date.now());
|
||||
|
||||
const shouldEnqueue = applyQueueDropPolicy({
|
||||
queue,
|
||||
|
||||
Reference in New Issue
Block a user