feat(compaction): opt-in hard-floor for incremental compaction below contextThreshold#619
Conversation
When enabled, evaluateIncrementalCompaction short-circuits with reason="below-context-threshold-floor" whenever currentTokenCount < contextThreshold * tokenBudget, regardless of cache state, leaf trigger, or activity band. Motivation: cold-cache catch-up passes can compact context away during idle gaps even when overall context usage is well below the configured threshold. Step-away patterns (idle 20 min → cold turn fires 2× leaf passes → idle 5 min → another 2× passes) can bleed conversations down to the freshTail. Users who want a strict "never compact below X%" policy now have a single flag to enforce it. Default false preserves existing behavior. Configurable via plugin config or env var LCM_RESPECT_THRESHOLD_AS_HARD_FLOOR=true.
Adversarial review found that respectThresholdAsHardFloor only gated evaluateIncrementalCompaction. In deferred mode, the after-turn flow records compaction debt based on `thresholdDecision.shouldCompact || rawLeafTrigger?.shouldCompact`, both of which use unguarded `compaction.evaluate` and `compaction.evaluateLeafTrigger` calls. Without this fix, debt would accumulate from below-floor leaf-trigger fires and later drain via paths that do not re-check the floor — silently leaking the hard-floor semantics. Fix: skip deferred debt recording entirely when the hard-floor flag is enabled and currentTokenCount is below the floor. Logs a single info line so operators can verify suppression. Tests added: - Boundary: currentTokenCount === minTokenFloor (exact equality; strict `<` permits compaction at the boundary, by design). - Boundary: currentTokenCount undefined with flag enabled (falls through to existing logic). - Integration: afterTurn in deferred mode with hard-floor enabled skips deferred-debt recording when below floor (regression guard for the leak). Suite: 864/864 passing (was 861).
Adversarial review pass — one HIGH finding fixedRan a 3-agent parallel adversarial review on the initial commit. Two reviewers independently flagged a HIGH-severity leak in deferred mode: the hard-floor only short-circuited Fixed in Other findings + dispositions
Suite864/864 passing (3 new tests added since the initial commit). |
…chema and uiHints The host-side OpenClaw config validator uses the plugin manifest's configSchema (additionalProperties: false). Without adding the new field there, hot-reload rejects the config with 'must NOT have additional properties'.
TL;DR
Adds an opt-in config flag
respectThresholdAsHardFloor(defaultfalse, no behavior change for existing users). When enabled,evaluateIncrementalCompactionshort-circuits withreason="below-context-threshold-floor"whenevercurrentTokenCount < contextThreshold * tokenBudget, regardless of cache state, leaf-trigger fires, or activity-band heuristics. The deferred-mode debt-recording path atengine.ts:6548is gated under the same predicate to prevent silent leakage of the floor through the deferred drain.This is a policy enhancement, not a bug fix. The default behavior continues to allow cold-cache catch-up passes to fire below threshold (the existing design that some users want). Users who instead want strict "never compact below X%" semantics — typically because of bursty/idle conversation patterns where step-away gaps cause cold-mode firing to bleed context — can now opt in with one flag.
The problem this addresses, visualized
Behavior matrix
false(default)truetruecontextThreshold × tokenBudgetshouldCompact=false reason="below-context-threshold-floor"truecontextThreshold × tokenBudgettrue(deferred mode)recordDeferredCompactionDebt(prevents drain leak)true(deferred mode)Diff overview
3 commits:
1c99131feat(compaction): add opt-in respectThresholdAsHardFloor flagsrc/db/config.ts: addrespectThresholdAsHardFloor: booleantoLcmConfig, parser entry with env-var support (LCM_RESPECT_THRESHOLD_AS_HARD_FLOOR), defaultfalse.src/engine.ts: 30-line guard at the top ofevaluateIncrementalCompaction, returns through the existinglogIncrementalCompactionDecisionso the newreasonshows up in standard telemetry.test/engine.test.tsandtest/config.test.ts.434eb28fix(compaction): plug deferred-mode hard-floor leak; add boundary testsengine.ts:6548-6603), debt is recorded based onthresholdDecision.shouldCompact || rawLeafTrigger?.shouldCompact— both use unguardedcompaction.evaluate/evaluateLeafTrigger. Without this fix, debt would accumulate from below-floor leaf-trigger fires and later drain via paths that don't re-check the floor.recordDeferredCompactionDebtand the deferred-drain scheduling, with an explanatory log line:[lcm] afterTurn: skipping deferred compaction debt below context-threshold floor ….currentTokenCountwith flag enabled, and an integration test for the deferred-mode skip.8cc4e63feat(manifest): expose respectThresholdAsHardFloor in plugin config schema and uiHintsopenclaw.plugin.json: addsrespectThresholdAsHardFloor: { type: "boolean" }toconfigSchema.properties(required so OpenClaw's host-side validator withadditionalProperties: falseaccepts the new field). Adds auiHintsentry with label and help text so the field shows up in the OpenClaw plugin settings UI.Naming conventions
respectThresholdAsHardFloor— follows the existing pattern of compound-clause boolean flags.LCM_RESPECT_THRESHOLD_AS_HARD_FLOOR— matches the existingLCM_*env pattern for plugin config overrides."below-context-threshold-floor"— follows the hyphenated convention of"budget-trigger","hot-cache-defer","hot-cache-budget-headroom","below-leaf-trigger".logIncrementalCompactionDecisionso the new reason appears in the same[lcm] incremental compaction decision: …lines operators already grep.What is intentionally NOT changed
compactFullSweepandcompactUntilUnderare not affected. Forced compaction (manualCompaction: true, overflow recovery, explicitcompact({force: true})) bypassesevaluateIncrementalCompactionentirely and continues to work above the soft floor as before. This matters for emergency overflow recovery, which should always be allowed.false(the default), zero lines of pre-existing logic execute differently. Verified via the regression testevaluateIncrementalCompaction preserves default behavior when respectThresholdAsHardFloor is disabled.Tests
test/engine.test.ts— 5 new tests:evaluateIncrementalCompaction short-circuits below contextThreshold when respectThresholdAsHardFloor is enabled— verifies short-circuit and thatevaluateLeafTrigger/evaluateare NOT called.evaluateIncrementalCompaction allows existing compaction logic at/above contextThreshold even when hard-floor is enabled— verifies budget-trigger still fires above floor.evaluateIncrementalCompaction preserves default behavior when respectThresholdAsHardFloor is disabled— regression guard for the default path.evaluateIncrementalCompaction allows existing logic at exact floor boundary even with hard-floor enabled— boundary test forcurrentTokenCount === minTokenFloor(uses<not<=, so the boundary admits compaction).evaluateIncrementalCompaction falls through to existing logic when currentTokenCount is undefined even with hard-floor enabled— verifies the type-guard fall-through case.afterTurn in deferred mode skips deferred-debt recording when below contextThreshold floor— integration test for the deferred-mode leak fix (commit434eb28).test/config.test.ts— 3 path coverages:falsein the hardcoded-defaults test.LCM_RESPECT_THRESHOLD_AS_HARD_FLOOR=true).Full suite: 860/860 passing.
Independence
PR #619 is independent of PR #621. They address different concerns:
afterTurnearly-return on emptyingestBatchskips compaction evaluation entirely (different code path).No merge-order dependency. Either PR can land first.
Test plan
npm run build— bundles cleanly (689kb).npm test— 860/860 passing.additionalProperties: false).