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
|
Codex review: needs real behavior proof before merge. Reviewed June 10, 2026, 1:11 AM ET / 05:11 UTC. Summary PR surface: Source +389, Tests +852, Docs +123. Total +1364 across 18 files. Reproducibility: not applicable. as a current-main bug report; this is a new plugin API proposal. The blocking availability defect is source-reproducible from the direct Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Land a rebased current-main implementation only after the intercept callback is bounded by the existing compaction timeout/abort machinery, the plugin API contract is maintainer-approved, release-owned changelog churn is removed, and redacted live proof shows an opted-in engine handling the event. Do we have a high-confidence way to reproduce the issue? Not applicable as a current-main bug report; this is a new plugin API proposal. The blocking availability defect is source-reproducible from the direct Is this the best way to solve the issue? No, not as submitted. The API direction is plausible, but the maintainable solution must reuse the current compaction timeout/abort machinery, port to current main's agent-runtime paths, remove release-owned changelog churn, and prove the live handled event path. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against e9671ed60327. Label changesLabel justifications:
Evidence reviewedPR surface: Source +389, Tests +852, Docs +123. Total +1364 across 18 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
`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 <dev@jetd.one> Co-authored-by: Josh Lehman <josh@martian.engineering>
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.
…_compact
Adds a new `compactionInterceptExtension` factory that bridges the
pi-coding-agent `session_before_compact` event to a context-engine plugin's
optional `interceptCompaction()` method (introduced in the prior commit).
Wiring details:
- New `compaction-intercept-runtime.ts` runtime registry, modeled after
`compaction-safeguard-runtime.ts`. Carries the resolved ContextEngine
reference for the active session, keyed by SessionManager identity.
- New `compaction-intercept.ts` extension registers an
`api.on("session_before_compact", ...)` handler. The handler resolves
the engine via the runtime registry, calls `engine.interceptCompaction()`
with sessionId / sessionFile (from `ReadonlySessionManager`),
contextWindow + currentTokenCount (from `ctx.getContextUsage()`), and
the event's firstKeptEntryId / tokensBefore / signal. On `handled: true`,
it returns `{ compaction }` to the SDK so the codex GPT path is skipped.
On `handled: false`, thrown errors, or no engine, it returns undefined
and the SDK falls back to its default compaction.
- `buildEmbeddedExtensionFactories` accepts a new optional
`activeContextEngine` and pushes the new factory AFTER the safeguard
factory so that, under the SDK's last-truthy-wins handler semantics,
intercept can override safeguard's fallback when both are active. The
factory is gated on `info.interceptsCompaction === true` and
`info.ownsCompaction !== true` (engines that fully own compaction bypass
the SDK event entirely).
- `run/attempt.ts:1708` (the in-attempt extension build for the user-facing
session) threads `activeContextEngine` through. `compact.ts:992` (the
inner compaction-LLM session) intentionally does NOT — that session has
no associated user-facing engine and would never trigger compaction on
itself.
No public-API change beyond the new exported runtime helpers. Behavior is
inert for engines that don't set `info.interceptsCompaction = true`.
…s compaction Adds an `engineOwnsPostCompactHeadroom` predicate and wires it through `applyPiCompactionSettingsFromConfig` so that when the active context engine either owns compaction outright (`info.ownsCompaction === true`) or intercepts the runtime's `session_before_compact` event (`info.interceptsCompaction === true`), Pi's reserve-token floor is treated as 0. The default 20K-token floor exists to give the next user turn enough headroom to land without immediately re-tripping the compaction threshold. Engines that intercept compaction with their own assembly (e.g. lossless-claw with `compactionTargetFraction = 0.35` — ~65% free post-compact) already provide far more headroom than the floor, so double-reserving is wasteful and can starve the assembled prompt of budget. Wiring: - `applyPiCompactionSettingsFromConfig` gains an optional `contextEngineInfo` param. When the predicate is true, `reserveTokensFloor` is set to 0 before the small-context cap is applied (so the cap is a no-op for the zeroed floor). - `createPreparedEmbeddedPiSettingsManager` forwards `contextEngineInfo` to the inner `applyPiCompactionSettingsFromConfig` call. - `run/attempt.ts` passes `activeContextEngine?.info` at BOTH the initial prepared-settings build AND the post-`resourceLoader.reload()` re-apply. Both calls must be threaded because `reload()` re-evaluates the floor from scratch. - `compact.ts` (inner compaction-LLM session) intentionally does NOT pass `contextEngineInfo` — that session is not user-facing and has no active engine. The existing comment block is expanded to cover both calls. Existing default behavior for engines that don't declare either flag is unchanged: `resolveCompactionReserveTokensFloor(cfg)` (default 20000) is still applied.
…zero Adds three test layers for the new context-engine compaction-intercept wiring: - `compaction-intercept.test.ts` (new file) — direct coverage of the extension handler: handled:true round-trip, handled:false fallthrough, thrown errors swallowed, AbortSignal propagation, null tokens → undefined currentTokenCount, no-runtime / no-method paths return undefined, plus runtime-registry set/get/clear/defensive null inputs. - `extensions.test.ts` (extended) — factory selection in `buildEmbeddedExtensionFactories`: intercept factory is included only when `info.interceptsCompaction === true` and `info.ownsCompaction !== true`, and is pushed AFTER the safeguard factory so that SDK last-truthy-wins semantics let intercept override safeguard. - `pi-settings.test.ts` (extended) — `applyPiCompactionSettingsFromConfig` auto-zero behavior: floor is zeroed for `interceptsCompaction` and `ownsCompaction` engines, preserved for legacy engines and when info is omitted, and explicit `reserveTokens` configurations still apply on top of the zeroed floor. Pure test additions; no source changes.
Three follow-up fixes flagged by reviewer before push: - **cli-compaction.ts**: thread `contextEngineInfo: contextEngine.info` into `createPreparedEmbeddedPiSettingsManager`. The `/compact` CLI path was the only remaining caller that resolved the engine but did not forward its info, so its reserve-token floor was NOT being auto-zeroed even though the same call passed engine info to `applyPiAutoCompactionGuard` on the next line. Now both calls are consistent with the in-attempt path. - **compaction-intercept.ts**: guard `getSessionFile()` returning undefined. `ReadonlySessionManager.getSessionFile()` is typed `string | undefined` in pi-coding-agent; an in-memory session (not yet persisted) would otherwise forward `undefined` as a `string` into the engine. The handler now bails to the default compaction path with a debug log when no file is present. - **compaction-intercept-runtime.ts + extensions.ts**: add `sessionKey?: string` to the runtime registry value so engines that route on session-key patterns (e.g. lossless-claw's ignored-/stateless-session patterns) see it inside the intercept handler. `ReadonlySessionManager` does not expose sessionKey (openclaw-level concept), so it's threaded via the runtime registry. `attempt.ts` passes `params.sessionKey` through `buildEmbeddedExtensionFactories`. Tests: - New `compaction-intercept.test.ts` case asserts handler bails on undefined sessionFile and another asserts sessionKey is forwarded. - New `extensions.test.ts` cases assert sessionKey is wired through the runtime registry, and that omitting sessionKey is back-compat.
Test helper `makeCtx` used `overrides.tokens ?? 232_000`, but `??` triggers on
both `null` and `undefined` — so passing `{ tokens: null }` substituted the
default 232K instead of preserving the explicit null. Replace with a
`"tokens" in overrides` discriminator so explicit null is honored.
The contract under test (ContextUsage.tokens === null → currentTokenCount
undefined in the engine request) was correct in `compaction-intercept.ts`;
this was a test-side bug only.
128/128 of the new tests now pass locally:
- compaction-intercept.test.ts: 14 cases
- extensions.test.ts: 9 cases (including 6 for the new factory wiring)
- pi-settings.test.ts: 47 cases (including 5 for the new auto-zero gate)
Three concrete fixes plus polish, after CI surfaced the issues:
## Compile / lint (P0 — unblocks 7 CI failures)
- `cli-compaction.ts`: add `contextEngineInfo?: ContextEngine["info"]` to
the `CliCompactionDeps.createPreparedEmbeddedPiSettingsManager` type so
the test-injectable shape matches the real signature. Without this
`tsc --noEmit` rejected the call site at line 219 across build-artifacts,
check-prod-types, check-test-types, check-strict-smoke, check-lint, and
the three check-additional-extension-* shards.
- `compaction-intercept.ts`: bind `runtime`, `engine`, `interceptFn` as
separate locals so TypeScript can narrow each independently. The prior
chained-optional guard `!engine?.interceptCompaction` did not propagate
to subsequent independent property reads on `engine`, leaving
`runtime.sessionKey` and `engine.interceptCompaction` flagged as
possibly-undefined at the call sites. Use `interceptFn.call(engine, ...)`
so the bound method retains its `this`.
- Lint `'unknown' overrides all other types in this union type` was a
cascade error from the TS failures (oxlint resolves `RegisteredLineCardCommand`
to `unknown` when the build can't compile). Resolved by fixing the TS
errors above.
## Gate semantics (P0 — architecture-review wave-B killer fix)
- `extensions.ts`: drop the `engineInfo.ownsCompaction !== true` exclusion
from the compaction-intercept factory gate. The two flags advertise
capability against DISTINCT compaction lanes:
* `ownsCompaction` → openclaw queued-compaction lane (compact.queued.ts)
* `interceptsCompaction` → pi-coding-agent SDK event lane (session_before_compact)
An engine can advertise BOTH because codex fires session_before_compact
on in-attempt overflow even when an engine owns the queued lane. The
prior exclusion silently disabled intercept for engines like lossless-claw
that legitimately own both lanes.
- `extensions.test.ts`: invert the ownsCompaction test to assert that
engines with BOTH flags get intercept registered, plus a new case
asserting that engines with ONLY ownsCompaction (no interceptsCompaction)
do NOT get intercept (declining the SDK lane is opt-out).
## Polish (P1)
- `compaction-intercept.ts`: wire the new `trigger` field on the request
payload (currently best-effort "in-attempt-auto" until the SDK surface
grows). The type was declared but never plumbed.
- `pi-settings.ts`: expand the inline comment on `engineOwnsPostCompactHeadroom`
to explicitly document the conflation of `ownsCompaction || interceptsCompaction`
— both flags imply the engine manages headroom, so auto-zero is correct
for either. Names the predicate as the single update point if the two
lanes ever split.
Local re-verification: 130/130 tests passed (2.91s) on the touched files
and their neighbors after the fixes.
Concrete fixes from the second-wave audit:
## P0 (lint failures from latest CI)
- `compaction-intercept.ts`: replace `if (result.handled !== true)` with
`if (!result.handled)` and `result.handled === false` with
`!result.handled`. oxlint `no-unnecessary-boolean-literal-compare`
rejects comparing discriminated-union booleans against literals. The
type narrowing is unchanged — the discriminated union still correctly
routes by truthiness.
- `compaction-intercept.ts`: restructure the engine-presence guard to
`typeof engine.interceptCompaction !== "function"`. The prior
`const interceptFn = engine?.interceptCompaction` triggered the
`typescript/unbound-method` lint rule (extracting a method as a value
can lose `this` binding at call time). The new form narrows
`engine.interceptCompaction` to a defined function while avoiding the
extracted-method-reference pattern; we call it as
`engine.interceptCompaction(request)` inline so `this` is preserved.
- `compaction-intercept.test.ts`: add braces to two single-line `if`
bodies that oxlint `curly` rule rejects.
## P1 (wave-B findings)
- `compaction-intercept.ts`: drop `inferTriggerFromEvent`. Wave-B P1-1
caught that the function always returned `"in-attempt-auto"` whenever
`event.preparation` was truthy (which is always, per SDK typing) —
effectively mis-attributing overflow retries and manual /compact events
as in-attempt-auto. Engines that condition cadence on `request.trigger`
would see indistinguishable values. The post-fix contract: pass
`trigger: undefined` and let engines treat absence as "host doesn't
disambiguate". When the SDK exposes a real trigger field, plumb it then.
- `compaction-intercept.ts`: rewrite the factory docstring to reflect the
wave-A gate change. The prior text claimed "no-op for engines that
fully own compaction" which contradicted the new gate (engines may
declare both `ownsCompaction` and `interceptsCompaction` and intercept
IS registered for them). New text names the SDK `{cancel: true}`
short-circuit limitation explicitly so future readers know auth-cancel
cases bypass intercept.
- `compaction-intercept.test.ts`: add a new handler-level integration
test that asserts a both-flags engine routes traffic through intercept
correctly (P1-4 — the wave-A factory-registration test only verified
the factory was added, not that the handler routed correctly).
- `compaction-intercept.test.ts`: add a test asserting `req.trigger ===
undefined` so the post-wave-B contract is exercised.
Local re-verification: 30/30 (intercept tests), 0 lint errors on the
affected files, 134/134 on the full pi-hooks + pi-settings + extensions
suite. The handler now passes the discriminated-union narrowing without
the `!== true` / `=== false` comparisons and without extracting the
method as a value.
Wave-3 audit found two stale docstrings that contradicted the wave-A gate change (which dropped the `ownsCompaction !== true` exclusion from the intercept factory gate). - `src/context-engine/types.ts`: `info.interceptsCompaction` docstring previously said engines that own compaction "never see the runtime event at all" — but the wave-A change made an `ownsCompaction + interceptsCompaction` engine see BOTH lanes. Updated to name the two distinct lanes (queued vs SDK event) and document that declaring both is correct for engines that want to handle both flows (lossless-claw v4.1 is the canonical case). - `src/agents/pi-embedded-runner/extensions.ts`: `activeContextEngine` param docstring said the factory registers when "info.interceptsCompaction === true AND does NOT own compaction outright" — but the gate code only checks `interceptsCompaction === true`. Updated to match. No behavior change; documentation alignment only.
`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.
3332278 to
f202ad6
Compare
|
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. |
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.