Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/components/Spinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { getDefaultCharacters, type SpinnerMode } from './Spinner/index.js';
import { SpinnerAnimationRow } from './Spinner/SpinnerAnimationRow.js';
import { useSettings } from '../hooks/useSettings.js';
import { isInProcessTeammateTask } from '../tasks/InProcessTeammateTask/types.js';
import { isLocalAgentTask } from '../tasks/LocalAgentTask/LocalAgentTask.js';
import { isBackgroundTask } from '../tasks/types.js';
import { getAllInProcessTeammateTasks } from '../tasks/InProcessTeammateTask/InProcessTeammateTask.js';
import { getEffortSuffix } from '../utils/effort.js';
Expand Down Expand Up @@ -214,7 +215,7 @@ function SpinnerWithVerbInner({
let teammateTokens = 0;
if (!showSpinnerTree) {
for (const task of Object.values(tasks)) {
if (isInProcessTeammateTask(task) && task.status === 'running') {
if (task.status === 'running' && (isInProcessTeammateTask(task) || isLocalAgentTask(task))) {
if (task.progress?.tokenCount) {
teammateTokens += task.progress.tokenCount;
}
Expand Down
20 changes: 20 additions & 0 deletions src/utils/cacheWarning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ interface CacheWarningState {
// 模块级状态,每个 querySource 独立跟踪
const cacheWarningStateBySource = new Map<string, CacheWarningState>()

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

const DEFAULT_CACHE_THRESHOLD = 80

/**
Expand Down Expand Up @@ -81,6 +87,13 @@ export function shouldShowCacheWarning(
let state = cacheWarningStateBySource.get(querySource)
if (!state) {
state = { lastHitRate: null, lastTimestamp: null }
// Evict oldest entry when at capacity so the Map stays bounded
if (cacheWarningStateBySource.size >= MAX_SOURCE_ENTRIES) {
const oldestKey = cacheWarningStateBySource.keys().next().value
if (oldestKey !== undefined) {
cacheWarningStateBySource.delete(oldestKey)
}
}
cacheWarningStateBySource.set(querySource, state)
}

Expand Down Expand Up @@ -132,3 +145,10 @@ export function createCacheWarningMessage(info: CacheHitRateInfo): Message {
isMeta: false,
} as Message
}

/**
* Reset the per-source tracking state — only used in tests.
*/
export function _resetCacheWarningStateForTest(): void {
cacheWarningStateBySource.clear()
}
50 changes: 49 additions & 1 deletion src/utils/swarm/inProcessRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
import type { CustomAgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
import { runAgent } from '@claude-code-best/builtin-tools/tools/AgentTool/runAgent.js'
import { awaitClassifierAutoApproval } from '@claude-code-best/builtin-tools/tools/BashTool/bashPermissions.js'
import type { AgentToolResult } from '@claude-code-best/builtin-tools/tools/AgentTool/agentToolUtils.js'
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js'
import { SEND_MESSAGE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/SendMessageTool/constants.js'
import { TASK_CREATE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/TaskCreateTool/constants.js'
Expand All @@ -63,7 +64,10 @@ import {
} from '../../utils/messages.js'
import { evictTaskOutput } from '../../utils/task/diskOutput.js'
import { evictTerminalTask } from '../../utils/task/framework.js'
import { tokenCountWithEstimation } from '../../utils/tokens.js'
import {
tokenCountWithEstimation,
getTokenCountFromUsage,
} from '../../utils/tokens.js'
import { createAbortController } from '../abortController.js'
import { type AgentContext, runWithAgentContext } from '../agentContext.js'
import {
Expand Down Expand Up @@ -915,6 +919,7 @@ export async function runInProcessTeammate(
invokingRequestId,
} = config
const { setAppState } = toolUseContext
const startTime = Date.now()

logForDebugging(
`[inProcessRunner] Starting agent loop for ${identity.agentId}`,
Expand Down Expand Up @@ -1463,6 +1468,48 @@ export async function runInProcessTeammate(
// Mark as completed when exiting the loop
let alreadyTerminal = false
let toolUseId: string | undefined

// Compute result so the detail dialog can show token usage.
// Walk backwards for the last API usage (cumulative input_tokens from the
// Anthropic API already includes all prior context).
let completionTokens = 0
let completionToolUseCount = 0
let lastAssistantContent: AgentToolResult['content'] = []
let lastUsage: AgentToolResult['usage'] | undefined
for (let i = allMessages.length - 1; i >= 0; i--) {
const m = allMessages[i]!
if (m.type === 'assistant') {
const blocks = (m.message?.content ?? []) as any[]
for (const b of blocks) {
if (b?.type === 'tool_use') completionToolUseCount++
}
const textBlocks = blocks.filter((b: any) => b?.type === 'text')
if (textBlocks.length > 0 && lastAssistantContent.length === 0) {
lastAssistantContent = textBlocks.map((b: any) => ({
type: 'text' as const,
text: b.text,
}))
}
if (!lastUsage && m.message?.usage) {
lastUsage = m.message.usage as AgentToolResult['usage']
completionTokens = getTokenCountFromUsage(
m.message.usage as Parameters<typeof getTokenCountFromUsage>[0],
Comment on lines +1482 to +1496
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace as any casts with proper typing.

Per the repository's coding guidelines, production src/**/*.{ts,tsx} code must not use as any; prefer Record<string, unknown>, type guards, or as unknown as SpecificType double assertions. The three sites here — (m.message?.content ?? []) as any[], blocks.filter((b: any) => …), and textBlocks.map((b: any) => …) — can all be expressed with a narrow content-block type guard. ContentBlockParam is already imported at the top of this file.

-        const blocks = (m.message?.content ?? []) as any[]
-        for (const b of blocks) {
-          if (b?.type === 'tool_use') completionToolUseCount++
-        }
-        const textBlocks = blocks.filter((b: any) => b?.type === 'text')
-        if (textBlocks.length > 0 && lastAssistantContent.length === 0) {
-          lastAssistantContent = textBlocks.map((b: any) => ({
-            type: 'text' as const,
-            text: b.text,
-          }))
-        }
+        const content = m.message?.content
+        const blocks: ContentBlockParam[] = Array.isArray(content) ? content : []
+        for (const b of blocks) {
+          if (typeof b === 'object' && b.type === 'tool_use') completionToolUseCount++
+        }
+        const textBlocks = blocks.filter(
+          (b): b is Extract<ContentBlockParam, { type: 'text' }> =>
+            typeof b === 'object' && b.type === 'text',
+        )
+        if (textBlocks.length > 0 && lastAssistantContent.length === 0) {
+          lastAssistantContent = textBlocks.map(b => ({ type: 'text' as const, text: b.text }))
+        }

As per coding guidelines: "Do not use as any in production code; use as unknown as SpecificType double assertion or add proper interface definitions instead" and "Use type guards to narrow union types rather than force type conversion".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const blocks = (m.message?.content ?? []) as any[]
for (const b of blocks) {
if (b?.type === 'tool_use') completionToolUseCount++
}
const textBlocks = blocks.filter((b: any) => b?.type === 'text')
if (textBlocks.length > 0 && lastAssistantContent.length === 0) {
lastAssistantContent = textBlocks.map((b: any) => ({
type: 'text' as const,
text: b.text,
}))
}
if (!lastUsage && m.message?.usage) {
lastUsage = m.message.usage as AgentToolResult['usage']
completionTokens = getTokenCountFromUsage(
m.message.usage as Parameters<typeof getTokenCountFromUsage>[0],
const content = m.message?.content
const blocks: ContentBlockParam[] = Array.isArray(content) ? content : []
for (const b of blocks) {
if (typeof b === 'object' && b.type === 'tool_use') completionToolUseCount++
}
const textBlocks = blocks.filter(
(b): b is Extract<ContentBlockParam, { type: 'text' }> =>
typeof b === 'object' && b.type === 'text',
)
if (textBlocks.length > 0 && lastAssistantContent.length === 0) {
lastAssistantContent = textBlocks.map(b => ({ type: 'text' as const, text: b.text }))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/swarm/inProcessRunner.ts` around lines 1482 - 1496, The three uses
of "as any" around m.message?.content, blocks.filter, and textBlocks.map should
be replaced with proper typing using the existing ContentBlockParam type and a
narrow type guard; create an isContentBlock(obj): obj is ContentBlockParam type
guard and use it to cast the incoming array (e.g., const blocks =
(m.message?.content ?? []) as unknown [] then filter with isContentBlock) so you
can safely count tool_use via completionToolUseCount, filter text blocks, and
map to lastAssistantContent without using any; also update the
getTokenCountFromUsage call sites to keep the usage typing (lastUsage and
m.message.usage) consistent using the AgentToolResult['usage'] type rather than
any.

)
}
if (completionTokens > 0 && lastAssistantContent.length > 0) break
}
}
Comment on lines +1479 to +1501
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tool-use count is undercounted: loop breaks before scanning earlier assistant turns.

The early-exit condition if (completionTokens > 0 && lastAssistantContent.length > 0) break triggers on the very first assistant message encountered while walking backwards (i.e., the most recent one), since that message typically supplies both usage and text content. As a result, completionToolUseCount only reflects tool uses from that single last assistant turn, not the cumulative total across the conversation — despite being assigned to totalToolUseCount in teammateResult.

If the intent is a true total, count tool uses in a separate pass (or don't break) and only short-circuit the content/usage lookups.

🛠️ Suggested fix
-    for (let i = allMessages.length - 1; i >= 0; i--) {
-      const m = allMessages[i]!
-      if (m.type === 'assistant') {
-        const blocks = (m.message?.content ?? []) as any[]
-        for (const b of blocks) {
-          if (b?.type === 'tool_use') completionToolUseCount++
-        }
-        const textBlocks = blocks.filter((b: any) => b?.type === 'text')
-        if (textBlocks.length > 0 && lastAssistantContent.length === 0) {
-          lastAssistantContent = textBlocks.map((b: any) => ({
-            type: 'text' as const,
-            text: b.text,
-          }))
-        }
-        if (!lastUsage && m.message?.usage) {
-          lastUsage = m.message.usage as AgentToolResult['usage']
-          completionTokens = getTokenCountFromUsage(
-            m.message.usage as Parameters<typeof getTokenCountFromUsage>[0],
-          )
-        }
-        if (completionTokens > 0 && lastAssistantContent.length > 0) break
-      }
-    }
+    for (let i = allMessages.length - 1; i >= 0; i--) {
+      const m = allMessages[i]!
+      if (m.type !== 'assistant') continue
+      const content = m.message?.content
+      const blocks = Array.isArray(content) ? content : []
+      for (const b of blocks) {
+        if (typeof b === 'object' && b !== null && 'type' in b && b.type === 'tool_use') {
+          completionToolUseCount++
+        }
+      }
+      if (lastAssistantContent.length === 0) {
+        const textBlocks = blocks.filter(
+          (b): b is { type: 'text'; text: string } =>
+            typeof b === 'object' && b !== null && 'type' in b && b.type === 'text',
+        )
+        if (textBlocks.length > 0) {
+          lastAssistantContent = textBlocks.map(b => ({ type: 'text' as const, text: b.text }))
+        }
+      }
+      if (!lastUsage && m.message?.usage) {
+        lastUsage = m.message.usage as AgentToolResult['usage']
+        completionTokens = getTokenCountFromUsage(
+          m.message.usage as Parameters<typeof getTokenCountFromUsage>[0],
+        )
+      }
+      // Do NOT break — keep counting tool_use blocks across the full history.
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/swarm/inProcessRunner.ts` around lines 1479 - 1501, The current
backward loop over allMessages sets lastAssistantContent/lastUsage and then
breaks, which prevents completionToolUseCount from aggregating tool_use blocks
from earlier assistant turns; update the loop logic so it always counts tool_use
blocks for every assistant message (increment completionToolUseCount for each
b.type === 'tool_use') but only set lastAssistantContent, lastUsage and
completionTokens the first time an assistant with text/usage is encountered (use
a foundContent/usage flag or check lastAssistantContent/lastUsage emptiness) and
do not break the loop early — finish the entire reverse scan so the final
completionToolUseCount reflects the cumulative total used later in
teammateResult. Ensure references: loop over allMessages, variables
completionToolUseCount, lastAssistantContent, lastUsage, completionTokens, and
function getTokenCountFromUsage remain consistent.


const teammateResult: AgentToolResult = {
agentId: identity.agentId,
agentType: 'teammate',
content: lastAssistantContent,
totalToolUseCount: completionToolUseCount,
totalDurationMs: Date.now() - startTime,
totalTokens: completionTokens,
usage: lastUsage as AgentToolResult['usage'],
} as unknown as AgentToolResult
Comment on lines +1503 to +1511
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the AgentToolResult definition and its agentType literal/required fields.
fd -t f 'agentToolUtils.(ts|tsx)' | head -20
rg -nP -C2 'export\s+(type|interface)\s+AgentToolResult\b'
rg -nP -C2 "agentType\s*:\s*['\"]teammate['\"]"

Repository: claude-code-best/claude-code

Length of output: 2097


🏁 Script executed:

sed -n '240,280p' packages/builtin-tools/src/tools/AgentTool/agentToolUtils.ts | cat -n

Repository: claude-code-best/claude-code

Length of output: 1471


🏁 Script executed:

sed -n '200,265p' packages/builtin-tools/src/tools/AgentTool/agentToolUtils.ts | cat -n

Repository: claude-code-best/claude-code

Length of output: 2460


🏁 Script executed:

sed -n '1470,1520p' src/utils/swarm/inProcessRunner.ts | cat -n

Repository: claude-code-best/claude-code

Length of output: 2488


Remove the double-cast as unknown as AgentToolResult after fixing the usage field type mismatch.

The lastUsage variable can be undefined (line 9: AgentToolResult['usage'] | undefined), but the schema at agentToolUtils.ts:39 requires usage as a non-optional object. Either make usage optional in the schema (usage: z.object({...}).optional()), ensure lastUsage is always defined, or conditionally include the field. The double-cast hides this incompatibility and should not be necessary once the shape matches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/swarm/inProcessRunner.ts` around lines 1503 - 1511, teammateResult
is being force-cast with "as unknown as AgentToolResult" to hide a type mismatch
for the usage field; remove the double-cast and fix the root cause by either
making the usage property optional in the schema at agentToolUtils.ts (change
usage: z.object({...}) to usage: z.object({...}).optional()), or ensure
lastUsage is always populated before building teammateResult (e.g. provide a
default usage object), or conditionally omit usage from teammateResult when
lastUsage is undefined; update the code that constructs teammateResult (and the
type of lastUsage) so no unsafe casting is required.


updateTaskState(
taskId,
task => {
Expand All @@ -1481,6 +1528,7 @@ export async function runInProcessTeammate(
status: 'completed' as const,
notified: true,
endTime: Date.now(),
result: teammateResult,
messages: task.messages?.length ? [task.messages.at(-1)!] : undefined,
pendingUserMessages: [],
inProgressToolUseIDs: undefined,
Expand Down