Merge d9907fb15ede84e8093d053807f5adddc5073a31 into 8a05c05596ca9ba0735dafd8e359885de4c2c969

This commit is contained in:
ryansorr 2026-03-21 01:09:02 -05:00 committed by GitHub
commit 4331b68430
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 50 additions and 6 deletions

View File

@ -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;

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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).

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;