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(), updatedAt: Date.now(),
contextTokens, contextTokens,
}; };
const modelChanged =
(entry.model !== undefined && entry.model !== modelUsed) ||
(entry.modelProvider !== undefined && entry.modelProvider !== providerUsed);
setSessionRuntimeModel(next, { setSessionRuntimeModel(next, {
provider: providerUsed, provider: providerUsed,
model: modelUsed, model: modelUsed,
}); });
if (modelChanged) {
next.totalTokens = undefined;
next.totalTokensFresh = false;
}
if (isCliProvider(providerUsed, cfg)) { if (isCliProvider(providerUsed, cfg)) {
const cliSessionId = result.meta.agentMeta?.sessionId?.trim(); const cliSessionId = result.meta.agentMeta?.sessionId?.trim();
if (cliSessionId) { if (cliSessionId) {
@ -105,9 +112,13 @@ export async function updateSessionStoreAfterAgentRun(params: {
if (typeof totalTokens === "number" && Number.isFinite(totalTokens) && totalTokens > 0) { if (typeof totalTokens === "number" && Number.isFinite(totalTokens) && totalTokens > 0) {
next.totalTokens = totalTokens; next.totalTokens = totalTokens;
next.totalTokensFresh = true; next.totalTokensFresh = true;
next.totalTokensEstimate = totalTokens;
} else { } else {
next.totalTokens = undefined; next.totalTokens = undefined;
next.totalTokensFresh = false; next.totalTokensFresh = false;
if (typeof totalTokens === "number" && Number.isFinite(totalTokens)) {
next.totalTokensEstimate = totalTokens;
}
} }
next.cacheRead = usage.cacheRead ?? 0; next.cacheRead = usage.cacheRead ?? 0;
next.cacheWrite = usage.cacheWrite ?? 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 { resolveQueueSettings } from "../auto-reply/reply/queue.js";
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.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 input = typeof entry?.inputTokens === "number" ? entry.inputTokens : 0;
const output = typeof entry?.outputTokens === "number" ? entry.outputTokens : 0; const output = typeof entry?.outputTokens === "number" ? entry.outputTokens : 0;
const ioTotal = input + output; const ioTotal = input + output;
const promptCache = typeof entry?.totalTokens === "number" ? entry.totalTokens : undefined; const promptCache = resolveTotalTokens(entry as any);
const runtimeMs = const runtimeMs =
typeof params.startedAt === "number" && typeof params.endedAt === "number" typeof params.startedAt === "number" && typeof params.endedAt === "number"
? Math.max(0, params.endedAt - params.startedAt) ? Math.max(0, params.endedAt - params.startedAt)

View File

@ -4,7 +4,10 @@ import {
resolveBootstrapTotalMaxChars, resolveBootstrapTotalMaxChars,
} from "../../agents/pi-embedded-helpers.js"; } from "../../agents/pi-embedded-helpers.js";
import { buildSystemPromptReport } from "../../agents/system-prompt-report.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 type { ReplyPayload } from "../types.js";
import { resolveCommandsSystemPromptBundle } from "./commands-system-prompt.js"; import { resolveCommandsSystemPromptBundle } from "./commands-system-prompt.js";
import type { HandleCommandsParams } from "./commands-types.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 report = await resolveContextReport(params);
const totalTokens = resolveFreshSessionTotalTokens(params.sessionEntry);
const session = { const session = {
totalTokens: params.sessionEntry?.totalTokens ?? null, totalTokens: totalTokens ?? null,
totalTokensFresh: params.sessionEntry?.totalTokensFresh ?? false,
inputTokens: params.sessionEntry?.inputTokens ?? null, inputTokens: params.sessionEntry?.inputTokens ?? null,
outputTokens: params.sessionEntry?.outputTokens ?? null, outputTokens: params.sessionEntry?.outputTokens ?? null,
contextTokens: params.contextTokens ?? null, contextTokens: params.contextTokens ?? null,

View File

@ -20,6 +20,7 @@ import {
resolveThreadFlag, resolveThreadFlag,
resolveSessionResetPolicy, resolveSessionResetPolicy,
resolveSessionResetType, resolveSessionResetType,
resolveFreshSessionTotalTokens,
resolveGroupSessionKey, resolveGroupSessionKey,
resolveSessionKey, resolveSessionKey,
resolveSessionTranscriptPath, resolveSessionTranscriptPath,
@ -478,7 +479,7 @@ export async function initSessionState(params: {
sessionStore[parentSessionKey] && sessionStore[parentSessionKey] &&
!alreadyForked !alreadyForked
) { ) {
const parentTokens = sessionStore[parentSessionKey].totalTokens ?? 0; const parentTokens = resolveFreshSessionTotalTokens(sessionStore[parentSessionKey]) ?? 0;
if (parentForkMaxTokens > 0 && parentTokens > parentForkMaxTokens) { if (parentForkMaxTokens > 0 && parentTokens > parentForkMaxTokens) {
// Parent context is too large — forking would create a thread session // Parent context is too large — forking would create a thread session
// that immediately overflows the model's context window. Start fresh // 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 { isCommandFlagEnabled } from "../config/commands.js";
import type { OpenClawConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/config.js";
import { import {
resolveFreshSessionTotalTokens,
resolveMainSessionKey, resolveMainSessionKey,
resolveSessionFilePath, resolveSessionFilePath,
resolveSessionFilePathOptions, resolveSessionFilePathOptions,
@ -460,7 +461,7 @@ export function buildStatusMessage(args: StatusArgs): string {
let outputTokens = entry?.outputTokens; let outputTokens = entry?.outputTokens;
let cacheRead = entry?.cacheRead; let cacheRead = entry?.cacheRead;
let cacheWrite = entry?.cacheWrite; 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 // Prefer prompt-size tokens from the session transcript when it looks larger
// (cached prompt tokens are often missing from agent meta/store). // (cached prompt tokens are often missing from agent meta/store).

View File

@ -142,6 +142,11 @@ export type SessionEntry = {
inputTokens?: number; inputTokens?: number;
outputTokens?: number; outputTokens?: number;
totalTokens?: 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. * Whether totalTokens reflects a fresh context snapshot for the latest run.
* Undefined means legacy/unknown freshness; false forces consumers to treat * 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 }) ?? lookupContextTokens(modelUsed, { allowAsyncLoad: false }) ??
DEFAULT_CONTEXT_TOKENS; DEFAULT_CONTEXT_TOKENS;
const modelChanged =
(cronSession.sessionEntry.model !== undefined &&
cronSession.sessionEntry.model !== modelUsed) ||
(cronSession.sessionEntry.modelProvider !== undefined &&
cronSession.sessionEntry.modelProvider !== providerUsed);
setSessionRuntimeModel(cronSession.sessionEntry, { setSessionRuntimeModel(cronSession.sessionEntry, {
provider: providerUsed, provider: providerUsed,
model: modelUsed, model: modelUsed,
}); });
if (modelChanged) {
cronSession.sessionEntry.totalTokens = undefined;
cronSession.sessionEntry.totalTokensFresh = false;
}
cronSession.sessionEntry.contextTokens = contextTokens; cronSession.sessionEntry.contextTokens = contextTokens;
if (isCliProvider(providerUsed, cfgWithAgentDefaults)) { if (isCliProvider(providerUsed, cfgWithAgentDefaults)) {
const cliSessionId = finalRunResult.meta?.agentMeta?.sessionId?.trim(); 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) { if (typeof totalTokens === "number" && Number.isFinite(totalTokens) && totalTokens > 0) {
cronSession.sessionEntry.totalTokens = totalTokens; cronSession.sessionEntry.totalTokens = totalTokens;
cronSession.sessionEntry.totalTokensFresh = true; cronSession.sessionEntry.totalTokensFresh = true;
cronSession.sessionEntry.totalTokensEstimate = totalTokens;
telemetryUsage.total_tokens = totalTokens; telemetryUsage.total_tokens = totalTokens;
} else { } else {
cronSession.sessionEntry.totalTokens = undefined; cronSession.sessionEntry.totalTokens = undefined;
cronSession.sessionEntry.totalTokensFresh = false; cronSession.sessionEntry.totalTokensFresh = false;
if (typeof totalTokens === "number" && Number.isFinite(totalTokens)) {
cronSession.sessionEntry.totalTokensEstimate = totalTokens;
}
} }
cronSession.sessionEntry.cacheRead = usage.cacheRead ?? 0; cronSession.sessionEntry.cacheRead = usage.cacheRead ?? 0;
cronSession.sessionEntry.cacheWrite = usage.cacheWrite ?? 0; cronSession.sessionEntry.cacheWrite = usage.cacheWrite ?? 0;

View File

@ -42,6 +42,7 @@ export function truncateLine(value: string, maxLength: number) {
export type TokenUsageLike = { export type TokenUsageLike = {
totalTokens?: unknown; totalTokens?: unknown;
totalTokensFresh?: unknown;
inputTokens?: unknown; inputTokens?: unknown;
outputTokens?: unknown; outputTokens?: unknown;
}; };
@ -50,7 +51,11 @@ export function resolveTotalTokens(entry?: TokenUsageLike) {
if (!entry || typeof entry !== "object") { if (!entry || typeof entry !== "object") {
return undefined; 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; return entry.totalTokens;
} }
const input = typeof entry.inputTokens === "number" ? entry.inputTokens : 0; const input = typeof entry.inputTokens === "number" ? entry.inputTokens : 0;

View File

@ -48,6 +48,7 @@ export type SessionInfo = {
inputTokens?: number | null; inputTokens?: number | null;
outputTokens?: number | null; outputTokens?: number | null;
totalTokens?: number | null; totalTokens?: number | null;
totalTokensEstimate?: number | null;
responseUsage?: ResponseUsageMode; responseUsage?: ResponseUsageMode;
updatedAt?: number | null; updatedAt?: number | null;
displayName?: string; displayName?: string;
@ -91,6 +92,7 @@ export type GatewayStatusSummary = {
age?: number | null; age?: number | null;
model?: string | null; model?: string | null;
totalTokens?: number | null; totalTokens?: number | null;
totalTokensEstimate?: number | null;
contextTokens?: number | null; contextTokens?: number | null;
remainingTokens?: number | null; remainingTokens?: number | null;
percentUsed?: number | null; percentUsed?: number | null;