Skip to content

[Bug]: sessions.json.estimatedCostUsd inflated 1x-72x by three persist sites that accumulate cost while snapshotting tokens #69347

Description

@antongoryachev

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

sessions.json.estimatedCostUsd is inflated 1×–72× over the real OpenRouter cost (the usage.cost.total field inside the JSONL transcript) because three persist sites snapshot tokens but accumulate cost.

Steps to reproduce

  1. Run an OpenClaw agent session that triggers multiple persist calls per run (auto-reply followup, agent-runner main path, heartbeat, etc.) — most multi-turn sessions do.
  2. Open $(openclaw dir)/agents/<agent>/sessions/sessions.json and find the entry for the session.
  3. Open $(openclaw dir)/agents/<agent>/sessions/<sessionId>.jsonl and sum usage.cost.total across every type=message / role=assistant line.
  4. Compare: sessions.json.estimatedCostUsd / Σ(JSONL usage.cost.total) should be ≈1.0 but is instead a multiple ranging from 1× to 72× depending on how many times internal persist handlers fired.

Expected behavior

sessions.json.estimatedCostUsd ≈ Σ(JSONL usage.cost.total) for the session, just as sessions.json.inputTokens == Σ(JSONL usage.input) already holds (tokens are snapshotted correctly; cost should be too).

Actual behavior

Cost is compounded once per persist call. Live sample from 2026-04-20 on a production VPS (OpenClaw 2026.4.15, 93 sessions scanned):

sessionId model sessions.json JSONL sum ratio
ee4a3bb3 z-ai/glm-5.1 $3.378 $0.047 72.59×
7c9db1d2 google/gemma-4-31b-it $0.309 $0.007 42.80×
f3b2742e anthropic/claude-sonnet-4 $5.727 $0.153 37.52×
ed17328f z-ai/glm-5.1 $2.672 $0.083 32.21×
2157016e z-ai/glm-5.1 $0.610 $0.046 13.14×

Single-turn Claude Sonnet sessions (N=87) consistently show ratio = 1.00× — the cost math itself is correct; only the write path is defective.

OpenClaw version

2026.4.15

Operating system

Ubuntu 24.04 (Docker)

Install method

npm global, running inside a Docker container

Model

Reproduced on z-ai/glm-5.1, anthropic/claude-sonnet-4, google/gemma-4-31b-it, z-ai/glm-5-turbo (provider: openrouter).

Provider / routing chain

openclaw -> openrouter ->

Additional provider/model setup details

Effect is independent of model/provider. Not related to pricing-config lookups — single-turn Claude Sonnet sessions compute the correct cost, proving estimateSessionRunCostUsd() is mathematically right. The defect is in the persist path, which accumulates rather than snapshots.

Logs, screenshots, and evidence

Root cause (three sites, same asymmetric pattern — tokens SNAPSHOT, cost ACCUMULATE):

  1. src/auto-reply/reply/session-usage.ts:135-156persistSessionUsageUpdate

    if (hasUsage) {
      patch.inputTokens = params.usage?.input ?? 0;    // SNAPSHOT
      patch.outputTokens = params.usage?.output ?? 0;  // SNAPSHOT
      patch.cacheRead = cacheUsage?.cacheRead ?? 0;    // SNAPSHOT
      patch.cacheWrite = cacheUsage?.cacheWrite ?? 0;  // SNAPSHOT
    }
    if (runEstimatedCostUsd !== undefined) {
      patch.estimatedCostUsd = existingEstimatedCostUsd + runEstimatedCostUsd;  // ACCUMULATE <- bug
    }
  2. src/agents/command/session-store.ts:122-136updateSessionStoreAfterAgentRun

    next.inputTokens = input;                    // SNAPSHOT
    next.outputTokens = output;                  // SNAPSHOT
    next.cacheRead = usage.cacheRead ?? 0;       // SNAPSHOT
    next.cacheWrite = usage.cacheWrite ?? 0;     // SNAPSHOT
    if (runEstimatedCostUsd !== undefined) {
      next.estimatedCostUsd =
        (resolveNonNegativeNumber(entry.estimatedCostUsd) ?? 0) + runEstimatedCostUsd;  // ACCUMULATE <- bug
    }
  3. src/cron/isolated-agent/run.ts:597-603 — cron session path (same pattern).

Why ratio is variable (not a pure N² accumulation): persistSessionUsageUpdate / updateSessionStoreAfterAgentRun can be invoked multiple times per agent run via at least four paths — agent-command finalize, auto-reply followup, agent-runner main path, and heartbeat/reply fallback transitions (see #62954). The usage object is always the run-cumulative total, so tokens snapshot cleanly to the correct final value, while cost adds existingCost + cost(cumulative_usage) on every redundant persist call. With 3 call sites × N runs × heartbeat writes, inflation is empirically 1×–72×.

Related prior bug on the underreporting side: #53734 (toNormalizedUsage() discards accumulated input/cache tokens — cost tracking underreports ~80% of actual billed usage). This issue is the overreporting counterpart.

Impact and severity

  • Affected: any consumer of sessions.json.estimatedCostUsd — OpenClaw-internal session cost summaries, per-model/day usage dashboards, budget enforcement, cost telemetry in plugins, and any third-party tooling that reads this file.
  • Severity: Behavior bug, high confidence; up to 72× overreporting on live sessions masks real cost trends and breaks any budget signal that uses this field.
  • Frequency: Deterministic — every multi-persist session is affected. Ratio correlates with persist-call count per run, not turn count.
  • Consequence: Budget-guard false alarms, misleading dashboards, incorrect per-agent/per-model cost reporting. Because the underlying math is correct and only the write path is defective, the fix is a one-line change per site.

Additional information

Fix: snapshot cost instead of accumulating, mirroring how tokens are already written. runEstimatedCostUsd = estimateSessionRunCostUsd(usage, ...) where usage is the run-cumulative usage, so snapshotting yields the same semantics as tokens already use. PR with the one-line-per-site fix incoming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions