Skip to content

fix(compaction): break safeguard cancel loop for sessions with no summarizable messages (#41981)#42215

Merged
jalehman merged 7 commits intoopenclaw:mainfrom
lml2468:fix/issue-41981-compaction-safeguard-loop
Mar 17, 2026
Merged

fix(compaction): break safeguard cancel loop for sessions with no summarizable messages (#41981)#42215
jalehman merged 7 commits intoopenclaw:mainfrom
lml2468:fix/issue-41981-compaction-safeguard-loop

Conversation

@lml2468
Copy link
Copy Markdown
Contributor

@lml2468 lml2468 commented Mar 10, 2026

What

Break the compaction safeguard cancel loop that blocks cron lanes when isolated sessions have no summarizable history.

Why

Fixes #41981 — Isolated cron sessions with sessionTarget: "isolated" get stuck in a compaction cancel loop:

  1. SDK auto-compaction triggers after each assistant response when shouldCompact() returns true
  2. prepareCompaction() marks all messages as "recent" (within keepRecentTokens), producing empty messagesToSummarize
  3. Safeguard detects no real conversation messages → cancels compaction
  4. SDK accepts cancellation but re-triggers on next assistant response → infinite loop
  5. Each loop iteration wastes time and floods logs, causing cron job timeout

How

Instead of returning { cancel: true } when messagesToSummarize is empty, return a minimal compaction result with a structured fallback summary. This writes a compaction boundary entry via sessionManager.appendCompaction(), which:

  • Marks the session as "recently compacted"
  • Causes _checkAutoCompaction to skip (assistant timestamps are before the compaction boundary)
  • Breaks the cancel loop

Also added per-session tracking (WeakSet) to downgrade repeated log messages from info to debug, reducing log noise for sessions that hit this path multiple times.

Testing

  • pnpm build && pnpm check — pending CI
  • npx vitest run src/agents/pi-extensions/compaction-safeguard.test.ts — 68/68 tests pass
  • Updated existing test: "cancels compaction when no real messages" now expects compaction result instead of cancel
  • New test: verifies fallback summary preserves previous summary structure
  • New test: verifies log downgrade on repeated attempts for same session
  • AI-assisted (OpenClaw agent, fully tested)

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Mar 10, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 10, 2026

Greptile Summary

This PR fixes a compaction cancel loop in isolated cron sessions (#41981) by replacing { cancel: true } returns (when messagesToSummarize is empty) with a minimal compaction boundary result that causes the SDK to mark the session as recently compacted, preventing immediate re-triggering.

  • Fix is mechanically correct — writing a compaction boundary instead of cancelling cleanly breaks the re-trigger loop without touching session history
  • buildStructuredFallbackSummary can produce duplicate section headings — when previousSummary is present but only partially structured (e.g. "## Decisions\nDid X." without the other four required headings), the entire string is embedded verbatim as the body of the template's ## Decisions block, duplicating any section markers that appear in it. The test at line 1537 exercises this exact case but only uses toContain assertions that mask the duplication
  • Stale namingnoRealMessagesCancelledSessions and its comment still say "cancelled" even though the behaviour is now "write empty boundary"; the local alias alreadyCancelled carries the same confusion
  • Log-level downgrade test has no log assertions — the test named "downgrades log level on repeated no-real-messages compaction for same session" only checks result.compaction is defined, never asserting which log method was invoked
  • The _summarizationInstructions parameter added to buildStructuredFallbackSummary is intentionally unused (prefixed with _); it appears reserved for future use

Confidence Score: 3/5

  • Safe to merge with minor caveats — the core loop-break fix is sound, but a logic edge case in fallback summary building can produce malformed output for partially-structured prior summaries.
  • The primary fix is well-reasoned and correctly addresses the infinite cancel loop. The main concern is that buildStructuredFallbackSummary silently produces duplicate section headings when previousSummary has some but not all required sections, and the corresponding test's misleading comment and weak assertions mean this edge case went unnoticed. In production this path is only hit when there is no real conversation content to compact, so the malformed output is low-impact but could accumulate confusion in session history over time. Variable naming and test coverage gaps reduce confidence slightly.
  • src/agents/pi-extensions/compaction-safeguard.ts lines 484-508 (buildStructuredFallbackSummary) and the corresponding test case at line 1537

Comments Outside Diff (1)

  1. src/agents/pi-extensions/compaction-safeguard.ts, line 488-508 (link)

    Duplicated headings when previousSummary contains partial sections

    When previousSummary is present but doesn't pass hasRequiredSummarySections (e.g. "## Decisions\nDid X."), the function falls into the else branch and embeds the entire previousSummary verbatim as the body of the ## Decisions block. If previousSummary itself starts with a section heading, the output will contain that heading twice:

    ## Decisions       ← injected by the template
    ## Decisions       ← from previousSummary
    Did X.
    
    ## Open TODOs
    None.
    ...
    

    The test at line 1537 demonstrates this exact case (previousSummary: "## Decisions\nUsed approach A."), but the assertions only call toContain("## Decisions") so the duplication isn't caught. The test comment also says "Fallback preserves previous summary when it has required sections", but the test data only contains one of the five required sections — so it actually exercises the template path, not the preserve path.

    Consider stripping leading section headings from previousSummary before embedding it, or placing it under a dedicated prose key ("Prior context") instead of reusing ## Decisions:

    const priorContext = trimmedPreviousSummary
      ? trimmedPreviousSummary.replace(/^##[^\n]*\n?/, "").trim() || "No prior history."
      : "No prior history.";
    return [
      "## Decisions",
      priorContext,
      ...
    ].join("\n");
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agents/pi-extensions/compaction-safeguard.ts
    Line: 488-508
    
    Comment:
    **Duplicated headings when `previousSummary` contains partial sections**
    
    When `previousSummary` is present but doesn't pass `hasRequiredSummarySections` (e.g. `"## Decisions\nDid X."`), the function falls into the else branch and embeds the entire `previousSummary` verbatim as the body of the `## Decisions` block. If `previousSummary` itself starts with a section heading, the output will contain that heading twice:
    
    ```
    ## Decisions       ← injected by the template
    ## Decisions       ← from previousSummary
    Did X.
    
    ## Open TODOs
    None.
    ...
    ```
    
    The test at line 1537 demonstrates this exact case (`previousSummary: "## Decisions\nUsed approach A."`), but the assertions only call `toContain("## Decisions")` so the duplication isn't caught. The test comment also says "Fallback preserves previous summary **when it has required sections**", but the test data only contains one of the five required sections — so it actually exercises the *template* path, not the preserve path.
    
    Consider stripping leading section headings from `previousSummary` before embedding it, or placing it under a dedicated prose key (`"Prior context"`) instead of reusing `## Decisions`:
    
    ```typescript
    const priorContext = trimmedPreviousSummary
      ? trimmedPreviousSummary.replace(/^##[^\n]*\n?/, "").trim() || "No prior history."
      : "No prior history.";
    return [
      "## Decisions",
      priorContext,
      ...
    ].join("\n");
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 0c6a371

Comment on lines +32 to +34
// Track session managers where compaction was already cancelled due to no real messages,
// so repeated attempts within the same session run only log at debug level.
const noRealMessagesCancelledSessions = new WeakSet<object>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale variable name and comment after behaviour change

The variable is still called noRealMessagesCancelledSessions and the comment above it still says "compaction was already cancelled", but the behaviour was changed by this very PR — we now write a compaction boundary instead of cancelling. The misleading name also bleeds into the local alreadyCancelled variable at line 711, which is equally confusing.

Suggested change
// Track session managers where compaction was already cancelled due to no real messages,
// so repeated attempts within the same session run only log at debug level.
const noRealMessagesCancelledSessions = new WeakSet<object>();
// Track session managers that have already hit the no-real-messages fast-path so
// repeated attempts within the same session lifetime only log at debug level.
const noRealMessagesBoundarySessions = new WeakSet<object>();

And the corresponding change to the local variable name on line 711:

const alreadyBoundaried = noRealMessagesBoundarySessions.has(ctx.sessionManager);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-extensions/compaction-safeguard.ts
Line: 32-34

Comment:
**Stale variable name and comment after behaviour change**

The variable is still called `noRealMessagesCancelledSessions` and the comment above it still says *"compaction was already cancelled"*, but the behaviour was changed by this very PR — we now write a compaction boundary instead of cancelling. The misleading name also bleeds into the local `alreadyCancelled` variable at line 711, which is equally confusing.

```suggestion
// Track session managers that have already hit the no-real-messages fast-path so
// repeated attempts within the same session lifetime only log at debug level.
const noRealMessagesBoundarySessions = new WeakSet<object>();
```

And the corresponding change to the local variable name on line 711:
```typescript
const alreadyBoundaried = noRealMessagesBoundarySessions.has(ctx.sessionManager);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1568 to +1599
it("downgrades log level on repeated no-real-messages compaction for same session", async () => {
const sessionManager = stubSessionManager();
const model = createAnthropicModelFixture();
setCompactionSafeguardRuntime(sessionManager, { model });

const mockEvent = {
preparation: {
messagesToSummarize: [] as AgentMessage[],
turnPrefixMessages: [] as AgentMessage[],
firstKeptEntryId: "entry-3",
tokensBefore: 1000,
fileOps: { read: [], edited: [], written: [] },
},
customInstructions: "",
signal: new AbortController().signal,
};

// First call
const { result: result1 } = await runCompactionScenario({
sessionManager,
event: mockEvent,
apiKey: "sk-test", // pragma: allowlist secret
});
expect(result1.compaction).toBeDefined();

// Second call with same sessionManager — should still work but log at debug
const { result: result2 } = await runCompactionScenario({
sessionManager,
event: mockEvent,
apiKey: "sk-test", // pragma: allowlist secret
});
expect(result2.compaction).toBeDefined();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test doesn't verify the log-level downgrade it claims to cover

The test is named "downgrades log level on repeated no-real-messages compaction for same session", but the assertions only check that result.compaction is defined — they say nothing about which log function was actually called. The claimed behaviour (switching from log.info to log.debug on the second invocation) is never asserted.

To make the test meaningful, consider spying on log.info and log.debug and asserting that:

  • The first call fires log.info exactly once
  • The second call fires log.debug exactly once (and log.info zero additional times)

Without this, the test is an integration smoke-test but not a unit test for the log-downgrade feature, and a future refactor could silently break that feature without the test catching it.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-extensions/compaction-safeguard.test.ts
Line: 1568-1599

Comment:
**Test doesn't verify the log-level downgrade it claims to cover**

The test is named "downgrades log level on repeated no-real-messages compaction for same session", but the assertions only check that `result.compaction` is defined — they say nothing about which log function was actually called. The claimed behaviour (switching from `log.info` to `log.debug` on the second invocation) is never asserted.

To make the test meaningful, consider spying on `log.info` and `log.debug` and asserting that:
- The first call fires `log.info` exactly once
- The second call fires `log.debug` exactly once (and `log.info` zero additional times)

Without this, the test is an integration smoke-test but not a unit test for the log-downgrade feature, and a future refactor could silently break that feature without the test catching it.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c6a371e98

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +719 to +723
const fallbackSummary = buildStructuredFallbackSummary(preparation.previousSummary);
return {
compaction: {
summary: fallbackSummary,
firstKeptEntryId: preparation.firstKeptEntryId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include split-turn prefixes before emitting empty compaction

This early return now writes a compaction result using only previousSummary, but it can trigger even when turnPrefixMessages still contains real conversation content (for split-turn preparations where messagesToSummarize is empty). In that case, the later split-turn summarization path (isSplitTurn + turnPrefixMessages) is skipped and the compaction boundary is still applied, so prefix context can be discarded instead of summarized; previously this path canceled compaction and preserved history. Please guard this branch with turnPrefixMessages as well (or summarize them before returning).

Useful? React with 👍 / 👎.

@byungsker

This comment was marked as spam.

@lml2468
Copy link
Copy Markdown
Contributor Author

lml2468 commented Mar 10, 2026

Thanks @byungsker! Both points addressed:

  1. Variable naming — already renamed to noRealMessagesBoundarySessions / alreadyBoundaried in commit 54c1462 (before your review landed).

  2. buildStructuredFallbackSummary(undefined) test — added explicit assertions verifying the minimal structured summary content (## DecisionsNo prior history., ## Open TODOsNone., etc.) in commit be20909.

69/69 tests pass.

@jalehman jalehman self-assigned this Mar 12, 2026
Copy link
Copy Markdown
Contributor

@jalehman jalehman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes Requested

Thank you for this contribution! We've reviewed this PR and have some feedback before it can move forward.

Repeat-attempt fast path can recreate the original cancel loop

The new session-lifetime WeakSet only guarantees that we write a fallback boundary the first time an empty preparation occurs. After a later assistant message, prepareCompaction() can produce another empty preparation in the same session, but at that point this path now returns { cancel: true } instead of writing a fresh boundary. That puts us back in the same SDK re-trigger loop this PR is trying to break.

Please change this so the decision to return a boundary vs. cancel is based on whether the current transcript state is still protected by a boundary, rather than only on whether the session has ever hit this path.

Add a regression test for boundary -> assistant message -> empty preparation

The repeated-attempt test currently calls the safeguard twice against the same empty preparation, which does not model the failing progression above. Please add coverage for the real sequence: empty preparation writes a boundary, a later assistant turn makes compaction eligible again, and a subsequent empty preparation still avoids the loop. That will make the intended guarantee explicit and keep this from regressing.


Once these are addressed, we'll re-review. Feel free to ask questions if anything is unclear.

@lml2468 lml2468 force-pushed the fix/issue-41981-compaction-safeguard-loop branch from be20909 to c7d17fc Compare March 13, 2026 04:45
@lml2468
Copy link
Copy Markdown
Contributor Author

lml2468 commented Mar 13, 2026

@jalehman Thank you for the thorough review! Both issues addressed in commit c7d17fcde:

1. Removed WeakSet cancel path — always write boundary on empty preparation

You were right that the WeakSet approach recreated the cancel loop after a new assistant message arrived. The fix removes noRealMessagesBoundarySessions entirely. Now every empty preparation writes a boundary entry. This is safe because the SDK's prepareCompaction() returns undefined when the last entry is a compaction boundary, blocking immediate re-triggering. The frequency is bounded to at most one boundary per LLM round-trip.

2. Updated regression test for boundary → assistant message → empty preparation

Replaced the "cancels on repeated" test with a new test that verifies two consecutive empty preparations both write boundaries (not cancel on the second call), modeling the scenario where a new assistant message makes compaction eligible again.

69/69 tests pass, oxlint clean.

@jalehman jalehman force-pushed the fix/issue-41981-compaction-safeguard-loop branch 3 times, most recently from 876b652 to d606314 Compare March 17, 2026 16:31
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d60631489f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

log.warn(
"Compaction safeguard: cancelling compaction with no real conversation messages to summarize.",
const hasRealSummarizable = preparation.messagesToSummarize.some(isRealConversationMessage);
const hasRealTurnPrefix = preparation.turnPrefixMessages.some(isRealConversationMessage);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle absent turnPrefixMessages in empty-prep fast path

The new guard calls preparation.turnPrefixMessages.some(...) unconditionally, but this hook already treats turnPrefixMessages as optional later (?? []), so an event payload that omits this field will now throw before the safeguard can return a compaction boundary. In that case the extension fails exactly in the no-summarizable-messages path this commit is trying to stabilize, so auto-compaction can keep re-attempting instead of being suppressed.

Useful? React with 👍 / 👎.

@jalehman jalehman force-pushed the fix/issue-41981-compaction-safeguard-loop branch from d606314 to 0b8cf5c Compare March 17, 2026 16:41
@openclaw-barnacle openclaw-barnacle Bot added the channel: discord Channel integration: discord label Mar 17, 2026
lml2468 and others added 7 commits March 17, 2026 09:42
…marizable messages (openclaw#41981)

When all messages are recent (within keepRecentTokens), prepareCompaction
produces an empty messagesToSummarize set. Previously the safeguard cancelled
compaction in this case, but the SDK re-triggers _checkAutoCompaction after
every assistant response — creating a cancel loop that blocks cron lanes.

Fix: return a minimal compaction result (structured fallback summary) instead
of cancelling. This writes a compaction boundary entry that marks the session
as recently compacted, preventing immediate re-triggering.

Also tracks per-session cancellation state to downgrade log level from info
to debug on repeated attempts, reducing log noise.

AI-assisted (OpenClaw agent, fully tested)
On the first hit, write a boundary entry to break the immediate loop
(SDK's prepareCompaction returns undefined when last entry is compaction).
On subsequent hits for the same session, cancel directly instead of
writing additional boundary entries — the boundary already exists and
cancelling is cheaper. This prevents boundary entry accumulation during
multi-tool-call cron runs while still breaking the initial cancel loop.

Addresses review feedback from cross-review.
- Rename noRealMessagesCancelledSessions → noRealMessagesBoundarySessions
  and alreadyCancelled → alreadyBoundaried (Greptile: stale variable names)
- Guard boundary fast-path with turnPrefixMessages check: skip fast-path
  when split-turn prefix has real content, so prefix context is properly
  summarized instead of discarded (Codex P1)
- Add test for split-turn turnPrefixMessages with real content
…al structured summary

Address byungsker review: explicitly assert fallback summary content
when previousSummary is undefined (no prior compaction history).
…l path

Remove the noRealMessagesBoundarySessions WeakSet that tracked whether a
session had ever written a boundary. After a new assistant message, the
SDK can trigger compaction again with an empty preparation — the WeakSet
would return cancel, recreating the original cancel loop (openclaw#41981).

Now every empty preparation writes a boundary. This is safe because the
SDK's prepareCompaction() returns undefined when the last entry is a
compaction boundary, blocking immediate re-triggering. The frequency is
bounded to at most one boundary per LLM round-trip.

Update test to verify repeated empty preparations both write boundaries
(not cancel on the second call).

Addresses maintainer review feedback.
@jalehman jalehman force-pushed the fix/issue-41981-compaction-safeguard-loop branch from 0b8cf5c to 7ce6bd8 Compare March 17, 2026 16:42
@jalehman jalehman merged commit 7b61b02 into openclaw:main Mar 17, 2026
8 checks passed
@jalehman
Copy link
Copy Markdown
Contributor

Merged via squash.

Thanks @lml2468!

nikolaisid pushed a commit to nikolaisid/openclaw that referenced this pull request Mar 18, 2026
…marizable messages (openclaw#41981) (openclaw#42215)

Merged via squash.

Prepared head SHA: 7ce6bd8
Co-authored-by: lml2468 <39320777+lml2468@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
analysoor-assistant pushed a commit to analysoor-assistant/openclaw that referenced this pull request Mar 18, 2026
…marizable messages (openclaw#41981) (openclaw#42215)

Merged via squash.

Prepared head SHA: 7ce6bd8
Co-authored-by: lml2468 <39320777+lml2468@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

(cherry picked from commit 7b61b02)
analysoor-assistant pushed a commit to analysoor-assistant/openclaw that referenced this pull request Mar 18, 2026
…marizable messages (openclaw#41981) (openclaw#42215)

Merged via squash.

Prepared head SHA: 7ce6bd8
Co-authored-by: lml2468 <39320777+lml2468@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

(cherry picked from commit 7b61b02)
ralyodio pushed a commit to ralyodio/openclaw that referenced this pull request Apr 3, 2026
…marizable messages (openclaw#41981) (openclaw#42215)

Merged via squash.

Prepared head SHA: 7ce6bd8
Co-authored-by: lml2468 <39320777+lml2468@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
juancatorr added a commit to juancatorr/openclaw that referenced this pull request Apr 8, 2026
* refactor(slack): share setup wizard base

* refactor(discord): share setup wizard base

* refactor(signal): reuse shared setup security

* refactor(imessage): reuse shared setup security

* refactor(setup): reuse patched adapters across channels

* refactor(imessage): share setup status base

* refactor(slack): share token credential setup

* refactor(setup): share env-aware patched adapters

* refactor(discord): use shared plugin base

* refactor(outbound): share base session helpers

* refactor(whatsapp): reuse login tool implementation

* refactor(providers): reuse simple api-key catalog helper

* refactor(plugins): share bundle path list helpers

* refactor(slack): reuse shared action adapter

* refactor(tts): share provider readiness checks

* refactor(plugins): share claiming hook loop

* refactor(plugins): share install target flow

* refactor(status): share scan helper state

* refactor(usage): share legacy pi auth token lookup

* refactor(device): share missing-scope helper

* refactor(config): share schema lookup helpers

* fix(plugin-sdk): restore core export boundary

* build: tighten lazy runtime boundaries

* fix(gateway): surface env override keys in exec approvals

* Agents: move bootstrap warnings out of system prompt (openclaw#48753)

Merged via squash.

Prepared head SHA: dc1d4d0
Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com>
Reviewed-by: @scoootscooob

* refactor: dedupe channel entrypoints and test bridges

* style: fix rebase formatting drift

* fix: resolve rebase type fallout in channel setup seams

* fix(macos): block canvas symlink escapes

* refactor: bundle lazy runtime surfaces

* fix: remove discord setup rebase marker

* docs(gateway): clarify URL allowlist semantics

* docs(changelog): restore 2026.2.27 heading

* fix: unblock full gate

* fix: stabilize full gate

* test: cover invalid main job store load

* build(test): ignore vitest scratch root

* refactor: dedupe bundled plugin entrypoints

* docs: add context engine documentation

Add dedicated docs page for the pluggable context engine system:
- Full lifecycle explanation (ingest, assemble, compact, afterTurn)
- Legacy engine behavior documentation
- Plugin engine authoring guide with code examples
- ContextEngine interface reference table
- ownsCompaction semantics
- Subagent lifecycle hooks (prepareSubagentSpawn, onSubagentEnded)
- systemPromptAddition mechanism
- Relationship to compaction, memory plugins, and session pruning
- Configuration reference and tips

Also:
- Add context-engine to docs nav (Agents > Fundamentals, after Context)
- Add /context-engine redirect
- Cross-link from context.md and compaction.md

* docs: add plugin installation steps to context engine page

Show the full workflow: install via openclaw plugins install,
enable in plugins.entries, then select in plugins.slots.contextEngine.
Uses lossless-claw as the concrete example.

* docs: address review feedback on context-engine page

- Rename 'Method' column to 'Member' with explicit Kind column since
  info is a property, not a callable method
- Document AssembleResult fields (estimatedTokens, systemPromptAddition)
  with types and optionality
- Add lifecycle timing notes for bootstrap, ingestBatch, and dispose
  so plugin authors know when each is invoked

* docs: fix context engine review notes

* fix: harden telegram and loader contracts

* refactor(tests): share setup wizard prompter

* refactor(telegram-tests): share native command helpers

* fix(telegram-tests): load plugin mocks before commands

* refactor(telegram-tests): share webhook settlement helper

* refactor(nextcloud-tests): share inbound authz setup

* refactor(feishu-tests): share card action event builders

* refactor(runtime-tests): share typing lease assertions

* refactor(hook-tests): share subagent hook helpers

* refactor(provider-tests): share discovery catalog helpers

* refactor(command-tests): share workspace harness

* refactor(contracts): share session binding assertions

* refactor(plugin-tests): share interactive dispatch assertions

* refactor(plugin-tests): share binding approval resolution

* refactor(usage-tests): share provider usage loader harness

* refactor(bundle-tests): share bundle mcp fixtures

* refactor(provider-tests): share codex catalog assertions

* refactor(apns-tests): share relay push params

* refactor(media-tests): share telegram redaction assertion

* refactor(heartbeat-tests): share seeded heartbeat run

* refactor(kilocode-tests): share reasoning payload capture

* refactor(kilocode-tests): share extra-params harness

* refactor(kilocode-tests): share cache retention wrapper

* refactor(attempt-tests): share wrapped stream helper

* refactor(payload-tests): share empty payload assertion

* Telegram: fix named-account DM topic session keys (openclaw#48773)

* refactor(compaction-tests): share aggregate timeout params

* refactor(compaction-tests): share snapshot assertions

* refactor(truncation-tests): share first tool result text helper

* refactor(system-prompt-tests): share session setup helper

* refactor(lanes-tests): share table-driven assertions

* refactor(google-tests): share schema tool fixture

* refactor(extension-tests): share safeguard factory setup

* refactor(openrouter-tests): share state dir helper

* refactor(thinking-tests): share assistant drop helper

* refactor(kilocode-tests): share eligibility assertions

* refactor(payload-tests): share empty payload helper

* refactor(model-tests): share template model mock helper

* refactor(image-tests): share empty prompt image assertions

* fix: restore full gate

* refactor: consolidate lazy runtime surfaces

* refactor: remove remaining extension core imports

* refactor(payload-tests): reuse empty payload helper

* refactor(image-tests): share empty ref assertions

* refactor(image-tests): share single-ref detection helper

* refactor(image-tests): share ref count assertions

* refactor(history-tests): share array content assertion

* refactor(runs-tests): share run handle factory

* refactor(skills-tests): share bundled diffs setup

* refactor(payload-tests): share single payload summary assertion

* refactor(payload-tests): table-drive recoverable tool suppressions

* refactor(payload-tests): table-drive sessions send suppressions

* refactor(history-tests): share pruned image assertions

* refactor(failover-tests): share observation base

* refactor(extension-tests): share safeguard runtime assertions

* fix(ci): quote changed extension matrix input

* refactor: split plugin testing seam from bundled extension helpers

* test: fix discord provider helper import

* Changelog: add Telegram DM topic session-key fix

* fix(ci): harden zizmor workflow diffing

* feat(image-generation): add image_generate tool

* test(image-generation): add live variant coverage

* docs(image-generation): remove nano banana stock docs

* fix(ci): restore local check suite

* chore: sync pnpm lockfile importers

* fix(ui): restore control-ui query token compatibility (openclaw#43979)

* fix(ui): restore control-ui query token imports

* chore(changelog): add entry for openclaw#43979 thanks @stim64045-spec

---------

Co-authored-by: 大禹 <dayu@dayudeMac-mini.local>
Co-authored-by: Val Alexander <bunsthedev@gmail.com>
Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>

* fix: update macOS node service to use current CLI command shape (closes openclaw#43171) (openclaw#46843)

Merged via squash.

Prepared head SHA: dbf2edd
Co-authored-by: Br1an67 <29810238+Br1an67@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
Reviewed-by: @ImLukeF

* fix(macos): stop relaunching the app after quit when launch-at-login is enabled (openclaw#40213)

Merged via squash.

Prepared head SHA: c702d98
Co-authored-by: stablegenius49 <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
Reviewed-by: @ImLukeF

* tests: add missing useNoBundledPlugins() to bundle MCP loader test

The "treats bundle MCP as a supported bundle surface" test was missing
the useNoBundledPlugins() call present in all surrounding bundle plugin
tests. Without it, loadOpenClawPlugins() scanned and loaded the full
real bundled plugins directory on every call (with cache:false), causing
excessive memory pressure and an OOM crash on Linux CI, which manifested
as the test timing out at 120s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix ssh sandbox key cp (openclaw#48924)

Signed-off-by: sallyom <somalley@redhat.com>

* tests: fix googlechat outbound partial mock

* tests(google): inject oauth credential fs stubs

* tests(feishu): mock conversation runtime seam

* tests(feishu): inject client runtime seam

* tests(contracts): fix provider catalog runtime wiring (openclaw#49040)

* fix(plugins): forward plugin subagent overrides (openclaw#48277)

Merged via squash.

Prepared head SHA: ffa4589
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* fix(security): block JVM, Python, and .NET env injection vectors in host exec sandbox (openclaw#49025)

Add JAVA_TOOL_OPTIONS, _JAVA_OPTIONS, JDK_JAVA_OPTIONS, PYTHONBREAKPOINT, and
DOTNET_STARTUP_HOOKS to blockedKeys in the host exec security policy.

Closes openclaw#22681

* CI: rename startup memory smoke (openclaw#49041)

* CI: guard gateway watch against duplicate runtime regressions (openclaw#49048)

* fix(hooks): pass sessionFile and sessionKey in after_compaction hook (openclaw#40781)

Merged via squash.

Prepared head SHA: 11e85f8
Co-authored-by: jarimustonen <1272053+jarimustonen@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* refactor: expose lazy runtime helper to plugins

* refactor: align telegram test support with plugin runtime seam

* fix(tlon): defer DM cite expansion until after auth

* fix(context-engine): preserve legacy plugin sessionKey interop (openclaw#44779)

Merged via squash.

Prepared head SHA: e04c6fb
Co-authored-by: hhhhao28 <112874572+hhhhao28@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* test: harden CI-sensitive test suites

* test: remove repeated update module imports

* test: inline bluebubbles action mocks

* test: reuse subagent orphan recovery imports

* test: flatten twitch send mocks

* test: stabilize pdf tool runtime mocks

* test: reuse run-node module imports

* test: reuse git commit module exports

* test: trim redundant context engine assertions

* test: merge duplicate update cli scenarios

* test: preload plugin sdk subpath imports

* test: cache provider discovery fixtures

* test: merge context lookup warmup cases

* test: trim lightweight status and capability suites

* test: merge telegram action matrix cases

* test: merge embeddings provider selection cases

* test: preload inbound contract fixtures

* test: merge message action media sandbox cases

* test: merge pid alive linux stat cases

* test: merge tts config gating cases

* test: trim signal and slack action cases

* test: harden commands test module seams

* test: merge bundle loader fixture cases

* test: merge loader cache partition cases

* test: merge loader setup entry matrix

* test: merge discord audit allowlist cases

* test: merge zalouser audit group cases

* test: merge audit auth precedence cases

* test: merge channel command audit cases

* test: merge browser control audit cases

* test: merge control ui audit cases

* test: merge feishu audit doc cases

* test: merge hooks audit risk cases

* test: merge gateway http audit cases

* test: share audit exposure severity helper

* test: merge loader provenance path cases

* test: merge loader escape path cases

* test: merge loader alias resolution cases

* test: merge install metadata audit cases

* test: merge loader duplicate registration cases

* test: merge loader http route cases

* test: merge audit extension and workspace cases

* test: merge loader single-plugin registration cases

* test: merge loader precedence cases

* test: merge audit exposure heuristic cases

* test: merge loader workspace warning cases

* test: merge audit hooks ingress cases

* test: merge audit extension allowlist severity cases

* test: merge loader provenance warning cases

* test: merge loader bundled telegram cases

* test: merge loader memory slot cases

* test: merge loader scoped load cases

* test: merge audit gateway auth presence cases

* test: merge audit browser container cases

* test: merge audit windows acl cases

* test: merge audit deny command cases

* test: merge audit code safety failure cases

* test: merge loader cache miss cases

* test: merge update cli service refresh cases

* test: merge slack action mapping cases

* test: merge command owner gating cases

* test: merge update status output cases

* test: merge command gateway config permission cases

* test: merge command approval scope cases

* test: merge command hook cases

* test: merge command config write denial cases

* test: merge command allowlist add cases

* test: merge update cli service refresh behavior

* test: merge command owner show gating cases

* test: merge discord action listing cases

* test: merge signal reaction mapping cases

* test: merge discord reaction id resolution cases

* test: merge telegram reaction id cases

* test: merge audit resolved inspection cases

* test: merge audit gateway auth guardrail cases

* test: merge audit sandbox docker danger cases

* test: merge audit discord allowlist cases

* test: merge audit sandbox docker config cases

* test: merge audit browser sandbox cases

* test: merge audit allowCommands cases

* test: merge audit install metadata cases

* test: merge audit code safety cases

* test: merge audit dangerous flag cases

* test: merge audit trust exposure cases

* test: merge audit channel command hygiene cases

* test: merge slack validation cases

* test: merge update cli dry run cases

* test: merge update cli outcome cases

* test: merge update cli validation cases

* test: merge action media root cases

* test: merge update cli restart behavior cases

* test: merge update cli channel cases

* test: stabilize full gate

* feat(agents): infer image generation defaults

* docs(image-generation): document implicit tool enablement

* refactor: dedupe plugin lazy runtime helpers

* fix(telegram): persist sticky IPv4 fallback across polling restarts (fixes openclaw#48177) (openclaw#48282)

* fix(telegram): persist sticky IPv4 fallback across polling restarts (fixes openclaw#48177)

Hoist resolveTelegramTransport() out of createTelegramBot() so the
transport (and its sticky IPv4 fallback state) persists across polling
restarts. Previously, each polling restart created a new transport with
stickyIpv4FallbackEnabled=false, causing repeated IPv6 timeouts on
hosts with unstable IPv6 connectivity.

Changes:
- bot.ts: accept optional telegramTransport in TelegramBotOptions
- monitor.ts: resolve transport once before polling loop
- polling-session.ts: pass transport through to bot creation

AI-assisted (Claude Sonnet 4). Tested: tsc --noEmit clean.

* Update extensions/telegram/src/polling-session.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* style: fix oxfmt formatting in bot.ts

* test: cover telegram transport reuse across restarts

* fix: preserve telegram sticky IPv4 fallback across polling restarts (openclaw#48282) (thanks @yassinebkr)

---------

Co-authored-by: Yassine <yassinebkr@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>

* ACP: harden startup and move configured routing behind plugin seams (openclaw#48197)

* ACPX: keep plugin-local runtime installs out of dist

* Gateway: harden ACP startup and service PATH

* ACP: reinitialize error-state configured bindings

* ACP: classify pre-turn runtime failures as session init failures

* Plugins: move configured ACP routing behind channel seams

* Telegram tests: align startup probe assertions after rebase

* Discord: harden ACP configured binding recovery

* ACP: recover Discord bindings after stale runtime exits

* ACPX: replace dead sessions during ensure

* Discord: harden ACP binding recovery

* Discord: fix review follow-ups

* ACP bindings: load channel snapshots across workspaces

* ACP bindings: cache snapshot channel plugin resolution

* Experiments: add ACP pluginification holy grail plan

* Experiments: rename ACP pluginification plan doc

* Experiments: drop old ACP pluginification doc path

* ACP: move configured bindings behind plugin services

* Experiments: update bindings capability architecture plan

* Bindings: isolate configured binding routing and targets

* Discord tests: fix runtime env helper path

* Tests: fix channel binding CI regressions

* Tests: normalize ACP workspace assertion on Windows

* Bindings: isolate configured binding registry

* Bindings: finish configured binding cleanup

* Bindings: finish generic cleanup

* Bindings: align runtime approval callbacks

* ACP: delete residual bindings barrel

* Bindings: restore legacy compatibility

* Revert "Bindings: restore legacy compatibility"

This reverts commit ac2ed68fa2426ecc874d68278c71c71ad363fcfe.

* Tests: drop ACP route legacy helper names

* Discord/ACP: fix binding regressions

---------

Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>

* feat: add bundled Chutes extension (openclaw#49136)

* refactor: generalize bundled provider discovery seams

* feat: land chutes extension via plugin-owned auth (openclaw#41416) (thanks @Veightor)

* docs(security): clarify wildcard Control UI origins

* refactor: clean extension api boundaries

* fix: remove duplicate setup helper imports

* fix(compaction): break safeguard cancel loop for sessions with no summarizable messages (openclaw#41981) (openclaw#42215)

Merged via squash.

Prepared head SHA: 7ce6bd8
Co-authored-by: lml2468 <39320777+lml2468@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman

* feat(mattermost): add retry logic and timeout handling for DM channel creation (openclaw#42398)

Merged via squash.

Prepared head SHA: 3db47be
Co-authored-by: JonathanJing <17068507+JonathanJing@users.noreply.github.com>
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
Reviewed-by: @mukhtharcm

* docs(hooks): clarify trust model and audit guidance

* test: stabilize memory async search close

* refactor: narrow extension public seams

* chore: add gog CLI support via custom Docker image

Add Dockerfile.gog to extend openclaw:local with the gog binary
(Google Workspace CLI). Also forward OPENAI_API_KEY and GEMINI_API_KEY
env vars in docker-compose.yml for both services.

Made-with: Cursor

* chore: inject GOG_KEYRING_PASSWORD for non-interactive gog auth

The gog CLI uses a file-based keyring to store Google OAuth tokens.
In Docker/headless environments, the keyring prompts for a passphrase
interactively, which blocks the agent from running gog autonomously.

Setting GOG_KEYRING_PASSWORD as an environment variable allows gog to
unlock the keyring without user interaction, enabling the agent to
execute Gmail, Calendar, Drive, and other Google Workspace commands
directly via the gog skill.

Setup steps to enable the gog skill in Docker:
1. Build the custom image: docker build -f Dockerfile.gog -t openclaw:gog .
2. Set OPENCLAW_IMAGE=openclaw:gog in .env
3. Set GOG_KEYRING_PASSWORD= in .env (empty = no passphrase)
4. Run: gog auth keyring file (inside container)
5. Run: gog auth credentials /path/to/client_secret.json
6. Run: gog auth add you@gmail.com --manual
7. Add to openclaw.json: tools.alsoAllow = ["exec", "process"]

Made-with: Cursor

* docs: add Docker commands reference and Mac migration plan

* docs: add docker save/load alternative to migration plan

* docs: add Docker+gog env example file

* config: clear unused OPENAI_API_KEY and document LLM auth in migration guide

* chore: remove .env from git tracking (contains secrets)

Made-with: Cursor

* docs: add openai-codex auth commands to migration guide

* docs: add gog token persistence, exec tool fix, and image migration cases

- docker-compose.yml: mount ~/.openclaw/gogcli as volume so gog tokens survive container restarts
- docs/docker-commands.html: add case 10 (gog token loss diagnosis + re-auth + volume persistence)
- docs/docker-commands.html: add case 11 (exec tool not enabled — tools.alsoAllow fix + persistence explanation)
- docs/docker-commands.html: add case 08 (migrate Docker Desktop images to Colima via docker save/load) + TOC entry

* docs: add Colima disk lock fix to docker-commands

* feat(docker): add local Whisper audio transcription to gog image

* docs: update docker-commands with TTS and contact number fixes

* docs: add gog token expiry troubleshooting and publish-app fix (caso 12)

* docs: add OpenAI Codex rate limit troubleshooting (caso 13)

* docs: add model set/status commands to caso 13

* docs: add provider/model switching guide (caso 14)

* docs: replace placeholder email with herbert.cadbury.bot@gmail.com

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: scoootscooob <zhentongfan@gmail.com>
Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com>
Co-authored-by: Josh Lehman <josh@martian.engineering>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: Frank Yang <frank.ekn@gmail.com>
Co-authored-by: stim64045-spec <stim64045@gmail.com>
Co-authored-by: 大禹 <dayu@dayudeMac-mini.local>
Co-authored-by: Val Alexander <bunsthedev@gmail.com>
Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
Co-authored-by: Br1an <932039080@qq.com>
Co-authored-by: Br1an67 <29810238+Br1an67@users.noreply.github.com>
Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
Co-authored-by: Stable Genius <stablegenius043@gmail.com>
Co-authored-by: stablegenius49 <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: Chris Kimpton <chris@kimptoc.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Sally O'Malley <somalley@redhat.com>
Co-authored-by: huntharo <harold@pwrdrvr.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Co-authored-by: Andrew Demczuk <andrew.demczuk@gmail.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Co-authored-by: Jari Mustonen <jari.mustonen@iki.fi>
Co-authored-by: jarimustonen <1272053+jarimustonen@users.noreply.github.com>
Co-authored-by: F_ool <112874572+hhhhao28@users.noreply.github.com>
Co-authored-by: Kwest OG <50209930+yassinebkr@users.noreply.github.com>
Co-authored-by: Yassine <yassinebkr@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Bob <dutifulbob@gmail.com>
Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Menglin Li <limenglin5911@gmail.com>
Co-authored-by: lml2468 <39320777+lml2468@users.noreply.github.com>
Co-authored-by: Jonathan Jing <JonathanJing@users.noreply.github.com>
Co-authored-by: JonathanJing <17068507+JonathanJing@users.noreply.github.com>
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…marizable messages (openclaw#41981) (openclaw#42215)

Merged via squash.

Prepared head SHA: 7ce6bd8
Co-authored-by: lml2468 <39320777+lml2468@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…marizable messages (openclaw#41981) (openclaw#42215)

Merged via squash.

Prepared head SHA: 7ce6bd8
Co-authored-by: lml2468 <39320777+lml2468@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling channel: discord Channel integration: discord size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Compaction safeguard loop blocks cron lane — isolated sessions never execute

3 participants