Skip to content

feat: tokenBudgetMax — absolute, window-independent injection ceiling - #358

Merged
xDarkicex merged 2 commits into
xDarkicex:mainfrom
Marvinthebored:feat/token-budget-max
Aug 1, 2026
Merged

feat: tokenBudgetMax — absolute, window-independent injection ceiling#358
xDarkicex merged 2 commits into
xDarkicex:mainfrom
Marvinthebored:feat/token-budget-max

Conversation

@Marvinthebored

Copy link
Copy Markdown
Contributor

Summary

Memory injection is sized as tokenBudgetFraction × model_context_window, so injection scales with the window. On a large-window model (e.g. a 1M-token model) a fresh session can show ~170–200k of context coming purely from memory injection — and because the recalled prefix changes per turn, it's rewritten into the prompt cache every turn.

tokenBudgetMax adds an absolute ceiling on injection, independent of the window:

Stage What Where
1 — base cap The budget handed to the daemon's assemble is capped to min(window, tokenBudgetMax / tokenBudgetFraction), so the daemon pre-trims its sub-channels. the assembleContextInternal call
2 — enforcer After all five injection paths land (main assemble, continuity, exact-recall, predictive_context, beforeTurn), the combined systemPromptAddition is truncated to tokenBudgetMax. the single post-assembly choke point

Stage 2 is the real enforcer: several paths (e.g. predictive_context) size against the real window via resolveEffectiveAssembleBudget, not the capped budget, so a base cap alone leaks.

Only injection is bounded. The cap is never applied to enforceTokenBudgetInvariant or compaction — those operate against the real window — so the usable conversation window is untouched. Unset preserves current behavior (no-op).

"libravdb-memory": {
  "config": {
    "tokenBudgetMax": 35000
  }
}

Why an absolute cap (not just a smaller fraction)

A fraction can't express "never inject more than N tokens regardless of model." Operators running the same plugin across a 200k model and a 1M model want a stable injection footprint (and a stable per-turn cache cost), not one that 5×'s when they switch models.

Testing

  • tsc --noEmit clean; tsc -p tsconfig.build.json + bundle clean.
  • Unit suite green, including 2 new tests:
    • cap active: with tokenBudgetMax: 1000, tokenBudgetFraction: 0.2 and a 1M window, the daemon receives tokenBudget = 5000 (stage 1) and the injected systemPromptAddition is truncated to ≤ the cap (stage 2: injectionBefore=9000 → injectionAfter=1000).
    • unset: the full window is passed through and injection is uncapped.
  • The same two-stage approach has run in a production multi-agent gateway (hand-patched on an earlier release); this is the clean port to current main with tests. Happy to validate any requested changes there.

Notes

Backward compatible and fully opt-in. Schema entry added (additionalProperties: false, so the key must be declared). A trim, when it fires, logs at info: LibraVDB tokenBudgetMax trim ... injectionBefore=... injectionAfter=... cap=....

Separately, the observed real-token cost of injection runs ~2× the plugin's estimate for dense/structured/CJK memory content (the APPROX_CHARS_PER_TOKEN = 4 assumption vs ~1.7 actual). That's a distinct token-accounting issue; this PR is just the ceiling mechanism. Happy to file the accounting observation separately if useful.

…ling

Memory injection is sized as tokenBudgetFraction × the model's context window,
so a large-window model (e.g. 1M) balloons per-turn injection proportionally —
on a 1M-window model a fresh session showed ~170-200k of context coming purely
from memory injection, rewritten into the cache every turn.

tokenBudgetMax caps injection in absolute tokens, independent of the window:

- Stage 1 (base cap): the budget handed to the daemon's assemble is capped to
  min(window, tokenBudgetMax / tokenBudgetFraction) so the daemon pre-trims its
  sub-channels.
- Stage 2 (enforcer): after all five injection paths land (main assemble,
  continuity, exact-recall, predictive_context, beforeTurn), the combined
  systemPromptAddition is truncated to tokenBudgetMax. This is the real ceiling
  because several paths size against the real window, not the capped budget.

Only injection is bounded — the cap is never applied to
enforceTokenBudgetInvariant or compaction, so the usable conversation window is
untouched. Unset preserves current behavior (no-op).

Adds the config type, JSON schema entry, and unit tests for both stages plus
the uncapped pass-through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@xDarkicex, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a097bcdd-98d4-43e0-9443-9226c8f6adc9

📥 Commits

Reviewing files that changed from the base of the PR and between 08fbac6 and bc23ee3.

📒 Files selected for processing (4)
  • openclaw.plugin.json
  • src/context-engine.ts
  • src/types.ts
  • test/unit/context-engine.test.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@compoodment compoodment left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Vale Review — PR #358

Quality: Q3/5 — promising
Head: 02291dc

Findings:

  • None

Proof gaps: Local targeted test compile was blocked by a stale/mismatched contracts dependency in the available dependency tree; PR author reports type/unit coverage, but I could not independently verify the new tests in this run.

Verdict: comment-only — the two-stage cap is well-scoped and covered by focused tests on paper; no proven blocker from the diff review.

– Vale

@xDarkicex xDarkicex added the release:patch Bump patch version on merge label Aug 1, 2026
@xDarkicex
xDarkicex merged commit 2b9f3b6 into xDarkicex:main Aug 1, 2026
1 check was pending
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:patch Bump patch version on merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants