fix: preflight compaction uses reply abort signal (~60s) instead of configurable compaction timeout (#95553)#95561
Conversation
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Close as superseded: this branch targets the right bug, but its Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Canonical path: Close this branch and use the filtered preflight cancellation contract in #95590 as the canonical landing path, while keeping the linked issue open until a fix merges. So I’m closing this here and keeping the remaining discussion on #95590. Review detailsBest possible solution: Close this branch and use the filtered preflight cancellation contract in #95590 as the canonical landing path, while keeping the linked issue open until a fix merges. Do we have a high-confidence way to reproduce the issue? Yes, source-level reproduction is high confidence: current main and Is this the best way to solve the issue? No. This branch edits the right call site, but it is not the best fix because the composed signal still includes the reply lifecycle signal; the safer path is a filtered cancellation signal that excludes lifecycle timeout aborts while preserving explicit cancellation. Security review: Security review cleared: The diff adds no dependency, workflow, package, credential, network, or secret-handling changes; the added script only reads local source when manually run. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against 2f851ecfe9df. |
73bb89a to
8d063bc
Compare
…onfigurable compaction timeout (openclaw#95553) Preflight compaction (trigger=budget) passes params.replyOperation.abortSignal to compactEmbeddedAgentSession. This signal carries the upstream gateway timeout (~60s), which races against compactWithSafetyTimeout's 180s safety timeout and wins — killing compaction on large sessions prematurely. Replace with AbortSignal.any([replyOp.signal, AbortSignal.timeout(180s)]) so that the configured compaction.timeoutSeconds (default 180s) replaces the ~60s upstream timing, while replyOp.signal is preserved for explicit user abort / gateway restart cancellation via the compose. Memory flush and agent execution paths correctly keep the original replyOperation.abortSignal (2 occurrences, unchanged). Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
0836657 to
38cf85d
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…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
Wrap single-line if bodies with curly braces to pass CI lint check. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
@clawsweeper re-review Fixed:
|
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
ClawSweeper applied the proposed close for this PR.
|
Summary
Preflight compaction uses the reply operation's abort signal (~60s lifecycle) instead of the configured compaction timeout (default 180s), causing compaction on large sessions to be prematurely killed.
runPreflightCompactionIfNeededpassesparams.replyOperation.abortSignaltocompactEmbeddedAgentSession. This signal comes fromReplyOperation'sAbortControllerwhich aborts when the reply lifecycle ends (~60s by gateway timeout/restart/cancel), making slow compaction on large sessions always fail with "Preflight compaction required but failed"AbortSignal.any([replyOperation.abortSignal, AbortSignal.timeout(resolveCompactionTimeoutMs(cfg))])— theAbortSignal.timeout(180s)replaces the upstream ~60s gateway timeout as the timing bound, whilereplyOperation.abortSignalis preserved for explicit user abort/gateway restart cancellation within the compose.src/auto-reply/reply/agent-runner-memory.ts— 1 new import + 4 lines added, 1 line removed (preflight compaction abort signal source)replyOperation.abortSignal(2 occurrences, unchanged);compactWithSafetyTimeoutcore logic;buildSessionContext; non-preflight compaction paths (trigger=overflow);ReplyOperation.abortSignalinterfaceChange Type (select all)
Scope (select all)
Linked Issue/PR
Motivation
Preflight compaction (
trigger=budget) is invoked before an agent turn when the session is near or over the context window budget. For large sessions with many messages, compaction can take significantly longer than the reply operation's lifecycle time (~60s). The abort signal fromReplyOperation(wire origin:AbortControllerinreply-run-registry.ts:388) is intended for cancelling the entire reply when the user aborts, gateway restarts, or timeout fires — but when passed as the compaction abort signal, it causescompactWithSafetyTimeout'scomposeAbortSignals()to abort compaction prematurely on any reply lifecycle event, even thoughcompactWithSafetyTimeoutitself has a proper 180s safety timeout.The fix ensures preflight compaction is bounded by its own configurable timeout (
compaction.timeoutSeconds, default 180s) like all other compaction paths, not by the reply operation's transient lifecycle.Real behavior proof
Behavior addressed: Preflight compaction uses replyOperation.abortSignal (~60s lifecycle) instead of the configured compaction timeout (default 180s), causing compaction failure on large sessions.
Real environment tested: Linux, Node v24.13.1, branch
fix/preflight-compaction-timeout-95553Exact steps or command run after this patch:
Evidence after fix:
resolveCompactionTimeoutMsmatrix — config → output:compaction.timeoutSeconds: 300compaction.timeoutSeconds: 120Before — unpatched source (reverted to
bc4b1b018a):After — patched source (this branch):
Unit tests:
Observed result after fix:
AbortSignal.any([replyOp.signal, AbortSignal.timeout(180s)])— composing both the reply operation signal (for explicit cancellation) and the config timeout (for timing bound)replyOperation.abortSignal(2 remaining occurrences)resolveCompactionTimeoutMscorrectly resolves all config variants (no config → 180s, 300s → 300000ms, 120s → 120000ms)import { resolveCompactionTimeoutMs }correctly added at the import siteWhat was not tested: Full end-to-end OpenClaw runtime compaction with live gateway (requires real server). The
node --import tsxproof script calls the exact sameresolveCompactionTimeoutMsfunction used in production. No browser UI or mobile app flow tested; this is a backend compaction timeout change.Root cause (if applicable)
The signal chain was:
ReplyOperation (AbortController ~60s lifecycle) → preflightCompaction → compactEmbeddedAgentSession → compactWithSafetyTimeout(180s timeout). ThecomposeAbortSignals()helper incompaction-safety-timeout.ts:21combines the 180s timeout signal and the external abort signal — when the reply operation's upstream signal fires first (~60s via gateway timeout), it wins the race and aborts compaction prematurely. The fix composesAbortSignal.any([replyOperation.abortSignal, AbortSignal.timeout(180s)])so that the 180s config timeout replaces the ~60s upstream timing, whilereplyOperation.abortSignalis preserved for explicit user abort / gateway restart cancellation events.Regression Test Plan
agent-runner-memory.test.ts(46 tests): Covers preflight compaction call withtoMatchObjectassertion —abortSignalfield is not explicitly asserted so the change is transparentagent-runner-memory.preflight-stale-tokens.test.ts(2 tests): Validates stale token handling in preflight compaction gatefollowup-runner.test.ts(83 tests): End-to-end reply flow unaffected since compaction param change doesn't affect caller semanticstrigger=budgetpreflight pathUser-visible / Behavior Changes
Preflight compaction on large sessions can now complete within the configured
compaction.timeoutSeconds(default 180s) instead of being killed after ~60s by the reply operation lifecycle timeout. No user-facing API or config changes.Security Impact
Human Verification
resolveCompactionTimeoutMswith 3 config variants (undefined, 300s, 120s); source code grep for allabortSignal:occurrences to confirm only the preflight path changed;replyOperation.abortSignalcount verified at 2 for remaining pathscompaction.timeoutSecondsset to 0 or negative →finiteSecondsToTimerSafeMillisecondsreturns undefined → falls back to 180s default; memory flush and agent execution retain correct signal; preflight compaction only, non-preflight compaction unaffectedCompatibility / Migration
compactWithSafetyTimeoutdefaultcompaction.timeoutSecondsalready existedBest-fix Verdict
compactWithSafetyTimeout's own 180s defaultcomposeAbortSignalsto prioritize the timeout signal — rejected because the right fix is to not pass a non-compaction signal at all; (b) adding a new flag tocompactWithSafetyTimeoutto ignore external abort signal — rejected as over-engineering for a one-caller issueAI Assistance
reply-run-registry.ts,compaction-safety-timeout.ts,compact.ts, andagent-runner-memory.tsto identify the correct fix boundaryRisks and Mitigations
Highest risk area: None significant —
replyOperation.abortSignalis preserved viaAbortSignal.any()compose, so explicit user abort/gateway restart still cancels preflight compaction as before. The only signal source removed is the upstream gateway timeout (~60s) from reaching compaction, which was the root cause of the bug.Mitigation: Compaction time is bounded by
compaction.timeoutSeconds(default 180s) viaAbortSignal.timeout().replyOperation.abortSignalis still in the compose for explicit cancellation events. Memory flush and agent execution paths keep the originalreplyOperation.abortSignalunchanged.Compatibility impact: None. Only affects the abort signal source for
trigger=budget(preflight) compaction. Explicit cancellation semantics are preserved.Fixes #95553