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
- 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.
- Open
$(openclaw dir)/agents/<agent>/sessions/sessions.json and find the entry for the session.
- Open
$(openclaw dir)/agents/<agent>/sessions/<sessionId>.jsonl and sum usage.cost.total across every type=message / role=assistant line.
- 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):
-
src/auto-reply/reply/session-usage.ts:135-156 — persistSessionUsageUpdate
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
}
-
src/agents/command/session-store.ts:122-136 — updateSessionStoreAfterAgentRun
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
}
-
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.
Bug type
Behavior bug (incorrect output/state without crash)
Beta release blocker
No
Summary
sessions.json.estimatedCostUsdis inflated 1×–72× over the real OpenRouter cost (theusage.cost.totalfield inside the JSONL transcript) because three persist sites snapshot tokens but accumulate cost.Steps to reproduce
$(openclaw dir)/agents/<agent>/sessions/sessions.jsonand find the entry for the session.$(openclaw dir)/agents/<agent>/sessions/<sessionId>.jsonland sumusage.cost.totalacross everytype=message/role=assistantline.sessions.json.estimatedCostUsd / Σ(JSONL usage.cost.total)should be≈1.0but 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 assessions.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):
ee4a3bb37c9db1d2f3b2742eed17328f2157016eSingle-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):
src/auto-reply/reply/session-usage.ts:135-156—persistSessionUsageUpdatesrc/agents/command/session-store.ts:122-136—updateSessionStoreAfterAgentRunsrc/cron/isolated-agent/run.ts:597-603— cron session path (same pattern).Why ratio is variable (not a pure N² accumulation):
persistSessionUsageUpdate/updateSessionStoreAfterAgentRuncan 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). Theusageobject is always the run-cumulative total, so tokens snapshot cleanly to the correct final value, while cost addsexistingCost + 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
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.Additional information
Fix: snapshot cost instead of accumulating, mirroring how tokens are already written.
runEstimatedCostUsd = estimateSessionRunCostUsd(usage, ...)whereusageis the run-cumulative usage, so snapshotting yields the same semantics as tokens already use. PR with the one-line-per-site fix incoming.