feat(context-engine): add interceptCompaction contract for context-engine plugins#81164
feat(context-engine): add interceptCompaction contract for context-engine plugins#81164100yenadmin wants to merge 13 commits into
Conversation
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Keep open: the PR contains a meaningful unique plugin API proposal, but it is not merge-ready because it targets retired Canonical path: Close this PR as superseded by #84083. So I’m closing this here and keeping the remaining discussion on #84083. Review detailsBest possible solution: Close this PR as superseded by #84083. Do we have a high-confidence way to reproduce the issue? Yes for the review blockers: source and live PR state show the branch is dirty, targets retired Is this the best way to solve the issue? No; the API direction may be useful, but the branch is not the best merge path until it is ported to current runtime surfaces, bounded, and proven with a handled intercept. Security review: Security review cleared: No concrete security or supply-chain regression was found; the diff changes plugin SDK/runtime compaction behavior, docs, tests, and changelog text without new dependencies, workflow permissions, secret handling, or downloaded code. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against 9d68f877ac3e. |
`shouldDisablePiAutoCompaction` was turning off Pi's threshold check whenever the engine declared `ownsCompaction: true` (or the user set safeguard mode). But the new `session_before_compact` intercept lane depends on Pi's threshold check still emitting that event — the engine registers an extension that takes over compaction from inside the event handler. With the old gate, declaring `interceptsCompaction: true` had no effect: Pi's `shouldCompact()` short-circuited on `!settings.enabled` before the threshold was ever evaluated, so the event never fired and the engine's intercept extension became dead code. Carve out an exception: when `interceptsCompaction === true`, leave Pi auto-compaction on so the event still emits. The engine's extension is last-truthy-wins on `session_before_compact`, so it deterministically overrides Pi's default summarizer. The silent-overflow-prone branch is intentionally NOT gated on `interceptsCompaction` — silent-overflow providers can truncate the prompt without warning, so Pi's auto-compaction must stay disabled regardless; the engine is expected to handle preemptive compaction through its own queued lane in that case. Refs openclaw#81164.
fe7455a to
cf79054
Compare
`shouldDisablePiAutoCompaction` was turning off Pi's threshold check whenever the engine declared `ownsCompaction: true` (or the user set safeguard mode). But the new `session_before_compact` intercept lane depends on Pi's threshold check still emitting that event — the engine registers an extension that takes over compaction from inside the event handler. With the old gate, declaring `interceptsCompaction: true` had no effect: Pi's `shouldCompact()` short-circuited on `!settings.enabled` before the threshold was ever evaluated, so the event never fired and the engine's intercept extension became dead code. Carve out an exception: when `interceptsCompaction === true`, leave Pi auto-compaction on so the event still emits. The engine's extension is last-truthy-wins on `session_before_compact`, so it deterministically overrides Pi's default summarizer. The silent-overflow-prone branch is intentionally NOT gated on `interceptsCompaction` — silent-overflow providers can truncate the prompt without warning, so Pi's auto-compaction must stay disabled regardless; the engine is expected to handle preemptive compaction through its own queued lane in that case. Refs openclaw#81164.
CI status note (post-rebase)All 12 currently-failing checks reproduce on
Verification:
Recommend either (a) merging a small fix-up commit on main that addresses #81425's residual lint/type/contract debt, or (b) applying |
cf79054 to
3332278
Compare
`shouldDisablePiAutoCompaction` was turning off Pi's threshold check whenever the engine declared `ownsCompaction: true` (or the user set safeguard mode). But the new `session_before_compact` intercept lane depends on Pi's threshold check still emitting that event — the engine registers an extension that takes over compaction from inside the event handler. With the old gate, declaring `interceptsCompaction: true` had no effect: Pi's `shouldCompact()` short-circuited on `!settings.enabled` before the threshold was ever evaluated, so the event never fired and the engine's intercept extension became dead code. Carve out an exception: when `interceptsCompaction === true`, leave Pi auto-compaction on so the event still emits. The engine's extension is last-truthy-wins on `session_before_compact`, so it deterministically overrides Pi's default summarizer. The silent-overflow-prone branch is intentionally NOT gated on `interceptsCompaction` — silent-overflow providers can truncate the prompt without warning, so Pi's auto-compaction must stay disabled regardless; the engine is expected to handle preemptive compaction through its own queued lane in that case. Refs openclaw#81164.
When OpenClaw's LCM context engine assembles context from the durable DB frontier, messages that were never persisted (inter-session subagent announcements, retry/overflow prompts with suppressPromptPersistence) are absent from the DB. The assembled output therefore omits them entirely, causing the model to miss live input events like subagent completions. This patch adds a reconciliation step after DB assembly: detect volatile live input in params.messages that is not represented in the assembled output, and append it within the token budget. Key design choices: - Volatile live inputs are never covered by summary substring matches. A summary containing similar text captures a *past* turn; the current volatile event is a distinct occurrence the model must see explicitly. - Only exact assembled-message matches count as coverage for volatile inputs. This prevents stale summaries from consuming live events. - Occurrence-level bipartite maximum matching (DFS augmenting-path) deduplicates volatile inputs against assembled context. - Live input takes priority: if appending volatile input exceeds the token budget, evict from the front of assembled DB context first. - Tool-result/tool-call pairs are kept intact during budget trimming; protected fresh-tail messages and exact live anchors are never evicted. - Tool names that are null/undefined/""/"unknown" are normalized to equivalent in coverage signatures, fixing anchor mismatches when the assembler fills missing tool names with "unknown". Fixes: subagent completion announcements lost from model context Related: openclaw/openclaw#80760, openclaw/openclaw#81164
When OpenClaw's LCM context engine assembles context from the durable DB frontier, messages that were never persisted (inter-session subagent announcements, retry/overflow prompts with suppressPromptPersistence) are absent from the DB. The assembled output therefore omits them entirely, causing the model to miss live input events like subagent completions. This patch adds a reconciliation step after DB assembly: detect volatile live input in params.messages that is not represented in the assembled output, and append it within the token budget. Key design choices: - Volatile live inputs are never covered by summary substring matches. A summary containing similar text captures a *past* turn; the current volatile event is a distinct occurrence the model must see explicitly. - Only exact assembled-message matches count as coverage for volatile inputs. This prevents stale summaries from consuming live events. - Occurrence-level bipartite maximum matching (DFS augmenting-path) deduplicates volatile inputs against assembled context. - Live input takes priority: if appending volatile input exceeds the token budget, evict from the front of assembled DB context first. - Tool-result/tool-call pairs are kept intact during budget trimming; protected fresh-tail messages and exact live anchors are never evicted. - Tool names that are null/undefined/""/"unknown" are normalized to equivalent in coverage signatures, fixing anchor mismatches when the assembler fills missing tool names with "unknown". Fixes: subagent completion announcements lost from model context Related: openclaw/openclaw#80760, openclaw/openclaw#81164
When OpenClaw's LCM context engine assembles context from the durable DB frontier, messages that were never persisted (inter-session subagent announcements, retry/overflow prompts with suppressPromptPersistence) are absent from the DB. The assembled output therefore omits them entirely, causing the model to miss live input events like subagent completions. This patch adds a reconciliation step after DB assembly: detect volatile live input in params.messages that is not represented in the assembled output, and append it within the token budget. Key design choices: - Volatile live inputs are never covered by summary substring matches. A summary containing similar text captures a *past* turn; the current volatile event is a distinct occurrence the model must see explicitly. - Only exact assembled-message matches count as coverage for volatile inputs. This prevents stale summaries from consuming live events. - Occurrence-level bipartite maximum matching (DFS augmenting-path) deduplicates volatile inputs against assembled context. - Live input takes priority: if appending volatile input exceeds the token budget, evict from the front of assembled DB context first. - Tool-result/tool-call pairs are kept intact during budget trimming; protected fresh-tail messages and exact live anchors are never evicted. - Tool names that are null/undefined/""/"unknown" are normalized to equivalent in coverage signatures, fixing anchor mismatches when the assembler fills missing tool names with "unknown". Fixes: subagent completion announcements lost from model context Related: openclaw/openclaw#80760, openclaw/openclaw#81164
* fix: preserve unpersisted volatile live input in LCM assembly When OpenClaw's LCM context engine assembles context from the durable DB frontier, messages that were never persisted (inter-session subagent announcements, retry/overflow prompts with suppressPromptPersistence) are absent from the DB. The assembled output therefore omits them entirely, causing the model to miss live input events like subagent completions. This patch adds a reconciliation step after DB assembly: detect volatile live input in params.messages that is not represented in the assembled output, and append it within the token budget. Key design choices: - Volatile live inputs are never covered by summary substring matches. A summary containing similar text captures a *past* turn; the current volatile event is a distinct occurrence the model must see explicitly. - Only exact assembled-message matches count as coverage for volatile inputs. This prevents stale summaries from consuming live events. - Occurrence-level bipartite maximum matching (DFS augmenting-path) deduplicates volatile inputs against assembled context. - Live input takes priority: if appending volatile input exceeds the token budget, evict from the front of assembled DB context first. - Tool-result/tool-call pairs are kept intact during budget trimming; protected fresh-tail messages and exact live anchors are never evicted. - Tool names that are null/undefined/""/"unknown" are normalized to equivalent in coverage signatures, fixing anchor mismatches when the assembler fills missing tool names with "unknown". Fixes: subagent completion announcements lost from model context Related: openclaw/openclaw#80760, openclaw/openclaw#81164 * fix: cover retry prompts and paired tool eviction --------- Co-authored-by: jet <[email protected]> Co-authored-by: Josh Lehman <[email protected]>
Adds an optional `interceptCompaction(request)` method to the ContextEngine interface alongside a new `info.interceptsCompaction?: boolean` capability flag. The new contract lets engines (e.g. lossless-claw) replace the runtime's default `session_before_compact` GPT-driven summarization with their own assembly while remaining inside the runtime's lifecycle — distinct from the existing `ownsCompaction` mode, which bypasses the runtime event entirely. - `CompactionInterceptRequest` mirrors the pi-coding-agent event payload so engines do not have to import SDK types directly. - `CompactionInterceptResult` is a discriminated union: `handled: true` to supply a replacement summary, or `handled: false` with a diagnostic `reason` to opt out and let the runtime fall back to its default path. - The method is documented as never-throwing across the call boundary; callers treat a thrown error as `handled: false`. The wiring that calls this method is added in follow-up commits in this PR (new ExtensionFactory + reserveTokensFloor auto-zero gated on `interceptsCompaction`). No behavior change yet — pure type addition. Re-exported via plugin-sdk so third-party plugins consuming `openclaw/plugin-sdk` see the new contract.
|
ClawSweeper PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
|
This assigned pull request has been automatically marked as stale after being open for 27 days. |
|
This assigned pull request has been automatically marked as stale after being open for 27 days. |
|
This assigned pull request has been automatically marked as stale after being open for 27 days. |
|
This assigned pull request has been automatically marked as stale after being open for 27 days. |
|
closing to make PR space |
TLDR - lets other context systems turn off/replace codex compaction (when it calls to compact it uses them versus its native).
Summary
Adds an optional
interceptCompaction(request)method to theContextEngineplugin contract so engines can replace the runtime's defaultsession_before_compactGPT-driven summarization with their own assembly — without leaving the runtime's compaction lifecycle.Motivating use case: lossless-claw assembles a lossless raw + summary pyramid on disk and wants to swap in its assembled context whenever pi-coding-agent would otherwise trigger codex's GPT compaction. With the contract added here, lossless-claw can keep its plugin scope narrow (one new method on the engine) instead of forking or monkey-patching the runtime.
Architecture — two compaction lanes, two capability flags
OpenClaw + pi-coding-agent expose two distinct compaction flows that this PR makes explicit:
Engines may declare BOTH flags (lossless-claw does): codex still fires
session_before_compacton in-attempt overflow even when the engine owns the openclaw queued lane.What's in this PR
src/context-engine/types.tsCompactionInterceptRequest/CompactionInterceptResulttypes, newinfo.interceptsCompaction?: booleanflag, new optionalinterceptCompaction?(...)method onContextEngine. Re-exported via plugin-sdk.src/agents/pi-hooks/compaction-intercept-runtime.ts(new)sessionKey. Modeled oncompaction-safeguard-runtime.ts.src/agents/pi-hooks/compaction-intercept.ts(new)ExtensionFactory. Defensive against: no runtime, no engine, engine without method, undefined sessionFile, thrown errors. Returns{compaction: {...}}onhandled:trueorundefinedon fall-through.src/agents/pi-embedded-runner/extensions.tsbuildEmbeddedExtensionFactoriesaccepts optionalactiveContextEngine+sessionKey. Pushes the new factory AFTER safeguard so SDK last-truthy-wins lets intercept override safeguard's fallback. Gate isinfo.interceptsCompaction === trueonly — no exclusion forownsCompaction.src/agents/pi-settings.tsengineOwnsPostCompactHeadroompredicate.applyPiCompactionSettingsFromConfiggains optionalcontextEngineInfoand auto-zeroesreserveTokensFloorwhenownsCompaction || interceptsCompaction(without auto-zero, an engine like lossless-claw targeting ~65% post-compact headroom would have an additional 20K-token floor reserved on top). (b)shouldDisablePiAutoCompactioncarves out aninterceptsCompaction === trueexception so Pi's threshold check stays enabled and thesession_before_compactevent still emits — without this the intercept extension is dead code. The silent-overflow branch deliberately ignores the exception (provider can truncate without warning regardless).src/agents/pi-embedded-runner/run/attempt.tsactiveContextEngine?.info+params.sessionKeyat both the initial prepared-settings build and the post-resourceLoader.reload()re-apply.src/agents/command/cli-compaction.tscontextEngineInfoso the/compactCLI path matches. The test-injectableCliCompactionDepsshape gains the new field.pi-embedded-runner/compact.ts:992)compaction-intercept.test.ts(~18 cases). Extendedextensions.test.ts(~10 cases). Extendedpi-settings.test.ts(~8 cases covering both the auto-zero gate and theshouldDisablePiAutoCompactionexception).Behavior changes
info.interceptsCompaction = truesee zero behavior change. The new ExtensionFactory is a no-op when no such engine is active.session_before_compactroutes throughengine.interceptCompaction(). Onhandled:true, the engine's summary replaces the codex GPT call. Onhandled:falseor thrown errors, the runtime falls back to its default. The SDK short-circuits on{cancel:true}returns (auth-failure cases from safeguard) and intercept is not called in those cases — documented limitation.interceptsCompactionorownsCompaction, Pi'sreserveTokensFlooris treated as 0 because the engine takes responsibility for post-compaction headroom.interceptsCompaction === true, Pi's_checkCompactionis NOT force-disabled (it would normally be whenownsCompaction === trueormode === "safeguard"). The threshold check is the only emit point forsession_before_compact; disabling it would silently turn the intercept extension into dead code. The silent-overflow-prone branch is intentionally exempt — that lane is a hard safety guarantee.Compatibility
packages/plugin-sdk/package.jsonis"private": true), so no version bump. Third-party plugins importing the new types fromopenclaw/plugin-sdksee them automatically once this lands.ContextEngine.interceptCompaction?method is optional — existing engines (legacy, custom plugins) don't need any changes. Newinfo.interceptsCompactionflag is also optional.{cancel: true}short-circuit limitation: when the safeguard returns cancel (auth/model failure), intercept is not called. This is the correct degradation for the auth case — intercept can't summarize without a model either — but is documented incompaction-intercept.tsso future readers understand the ordering tradeoff.Adversarial review
Four independent adversarial review waves were performed by separate research agents reading the source code from scratch. Findings addressed in order:
058997508ab): 1 P0 architecture-killer (gate exclusion blocking the only legitimate consumer), 3 P0 CI compile/lint failures, 3 P1 polish items.b97b9e32e45): 1 P0 lint cascade (!== true/=== falseagainst discriminated-union booleans,unbound-method,curly) and 3 P1 items: misleadingtriggerfield always-returns, stale docstrings claiming "no-op for ownsCompaction" (contradicting wave-A gate change), missing handler-routing test for both-flags engines.049d7d640a5): 2 stale docstrings intypes.tsandextensions.tsleft over from the wave-A gate change.fe7455a9d9e): runtime smoke-test against lossless-claw discovered that the intercept extension was registered correctly but the underlyingsession_before_compactevent never emitted. Root cause:shouldDisablePiAutoCompactioninpi-settings.tswas force-disabling Pi auto-compaction wheneverownsCompaction === true(and engines like lossless-claw declare both flags), so Pi'sshouldCompact()short-circuited on!settings.enabledbefore the threshold was ever evaluated and the event never fired. Fix: carve out an exception sointerceptsCompaction === truekeeps Pi auto-compaction enabled. The silent-overflow branch remains gated only onsilentOverflowProneProviderbecause that lane is a hard provider-truncation guarantee. New test cases cover the carve-out.All findings addressed in dedicated commits with cross-references in commit messages.
Real behavior proof
Behavior addressed: context-engine plugins that declare both
ownsCompaction: trueandinterceptsCompaction: truemust keep Pi auto-compaction enabled so Pi can emitsession_before_compact, while still letting the plugin replace the default GPT-driven compaction summary throughengine.interceptCompaction(request).Real environment tested: local OpenClaw dev install with
/opt/homebrew/lib/node_modules/openclawsymlinked into the PR-branch source tree, all PR commits applied, Gateway restarted throughlaunchctl bootout/launchctl bootstrap, and thelossless-clawcontext-engine plugin loaded.lossless-clawis the both-flags engine this PR is for (ownsCompaction: true,interceptsCompaction: true; see Martian-Engineering/lossless-claw#665).Exact steps or command run after this patch:
Evidence after fix:
Observed result after fix: the compiled Gateway bundle contains the new
interceptsCompactionexception, Gateway boots successfully withlossless-clawloaded, and the table-driven runtime tests pass for the exact both-flags case that previously disabled Pi auto-compaction. Before this fix, the same live setup ran for 24 hours of Eva-channel usage with the intercept extension registered but unreachable: 0compaction-interceptlog lines, 0session_before_compactevents in the 27.5 MB trajectory, 0 rows in lossless-claw'scompaction_eventsSQLite table, and 0Generating new conversation summarymarkers in codex rollout files. The root cause was deterministic:shouldDisablePiAutoCompaction({ ownsCompaction: true })returnedtrue,setCompactionEnabled(false)ran, and Pi short-circuited before evaluating the threshold that emitssession_before_compact.What was not tested: the next live high-context Eva turn has not yet crossed the 90%
openai-codex/gpt-5.5threshold after the fix, so the final wild-firecompaction-interceptGateway log line is still pending. A monitor is tailing~/.openclaw/logs/gateway.log; this PR will be updated when that event emits.