Skip to content

Commit 80b46d2

Browse files
Merge pull request #1225 from Evsdrg/main
fix: 修复子代理 token 显示为 0 + cacheWarningStateBySource Map 内存泄漏
2 parents 78d46aa + b3d28bc commit 80b46d2

3 files changed

Lines changed: 71 additions & 2 deletions

File tree

src/components/Spinner.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { getDefaultCharacters, type SpinnerMode } from './Spinner/index.js';
2323
import { SpinnerAnimationRow } from './Spinner/SpinnerAnimationRow.js';
2424
import { useSettings } from '../hooks/useSettings.js';
2525
import { isInProcessTeammateTask } from '../tasks/InProcessTeammateTask/types.js';
26+
import { isLocalAgentTask } from '../tasks/LocalAgentTask/LocalAgentTask.js';
2627
import { isBackgroundTask } from '../tasks/types.js';
2728
import { getAllInProcessTeammateTasks } from '../tasks/InProcessTeammateTask/InProcessTeammateTask.js';
2829
import { getEffortSuffix } from '../utils/effort.js';
@@ -214,7 +215,7 @@ function SpinnerWithVerbInner({
214215
let teammateTokens = 0;
215216
if (!showSpinnerTree) {
216217
for (const task of Object.values(tasks)) {
217-
if (isInProcessTeammateTask(task) && task.status === 'running') {
218+
if (task.status === 'running' && (isInProcessTeammateTask(task) || isLocalAgentTask(task))) {
218219
if (task.progress?.tokenCount) {
219220
teammateTokens += task.progress.tokenCount;
220221
}

src/utils/cacheWarning.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ interface CacheWarningState {
2424
// 模块级状态,每个 querySource 独立跟踪
2525
const cacheWarningStateBySource = new Map<string, CacheWarningState>()
2626

27+
// Limit the number of tracked sources to prevent unbounded Map growth.
28+
// querySource strings are effectively unbounded (typed as `any`), so a
29+
// long-running session that spawns many subagents could leak memory.
30+
// Evict the oldest entry (by insertion order) when the limit is exceeded.
31+
const MAX_SOURCE_ENTRIES = 50
32+
2733
const DEFAULT_CACHE_THRESHOLD = 80
2834

2935
/**
@@ -81,6 +87,13 @@ export function shouldShowCacheWarning(
8187
let state = cacheWarningStateBySource.get(querySource)
8288
if (!state) {
8389
state = { lastHitRate: null, lastTimestamp: null }
90+
// Evict oldest entry when at capacity so the Map stays bounded
91+
if (cacheWarningStateBySource.size >= MAX_SOURCE_ENTRIES) {
92+
const oldestKey = cacheWarningStateBySource.keys().next().value
93+
if (oldestKey !== undefined) {
94+
cacheWarningStateBySource.delete(oldestKey)
95+
}
96+
}
8497
cacheWarningStateBySource.set(querySource, state)
8598
}
8699

@@ -132,3 +145,10 @@ export function createCacheWarningMessage(info: CacheHitRateInfo): Message {
132145
isMeta: false,
133146
} as Message
134147
}
148+
149+
/**
150+
* Reset the per-source tracking state — only used in tests.
151+
*/
152+
export function _resetCacheWarningStateForTest(): void {
153+
cacheWarningStateBySource.clear()
154+
}

src/utils/swarm/inProcessRunner.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
import type { CustomAgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
4848
import { runAgent } from '@claude-code-best/builtin-tools/tools/AgentTool/runAgent.js'
4949
import { awaitClassifierAutoApproval } from '@claude-code-best/builtin-tools/tools/BashTool/bashPermissions.js'
50+
import type { AgentToolResult } from '@claude-code-best/builtin-tools/tools/AgentTool/agentToolUtils.js'
5051
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js'
5152
import { SEND_MESSAGE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/SendMessageTool/constants.js'
5253
import { TASK_CREATE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/TaskCreateTool/constants.js'
@@ -63,7 +64,10 @@ import {
6364
} from '../../utils/messages.js'
6465
import { evictTaskOutput } from '../../utils/task/diskOutput.js'
6566
import { evictTerminalTask } from '../../utils/task/framework.js'
66-
import { tokenCountWithEstimation } from '../../utils/tokens.js'
67+
import {
68+
tokenCountWithEstimation,
69+
getTokenCountFromUsage,
70+
} from '../../utils/tokens.js'
6771
import { createAbortController } from '../abortController.js'
6872
import { type AgentContext, runWithAgentContext } from '../agentContext.js'
6973
import {
@@ -915,6 +919,7 @@ export async function runInProcessTeammate(
915919
invokingRequestId,
916920
} = config
917921
const { setAppState } = toolUseContext
922+
const startTime = Date.now()
918923

919924
logForDebugging(
920925
`[inProcessRunner] Starting agent loop for ${identity.agentId}`,
@@ -1463,6 +1468,48 @@ export async function runInProcessTeammate(
14631468
// Mark as completed when exiting the loop
14641469
let alreadyTerminal = false
14651470
let toolUseId: string | undefined
1471+
1472+
// Compute result so the detail dialog can show token usage.
1473+
// Walk backwards for the last API usage (cumulative input_tokens from the
1474+
// Anthropic API already includes all prior context).
1475+
let completionTokens = 0
1476+
let completionToolUseCount = 0
1477+
let lastAssistantContent: AgentToolResult['content'] = []
1478+
let lastUsage: AgentToolResult['usage'] | undefined
1479+
for (let i = allMessages.length - 1; i >= 0; i--) {
1480+
const m = allMessages[i]!
1481+
if (m.type === 'assistant') {
1482+
const blocks = (m.message?.content ?? []) as any[]
1483+
for (const b of blocks) {
1484+
if (b?.type === 'tool_use') completionToolUseCount++
1485+
}
1486+
const textBlocks = blocks.filter((b: any) => b?.type === 'text')
1487+
if (textBlocks.length > 0 && lastAssistantContent.length === 0) {
1488+
lastAssistantContent = textBlocks.map((b: any) => ({
1489+
type: 'text' as const,
1490+
text: b.text,
1491+
}))
1492+
}
1493+
if (!lastUsage && m.message?.usage) {
1494+
lastUsage = m.message.usage as AgentToolResult['usage']
1495+
completionTokens = getTokenCountFromUsage(
1496+
m.message.usage as Parameters<typeof getTokenCountFromUsage>[0],
1497+
)
1498+
}
1499+
if (completionTokens > 0 && lastAssistantContent.length > 0) break
1500+
}
1501+
}
1502+
1503+
const teammateResult: AgentToolResult = {
1504+
agentId: identity.agentId,
1505+
agentType: 'teammate',
1506+
content: lastAssistantContent,
1507+
totalToolUseCount: completionToolUseCount,
1508+
totalDurationMs: Date.now() - startTime,
1509+
totalTokens: completionTokens,
1510+
usage: lastUsage as AgentToolResult['usage'],
1511+
} as unknown as AgentToolResult
1512+
14661513
updateTaskState(
14671514
taskId,
14681515
task => {
@@ -1481,6 +1528,7 @@ export async function runInProcessTeammate(
14811528
status: 'completed' as const,
14821529
notified: true,
14831530
endTime: Date.now(),
1531+
result: teammateResult,
14841532
messages: task.messages?.length ? [task.messages.at(-1)!] : undefined,
14851533
pendingUserMessages: [],
14861534
inProgressToolUseIDs: undefined,

0 commit comments

Comments
 (0)