mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 12:24:59 +00:00
fix(agents): cap embedded runner retry loop
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Security/Agents: cap embedded Pi runner outer retry loop to 24 attempts and return an explicit `retry_limit` error payload when retries never converge, preventing unbounded internal retry cycles (`GHSA-76m6-pj3w-v7mf`).
|
||||||
- Agents/Tool images: include source filenames in `agents/tool-images` resize logs so compression events can be traced back to specific files.
|
- Agents/Tool images: include source filenames in `agents/tool-images` resize logs so compression events can be traced back to specific files.
|
||||||
- Providers/OAuth: harden Qwen and Chutes refresh handling by validating refresh response expiry values and preserving prior refresh tokens when providers return empty refresh token fields, with regression coverage for empty-token responses.
|
- Providers/OAuth: harden Qwen and Chutes refresh handling by validating refresh response expiry values and preserving prior refresh tokens when providers return empty refresh token fields, with regression coverage for empty-token responses.
|
||||||
- Models/Kimi-Coding: add missing implicit provider template for `kimi-coding` with correct `anthropic-messages` API type and base URL, fixing 403 errors when using Kimi for Coding. (#22409)
|
- Models/Kimi-Coding: add missing implicit provider template for `kimi-coding` with correct `anthropic-messages` API type and base URL, fixing 403 errors when using Kimi for Coding. (#22409)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import "./run.overflow-compaction.mocks.shared.js";
|
import "./run.overflow-compaction.mocks.shared.js";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { pickFallbackThinkingLevel } from "../pi-embedded-helpers.js";
|
||||||
import { compactEmbeddedPiSessionDirect } from "./compact.js";
|
import { compactEmbeddedPiSessionDirect } from "./compact.js";
|
||||||
import { runEmbeddedPiAgent } from "./run.js";
|
import { runEmbeddedPiAgent } from "./run.js";
|
||||||
import { makeAttemptResult, mockOverflowRetrySuccess } from "./run.overflow-compaction.fixture.js";
|
import { makeAttemptResult, mockOverflowRetrySuccess } from "./run.overflow-compaction.fixture.js";
|
||||||
@@ -16,6 +17,7 @@ const mockedSessionLikelyHasOversizedToolResults = vi.mocked(sessionLikelyHasOve
|
|||||||
const mockedTruncateOversizedToolResultsInSession = vi.mocked(
|
const mockedTruncateOversizedToolResultsInSession = vi.mocked(
|
||||||
truncateOversizedToolResultsInSession,
|
truncateOversizedToolResultsInSession,
|
||||||
);
|
);
|
||||||
|
const mockedPickFallbackThinkingLevel = vi.mocked(pickFallbackThinkingLevel);
|
||||||
|
|
||||||
describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
|
describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -106,4 +108,29 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
|
|||||||
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(4);
|
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(4);
|
||||||
expect(result.meta.error?.kind).toBe("context_overflow");
|
expect(result.meta.error?.kind).toBe("context_overflow");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns retry_limit when repeated retries never converge", async () => {
|
||||||
|
mockedRunEmbeddedAttempt.mockReset();
|
||||||
|
mockedCompactDirect.mockReset();
|
||||||
|
mockedPickFallbackThinkingLevel.mockReset();
|
||||||
|
mockedRunEmbeddedAttempt.mockResolvedValue(
|
||||||
|
makeAttemptResult({ promptError: new Error("unsupported reasoning mode") }),
|
||||||
|
);
|
||||||
|
mockedPickFallbackThinkingLevel.mockReturnValue("low");
|
||||||
|
|
||||||
|
const result = await runEmbeddedPiAgent({
|
||||||
|
sessionId: "test-session",
|
||||||
|
sessionKey: "test-key",
|
||||||
|
sessionFile: "/tmp/session.json",
|
||||||
|
workspaceDir: "/tmp/workspace",
|
||||||
|
prompt: "hello",
|
||||||
|
timeoutMs: 30000,
|
||||||
|
runId: "run-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(24);
|
||||||
|
expect(mockedCompactDirect).not.toHaveBeenCalled();
|
||||||
|
expect(result.meta.error?.kind).toBe("retry_limit");
|
||||||
|
expect(result.payloads?.[0]?.isError).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ function createCompactionDiagId(): string {
|
|||||||
return `ovf-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
return `ovf-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defensive guard for the outer run loop across all retry branches.
|
||||||
|
const MAX_RUN_RETRY_ITERATIONS = 24;
|
||||||
|
|
||||||
const hasUsageValues = (
|
const hasUsageValues = (
|
||||||
usage: ReturnType<typeof normalizeUsage>,
|
usage: ReturnType<typeof normalizeUsage>,
|
||||||
): usage is NonNullable<ReturnType<typeof normalizeUsage>> =>
|
): usage is NonNullable<ReturnType<typeof normalizeUsage>> =>
|
||||||
@@ -475,13 +478,42 @@ export async function runEmbeddedPiAgent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MAX_OVERFLOW_COMPACTION_ATTEMPTS = 3;
|
const MAX_OVERFLOW_COMPACTION_ATTEMPTS = 3;
|
||||||
|
const MAX_RUN_LOOP_ITERATIONS = MAX_RUN_RETRY_ITERATIONS;
|
||||||
let overflowCompactionAttempts = 0;
|
let overflowCompactionAttempts = 0;
|
||||||
let toolResultTruncationAttempted = false;
|
let toolResultTruncationAttempted = false;
|
||||||
const usageAccumulator = createUsageAccumulator();
|
const usageAccumulator = createUsageAccumulator();
|
||||||
let lastRunPromptUsage: ReturnType<typeof normalizeUsage> | undefined;
|
let lastRunPromptUsage: ReturnType<typeof normalizeUsage> | undefined;
|
||||||
let autoCompactionCount = 0;
|
let autoCompactionCount = 0;
|
||||||
|
let runLoopIterations = 0;
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (runLoopIterations >= MAX_RUN_LOOP_ITERATIONS) {
|
||||||
|
const message = `Exceeded retry limit after ${runLoopIterations} attempts.`;
|
||||||
|
log.error(
|
||||||
|
`[run-retry-limit] sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
||||||
|
`provider=${provider}/${modelId} attempts=${runLoopIterations}`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
payloads: [
|
||||||
|
{
|
||||||
|
text:
|
||||||
|
"Request failed after repeated internal retries. " +
|
||||||
|
"Please try again, or use /new to start a fresh session.",
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
durationMs: Date.now() - started,
|
||||||
|
agentMeta: {
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
provider,
|
||||||
|
model: model.id,
|
||||||
|
},
|
||||||
|
error: { kind: "retry_limit", message },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
runLoopIterations += 1;
|
||||||
attemptedThinking.add(thinkLevel);
|
attemptedThinking.add(thinkLevel);
|
||||||
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,12 @@ export type EmbeddedPiRunMeta = {
|
|||||||
aborted?: boolean;
|
aborted?: boolean;
|
||||||
systemPromptReport?: SessionSystemPromptReport;
|
systemPromptReport?: SessionSystemPromptReport;
|
||||||
error?: {
|
error?: {
|
||||||
kind: "context_overflow" | "compaction_failure" | "role_ordering" | "image_size";
|
kind:
|
||||||
|
| "context_overflow"
|
||||||
|
| "compaction_failure"
|
||||||
|
| "role_ordering"
|
||||||
|
| "image_size"
|
||||||
|
| "retry_limit";
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
/** Stop reason for the agent run (e.g., "completed", "tool_calls"). */
|
/** Stop reason for the agent run (e.g., "completed", "tool_calls"). */
|
||||||
|
|||||||
Reference in New Issue
Block a user