Skip to content

[Bug]: session_status totalTokens always shows contextTokens (100%) after multi-turn tool-use runs #17016

@alextempr

Description

@alextempr

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

  1. Configure OpenClaw with embedded provider (API key, not CLI) using Claude with prompt caching enabled
  2. Start a conversation that triggers tool use (any tool call works)
  3. After the agent completes a multi-turn run (2+ API calls in one response), check session_status or /status
  4. totalTokens will show equal to contextTokens (e.g., 200000/200000 = 100%)
  5. Single-turn responses (no tool use) show correct values
  6. 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:

  1. recordAssistantUsage accumulates cacheRead per API turn → correct per-turn tracking exists via lastTurnUsage
  2. getUsageTotals() correctly includes lastTurnTotal field
  3. BUG: mergeUsageIntoAccumulator(usageAccumulator, attemptUsage) does NOT propagate lastTurnTotal
  4. BUG: toNormalizedUsage(usageAccumulator) does NOT include lastTurnTotal
  5. deriveSessionTotalTokens already checks for lastTurnTotal but it's always undefined
  6. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions