mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
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:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user