fix: preserve runtime token budget in deferred context-engine maintenance#66820
Conversation
Greptile SummaryThis PR fixes deferred context-engine maintenance falling back to a synthetic 128k token budget by threading Confidence Score: 5/5Safe to merge — focused, additive fix with no breaking changes and good test coverage of the previously-unthreaded token budget path. All changed surfaces are purely additive (new optional parameters and explicit type fields), existing call-sites pass the values correctly, the spread propagation through No files require special attention. Reviews (1): Last reviewed commit: "fix(context-engine): pass deferred maint..." | Re-trigger Greptile |
Thread tokenBudget through the after-turn runtime context so background context-engine maintenance reuses the real model context window instead of falling back to 128k. Also pass through a best-effort currentTokenCount from the latest call total and make the runtime context type explicit about both fields. Regeneration-Prompt: | OpenClaw already passed the real context token budget into direct context-engine calls like afterTurn and assemble, but deferred maintain() reused only the runtimeContext object and that object did not carry tokenBudget. Lossless Claw therefore fell back to 128k during background maintenance, which made budget-trigger fire much more aggressively than the live model context warranted. Thread the real contextTokenBudget into buildAfterTurnRuntimeContext so deferred maintenance receives the same budget, and pass a straightforward best-effort currentTokenCount from the latest call total while the relevant data is already in scope. Keep the change additive, update the runtime-context type, and cover the background maintenance/runtime-context behavior with focused tests.
eabef29 to
95c5d3c
Compare
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🟡 Unvalidated token usage telemetry in derivePromptTokens can cause incorrect/abusive token counts
Description
Security/robustness impact when provider/SDK telemetry is malformed or attacker-controlled:
Vulnerable code: export function derivePromptTokens(usage?: {
input?: number;
cacheRead?: number;
cacheWrite?: number;
}): number | undefined {
if (!usage) {
return undefined;
}
const input = usage.input ?? 0;
const cacheRead = usage.cacheRead ?? 0;
const cacheWrite = usage.cacheWrite ?? 0;
const sum = input + cacheRead + cacheWrite;
return sum > 0 ? sum : undefined;
}RecommendationHarden
Example fix: function clampNonNegativeFinite(v: unknown): number {
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
}
export function derivePromptTokens(usage?: {
input?: unknown;
cacheRead?: unknown;
cacheWrite?: unknown;
}, opts?: { max?: number }): number | undefined {
if (!usage) return undefined;
const input = clampNonNegativeFinite(usage.input);
const cacheRead = clampNonNegativeFinite(usage.cacheRead);
const cacheWrite = clampNonNegativeFinite(usage.cacheWrite);
let sum = input + cacheRead + cacheWrite;
if (opts?.max !== undefined) sum = Math.min(sum, opts.max);
return sum > 0 ? Math.floor(sum) : undefined;
}If you prefer centralizing sanitation, clamp Analyzed PR: #66820 at commit Last updated on: 2026-04-14T22:21:47Z |
Summary
runtimeContextwithout the active model's token budget, so maintenance fell back to a synthetic default budget instead of the real one from the turn that queued the work.buildAfterTurnRuntimeContext()now carries forwardtokenBudget, and it also forwards a best-effortcurrentTokenCountwhen the latest call usage total is available; deferred maintenance tests now lock in those fields.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
contextTokenBudget, so deferred maintenance later ran with no budget inruntimeContextand fell back to the context engine's internal default.afterTurn()token budget wiring but did not assert that deferred maintenance received the same runtime budget context.afterTurn()call arguments, so omissions in the runtime-context shape only show up once work is replayed later.Regression Test Plan (if applicable)
src/agents/pi-embedded-runner/context-engine-maintenance.test.tsandsrc/agents/pi-embedded-runner/run/attempt.test.tstokenBudgetandcurrentTokenCount, and deferred maintenance receives those same values when it invokes the context engine.afterTurn()budget handling was already exercised indirectly; the deferred maintenance path was not.User-visible / Behavior Changes
None.
Diagram (if applicable)
Security Impact (required)
Yes/No) NoYes/No) NoYes/No) NoYes/No) NoYes/No) NoYes, explain risk + mitigation:Repro + Verification
Environment
Steps
contextTokenBudget.maintain().Expected
tokenBudgetas the completed turn, pluscurrentTokenCountwhen available.Actual
tokenBudgetinruntimeContextand relied on the engine fallback.Evidence
Attach at least one:
Human Verification (required)
currentTokenCountis only included when a finite positive total is available; existing runtime-context fields remain intact.Review Conversations
If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.
Compatibility / Migration
Yes/No) YesYes/No) NoYes/No) NoRisks and Mitigations
currentTokenCountcould be stale or absent on some runs.