fix(compaction): preflight compaction uses configured compaction timeout instead of reply operation abort signal#95580
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed June 21, 2026, 10:28 PM ET / 02:28 UTC. Summary PR surface: Source +52, Tests +125. Total +177 across 2 files. Reproducibility: yes. at source level: current main and v2026.6.9 pass the raw reply-operation abort signal into required budget/preflight compaction, while compactWithSafetyTimeout races caller aborts before the configured timeout. I did not live-reproduce the reporter's slow vLLM/Qwen deployment. Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Adopt one lifecycle-owned preflight cancellation contract that ignores normal reply lifecycle timeout, still propagates explicit user abort and restart cancellation, cleans up helper state on every compact settlement path, and is backed by redacted real slow-preflight proof. Do we have a high-confidence way to reproduce the issue? Yes, at source level: current main and v2026.6.9 pass the raw reply-operation abort signal into required budget/preflight compaction, while compactWithSafetyTimeout races caller aborts before the configured timeout. I did not live-reproduce the reporter's slow vLLM/Qwen deployment. Is this the best way to solve the issue? No as written. The derived cancellation signal is the right direction, but cleanup needs to be exception-safe and the branch still needs real slow-preflight proof plus a maintainer choice among competing candidate fixes. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against ad304e790d0c. Label changesLabel justifications:
Evidence reviewedPR surface: Source +52, Tests +125. Total +177 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
07f41c0 to
aaeb512
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
aaeb512 to
a078f14
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
a078f14 to
38bd522
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
38bd522 to
6cde290
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
39c1676 to
59ddd5f
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…action Preflight compaction received replyOperation.abortSignal (~60s lifecycle timeout) via compactWithSafetyTimeout's Promise.race, causing slow compactions to be killed before the configured compaction.timeoutSeconds. Create createPreflightCompactionCancelSignal with three-layer protection: 1. abortSignal.reason inspection: propagate explicit cancel, filter TimeoutError (lifecycle) through a one-shot event listener 2. When TimeoutError is filtered, start a polling interval on replyOperation.result (set only on explicit user/restart abort) 3. Check replyOperation.result immediately after construction for the already-aborted case compactWithSafetyTimeout remains the sole timeout owner. Related to openclaw#95553
59ddd5f to
f2841d0
Compare
Dependency GuardThis PR changes dependency-related files. Maintainers should confirm these changes are intentional. Changed files:
Maintainer follow-up:
|
Dependency graph changes are blockedOpenClaw does not accept dependency graph changes through PRs unless a repository admin or security explicitly authorizes the current head SHA. Dependency updates are generated internally by maintainers so external PRs cannot change the resolved graph. Detected dependency graph changes:
To remove lockfile changes, restore them from the target branch: git fetch origin
git checkout 'origin/main' -- 'pnpm-lock.yaml'
git commit -m 'chore: remove dependency lockfile change'
git pushIf this PR intentionally needs a dependency graph change, ask a repository admin or member of The action will approve the current head SHA ( |
…law#95553) Adds a user-facing config gate to disable preflight (budget-triggered) compaction. When compaction.preflight.enabled === false, the pre-turn threshold check in runPreflightCompactionIfNeeded short-circuits to the existing session entry, so subsequent turns fall through to overflow recovery (which already honors compaction.timeoutSeconds with a 15min budget). The check is '=== false' against the literal value, so an unset key preserves the existing default-on behavior — the fix is strictly additive and does not change shipped behavior for any current user. This complements the four open PRs (openclaw#95561, openclaw#95563, openclaw#95580, openclaw#95590) that are all working on the abortSignal source for the preflight path. None of those PRs addresses the user-facing config dimension that the issue explicitly requests. Changes: - src/config/types.agent-defaults.ts: new AgentCompactionPreflightConfig type with optional enabled boolean - src/config/zod-schema.agent-defaults.ts: zod schema with strict mode - src/config/schema.help.ts + schema.labels.ts: help text + label - src/auto-reply/reply/agent-runner-memory.ts: 6-line early return in runPreflightCompactionIfNeeded, placed after isHeartbeat/isCli and before codex runtime so the gate is the only OpenClaw-specific path affected - src/auto-reply/reply/agent-runner-memory.test.ts: 1 new test (skips preflight compaction when enabled === false) using the same test fixture pattern as adjacent tests - scripts/repro/issue-95553-preflight-disabled-gate.mts: standalone real-environment proof that exercises the production gate Refs openclaw#95553
Summary
Preflight (budget-triggered) compaction received
replyOperation.abortSignal, causingcompactWithSafetyTimeout'sPromise.raceto kill slow compactions at ~60s — the reply lifecycle timeout — ignoringcompaction.timeoutSeconds. The fix introduces a cancellation-only signal that forwards explicit user abort and gateway restart but filters out the lifecycle timeout, lettingcompactWithSafetyTimeoutbe the sole timeout owner.replyOperation.abortSignalfires on three triggers: (1) user abort, (2) gateway restart, (3) lifecycle timeout at ~60s. Preflight compaction should cancel on (1) and (2) but not on (3), yet all three fire the sameAbortController. Slow compactions on large sessions never complete.createPreflightCompactionCancelSignal(replyOperation)that inspectsabortSignal.reason.name: propagate for user abort / restart, suppress forTimeoutError(lifecycle timeout).compactWithSafetyTimeouthandles the 180s default timeout.src/auto-reply/reply/agent-runner-memory.ts— +17 lines: newcreatePreflightCompactionCancelSignalhelper + use it at the compaction call site.src/auto-reply/reply/agent-runner-memory.test.ts— +79 lines: 3 tests (1 integration + 2 direct unit tests).runEmbeddedAgentstill usesreplyOperation.abortSignaldirectly. Non-preflight compaction paths.compactWithSafetyTimeoutcore logic.resolveCompactionTimeoutMs.Change Type (select all)
Scope (select all)
Linked Issue/PR
Motivation
OpenClaw runs two compaction paths with different effective timeouts:
trigger=overflowresolveCompactionTimeoutMs(cfg)viacompactWithSafetyTimeouttrigger=budgetreplyOperation.abortSignal(lifecycle timeout, NOT config-derived)The preflight path fires before every turn where
totalTokens >= contextWindow - reserveTokensFloor - softThresholdTokens. On backends where summarizing a large context takes >60s, preflight compaction can never complete. The reply lifecycle timeout (TimeoutError) fromsrc/gateway/chat-abort.tsaborts the controller, andcompactWithSafetyTimeout'sPromise.racekills the compaction.The issue reporter observed: 24 of 26 incomplete compactions clustered at exactly 60–61s, while successful compactions had median duration 278s.
Real behavior proof
compaction.timeoutSeconds.AbortErrorAbortError(code:OPENCLAW_RESTART_ABORT)TimeoutErrorObserved result after fix:
replyOperation.abortSignal) — verified by reference inequality testTimeoutError(reason name) does not propagate — verified by unit testagent-runner-memory.test.tscontinue to passWhat was not tested: Live end-to-end verification with slow compaction backends (vLLM/Qwen). Unit tests prove the signal filtering logic; real-world 60s→278s completion improvement requires a slow-backend deployment.
Root Cause
Provenance:
introduced bycommit5374c7a8a(Peter Steinberger, 2026-05-30) which created bothcompactWithSafetyTimeout(withPromise.raceagainst external abort) andcreateReplyOperation(withAbortController). The preflight path connected them via commit280d1cb97(Vincent Koc, 2026-06-09).Data flow:
agent-runner.ts→runPreflightCompactionIfNeeded→compactEmbeddedAgentSession→compactWithSafetyTimeout→Promise.race([compactPromise, externalAbortPromise]). The lifecycle timeout fires viasrc/gateway/server-maintenance.ts:284(setInterval 60s) →chat-abort.ts:476(abort("timeout")) →replyOperation.abortSignal→Promise.racekills compaction.Confidence:
clear— source inspection confirms the signal chain, unit tests validate the filtering logic.User-visible / Behavior Changes
compaction.timeoutSeconds(default 180s) instead of being killed at ~60s by the lifecycle timeout.Security Impact
Human Verification
createPreflightCompactionCancelSignalis called); abort reason inspectionCompatibility / Migration
Best-fix Verdict
abortSignal.reason.name— and creates a per-call cancellation-only signal. This preserves the explicit cancel contract (stop/restart) while removing the lifecycle timeout race.compactWithSafetyTimeoutis the sole timeout owner.AbortSignal.any+AbortSignal.timeout. Rejected — duplicates timeout ownership and the reply signal can still win first.AI Assistance
Risks and Mitigations
abortSignal.reasoninspection assumesTimeoutErrorname reliably identifies lifecycle timeout vs other timeout errors. Validated againstchat-abort.ts:476which createsreason.name = "TimeoutError".TimeoutErroris suppressed. Any other Error (including provider-level timeouts) propagates through and cancels compaction.