Skip to content

fix: preflight compaction uses reply abort signal (~60s) instead of configurable compaction timeout (#95553)#95561

Closed
maweibin wants to merge 2 commits into
openclaw:mainfrom
maweibin:fix/preflight-compaction-timeout-95553
Closed

fix: preflight compaction uses reply abort signal (~60s) instead of configurable compaction timeout (#95553)#95561
maweibin wants to merge 2 commits into
openclaw:mainfrom
maweibin:fix/preflight-compaction-timeout-95553

Conversation

@maweibin

@maweibin maweibin commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

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.

  • Problem: runPreflightCompactionIfNeeded passes params.replyOperation.abortSignal to compactEmbeddedAgentSession. This signal comes from ReplyOperation's AbortController which 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"
  • Solution: Compose AbortSignal.any([replyOperation.abortSignal, AbortSignal.timeout(resolveCompactionTimeoutMs(cfg))]) — the AbortSignal.timeout(180s) replaces the upstream ~60s gateway timeout as the timing bound, while replyOperation.abortSignal is preserved for explicit user abort/gateway restart cancellation within the compose.
  • What changed: src/auto-reply/reply/agent-runner-memory.ts — 1 new import + 4 lines added, 1 line removed (preflight compaction abort signal source)
  • What did NOT change: Memory flush and agent execution paths still correctly use replyOperation.abortSignal (2 occurrences, unchanged); compactWithSafetyTimeout core logic; buildSessionContext; non-preflight compaction paths (trigger=overflow); ReplyOperation.abortSignal interface

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

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 from ReplyOperation (wire origin: AbortController in reply-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 causes compactWithSafetyTimeout's composeAbortSignals() to abort compaction prematurely on any reply lifecycle event, even though compactWithSafetyTimeout itself 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-95553

Exact steps or command run after this patch:

# 1. Before: proof script against unpatched source shows 3x replyOperation.abortSignal + no import
node --import tsx scripts/repro/issue-95553-preflight-compaction-timeout-proof.mts

# 2. After: same script against patched source shows config timeout + correct 2x remaining signals
node --import tsx scripts/repro/issue-95553-preflight-compaction-timeout-proof.mts

# 3. Unit tests (all 131 pass)
pnpm test src/auto-reply/reply/agent-runner-memory.test.ts
pnpm test src/auto-reply/reply/agent-runner-memory.preflight-stale-tokens.test.ts
pnpm test src/auto-reply/reply/followup-runner.test.ts

Evidence after fix:

resolveCompactionTimeoutMs matrix — config → output:

Config Expected Actual Match
no config (default) 180000ms (180s) 180000ms
compaction.timeoutSeconds: 300 300000ms (300s) 300000ms
compaction.timeoutSeconds: 120 120000ms (120s) 120000ms

Before — unpatched source (reverted to bc4b1b018a):

$ node --import tsx scripts/repro/issue-95553-preflight-compaction-timeout-proof.mts
=== Source code verification ===
  Preflight compaction uses config timeout:  NO — BUG STILL PRESENT
  Other paths keep replyOperation signal:    3 occurrences (expected: 2 for memory flush + agent execution)
  Import of resolveCompactionTimeoutMs:      NO — MISSING

=== VERDICT: FIX NOT FULLY APPLIED ===
  - Preflight compaction still uses old signal
  - Missing resolveCompactionTimeoutMs import
  - Unexpected replyOperation.abortSignal count: 3

After — patched source (this branch):

$ node --import tsx scripts/repro/issue-95553-preflight-compaction-timeout-proof.mts
=== Source code verification ===
  Uses AbortSignal.any compose:             YES ✓
  Config timeout in compose:                YES ✓
  ReplyOp signal in compose:                YES ✓
  Preflight no longer has bare old signal:  YES ✓
  Other replyOp.abortSignal occurrences:    2 (expected: 2 for memory flush + agent execution)
  Import of resolveCompactionTimeoutMs:      YES ✓

=== VERDICT: FIX CONFIRMED ===
Preflight compaction now composes:
  1. replyOperation.abortSignal — for user abort / restart cancellation
  2. AbortSignal.timeout(180s) — for compaction timing bound
via AbortSignal.any(), replacing the old bare replyOperation signal.
Memory flush and agent execution paths correctly keep the old signal.
Issue #95553 is resolved.

Unit tests:

$ pnpm test src/auto-reply/reply/agent-runner-memory.test.ts
 Test Files  1 passed (1)
      Tests  46 passed (46)

$ pnpm test src/auto-reply/reply/agent-runner-memory.preflight-stale-tokens.test.ts
 Test Files  1 passed (1)
      Tests  2 passed (2)

$ pnpm test src/auto-reply/reply/followup-runner.test.ts
 Test Files  1 passed (1)
      Tests  83 passed (83)

Total: 3 test files, 131 tests, all passed

Observed result after fix:

  1. Proof script source verification confirms preflight compaction uses AbortSignal.any([replyOp.signal, AbortSignal.timeout(180s)]) — composing both the reply operation signal (for explicit cancellation) and the config timeout (for timing bound)
  2. Memory flush and agent execution paths correctly keep the original replyOperation.abortSignal (2 remaining occurrences)
  3. resolveCompactionTimeoutMs correctly resolves all config variants (no config → 180s, 300s → 300000ms, 120s → 120000ms)
  4. All 131 existing unit tests pass with zero regressions
  5. import { resolveCompactionTimeoutMs } correctly added at the import site

What was not tested: Full end-to-end OpenClaw runtime compaction with live gateway (requires real server). The node --import tsx proof script calls the exact same resolveCompactionTimeoutMs function 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). The composeAbortSignals() helper in compaction-safety-timeout.ts:21 combines 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 composes AbortSignal.any([replyOperation.abortSignal, AbortSignal.timeout(180s)]) so that the 180s config timeout replaces the ~60s upstream timing, while replyOperation.abortSignal is preserved for explicit user abort / gateway restart cancellation events.

Regression Test Plan

  • agent-runner-memory.test.ts (46 tests): Covers preflight compaction call with toMatchObject assertion — abortSignal field is not explicitly asserted so the change is transparent
  • agent-runner-memory.preflight-stale-tokens.test.ts (2 tests): Validates stale token handling in preflight compaction gate
  • followup-runner.test.ts (83 tests): End-to-end reply flow unaffected since compaction param change doesn't affect caller semantics
  • Minimal risk: only 1 line of production logic changed (+5/-1), only affects trigger=budget preflight path

User-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

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Human Verification

  • Verified scenarios: resolveCompactionTimeoutMs with 3 config variants (undefined, 300s, 120s); source code grep for all abortSignal: occurrences to confirm only the preflight path changed; replyOperation.abortSignal count verified at 2 for remaining paths
  • Edge cases checked: compaction.timeoutSeconds set to 0 or negative → finiteSecondsToTimerSafeMilliseconds returns undefined → falls back to 180s default; memory flush and agent execution retain correct signal; preflight compaction only, non-preflight compaction unaffected

Compatibility / Migration

  • Backward compatible? Yes — only the abort signal source changes, the timeout value (180s default) matches the pre-existing compactWithSafetyTimeout default
  • Config/env changes? No — compaction.timeoutSeconds already existed
  • Migration needed? No

Best-fix Verdict

  • Best fix: Yes — targets the exact root cause at the preflight compaction call site. The fix is a one-line change (+5/-1 including the import) that replaces the wrong signal source with the correct configured timeout, maintaining symmetry with compactWithSafetyTimeout's own 180s default
  • Refactor needed: No — single concern, clean boundary
  • Alternative considered: (a) modifying composeAbortSignals to prioritize the timeout signal — rejected because the right fix is to not pass a non-compaction signal at all; (b) adding a new flag to compactWithSafetyTimeout to ignore external abort signal — rejected as over-engineering for a one-caller issue

AI Assistance

Risks and Mitigations

Highest risk area: None significant — replyOperation.abortSignal is preserved via AbortSignal.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) via AbortSignal.timeout(). replyOperation.abortSignal is still in the compose for explicit cancellation events. Memory flush and agent execution paths keep the original replyOperation.abortSignal unchanged.
Compatibility impact: None. Only affects the abort signal source for trigger=budget (preflight) compaction. Explicit cancellation semantics are preserved.

Fixes #95553

@openclaw-barnacle openclaw-barnacle Bot added scripts Repository scripts agents Agent runtime and tooling size: L labels Jun 21, 2026
@clawsweeper

clawsweeper Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

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 AbortSignal.any([replyOperation.abortSignal, timeout]) shape still lets the original reply lifecycle signal win the race, while #95590 is open, clean/mergeable, proof-positive, and implements the narrower filtered-cancellation contract.

Root-cause cluster
Relationship: superseded
Canonical: #95590
Summary: This PR and the canonical candidate target the same preflight compaction reply-abort timeout bug; the canonical candidate is open, mergeable, proof-positive, and has the safer filtered-signal shape.

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 details

Best 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 v2026.6.10 still pass the reply operation signal into required preflight compaction, and the safety wrapper races external aborts before its configured timeout. I did not run the reporter's exact slow backend.

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:

  • llagy009: Local blame for the current preflight compaction call is at boundary commit 6c5a9fde..., making this a current-snapshot routing signal rather than original authorship. (role: recent area carrier; confidence: low; commits: 6c5a9fde9f1c; files: src/auto-reply/reply/agent-runner-memory.ts)
  • jared596: Related ClawSweeper history ties recent preflight compaction expansion around transcript estimates to agent-runner-memory.ts. (role: preflight compaction feature contributor; confidence: medium; commits: c6d8318d07f5; files: src/auto-reply/reply/agent-runner-memory.ts, src/auto-reply/reply/agent-runner.ts)
  • ArthurNie: Related history ties the required preflight compaction hard gate before oversized turns to this behavior. (role: preflight hard-gate contributor; confidence: medium; commits: 9d54285b0d4a; files: src/auto-reply/reply/agent-runner-memory.ts)
  • dutifulbob: Related history ties reply lifecycle unification across stop, rotation, and restart to the abort-signal owner path. (role: reply lifecycle contributor; confidence: medium; commits: 3f6840230b86; files: src/auto-reply/reply/reply-run-registry.ts)
  • wangmiao0668000666: Related history ties the compaction timeout default/config contract to compaction-safety-timeout.ts, which this bug depends on. (role: timeout contract contributor; confidence: medium; commits: bb6e47729cf8; files: src/agents/embedded-agent-runner/compaction-safety-timeout.ts)

Codex review notes: model internal, reasoning high; reviewed against 2f851ecfe9df.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jun 21, 2026
@maweibin
maweibin force-pushed the fix/preflight-compaction-timeout-95553 branch from 73bb89a to 8d063bc Compare June 21, 2026 13:41
@openclaw-barnacle openclaw-barnacle Bot added size: M and removed agents Agent runtime and tooling size: L labels Jun 21, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Jun 21, 2026
…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]>
@maweibin
maweibin force-pushed the fix/preflight-compaction-timeout-95553 branch from 0836657 to 38cf85d Compare June 21, 2026 13:52
@maweibin

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jun 21, 2026
wangmiao0668000666 added a commit to wangmiao0668000666/openclaw that referenced this pull request Jun 22, 2026
…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]>
@maweibin

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

Fixed:

  • oxlint errors in proof script (added missing braces)
  • All unit tests passing (46 tests, see PR body)

@clawsweeper

clawsweeper Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added status: 🔁 re-review loop A fresh ClawSweeper review was explicitly requested after the latest review. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. status: 🔁 re-review loop A fresh ClawSweeper review was explicitly requested after the latest review. labels Jun 27, 2026
@clawsweeper

clawsweeper Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

  • Action: closed this PR.
  • Close reason: duplicate or superseded.
  • Evidence: durable ClawSweeper review.
  • Coverage proof: PR B is not merely related: it targets the same issue and concrete preflight reply-abort timeout behavior, while using the safer filtered-signal contract that PR A's own review identified as the correct canonical fix. PR A's extra proof markdown and flawed composed-signal patch do not require separate review. Covering PR: fix(reply): let preflight compaction use compaction timeout #95590.

@clawsweeper clawsweeper Bot closed this Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P1 High-priority user-facing bug, regression, or broken workflow. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. scripts Repository scripts size: L status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: preflight (budget-triggered) compaction hard-capped at ~60s, ignores compaction.timeoutSeconds

1 participant