Skip to content

feat(config): add ratio-based sibling fields for compaction token budgets#66517

Closed
cheapestinference wants to merge 2 commits into
openclaw:mainfrom
cheapestinference:feat/ratio-based-context-budgets
Closed

feat(config): add ratio-based sibling fields for compaction token budgets#66517
cheapestinference wants to merge 2 commits into
openclaw:mainfrom
cheapestinference:feat/ratio-based-context-budgets

Conversation

@cheapestinference

Copy link
Copy Markdown
Contributor

Motivation

OpenClaw users increasingly run multiple agents side-by-side against models
with wildly different context windows — e.g. GLM 200k, Claude 200k, Kimi K2
1M, and smaller local models. The compaction config today mixes ratios
(softTrimRatio, hardClearRatio, maxHistoryShare) with absolute-token
budgets (reserveTokens, keepRecentTokens, reserveTokensFloor, and
memoryFlush.softThresholdTokens). An absolute like reserveTokens: 50000
reserves 25% of a 200k window but only 5% of a 1M window, and over-reserves
aggressively against an 8k window. The same config behaves completely
differently depending on the model, so per-model configs end up fragmented.

This PR introduces additive *Share sibling fields so a single config
can scale correctly across heterogeneous models.

Fields that got a ratio sibling

  • agents.defaults.compaction.reserveTokensShare
  • agents.defaults.compaction.keepRecentTokensShare
  • agents.defaults.compaction.reserveTokensFloorShare
  • agents.defaults.compaction.memoryFlush.softThresholdTokensShare

All four are validated as number in [0.01, 0.9] in
zod-schema.agent-defaults.ts, mirroring the existing maxHistoryShare
range convention.

Precedence rules

  1. If *Share is set and the model's contextWindowTokens is known
    at runtime → Math.floor(contextWindowTokens * share).
  2. Otherwise, if the absolute field (reserveTokens, keepRecentTokens,
    reserveTokensFloor, softThresholdTokens) is set → use it as before.
  3. Otherwise → the existing default (DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR,
    memory-core defaults, etc.).

reserveTokensFloor (resolved from share or absolute) is still applied as
an absolute minimum on the final reserveTokens value, so a small
share against a large window never drops below the safety floor.

Backward compatibility

  • No existing absolute field is removed. Users who prefer hard numbers
    for predictability keep that option.
  • When only absolute fields are configured, behavior is byte-for-byte
    identical to pre-PR behavior.
  • When contextWindowTokens is unknown (e.g. older code paths that don't
    plumb it yet), the share is silently ignored and the absolute/default
    path applies. No runtime error, no config error.
  • Plugin SDK surface is unchanged; the MemoryFlushPlan still ships with
    absolute softThresholdTokens / reserveTokensFloor so external plugins
    keep working. Share-based overrides are applied at the consumption site
    (applyMemoryFlushSharesToPlan) where the context window is resolved.

Implementation notes

  • New helper resolveShareBasedTokenBudget(...) centralizes the share →
    absolute → fallback precedence.
  • applyPiCompactionSettingsFromConfig takes an optional
    contextWindowTokens and resolves shares against it. Call sites in
    pi-embedded-runner/run/attempt.ts and pi-embedded-runner/compact.ts
    now pass model.contextWindow.
  • resolveCompactionReserveTokensFloor gained an optional
    contextWindowTokens arg; when present, it resolves
    reserveTokensFloorShare first.
  • applyMemoryFlushSharesToPlan overrides the plugin-provided flush plan's
    softThresholdTokens and reserveTokensFloor with share-based values
    when configured and a context window is known, leaving the rest of the
    plan untouched.
  • Each new field has a one-sentence JSDoc explaining the relationship to
    contextWindowTokens, and matching entries in schema.help.ts /
    schema.labels.ts. schema.base.generated.ts was regenerated via
    pnpm config:schema:gen.

Test coverage summary

Extended src/agents/pi-settings.test.ts and
src/auto-reply/reply/reply-state.test.ts with the following coverage:

  • Only absolute set → uses absolute (regression)
  • Only share set → computes from context window
  • Both set → share wins (documented precedence)
  • Share + floor → floor lifts the value when share yields less
  • Share without contextWindowTokens → falls back to absolute (backward compat)
  • 1M window + share → scales up reasonably
  • 8k window + share → avoids over-reserving
  • keepRecentTokensShare resolves against the window
  • applyMemoryFlushSharesToPlan preserves a plan unchanged when no share is set,
    applies softThresholdTokensShare / reserveTokensFloorShare, and scales
    correctly across 8k ↔ 1M windows
  • resolveShareBasedTokenBudget handles the full share/absolute/fallback lattice

Test plan

  • pnpm run config:schema:gen — schema.base.generated.ts regenerated cleanly
  • node scripts/run-vitest.mjs run --config test/vitest/vitest.agents.config.ts src/agents/ — 1343 pass
  • node scripts/run-vitest.mjs run --config test/vitest/vitest.auto-reply-reply.config.ts src/auto-reply/reply/ — 1061 pass
  • node scripts/run-vitest.mjs run --config test/vitest/vitest.config.ts src/config/ — 2080 pass
  • node scripts/run-vitest.mjs run --config test/vitest/vitest.plugins.config.ts src/plugins/ — 36 pass
  • node scripts/run-vitest.mjs run --config test/vitest/vitest.extension-memory.config.ts — 564 pass, 3 skipped
  • pnpm tsgo — no new type errors in touched files (pre-existing unrelated errors in whatsapp/ui remain)
  • pnpm lint — no new oxlint errors in touched files

…gets

Adds `*Share` sibling fields so a single agent config behaves sensibly
across heterogeneous context windows (GLM/Claude 200k, Kimi K2 1M, small-window
models). When a share is set, the absolute-token budget is computed as
`Math.floor(contextWindowTokens * share)`; otherwise the existing absolute
field applies. Backward compatible — no existing absolute field is removed.

Fields extended:
- agents.defaults.compaction.reserveTokensShare
- agents.defaults.compaction.keepRecentTokensShare
- agents.defaults.compaction.reserveTokensFloorShare
- agents.defaults.compaction.memoryFlush.softThresholdTokensShare

Precedence: share (with known contextWindowTokens) > absolute > default.
The resolved reserveTokensFloor is still applied as an absolute minimum
on the final reserve-tokens value.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: L labels Apr 14, 2026
@greptile-apps

greptile-apps Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds four ratio-based *Share sibling fields (reserveTokensShare, keepRecentTokensShare, reserveTokensFloorShare, softThresholdTokensShare) to the compaction config so a single config can scale correctly across heterogeneous model context windows. The core helper (resolveShareBasedTokenBudget), schema types, and most call sites are implemented cleanly.

  • P1: In runPreflightCompactionIfNeeded (agent-runner-memory.ts lines 390–393), when no memory flush plan is configured, the reserveTokensFloor fallback reads the raw absolute config field (?? 20_000) instead of calling resolveCompactionReserveTokensFloor(cfg, contextWindowTokens). This silently ignores reserveTokensFloorShare on the preflight compaction threshold path when memory flush is disabled, causing the gate to fire too late for large-window models.

Confidence Score: 4/5

  • Safe to merge after fixing the preflight compaction threshold fallback that ignores reserveTokensFloorShare when memory flush is disabled.
  • One P1 behavioral bug: reserveTokensFloorShare is not honored in the runPreflightCompactionIfNeeded path when no memory flush plan is configured, causing incorrect compaction threshold calculation. All other share fields, the core helper, schema types, and the memory-flush path are correctly implemented with solid test coverage.
  • src/auto-reply/reply/agent-runner-memory.ts — the reserveTokensFloor fallback at lines 390–393 needs to call resolveCompactionReserveTokensFloor instead of accessing the raw absolute config field.

Comments Outside Diff (1)

  1. src/auto-reply/reply/agent-runner-memory.ts, line 390-393 (link)

    P1 reserveTokensFloorShare silently ignored in preflight path when no memory flush plan

    When rawMemoryFlushPlan is null (memory flush not configured), memoryFlushPlan is null, so the ?? params.cfg.agents?.defaults?.compaction?.reserveTokensFloor ?? 20_000 fallback uses the raw absolute value and never consults resolveCompactionReserveTokensFloor. A user who sets reserveTokensFloorShare = 0.1 without a memory flush plan will get a floor of 20 000 instead of the share-based value (e.g. 100 000 for a 1M window), making the preflight compaction threshold too permissive and triggering compaction later than intended.

    The fix is to replace the raw fallback with the helper that already handles both share and absolute:

    resolveCompactionReserveTokensFloor is already imported in memory-flush.ts; it needs to be re-exported from there or imported directly from pi-settings.js in this file.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/auto-reply/reply/agent-runner-memory.ts
    Line: 390-393
    
    Comment:
    **`reserveTokensFloorShare` silently ignored in preflight path when no memory flush plan**
    
    When `rawMemoryFlushPlan` is null (memory flush not configured), `memoryFlushPlan` is null, so the `?? params.cfg.agents?.defaults?.compaction?.reserveTokensFloor ?? 20_000` fallback uses the raw absolute value and never consults `resolveCompactionReserveTokensFloor`. A user who sets `reserveTokensFloorShare = 0.1` without a memory flush plan will get a floor of 20 000 instead of the share-based value (e.g. 100 000 for a 1M window), making the preflight compaction threshold too permissive and triggering compaction later than intended.
    
    The fix is to replace the raw fallback with the helper that already handles both share and absolute:
    
    
    
    `resolveCompactionReserveTokensFloor` is already imported in `memory-flush.ts`; it needs to be re-exported from there or imported directly from `pi-settings.js` in this file.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/agent-runner-memory.ts
Line: 390-393

Comment:
**`reserveTokensFloorShare` silently ignored in preflight path when no memory flush plan**

When `rawMemoryFlushPlan` is null (memory flush not configured), `memoryFlushPlan` is null, so the `?? params.cfg.agents?.defaults?.compaction?.reserveTokensFloor ?? 20_000` fallback uses the raw absolute value and never consults `resolveCompactionReserveTokensFloor`. A user who sets `reserveTokensFloorShare = 0.1` without a memory flush plan will get a floor of 20 000 instead of the share-based value (e.g. 100 000 for a 1M window), making the preflight compaction threshold too permissive and triggering compaction later than intended.

The fix is to replace the raw fallback with the helper that already handles both share and absolute:

```suggestion
  const reserveTokensFloor =
    memoryFlushPlan?.reserveTokensFloor ??
    resolveCompactionReserveTokensFloor(params.cfg, contextWindowTokens);
```

`resolveCompactionReserveTokensFloor` is already imported in `memory-flush.ts`; it needs to be re-exported from there or imported directly from `pi-settings.js` in this file.

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

---

This is a comment left during a code review.
Path: src/agents/pi-settings.ts
Line: 99

Comment:
**Inconsistent `>= 0` guard vs `> 0` in sibling helper**

`resolveShareBasedTokenBudget` uses `if (computed > 0)` to decide whether the share path produced a usable value and falls back to the absolute/default otherwise. Here the check is `computed >= 0`, so when `Math.floor(contextWindowTokens * share) === 0` (e.g. a 50-token window with `share = 0.01`), the function returns 0 as the floor instead of falling through to `reserveTokensFloor`/`DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR`. For any realistic context window this doesn't matter, but the inconsistency is surprising for future readers.

```suggestion
    if (computed > 0) {
```

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

Reviews (1): Last reviewed commit: "feat(config): add ratio-based sibling fi..." | Re-trigger Greptile

Comment thread src/agents/pi-settings.ts
contextWindowTokens > 0
) {
const computed = Math.floor(contextWindowTokens * share);
if (computed >= 0) {

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.

P2 Inconsistent >= 0 guard vs > 0 in sibling helper

resolveShareBasedTokenBudget uses if (computed > 0) to decide whether the share path produced a usable value and falls back to the absolute/default otherwise. Here the check is computed >= 0, so when Math.floor(contextWindowTokens * share) === 0 (e.g. a 50-token window with share = 0.01), the function returns 0 as the floor instead of falling through to reserveTokensFloor/DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR. For any realistic context window this doesn't matter, but the inconsistency is surprising for future readers.

Suggested change
if (computed >= 0) {
if (computed > 0) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-settings.ts
Line: 99

Comment:
**Inconsistent `>= 0` guard vs `> 0` in sibling helper**

`resolveShareBasedTokenBudget` uses `if (computed > 0)` to decide whether the share path produced a usable value and falls back to the absolute/default otherwise. Here the check is `computed >= 0`, so when `Math.floor(contextWindowTokens * share) === 0` (e.g. a 50-token window with `share = 0.01`), the function returns 0 as the floor instead of falling through to `reserveTokensFloor`/`DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR`. For any realistic context window this doesn't matter, but the inconsistency is surprising for future readers.

```suggestion
    if (computed > 0) {
```

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

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

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f22fd96b69

ℹ️ 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 +37 to +41
>(params: { plan: TPlan; cfg?: OpenClawConfig; contextWindowTokens: number }): TPlan {
const compactionCfg = params.cfg?.agents?.defaults?.compaction;
const memoryFlushCfg = compactionCfg?.memoryFlush;
const softThresholdTokens = resolveShareBasedTokenBudget({
share: memoryFlushCfg?.softThresholdTokensShare,

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.

P2 Badge Ignore share overrides when context window is a fallback guess

This helper always consumes a numeric contextWindowTokens, so callers that only have the fallback from resolveMemoryFlushContextWindowTokens (defaulting unknown models to 200k) still apply softThresholdTokensShare/reserveTokensFloorShare instead of falling back to the plan’s absolute values. In custom/unknown-model runs, that can inflate flush thresholds and defer compaction/flush decisions far past the real window size. The share override should only run when the model window is truly resolved, otherwise keep the existing absolute plan values.

Useful? React with 👍 / 👎.

Audited every site that consumes the new `*Share` fields and confirmed the
context-window base is always the SESSION-active model:

- `pi-embedded-runner/run/attempt.ts` reads `params.model.contextWindow`,
  where `params.model` is `effectiveModel` from `resolveEffectiveRuntimeModel`
  using the per-run `provider`/`modelId` (which already honors session model
  overrides via `resolveHookModelSelection` and persisted session overrides).
- `pi-embedded-runner/compact.ts` reads `model?.contextWindow` from the model
  resolved by `resolveModelAsync(provider, modelId, …)` where provider/modelId
  flow from `params.provider`/`params.model` (session-scoped) through
  `resolveEmbeddedCompactionTarget`, with the documented `compaction.model`
  override taking precedence only when explicitly configured.
- `auto-reply/reply/memory-flush.ts` resolves the window via
  `resolveMemoryFlushContextWindowTokens` from `followupRun.run.model` —
  the per-run model captured at queue time, which is session-scoped.

No code path computes the share against a statically baked agent-default
model, so no fix to the resolution logic is required. Documentation,
however, was implicit. This commit:

- Extends JSDoc on each `*Share` field in `types.agent-defaults.ts` to
  state explicitly that the base is the context window of the model
  active for the CURRENT SESSION, falling back to the agent's default
  model, then to DEFAULT_CONTEXT_TOKENS.
- Adds a regression test in `reply-state.test.ts` proving that two
  sessions of the same agent (one inheriting the agent default
  glm-4.7@200k, one overriding to kimi-k2@1M) compute different
  `reserveTokensFloor` and `softThresholdTokens` from the same share
  config — locking in the session-scoped semantics so a future refactor
  cannot silently regress to agent-scoped resolution.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

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

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a96f4450f8

ℹ️ 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".

cwd: effectiveWorkspace,
agentDir,
cfg: params.config,
contextWindowTokens: params.model.contextWindow,

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.

P2 Badge Avoid resolving share budgets from fallback context windows

Passing params.model.contextWindow here causes share-based compaction budgets to run even when the model window is not actually known, because the model resolver synthesizes contextWindow from DEFAULT_CONTEXT_TOKENS when metadata is missing (see src/agents/pi-embedded-runner/model.ts:551-554). With this commit, that fallback value is treated as authoritative, so reserveTokensShare/keepRecentTokensShare/reserveTokensFloorShare are computed from 200k instead of falling back to absolute/default values, which can badly over-reserve on small or custom models; this same pattern is also introduced in src/agents/pi-embedded-runner/compact.ts.

Useful? React with 👍 / 👎.

@cheapestinference

Copy link
Copy Markdown
Contributor Author

CI note — 2 jobs failed on initial run but do not appear to be caused by this PR:

  • checks-node-agentictool-result-context-guard.test.ts:512 (1ms timestamp drift in a snapshot) and models-config.providers.google-antigravity.test.ts:78 (structural vs reference equality)
  • checks-node-core — exit 1 at setup (5s)

Neither test file is touched by this PR (verified via git log --oneline origin/main..HEAD -- <file> — empty). Main itself has been red for most recent pushes today, suggesting broader flakiness not tied to this change.

Happy to rebase or push an empty commit to re-trigger CI once a maintainer confirms. The PR itself passes locally:

  • pi-settings.test.ts: 25/25
  • auto-reply/reply/ suite: 1062/1062 (+1 new test)
  • Typecheck and lint clean on touched files.

@clawsweeper

clawsweeper Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close as superseded: the same percentage/share-based compaction-config work is now tracked by the canonical feature issue and a newer current-main PR, while this older branch is dirty, still has behavioral review blockers, and lacks real behavior proof.

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Review details

Best possible solution:

Use the canonical feature issue and newer current-main PR as the merge path, preserving useful ideas from this branch only if maintainers choose that API shape.

Do we have a high-confidence way to reproduce the issue?

Not applicable: this is a new public config capability, not a broken existing behavior. Source inspection gives high confidence that current main is still absolute-token-only for these compaction budgets.

Is this the best way to solve the issue?

No. The active current-main path in #81176 is a better place to settle the API, while this branch needs a substantial rebase, behavioral fixes, and real-run proof before it could merge.

Security review:

Security review cleared: The diff is limited to config schema/help/generated metadata, compaction runtime logic, and tests; no workflow, dependency, lockfile, secret-handling, publishing, or artifact-download changes were found.

What I checked:

  • Current main remains absolute-token-only: Current main still has numeric reserveTokens, keepRecentTokens, and reserveTokensFloor schema entries and no reserveTokensShare, keepRecentTokensShare, reserveTokensFloorShare, softThresholdTokensShare, resolveShareBasedTokenBudget, or applyMemoryFlushSharesToPlan implementation. (src/config/zod-schema.agent-defaults.ts:174, 3b7d01b63f02)
  • Current runtime uses absolute budgets with a context cap: applyPiCompactionSettingsFromConfig reads absolute numeric config values and caps the reserve floor against contextTokenBudget, which any share implementation must preserve. (src/agents/pi-settings.ts:68, 3b7d01b63f02)
  • PR head still has behavioral review blockers: On the PR head, preflight compaction applies share overrides only when a memory-flush plan exists, then falls back to raw reserveTokensFloor or 20_000; a share-only floor is ignored when no flush plan is configured. (src/auto-reply/reply/agent-runner-memory.ts:390, a96f4450f816)
  • PR head uses fallback windows as authoritative: applyMemoryFlushSharesToPlan accepts any numeric contextWindowTokens, while the resolver can synthesize DEFAULT_CONTEXT_TOKENS; that contradicts the PR's documented fallback-to-absolute behavior for unknown model windows. (src/auto-reply/reply/memory-flush.ts:37, a96f4450f816)
  • Newer canonical PR exists: feat(agents): add context-window-relative compaction budget shares #81176 is open, targets the same context-window-relative compaction budget feature from [Feature]: Percentage-based compaction config #72790, and is based on newer current-main plumbing. (9ab9b50ed1ba)
  • Live PR state is dirty and unmerged: Live GitHub metadata reports this PR as open, unmerged, mergeStateStatus: DIRTY, with head a96f4450f81687dd50b0c953bbcfac27cf96b1f6; keeping two competing open implementations is not useful now that the newer PR and feature issue own the path. (a96f4450f816)

Likely related people:

  • Takhoffman: Commit c1ac37a exposed reserveTokens, keepRecentTokens, and reserveTokensFloor across the config, schema, and Pi settings paths this feature extends. (role: introduced public compaction config surface; confidence: high; commits: c1ac37a6410a; files: src/agents/pi-settings.ts, src/config/types.agent-defaults.ts, src/config/zod-schema.agent-defaults.ts)
  • openperf: Commit 4bc46cc added the current context-budget cap for reserveTokensFloor, which a share resolver must preserve for small-context models. (role: adjacent runtime contributor; confidence: medium; commits: 4bc46ccfedc4; files: src/agents/pi-settings.ts, src/agents/pi-settings.test.ts, src/agents/pi-project-settings.ts)
  • steipete: Commit 29af4ad recently touched memory flush, config metadata, docs, and preflight compaction threshold paths that intersect this feature. (role: recent memory and compaction contributor; confidence: medium; commits: 29af4add2a8e; files: src/auto-reply/reply/agent-runner-memory.ts, src/auto-reply/reply/memory-flush.ts, src/config/types.agent-defaults.ts)
  • amknight: Commit bf3b994 recently adjusted preflight compaction pressure estimation in agent-runner-memory.ts, so this account is relevant for threshold-behavior review. (role: recent adjacent preflight contributor; confidence: low; commits: bf3b99437816; files: src/auto-reply/reply/agent-runner-memory.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 3b7d01b63f02.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant