-
-
Notifications
You must be signed in to change notification settings - Fork 69.3k
[Bug]: session_status totalTokens always shows contextTokens (100%) after multi-turn tool-use runs #17016
Description
Summary
session_status (and /status) always reports totalTokens equal to contextTokens (200000/200000 = 100%) after any multi-turn tool-use run when using the embedded provider flow (API key direct, not CLI). The root cause is that recordAssistantUsage accumulates cacheRead across ALL API turns in a run, and deriveSessionTotalTokens operates on accumulated values that always exceed contextTokens, causing the Math.min(accumulated, contextTokens) clamp to always return contextTokens.
The fix infrastructure already exists: deriveSessionTotalTokens checks for lastTurnTotal (per-turn usage), but mergeUsageIntoAccumulator and toNormalizedUsage drop this field before it reaches the function.
Steps to reproduce
- Configure OpenClaw with embedded provider (API key, not CLI) using Claude with prompt caching enabled
- Start a conversation that triggers tool use (any tool call works)
- After the agent completes a multi-turn run (2+ API calls in one response), check
session_statusor/status totalTokenswill show equal tocontextTokens(e.g., 200000/200000 = 100%)- Single-turn responses (no tool use) show correct values
- The bug re-triggers on every subsequent multi-turn run
Expected behavior
session_status should report the actual context utilization based on the last API turn's token count (which correctly reflects the current context window usage), not accumulated totals across all turns in the run.
Actual behavior
totalTokens is always clamped to contextTokens (200000) after any multi-turn run. Example from session transcript:
| Run | API Turns | Accumulated input+cacheRead+cacheWrite | Stored totalTokens |
|---|---|---|---|
| 8 turns | 8 | 278,606 | 200,000 |
| 15 turns | 15 | 1,028,331 | 200,000 |
| 29 turns | 29 | 3,665,887 | 200,000 |
No single API turn ever had totalTokens = 200000 (max observed: 180,598). The 200000 value only comes from clamping accumulated multi-turn usage.
OpenClaw version
2026.2.9
Operating system
macOS 15.4 (Apple Silicon M4)
Install method
npm global
Logs, screenshots, and evidence
Impact and severity
Affected: All users with embedded provider flow (API key direct) and prompt caching enabled (likely most Anthropic API users)
Severity: Medium — cosmetic/informational, does NOT affect actual API calls or context management
Frequency: 100% reproducible on every multi-turn tool-use run
Consequence: Users cannot gauge actual context utilization via session_status. The status bar always shows 100% after any tool use, which is misleading and can cause unnecessary concern about context exhaustion. Also affects any automation that relies on totalTokens for decisions (e.g., custom compaction triggers).
Additional information
Root Cause Analysis
The data flow in runEmbeddedPiAgent:
recordAssistantUsageaccumulatescacheReadper API turn → correct per-turn tracking exists vialastTurnUsagegetUsageTotals()correctly includeslastTurnTotalfield- BUG:
mergeUsageIntoAccumulator(usageAccumulator, attemptUsage)does NOT propagatelastTurnTotal - BUG:
toNormalizedUsage(usageAccumulator)does NOT includelastTurnTotal deriveSessionTotalTokensalready checks forlastTurnTotalbut it's always undefined- Falls through to
derivePromptTokens(input + cacheRead + cacheWrite)on accumulated values → always exceeds contextTokens → clamped to 200000
Suggested Fix
In runEmbeddedPiAgent, track lastTurnTotal separately alongside the accumulator:
// After creating usageAccumulator:
let lastTurnTotalForSession = undefined;
// After each mergeUsageIntoAccumulator call:
if (attempt.attemptUsage?.lastTurnTotal > 0) {
lastTurnTotalForSession = attempt.attemptUsage.lastTurnTotal;
}
// Before passing usage to agentMeta:
const usage = toNormalizedUsage(usageAccumulator);
if (usage && lastTurnTotalForSession > 0) {
usage.lastTurnTotal = lastTurnTotalForSession;
}This makes the existing deriveSessionTotalTokens check work correctly without changing the accumulation logic.
Alternatively, mergeUsageIntoAccumulator could be updated to preserve lastTurnTotal (overwrite, not accumulate).