


























@@ -10,6 +10,11 @@ import {
1010import * as sessionTypesModule from "../../config/sessions.js";
1111import type { SessionEntry } from "../../config/sessions.js";
1212import { loadSessionStore, saveSessionStore } from "../../config/sessions.js";
13+import {
14+onInternalDiagnosticEvent,
15+resetDiagnosticEventsForTest,
16+type DiagnosticEventPayload,
17+} from "../../infra/diagnostic-events.js";
1318import {
1419clearMemoryPluginState,
1520registerMemoryFlushPlanResolver,
@@ -138,6 +143,7 @@ type RunWithModelFallbackParams = {
138143};
139144140145beforeEach(() => {
146+resetDiagnosticEventsForTest();
141147embeddedRunTesting.resetActiveEmbeddedRuns();
142148replyRunRegistryTesting.resetReplyRunRegistry();
143149runEmbeddedPiAgentMock.mockClear();
@@ -169,6 +175,7 @@ beforeEach(() => {
169175});
170176171177afterEach(() => {
178+resetDiagnosticEventsForTest();
172179vi.useRealTimers();
173180clearMemoryPluginState();
174181replyRunRegistryTesting.resetReplyRunRegistry();
@@ -289,6 +296,167 @@ describe("runReplyAgent auto-compaction token update", () => {
289296// totalTokens should use lastCallUsage (55k), not accumulated (75k)
290297expect(stored[sessionKey].totalTokens).toBe(55_000);
291298});
299+300+it("reports live diagnostic context from promptTokens, not provider usage totals", async () => {
301+const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-usage-diagnostic-"));
302+const storePath = path.join(tmp, "sessions.json");
303+const sessionKey = "main";
304+const sessionEntry = {
305+sessionId: "session",
306+updatedAt: Date.now(),
307+totalTokens: 50_000,
308+};
309+310+await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
311+312+runEmbeddedPiAgentMock.mockResolvedValue({
313+payloads: [{ text: "ok" }],
314+meta: {
315+agentMeta: {
316+usage: { input: 75_000, output: 5_000, cacheRead: 25_000, total: 105_000 },
317+lastCallUsage: { input: 55_000, output: 2_000, cacheRead: 25_000, total: 82_000 },
318+promptTokens: 44_000,
319+},
320+},
321+});
322+323+const diagnostics: DiagnosticEventPayload[] = [];
324+const unsubscribe = onInternalDiagnosticEvent((event) => {
325+diagnostics.push(event);
326+});
327+const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
328+ storePath,
329+ sessionEntry,
330+});
331+332+try {
333+await runReplyAgent({
334+commandBody: "hello",
335+ followupRun,
336+queueKey: "main",
337+ resolvedQueue,
338+shouldSteer: false,
339+shouldFollowup: false,
340+isActive: false,
341+isStreaming: false,
342+ typing,
343+ sessionCtx,
344+ sessionEntry,
345+sessionStore: { [sessionKey]: sessionEntry },
346+ sessionKey,
347+ storePath,
348+defaultModel: "anthropic/claude-opus-4-6",
349+agentCfgContextTokens: 200_000,
350+resolvedVerboseLevel: "off",
351+isNewSession: false,
352+blockStreamingEnabled: false,
353+resolvedBlockStreamingBreak: "message_end",
354+shouldInjectGroupIntro: false,
355+typingMode: "instant",
356+});
357+} finally {
358+unsubscribe();
359+}
360+361+const usageEvent = diagnostics.find((event) => event.type === "model.usage");
362+expect(usageEvent).toMatchObject({
363+type: "model.usage",
364+usage: {
365+input: 75_000,
366+output: 5_000,
367+cacheRead: 25_000,
368+promptTokens: 100_000,
369+total: 105_000,
370+},
371+context: {
372+limit: 200_000,
373+used: 44_000,
374+},
375+});
376+});
377+378+it("falls back to last-call prompt usage for live diagnostic context", async () => {
379+const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-usage-diagnostic-last-"));
380+const storePath = path.join(tmp, "sessions.json");
381+const sessionKey = "main";
382+const sessionEntry = {
383+sessionId: "session",
384+updatedAt: Date.now(),
385+totalTokens: 50_000,
386+};
387+388+await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
389+390+runEmbeddedPiAgentMock.mockResolvedValue({
391+payloads: [{ text: "ok" }],
392+meta: {
393+agentMeta: {
394+usage: { input: 75_000, output: 5_000, cacheRead: 25_000, total: 105_000 },
395+lastCallUsage: {
396+input: 55_000,
397+output: 2_000,
398+cacheRead: 25_000,
399+cacheWrite: 1_000,
400+total: 83_000,
401+},
402+},
403+},
404+});
405+406+const diagnostics: DiagnosticEventPayload[] = [];
407+const unsubscribe = onInternalDiagnosticEvent((event) => {
408+diagnostics.push(event);
409+});
410+const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
411+ storePath,
412+ sessionEntry,
413+});
414+415+try {
416+await runReplyAgent({
417+commandBody: "hello",
418+ followupRun,
419+queueKey: "main",
420+ resolvedQueue,
421+shouldSteer: false,
422+shouldFollowup: false,
423+isActive: false,
424+isStreaming: false,
425+ typing,
426+ sessionCtx,
427+ sessionEntry,
428+sessionStore: { [sessionKey]: sessionEntry },
429+ sessionKey,
430+ storePath,
431+defaultModel: "anthropic/claude-opus-4-6",
432+agentCfgContextTokens: 200_000,
433+resolvedVerboseLevel: "off",
434+isNewSession: false,
435+blockStreamingEnabled: false,
436+resolvedBlockStreamingBreak: "message_end",
437+shouldInjectGroupIntro: false,
438+typingMode: "instant",
439+});
440+} finally {
441+unsubscribe();
442+}
443+444+const usageEvent = diagnostics.find((event) => event.type === "model.usage");
445+expect(usageEvent).toMatchObject({
446+type: "model.usage",
447+usage: {
448+input: 75_000,
449+output: 5_000,
450+cacheRead: 25_000,
451+promptTokens: 100_000,
452+total: 105_000,
453+},
454+context: {
455+limit: 200_000,
456+used: 81_000,
457+},
458+});
459+});
292460});
293461294462describe("runReplyAgent block streaming", () => {
@@ -913,6 +1081,7 @@ describe("runReplyAgent Active Memory inline debug", () => {
9131081model: "claude",
9141082usage: { input: 1200, output: 45, cacheRead: 800, cacheWrite: 200, total: 2245 },
9151083lastCallUsage: { input: 1000, output: 45, cacheRead: 750, cacheWrite: 150, total: 1945 },
1084+promptTokens: 1250,
9161085compactionCount: 1,
9171086},
9181087},
@@ -987,6 +1156,7 @@ describe("runReplyAgent Active Memory inline debug", () => {
9871156expect(traceText).toContain("🔎 Usage (Session Total):");
9881157expect(traceText).toContain("🔎 Usage (Last Turn Total):");
9891158expect(traceText).toContain("🔎 Context Window (Last Model Request):");
1159+expect(traceText).toContain("used=1,250 tok (1.3k)");
9901160expect(traceText).toContain("🔎 Execution Result:");
9911161expect(traceText).toContain("winner=anthropic/claude");
9921162expect(traceText).toContain("fallbackUsed=yes");
@@ -1025,7 +1195,7 @@ describe("runReplyAgent Active Memory inline debug", () => {
10251195expect(traceText).toContain("🔎 Model Input (User Role):");
10261196expect(traceText).toContain("🔎 Model Output (Assistant Role):");
10271197expect(traceText).toContain(
1028-"Summary: winner=claude 🧠 low fallback=yes attempts=2 stop=end_turn prompt=1.9k/200k ⬇️ 1.2k ⬆️ 45 ♻️ 800 🆕 200 🔢 2.2k tools=2 compactions=1",
1198+"Summary: winner=claude 🧠 low fallback=yes attempts=2 stop=end_turn prompt=1.3k/200k ⬇️ 1.2k ⬆️ 45 ♻️ 800 🆕 200 🔢 2.2k tools=2 compactions=1",
10291199);
10301200expect(traceText.indexOf("🔎 Execution Result:")).toBeGreaterThan(
10311201traceText.indexOf("🔎 Context Window (Last Model Request):"),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。