fix(core): preserve totalTokens on zero usage reports
When an LLM provider (like vLLM) reports 0 tokens used, OpenClaw previously cleared the session's totalTokens by setting it to undefined. This caused the TUI to display 'tokens ?' during active sessions. By removing the else block, the last known token count is preserved, which is especially important when using context engines like lossless-claw that estimate tokens independently.
This commit is contained in:
parent
6b4c24c2e5
commit
d9907fb15e
@ -68,10 +68,17 @@ export async function updateSessionStoreAfterAgentRun(params: {
|
||||
updatedAt: Date.now(),
|
||||
contextTokens,
|
||||
};
|
||||
const modelChanged =
|
||||
(entry.model !== undefined && entry.model !== modelUsed) ||
|
||||
(entry.modelProvider !== undefined && entry.modelProvider !== providerUsed);
|
||||
setSessionRuntimeModel(next, {
|
||||
provider: providerUsed,
|
||||
model: modelUsed,
|
||||
});
|
||||
if (modelChanged) {
|
||||
next.totalTokens = undefined;
|
||||
next.totalTokensFresh = false;
|
||||
}
|
||||
if (isCliProvider(providerUsed, cfg)) {
|
||||
const cliSessionId = result.meta.agentMeta?.sessionId?.trim();
|
||||
if (cliSessionId) {
|
||||
@ -105,9 +112,13 @@ export async function updateSessionStoreAfterAgentRun(params: {
|
||||
if (typeof totalTokens === "number" && Number.isFinite(totalTokens) && totalTokens > 0) {
|
||||
next.totalTokens = totalTokens;
|
||||
next.totalTokensFresh = true;
|
||||
next.totalTokensEstimate = totalTokens;
|
||||
} else {
|
||||
next.totalTokens = undefined;
|
||||
next.totalTokensFresh = false;
|
||||
if (typeof totalTokens === "number" && Number.isFinite(totalTokens)) {
|
||||
next.totalTokensEstimate = totalTokens;
|
||||
}
|
||||
}
|
||||
next.cacheRead = usage.cacheRead ?? 0;
|
||||
next.cacheWrite = usage.cacheWrite ?? 0;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { resolveTotalTokens } from "../shared/subagents-format.js";
|
||||
import { resolveQueueSettings } from "../auto-reply/reply/queue.js";
|
||||
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||
import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js";
|
||||
@ -550,7 +551,7 @@ async function buildCompactAnnounceStatsLine(params: {
|
||||
const input = typeof entry?.inputTokens === "number" ? entry.inputTokens : 0;
|
||||
const output = typeof entry?.outputTokens === "number" ? entry.outputTokens : 0;
|
||||
const ioTotal = input + output;
|
||||
const promptCache = typeof entry?.totalTokens === "number" ? entry.totalTokens : undefined;
|
||||
const promptCache = resolveTotalTokens(entry as any);
|
||||
const runtimeMs =
|
||||
typeof params.startedAt === "number" && typeof params.endedAt === "number"
|
||||
? Math.max(0, params.endedAt - params.startedAt)
|
||||
|
||||
@ -4,7 +4,10 @@ import {
|
||||
resolveBootstrapTotalMaxChars,
|
||||
} from "../../agents/pi-embedded-helpers.js";
|
||||
import { buildSystemPromptReport } from "../../agents/system-prompt-report.js";
|
||||
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
|
||||
import {
|
||||
resolveFreshSessionTotalTokens,
|
||||
type SessionSystemPromptReport,
|
||||
} from "../../config/sessions/types.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import { resolveCommandsSystemPromptBundle } from "./commands-system-prompt.js";
|
||||
import type { HandleCommandsParams } from "./commands-types.js";
|
||||
@ -96,8 +99,10 @@ export async function buildContextReply(params: HandleCommandsParams): Promise<R
|
||||
}
|
||||
|
||||
const report = await resolveContextReport(params);
|
||||
const totalTokens = resolveFreshSessionTotalTokens(params.sessionEntry);
|
||||
const session = {
|
||||
totalTokens: params.sessionEntry?.totalTokens ?? null,
|
||||
totalTokens: totalTokens ?? null,
|
||||
totalTokensFresh: params.sessionEntry?.totalTokensFresh ?? false,
|
||||
inputTokens: params.sessionEntry?.inputTokens ?? null,
|
||||
outputTokens: params.sessionEntry?.outputTokens ?? null,
|
||||
contextTokens: params.contextTokens ?? null,
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
resolveThreadFlag,
|
||||
resolveSessionResetPolicy,
|
||||
resolveSessionResetType,
|
||||
resolveFreshSessionTotalTokens,
|
||||
resolveGroupSessionKey,
|
||||
resolveSessionKey,
|
||||
resolveSessionTranscriptPath,
|
||||
@ -478,7 +479,7 @@ export async function initSessionState(params: {
|
||||
sessionStore[parentSessionKey] &&
|
||||
!alreadyForked
|
||||
) {
|
||||
const parentTokens = sessionStore[parentSessionKey].totalTokens ?? 0;
|
||||
const parentTokens = resolveFreshSessionTotalTokens(sessionStore[parentSessionKey]) ?? 0;
|
||||
if (parentForkMaxTokens > 0 && parentTokens > parentForkMaxTokens) {
|
||||
// Parent context is too large — forking would create a thread session
|
||||
// that immediately overflows the model's context window. Start fresh
|
||||
|
||||
@ -14,6 +14,7 @@ import { resolveChannelModelOverride } from "../channels/model-overrides.js";
|
||||
import { isCommandFlagEnabled } from "../config/commands.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
resolveFreshSessionTotalTokens,
|
||||
resolveMainSessionKey,
|
||||
resolveSessionFilePath,
|
||||
resolveSessionFilePathOptions,
|
||||
@ -460,7 +461,7 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
let outputTokens = entry?.outputTokens;
|
||||
let cacheRead = entry?.cacheRead;
|
||||
let cacheWrite = entry?.cacheWrite;
|
||||
let totalTokens = entry?.totalTokens ?? (entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0);
|
||||
let totalTokens = resolveFreshSessionTotalTokens(entry) ?? (entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0);
|
||||
|
||||
// Prefer prompt-size tokens from the session transcript when it looks larger
|
||||
// (cached prompt tokens are often missing from agent meta/store).
|
||||
|
||||
@ -142,6 +142,11 @@ export type SessionEntry = {
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
/**
|
||||
* Last known total tokens (including summaries), used for display when
|
||||
* a fresh model-reported count is unavailable.
|
||||
*/
|
||||
totalTokensEstimate?: number;
|
||||
/**
|
||||
* Whether totalTokens reflects a fresh context snapshot for the latest run.
|
||||
* Undefined means legacy/unknown freshness; false forces consumers to treat
|
||||
|
||||
@ -752,10 +752,19 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
lookupContextTokens(modelUsed, { allowAsyncLoad: false }) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
const modelChanged =
|
||||
(cronSession.sessionEntry.model !== undefined &&
|
||||
cronSession.sessionEntry.model !== modelUsed) ||
|
||||
(cronSession.sessionEntry.modelProvider !== undefined &&
|
||||
cronSession.sessionEntry.modelProvider !== providerUsed);
|
||||
setSessionRuntimeModel(cronSession.sessionEntry, {
|
||||
provider: providerUsed,
|
||||
model: modelUsed,
|
||||
});
|
||||
if (modelChanged) {
|
||||
cronSession.sessionEntry.totalTokens = undefined;
|
||||
cronSession.sessionEntry.totalTokensFresh = false;
|
||||
}
|
||||
cronSession.sessionEntry.contextTokens = contextTokens;
|
||||
if (isCliProvider(providerUsed, cfgWithAgentDefaults)) {
|
||||
const cliSessionId = finalRunResult.meta?.agentMeta?.sessionId?.trim();
|
||||
@ -790,10 +799,14 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
if (typeof totalTokens === "number" && Number.isFinite(totalTokens) && totalTokens > 0) {
|
||||
cronSession.sessionEntry.totalTokens = totalTokens;
|
||||
cronSession.sessionEntry.totalTokensFresh = true;
|
||||
cronSession.sessionEntry.totalTokensEstimate = totalTokens;
|
||||
telemetryUsage.total_tokens = totalTokens;
|
||||
} else {
|
||||
cronSession.sessionEntry.totalTokens = undefined;
|
||||
cronSession.sessionEntry.totalTokensFresh = false;
|
||||
if (typeof totalTokens === "number" && Number.isFinite(totalTokens)) {
|
||||
cronSession.sessionEntry.totalTokensEstimate = totalTokens;
|
||||
}
|
||||
}
|
||||
cronSession.sessionEntry.cacheRead = usage.cacheRead ?? 0;
|
||||
cronSession.sessionEntry.cacheWrite = usage.cacheWrite ?? 0;
|
||||
|
||||
@ -42,6 +42,7 @@ export function truncateLine(value: string, maxLength: number) {
|
||||
|
||||
export type TokenUsageLike = {
|
||||
totalTokens?: unknown;
|
||||
totalTokensFresh?: unknown;
|
||||
inputTokens?: unknown;
|
||||
outputTokens?: unknown;
|
||||
};
|
||||
@ -50,7 +51,11 @@ export function resolveTotalTokens(entry?: TokenUsageLike) {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof entry.totalTokens === "number" && Number.isFinite(entry.totalTokens)) {
|
||||
if (
|
||||
entry.totalTokensFresh !== false &&
|
||||
typeof entry.totalTokens === "number" &&
|
||||
Number.isFinite(entry.totalTokens)
|
||||
) {
|
||||
return entry.totalTokens;
|
||||
}
|
||||
const input = typeof entry.inputTokens === "number" ? entry.inputTokens : 0;
|
||||
|
||||
@ -48,6 +48,7 @@ export type SessionInfo = {
|
||||
inputTokens?: number | null;
|
||||
outputTokens?: number | null;
|
||||
totalTokens?: number | null;
|
||||
totalTokensEstimate?: number | null;
|
||||
responseUsage?: ResponseUsageMode;
|
||||
updatedAt?: number | null;
|
||||
displayName?: string;
|
||||
@ -91,6 +92,7 @@ export type GatewayStatusSummary = {
|
||||
age?: number | null;
|
||||
model?: string | null;
|
||||
totalTokens?: number | null;
|
||||
totalTokensEstimate?: number | null;
|
||||
contextTokens?: number | null;
|
||||
remainingTokens?: number | null;
|
||||
percentUsed?: number | null;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user