Skip to content

Commit cb98479

Browse files
fix(subagents): roll token usage formatters over to m
Roll both subagent token usage formatters over to the million unit when rounded thousands reach the next unit. The original fix covers `formatTokenShort`, which feeds the subagent list usage line. The maintainer follow-up applies the same unit-boundary rule to compact subagent announcement stats, preserving that formatter's one-decimal style while preventing `1000.0k` output. Verification: - focused runtime probe for list and compact announce stats at 999,999 tokens - `oxfmt --check` on touched formatter/test files - `git diff --check origin/main..HEAD` - `node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test-pr88209.tsbuildinfo` - autoreview local closeout clean - exact-head CI passed for Real behavior proof, check-test-types, check-prod-types, check-guards, security-fast, and preflight Known unrelated current-main reds at merge: `check-lint`, `checks-node-agentic-gateway-methods`, and `checks-node-agentic-control-plane-agent-chat`. Co-authored-by: coder999999999 <coder999999999@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5498771 commit cb98479

4 files changed

Lines changed: 51 additions & 2 deletions

File tree

src/agents/subagent-announce-output.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@ import { afterEach, describe, expect, it, vi } from "vitest";
22
import {
33
testing,
44
applySubagentWaitOutcome,
5+
buildCompactAnnounceStatsLine,
56
buildChildCompletionFindings,
67
readSubagentOutput,
78
} from "./subagent-announce-output.js";
89

910
type CallGateway = typeof import("../gateway/call.js").callGateway;
11+
type GetRuntimeConfig = typeof import("./subagent-announce.runtime.js").getRuntimeConfig;
12+
type ReadSessionEntry = typeof import("./subagent-announce.runtime.js").readSessionEntry;
1013
type ReadSessionMessagesAsync =
1114
typeof import("./subagent-announce.runtime.js").readSessionMessagesAsync;
15+
type ResolveAgentIdFromSessionKey =
16+
typeof import("./subagent-announce.runtime.js").resolveAgentIdFromSessionKey;
17+
type ResolveStorePath = typeof import("./subagent-announce.runtime.js").resolveStorePath;
1218

1319
function installOutputDeps(params: {
1420
messages: Array<unknown>;
@@ -53,6 +59,33 @@ function sessionsYieldTurn(message = "Waiting for subagent completion.") {
5359
];
5460
}
5561

62+
describe("buildCompactAnnounceStatsLine", () => {
63+
afterEach(() => {
64+
testing.setDepsForTest();
65+
});
66+
67+
it("rolls one-decimal thousand token stats over to the million unit", async () => {
68+
testing.setDepsForTest({
69+
getRuntimeConfig: (() => ({ session: { store: "memory" } })) as GetRuntimeConfig,
70+
readSessionEntry: (() => ({
71+
sessionId: "child-session",
72+
updatedAt: 0,
73+
inputTokens: 999_999,
74+
outputTokens: 0,
75+
totalTokens: 999_999,
76+
})) as ReadSessionEntry,
77+
resolveAgentIdFromSessionKey: (() => "main") as ResolveAgentIdFromSessionKey,
78+
resolveStorePath: (() => "/tmp/openclaw-session-store") as ResolveStorePath,
79+
});
80+
81+
await expect(
82+
buildCompactAnnounceStatsLine({
83+
sessionKey: "agent:main:subagent:child",
84+
}),
85+
).resolves.toBe("Stats: runtime n/a • tokens 1.0m (in 1.0m / out 0)");
86+
});
87+
});
88+
5689
describe("readSubagentOutput", () => {
5790
afterEach(() => {
5891
testing.setDepsForTest();

src/agents/subagent-announce-output.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,13 @@ function formatTokenCount(value?: number) {
511511
return `${(value / 1_000_000).toFixed(1)}m`;
512512
}
513513
if (value >= 1_000) {
514-
return `${(value / 1_000).toFixed(1)}k`;
514+
const formattedThousands = (value / 1_000).toFixed(1);
515+
// Keep the compact stats unit scheme stable when one-decimal rounding
516+
// reaches the next unit, e.g. 999_999 -> 1000.0k.
517+
if (Number(formattedThousands) >= 1_000) {
518+
return `${(value / 1_000_000).toFixed(1)}m`;
519+
}
520+
return `${formattedThousands}k`;
515521
}
516522
return String(Math.round(value));
517523
}

src/shared/subagents-format.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ describe("shared/subagents-format", () => {
2525
expect(formatTokenShort(1_500)).toBe("1.5k");
2626
expect(formatTokenShort(10_000)).toBe("10k");
2727
expect(formatTokenShort(15_400)).toBe("15k");
28+
// Rollover boundary: rounding to thousands must not emit an out-of-scheme
29+
// "1000k" — it has to advance to the million branch.
30+
expect(formatTokenShort(999_499)).toBe("999k");
31+
expect(formatTokenShort(999_500)).toBe("1m");
32+
expect(formatTokenShort(999_999)).toBe("1m");
2833
expect(formatTokenShort(1_000_000)).toBe("1m");
2934
expect(formatTokenShort(1_250_000)).toBe("1.3m");
3035
});

src/shared/subagents-format.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ export function formatTokenShort(value?: number) {
1212
return `${(n / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
1313
}
1414
if (n < 1_000_000) {
15-
return `${Math.round(n / 1_000)}k`;
15+
const thousands = Math.round(n / 1_000);
16+
// Rounding can reach 1000 (e.g. 999_500 -> 1000); fall through to the
17+
// million branch instead of emitting an out-of-scheme "1000k".
18+
if (thousands < 1_000) {
19+
return `${thousands}k`;
20+
}
1621
}
1722
return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}m`;
1823
}

0 commit comments

Comments
 (0)