Skip to content

[Bug]: Compaction safeguard mode produces empty fallback summaries that loop until 60s aggregate timeout #78300

Description

@jw8957

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

When agents.defaults.compaction.mode = "safeguard", certain sessions repeatedly trigger the safeguard's "no real conversation messages" branch and write a 144-char fallback summary (## Decisions\nNo prior history.\n...). The actual compaction LLM call then runs in parallel but is canceled after the 60s COMPACTION_RETRY_AGGREGATE_TIMEOUT_MS, and the runtime falls back to a "pre-compaction snapshot" — meaning the transcript still has full size, but a placeholder compaction marker has been recorded. On the next turn the same session re-enters the same path, creating a loop of 60s-stalled turns with empty summaries.

Switching to mode: "default" makes the symptom go away (no safeguard hook → no empty-fallback writes).

The sessions where this triggers are real working sessions with substantive user and assistant messages plus tool calls. A simulation replay of isRealConversationMessage on the same transcript reports 14/108 "real" messages — well above the threshold the safeguard's if (!hasRealSummarizable && !hasRealTurnPrefix) check should require — so something in the message set passed to the safeguard hook is being seen as "no real conversation" even though the file-on-disk transcript clearly contains real conversation.

This affects:

  • Cron-triggered sessions (e.g. daily-report cron at 38K tokens)
  • Newly-reset interactive sessions (/new flow)
  • Long DM sessions

I have not yet identified the upstream commit that introduced the regression — containsRealConversationMessages and the 60s aggregate timer existed in 2026.4.23 (still ran fine for me there), so the divergence likely lives in how messagesToSummarize is constructed for the session_before_compact event. Happy to dig further if maintainers point at the right area.

Steps to reproduce

  1. Run OpenClaw 2026.5.3-1 with agents.defaults.compaction.mode: "safeguard" and Anthropic-style provider (modelsproxy → claude-opus-4-7 in my case)
  2. Either:
    • Trigger a cron task that does a few read / exec tool calls (the daily report pipeline I have hits this every morning), OR
    • Start a /new session that does a few rounds of investigation (read files, run exec, etc.)
  3. Wait until preflight compaction triggers (this happens well before context limit because of truncateAfterCompaction being false in my config — I am NOT setting maxActiveTranscriptBytes)
  4. Watch the gateway logs

Expected: compaction summary captures the conversation so far.
Actual: 144-char fallback summary; 60s aggregate timeout; pre-compaction snapshot used; same session re-triggers compaction on the next turn.

Expected behavior

Either:

  1. Fix root cause — the safeguard hook should agree with the underlying transcript on whether real conversation exists. Real cron / interactive sessions with user prompts + assistant tool-using turns should always summarize, not fall through to "No prior history".
  2. Surface the gap — if the safeguard intentionally treats some message classes as non-conversation (e.g. all toolCall blocks with empty assistant text), the fallback summary should at least note "compaction skipped — N messages not classified as conversation" rather than "No prior history", to help operators diagnose.
  3. Make default the documented recommendation for users without specific safeguard requirements — currently the schema help text says "Keep 'default' unless you observe aggressive history loss near limit boundaries" but the wizard / docs are silent on the regression risk of safeguard.

Actual behavior

Logs show repeated:

[compaction-safeguard] Compaction safeguard: no real conversation messages to summarize; writing compaction boundary to suppress re-trigger loop.
[agent/embedded] compaction retry aggregate timeout (60000ms): proceeding with pre-compaction state runId=... sessionId=...
[agent/embedded] using pre-compaction snapshot: timed out during compaction runId=... sessionId=...

Recorded compaction entries in transcript files have:

{"type":"compaction","summary":"## Decisions\nNo prior history.\n\n## Open TODOs\nNone.\n\n## Constraints/Rules\nNone.\n\n## Pending user asks\nNone.\n\n## Exact identifiers\nNone captured.","tokensBefore":38085,"fromHook":true}

User-visible impact:

  • Each compaction-loop turn adds ~60s of dead time
  • API token usage is wasted on the aborted summarization calls
  • Session history is not actually compressed — pre-compaction snapshot is restored, so the next turn faces the same pressure
  • Cron pipelines that should finish in 5–10 min stretch to 15+ min

OpenClaw version

2026.5.3-1 (commit 2eae30e)

Operating system

Ubuntu 20.04 LTS, kernel 5.10.25, x64

Install method

npm install -g openclaw via tarball (proxy-bound install)

Model

claude-opus-4-7

Provider / routing chain

feishu (DM / cron) → openclaw gateway → model-provider(enterprize) → anthropic-messages

Additional provider/model setup details

  • Compaction model NOT explicitly overridden — uses session default
  • agents.defaults.compaction.mode: "safeguard" (was the trigger; switching to "default" resolved symptoms)
  • agents.defaults.compaction.timeoutSeconds: not set, so 900s default
  • agents.defaults.compaction.truncateAfterCompaction: not set (so byte-trigger is disabled, only token threshold is active)

Logs, screenshots, and evidence

Repro count over ~12 hours on 2026-05-06

Trigger time (HKT) Session tokensBefore Outcome
08:42 abf1edff (caixuedan-daily-report cron) 38085 Empty fallback + 60s timeout
11:02 75e6e379 (/new interactive) 76486 Empty fallback + 60s timeout
11:31 75e6e379 same Empty fallback (re-loop)
11:51 75e6e379 same 60s timeout
12:11 001e8097 (DM) 69035 Empty fallback + 60s timeout
12:14, 12:19 75e6e379 again More re-loops
12:20 75e6e379 again 60s timeout

For comparison, on 2026-05-05 (same install, same config) one compaction at 178K tokens did produce a real ~5KB summary on session 963e608a. So the safeguard path is not 100% broken — it's an as-yet-uncharacterized condition that some sessions hit and others don't.

Sample log excerpt

May 06 08:42:16 ... [compaction-safeguard] Compaction safeguard: no real conversation messages to summarize; writing compaction boundary to suppress re-trigger loop.
May 06 08:43:16 ... [agent/embedded] compaction retry aggregate timeout (60000ms): proceeding with pre-compaction state runId=abf1edff-9629-4b4b-9168-fe4ec4d1c41a sessionId=abf1edff-9629-4b4b-9168-fe4ec4d1c41a
May 06 08:43:16 ... [agent/embedded] using pre-compaction snapshot: timed out during compaction runId=abf1edff-9629-4b4b-9168-fe4ec4d1c41a sessionId=abf1edff-9629-4b4b-9168-fe4ec4d1c41a

Sample empty fallback summary entry from transcript

{
  "type": "compaction",
  "id": "17c34363",
  "parentId": "3aa68b51",
  "timestamp": "2026-05-06T00:42:16.544Z",
  "summary": "## Decisions\nNo prior history.\n\n## Open TODOs\nNone.\n\n## Constraints/Rules\nNone.\n\n## Pending user asks\nNone.\n\n## Exact identifiers\nNone captured.",
  "firstKeptEntryId": "1e91a7ec",
  "tokensBefore": 38085,
  "fromHook": true
}

Workaround that resolved my issue

 "compaction": {
-  "mode": "safeguard"
+  "mode": "default"
 }

After systemctl --user restart openclaw-gateway no further [compaction-safeguard] log lines appear.

Hypothesis (for maintainer triage)

compaction-successor-transcript-D4FqvNh7.js line 3585 (in 2026.5.3-1 dist):

if (!hasRealSummarizable && !hasRealTurnPrefix) {
    log.info("Compaction safeguard: no real conversation messages to summarize; writing compaction boundary to suppress re-trigger loop.");
    return { compaction: {
        summary: buildStructuredFallbackSummary(preparation.previousSummary),
        firstKeptEntryId: preparation.firstKeptEntryId,
        tokensBefore: preparation.tokensBefore
    } };
}

hasRealSummarizable is computed from stripRuntimeContextCustomMessages(preparation.messagesToSummarize). My guess: preparation.messagesToSummarize in some code paths contains only the slice of messages already split off as "to-summarize" while real conversation messages are in preparation.turnPrefixMessages or a different field, so the OR-check (!hasRealSummarizable && !hasRealTurnPrefix) should be evaluating both, but one or both views may be wrong post-strip.

A repro with [compaction-diag] debug logging enabled would pinpoint which of messagesToSummarize / turnPrefixMessages is empty when the safeguard fires.

Happy to provide the affected *.jsonl transcripts (after redaction) and a longer log window if useful.

Impact and severity

No response

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingregressionBehavior that previously worked and now fails

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions