fix(status): avoid cumulative usage for context percent (#92604)

* fix(status): avoid cumulative usage for context percent

* fix(status): preserve legacy context totals
This commit is contained in:
Ashish Patel
2026-06-14 05:37:08 +05:30
committed by GitHub
parent b72634f56d
commit 8f62ec6177
2 changed files with 107 additions and 9 deletions
+91
View File
@@ -109,6 +109,7 @@ describe("buildStatusMessage", () => {
inputTokens: 1200,
outputTokens: 800,
totalTokens: 16_000,
totalTokensFresh: true,
contextTokens: 32_000,
thinkingLevel: "low",
verboseLevel: "on",
@@ -221,6 +222,30 @@ describe("buildStatusMessage", () => {
expect(normalized).not.toContain("Context: 3.8m/1.0m");
});
it("preserves legacy unknown-freshness totalTokens as context usage", () => {
const text = buildStatusMessage({
agent: {
model: "anthropic/test:opus",
contextTokens: 1_000_000,
},
sessionEntry: {
sessionId: "abc",
updatedAt: 0,
totalTokens: 25_000,
contextTokens: 1_000_000,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
queue: { mode: "collect", depth: 0 },
modelAuth: "api-key",
now: 10 * 60_000,
});
const normalized = normalizeTestText(text);
expect(normalized).toContain("Context: 25k/1.0m");
expect(normalized).not.toContain("Context: ?/1.0m");
});
it("uses estimated context budget status when fresh totalTokens are unavailable", () => {
const text = buildStatusMessage({
agent: {
@@ -856,6 +881,7 @@ describe("buildStatusMessage", () => {
channel: "discord",
groupId: "123",
totalTokens: 49_000,
totalTokensFresh: true,
contextTokens: 1_048_576,
},
sessionKey: "agent:main:main",
@@ -891,6 +917,7 @@ describe("buildStatusMessage", () => {
sessionId: "ctx1m",
updatedAt: 0,
totalTokens: 200_000,
totalTokensFresh: true,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
@@ -909,6 +936,7 @@ describe("buildStatusMessage", () => {
sessionId: "opus47",
updatedAt: 0,
totalTokens: 200_000,
totalTokensFresh: true,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
@@ -928,6 +956,7 @@ describe("buildStatusMessage", () => {
modelOverride: "small-model",
contextTokens: 4_096,
totalTokens: 1_024,
totalTokensFresh: true,
};
applyModelOverrideToSessionEntry({
@@ -981,6 +1010,7 @@ describe("buildStatusMessage", () => {
fallbackNoticeActiveModel: "minimax-portal/MiniMax-M2.7",
fallbackNoticeReason: "model not allowed",
totalTokens: 49_000,
totalTokensFresh: true,
contextTokens: 1_048_576,
},
sessionKey: "agent:main:main",
@@ -1113,6 +1143,7 @@ describe("buildStatusMessage", () => {
fallbackNoticeActiveModel: "minimax-portal/MiniMax-M2.7",
fallbackNoticeReason: "model not allowed",
totalTokens: 49_000,
totalTokensFresh: true,
contextTokens: 1_048_576,
},
sessionKey: "agent:main:main",
@@ -1157,6 +1188,7 @@ describe("buildStatusMessage", () => {
fallbackNoticeActiveModel: "minimax-portal/MiniMax-M2.7",
fallbackNoticeReason: "model not allowed",
totalTokens: 49_000,
totalTokensFresh: true,
contextTokens: 123_456,
},
sessionKey: "agent:main:main",
@@ -1203,6 +1235,7 @@ describe("buildStatusMessage", () => {
fallbackNoticeActiveModel: "minimax-portal/MiniMax-M2.7",
fallbackNoticeReason: "model not allowed",
totalTokens: 49_000,
totalTokensFresh: true,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
@@ -1248,6 +1281,7 @@ describe("buildStatusMessage", () => {
fallbackNoticeActiveModel: "minimax-portal/MiniMax-M2.7",
fallbackNoticeReason: "model not allowed",
totalTokens: 49_000,
totalTokensFresh: true,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
@@ -1292,6 +1326,7 @@ describe("buildStatusMessage", () => {
fallbackNoticeActiveModel: "minimax-portal/MiniMax-M2.7",
fallbackNoticeReason: "model not allowed",
totalTokens: 49_000,
totalTokensFresh: true,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
@@ -1333,6 +1368,7 @@ describe("buildStatusMessage", () => {
fallbackNoticeActiveModel: "custom-runtime/unknown-fallback-model",
fallbackNoticeReason: "model not allowed",
totalTokens: 49_000,
totalTokensFresh: true,
contextTokens: 128_000,
},
sessionKey: "agent:main:main",
@@ -1981,6 +2017,54 @@ describe("buildStatusMessage", () => {
);
});
it("does not let legacy cumulative session totals override fresh transcript context usage", async () => {
await withTempHome(
async (dir) => {
const sessionId = "sess-legacy-cumulative-context";
writeTranscriptUsageLog({
dir,
agentId: "main",
sessionId,
usage: {
input: 10_000,
output: 1_000,
cacheRead: 26_000,
cacheWrite: 0,
totalTokens: 36_000,
},
});
const text = buildStatusMessage({
agent: {
model: "anthropic/claude-opus-4-6",
contextTokens: 1_000_000,
},
sessionEntry: {
sessionId,
updatedAt: 0,
inputTokens: 16,
outputTokens: 5_100,
cacheRead: 2_300_000,
cacheWrite: 11_000,
totalTokens: 2_300_000,
contextTokens: 1_000_000,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
queue: { mode: "collect", depth: 0 },
includeTranscriptUsage: true,
modelAuth: "api-key",
});
const normalized = normalizeTestText(text);
expect(normalized).toContain("Cache: 100% hit · 2.3m cached, 11k new");
expect(normalized).toContain("Context: 36k/1.0m (4%)");
expect(normalized).not.toContain("Context: 2.3m/1.0m");
},
{ prefix: "openclaw-status-" },
);
});
it("reads transcript usage for non-default agents", async () => {
await withTempHome(
async (dir) => {
@@ -2237,6 +2321,7 @@ describe("buildStatusMessage", () => {
sessionId: "sess-runtime-slash-id",
updatedAt: 0,
totalTokens: 1205,
totalTokensFresh: true,
model: "google/gemini-2.5-pro",
},
sessionKey: "agent:main:main",
@@ -2279,6 +2364,7 @@ describe("buildStatusMessage", () => {
fallbackNoticeActiveModel: "fake-minimax/FakeMiniMax-M2.5",
fallbackNoticeReason: "model not allowed",
totalTokens: 49_000,
totalTokensFresh: true,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
@@ -2314,6 +2400,7 @@ describe("buildStatusMessage", () => {
updatedAt: 0,
model: "openai/gpt-4o",
totalTokens: 49_000,
totalTokensFresh: true,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
@@ -2384,6 +2471,7 @@ describe("buildStatusMessage", () => {
sessionId: "sess-anthropic-qualified-context",
updatedAt: 0,
totalTokens: 25_000,
totalTokensFresh: true,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
@@ -2408,6 +2496,7 @@ describe("buildStatusMessage", () => {
sessionId: "sess-openai-chatgpt-cap-context",
updatedAt: 0,
totalTokens: 25_000,
totalTokensFresh: true,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
@@ -2432,6 +2521,7 @@ describe("buildStatusMessage", () => {
sessionId: "sess-openai-chatgpt-runtime-cap-context",
updatedAt: 0,
totalTokens: 25_000,
totalTokensFresh: true,
},
sessionKey: "agent:main:main",
sessionScope: "per-sender",
@@ -2469,6 +2559,7 @@ describe("buildStatusMessage", () => {
fallbackNoticeActiveModel: "custom-runtime/unknown-fallback-model",
fallbackNoticeReason: "model not allowed",
totalTokens: 49_000,
totalTokensFresh: true,
contextTokens: 128_000,
},
sessionKey: "agent:main:main",
+16 -9
View File
@@ -293,6 +293,7 @@ const readUsageFromSessionLog = (
cacheWrite: number;
promptTokens: number;
total: number;
totalTokensFresh: boolean;
model?: string;
}
| undefined => {
@@ -350,6 +351,7 @@ const readUsageFromSessionLog = (
cacheWrite,
promptTokens,
total,
totalTokensFresh: snapshot.totalTokensFresh === true,
model,
};
} catch {
@@ -631,15 +633,13 @@ export function buildStatusMessage(args: StatusArgs): string {
let cacheRead = entry?.cacheRead;
let cacheWrite = entry?.cacheWrite;
const freshTotalTokens = resolveFreshSessionTotalTokens(entry);
// Undefined freshness is legacy, not stale: keep persisted totals for /status,
// but let a fresh transcript prompt snapshot replace them when available.
const allowTranscriptContextUsage = entry?.totalTokensFresh !== false;
let totalTokens =
freshTotalTokens ??
(entry?.totalTokensFresh === false
? undefined
: (entry?.totalTokens ?? (entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0)));
let totalTokens = freshTotalTokens;
// Prefer prompt-size tokens from the session transcript when it looks larger
// (cached prompt tokens are often missing from agent meta/store).
// Explicitly stale session/cache usage can still hydrate Tokens/Cache lines
// but must not become Context.
if (args.includeTranscriptUsage) {
const logUsage = readUsageFromSessionLog(
entry?.sessionId,
@@ -649,10 +649,17 @@ export function buildStatusMessage(args: StatusArgs): string {
args.sessionStorePath,
);
if (logUsage) {
const candidate = logUsage.promptTokens || logUsage.total;
const candidate = logUsage.totalTokensFresh
? logUsage.promptTokens || logUsage.total
: undefined;
if (
allowTranscriptContextUsage &&
(!totalTokens || totalTokens === 0 || candidate > totalTokens)
candidate !== undefined &&
candidate > 0 &&
(entry?.totalTokensFresh !== true ||
!totalTokens ||
totalTokens === 0 ||
candidate > totalTokens)
) {
totalTokens = candidate;
}