mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
refactor(agents): use loop detection switch for post-compaction guard
This commit is contained in:
@@ -67,7 +67,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/media: derive no-caption inbound media placeholders from saved MIME metadata instead of the Telegram `photo` shape, so non-image and mixed attachments no longer reach the model as `<media:image>`. Fixes #69793. Thanks @aspalagin.
|
||||
- Agents/cache: keep per-turn runtime context out of ordinary chat system prompts while still delivering hidden current-turn context, restoring prompt-cache reuse on chat continuations. Fixes #77431. Thanks @Udjin79.
|
||||
- Gateway/startup: include resolved thinking and fast-mode defaults in the `agent model` startup log line, defaulting unset startup thinking to `medium` without mixing in reasoning visibility.
|
||||
- Agents/Tools: add post-compaction loop guard in `pi-embedded-runner` that arms after auto-compaction-retry and aborts the run with `compaction_loop_persisted` when the agent emits the same `(tool, args, result)` triple `windowSize` times (default 3) within that window. Configurable via `tools.loopDetection.postCompactionGuard.{enabled,windowSize}`. Targets the failure mode where context-overflow + compaction does not break a tool-call loop. Refs #77474; carries forward #21597. Thanks @efpiva.
|
||||
- Agents/Tools: add post-compaction loop guard in `pi-embedded-runner` that arms after auto-compaction-retry and aborts the run with `compaction_loop_persisted` when the agent emits the same `(tool, args, result)` triple `windowSize` times (default 3) within that window. Disable via existing `tools.loopDetection.enabled`; tune via `tools.loopDetection.postCompactionGuard.windowSize`. Targets the failure mode where context-overflow + compaction does not break a tool-call loop. Refs #77474; carries forward #21597. Thanks @efpiva.
|
||||
- Gateway/watch: suppress sync-I/O trace output during `pnpm gateway:watch --benchmark` unless explicitly requested, so CPU profiling no longer floods the terminal with stack traces.
|
||||
- Gateway/watch: when benchmark sync-I/O tracing is explicitly enabled, tee trace blocks to the benchmark output log and filter them from the terminal pane while keeping normal Gateway logs visible.
|
||||
- Plugins/runtime-deps: include `json5` in the memory-core plugin runtime dependency set so packaged `memory_search` sandboxes can resolve generated OpenClaw runtime chunks that parse JSON5 config. Fixes #77461.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
a963f7242f75fbc137a7891b8056c199b42eeb8f4887aeaaa079bff6394feb89 config-baseline.json
|
||||
51f525cfffa40659d6273e56705e91f3cb1255b217dadab59c32f77e0f770b37 config-baseline.core.json
|
||||
21a7c6f83f21d2de3b580fbcc5e9e507bc43b97714a338627caff089cb8ff29d config-baseline.json
|
||||
06f12009f24c2120fb572c808a0b7858d1c52c2ef2386e0fa6244739b2e67680 config-baseline.core.json
|
||||
cd7c0c7fb1435bc7e59099e9ac334462d5ad444016e9ab4512aae63a238f78dc config-baseline.channel.json
|
||||
9832b30a696930a3da7efccf38073137571e1b66cae84e54d747b733fdafcc54 config-baseline.plugin.json
|
||||
|
||||
@@ -96,8 +96,8 @@ This is a separate code path from the global `tools.loopDetection` detectors. It
|
||||
{
|
||||
tools: {
|
||||
loopDetection: {
|
||||
enabled: true, // existing master switch; set false to disable loop guards
|
||||
postCompactionGuard: {
|
||||
enabled: true, // default: true
|
||||
windowSize: 3, // default: 3
|
||||
},
|
||||
},
|
||||
@@ -105,7 +105,6 @@ This is a separate code path from the global `tools.loopDetection` detectors. It
|
||||
}
|
||||
```
|
||||
|
||||
- `enabled`: master switch for the guard.
|
||||
- `windowSize`: number of post-compaction tool calls during which the guard stays armed _and_ the count of identical (tool, args, result) triples that triggers an abort.
|
||||
|
||||
The guard never aborts when results are changing, only when results are byte-identical across the window. It is intentionally narrow: it fires only in the immediate aftermath of a compaction-retry.
|
||||
|
||||
@@ -92,8 +92,8 @@ describe("createPostCompactionLoopGuard", () => {
|
||||
expect(guard.snapshot().remainingAttempts).toBe(2);
|
||||
});
|
||||
|
||||
it("respects enabled: false (always returns shouldAbort: false even when armed)", () => {
|
||||
const guard = createPostCompactionLoopGuard({ enabled: false, windowSize: 3 });
|
||||
it("respects the parent loop detection disabled state", () => {
|
||||
const guard = createPostCompactionLoopGuard({ windowSize: 3 }, { enabled: false });
|
||||
guard.armPostCompaction();
|
||||
guard.observe(callOutcome("gateway", { x: 1 }, "r1"));
|
||||
guard.observe(callOutcome("gateway", { x: 1 }, "r1"));
|
||||
|
||||
@@ -45,9 +45,10 @@ function asPositiveInt(value: number | undefined, fallback: number): number {
|
||||
|
||||
export function createPostCompactionLoopGuard(
|
||||
config?: ToolLoopPostCompactionGuardConfig,
|
||||
options?: { enabled?: boolean },
|
||||
): PostCompactionLoopGuard {
|
||||
const state: GuardState = {
|
||||
enabled: config?.enabled ?? true,
|
||||
enabled: options?.enabled ?? true,
|
||||
windowSize: asPositiveInt(config?.windowSize, DEFAULT_WINDOW_SIZE),
|
||||
remainingAttempts: 0,
|
||||
history: [],
|
||||
|
||||
@@ -274,7 +274,7 @@ describe("post-compaction loop guard wired into runEmbeddedPiAgent", () => {
|
||||
config: {
|
||||
tools: {
|
||||
loopDetection: {
|
||||
postCompactionGuard: { enabled: true, windowSize: 2 },
|
||||
postCompactionGuard: { windowSize: 2 },
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
@@ -322,7 +322,7 @@ describe("post-compaction loop guard wired into runEmbeddedPiAgent", () => {
|
||||
config: {
|
||||
tools: {
|
||||
loopDetection: {
|
||||
postCompactionGuard: { enabled: true, windowSize: 2 },
|
||||
postCompactionGuard: { windowSize: 2 },
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
@@ -345,6 +345,54 @@ describe("post-compaction loop guard wired into runEmbeddedPiAgent", () => {
|
||||
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not arm the post-compaction guard when loop detection is disabled", async () => {
|
||||
const overflowError = makeOverflowError();
|
||||
|
||||
mockedRunEmbeddedAttempt.mockImplementationOnce(async () =>
|
||||
makeAttemptResult({ promptError: overflowError }),
|
||||
);
|
||||
mockedRunEmbeddedAttempt.mockImplementationOnce(async (attemptParams: unknown) => {
|
||||
const onToolOutcome = (attemptParams as { onToolOutcome?: ToolOutcomeObserver })
|
||||
.onToolOutcome;
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
await executeWrappedToolOutcome(
|
||||
"gateway",
|
||||
{ action: "lookup", path: "x" },
|
||||
"identical-result",
|
||||
onToolOutcome,
|
||||
);
|
||||
}
|
||||
return makeAttemptResult({
|
||||
promptError: null,
|
||||
toolMetas: [{ toolName: "gateway" }, { toolName: "gateway" }, { toolName: "gateway" }],
|
||||
});
|
||||
});
|
||||
|
||||
mockedCompactDirect.mockResolvedValueOnce(
|
||||
makeCompactionSuccess({
|
||||
summary: "Compacted session",
|
||||
firstKeptEntryId: "entry-5",
|
||||
tokensBefore: 150000,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runEmbeddedPiAgent({
|
||||
...baseParams,
|
||||
config: {
|
||||
tools: {
|
||||
loopDetection: {
|
||||
enabled: false,
|
||||
postCompactionGuard: { windowSize: 2 },
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(result.meta.error).toBeUndefined();
|
||||
expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
|
||||
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("aborts post-compaction loop from the live tool path even when toolCallHistory is at its trim cap", async () => {
|
||||
// Long-running sessions accumulate up to historySize (default 30) records
|
||||
// in toolCallHistory. The live observer must still see the new outcome
|
||||
|
||||
@@ -797,6 +797,7 @@ export async function runEmbeddedPiAgent(
|
||||
});
|
||||
const postCompactionGuard = createPostCompactionLoopGuard(
|
||||
resolvedLoopDetectionConfig?.postCompactionGuard,
|
||||
{ enabled: resolvedLoopDetectionConfig?.enabled !== false },
|
||||
);
|
||||
const observePostCompactionToolOutcome = (
|
||||
observation: PostCompactionGuardObservation,
|
||||
|
||||
@@ -8334,9 +8334,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
postCompactionGuard: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
windowSize: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
@@ -18272,12 +18269,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
postCompactionGuard: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
title: "Post-compaction Loop Guard",
|
||||
description:
|
||||
"Enable the post-compaction loop guard that aborts the run when the agent repeats the same (tool, args, result) triple windowSize times immediately after auto-compaction-retry (default: true).",
|
||||
},
|
||||
windowSize: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
@@ -25649,11 +25640,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
help: "Global no-progress breaker threshold (default: 30).",
|
||||
tags: ["reliability", "tools"],
|
||||
},
|
||||
"tools.loopDetection.postCompactionGuard.enabled": {
|
||||
label: "Post-compaction Loop Guard",
|
||||
help: "Enable the post-compaction loop guard that aborts the run when the agent repeats the same (tool, args, result) triple windowSize times immediately after auto-compaction-retry (default: true).",
|
||||
tags: ["tools"],
|
||||
},
|
||||
"tools.loopDetection.postCompactionGuard.windowSize": {
|
||||
label: "Post-compaction Loop Guard Window Size",
|
||||
help: "Number of post-compaction attempts during which the guard stays armed (default: 3). Lower values are stricter; higher values give the agent more attempts before abort.",
|
||||
|
||||
@@ -658,8 +658,6 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"tools.loopDetection.detectors.knownPollNoProgress":
|
||||
"Enable known poll tool no-progress loop detection (default: true).",
|
||||
"tools.loopDetection.detectors.pingPong": "Enable ping-pong loop detection (default: true).",
|
||||
"tools.loopDetection.postCompactionGuard.enabled":
|
||||
"Enable the post-compaction loop guard that aborts the run when the agent repeats the same (tool, args, result) triple windowSize times immediately after auto-compaction-retry (default: true).",
|
||||
"tools.loopDetection.postCompactionGuard.windowSize":
|
||||
"Number of post-compaction attempts during which the guard stays armed (default: 3). Lower values are stricter; higher values give the agent more attempts before abort.",
|
||||
"tools.exec.notifyOnExit":
|
||||
|
||||
@@ -215,7 +215,6 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"tools.loopDetection.unknownToolThreshold": "Unknown-tool Loop Threshold",
|
||||
"tools.loopDetection.criticalThreshold": "Tool-loop Critical Threshold",
|
||||
"tools.loopDetection.globalCircuitBreakerThreshold": "Tool-loop Global Circuit Breaker Threshold",
|
||||
"tools.loopDetection.postCompactionGuard.enabled": "Post-compaction Loop Guard",
|
||||
"tools.loopDetection.postCompactionGuard.windowSize": "Post-compaction Loop Guard Window Size",
|
||||
"tools.loopDetection.detectors.genericRepeat": "Tool-loop Generic Repeat Detection",
|
||||
"tools.loopDetection.detectors.knownPollNoProgress": "Tool-loop Poll No-Progress Detection",
|
||||
|
||||
@@ -167,8 +167,6 @@ export type ToolLoopDetectionDetectorConfig = {
|
||||
};
|
||||
|
||||
export type ToolLoopPostCompactionGuardConfig = {
|
||||
/** Enable a strict guard that aborts when the agent re-enters the same tool-call loop immediately after a successful auto-compaction-retry (default: true). */
|
||||
enabled?: boolean;
|
||||
/** How many attempts post-compaction the guard remains armed (default: 3). */
|
||||
windowSize?: number;
|
||||
};
|
||||
|
||||
@@ -504,7 +504,6 @@ const ToolLoopDetectionDetectorSchema = z
|
||||
|
||||
const ToolLoopPostCompactionGuardSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
windowSize: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
@@ -9,7 +9,6 @@ describe("OpenClawSchema tools.loopDetection.postCompactionGuard validation", ()
|
||||
loopDetection: {
|
||||
enabled: true,
|
||||
postCompactionGuard: {
|
||||
enabled: false,
|
||||
windowSize: 5,
|
||||
},
|
||||
},
|
||||
@@ -34,7 +33,6 @@ describe("OpenClawSchema tools.loopDetection.postCompactionGuard validation", ()
|
||||
tools: {
|
||||
loopDetection: {
|
||||
postCompactionGuard: {
|
||||
enabled: true,
|
||||
windowSize: 3,
|
||||
bogus: "key",
|
||||
},
|
||||
@@ -73,7 +71,7 @@ describe("OpenClawSchema tools.loopDetection.postCompactionGuard validation", ()
|
||||
it("validates via ToolsSchema directly", () => {
|
||||
const result = ToolsSchema.safeParse({
|
||||
loopDetection: {
|
||||
postCompactionGuard: { enabled: true, windowSize: 4 },
|
||||
postCompactionGuard: { windowSize: 4 },
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user