Skip to content

Commit db805ed

Browse files
authored
Merge branch 'claude-code-best:main' into main
2 parents 5134e9a + 0face46 commit db805ed

8 files changed

Lines changed: 144 additions & 40 deletions

File tree

src/cli/print.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -377,9 +377,6 @@ const cronJitterConfigModule =
377377
require('../utils/cronJitterConfig.js') as typeof import('../utils/cronJitterConfig.js')
378378
const cronGate =
379379
require('@claude-code-best/builtin-tools/tools/ScheduleCronTool/prompt.js') as typeof import('@claude-code-best/builtin-tools/tools/ScheduleCronTool/prompt.js')
380-
const extractMemoriesModule = feature('EXTRACT_MEMORIES')
381-
? (require('../services/extractMemories/extractMemories.js') as typeof import('../services/extractMemories/extractMemories.js'))
382-
: null
383380
/* eslint-enable @typescript-eslint/no-require-imports */
384381

385382
const SHUTDOWN_TEAM_PROMPT = `<system-reminder>
@@ -985,7 +982,14 @@ export async function runHeadless(
985982
// the forked agent mid-flight. Gated by isExtractModeActive so the
986983
// tengu_slate_thimble flag controls non-interactive extraction end-to-end.
987984
if (feature('EXTRACT_MEMORIES') && isExtractModeActive()) {
988-
await extractMemoriesModule!.drainPendingExtraction()
985+
try {
986+
const { drainPendingExtraction } = await import(
987+
'../services/extractMemories/extractMemories.js'
988+
)
989+
await drainPendingExtraction()
990+
} catch {
991+
// Module load failure — non-critical at shutdown
992+
}
989993
}
990994

991995
gracefulShutdownSync(

src/components/Spinner.tsx

Lines changed: 16 additions & 8 deletions
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';
@@ -209,15 +210,22 @@ function SpinnerWithVerbInner({
209210
const hasRunningTeammates = runningTeammates.length > 0;
210211
const allIdle = hasRunningTeammates && runningTeammates.every(t => t.isIdle);
211212

212-
// Gather aggregate token stats from all running swarm teammates
213-
// In spinner-tree mode, skip aggregation (teammates have their own lines in the tree)
213+
// Gather aggregate token stats from all running agents.
214+
// In spinner-tree mode, skip in-process teammates (they have their own
215+
// per-teammate lines in the tree) but still count local-agent tasks
216+
// (background agents) which have no dedicated tree rows.
214217
let teammateTokens = 0;
215-
if (!showSpinnerTree) {
216-
for (const task of Object.values(tasks)) {
217-
if (isInProcessTeammateTask(task) && task.status === 'running') {
218-
if (task.progress?.tokenCount) {
219-
teammateTokens += task.progress.tokenCount;
220-
}
218+
for (const task of Object.values(tasks)) {
219+
if (task.status !== 'running') continue;
220+
if (isInProcessTeammateTask(task)) {
221+
if (!showSpinnerTree && task.progress?.tokenCount) {
222+
teammateTokens += task.progress.tokenCount;
223+
}
224+
continue;
225+
}
226+
if (isLocalAgentTask(task)) {
227+
if (task.progress?.tokenCount) {
228+
teammateTokens += task.progress.tokenCount;
221229
}
222230
}
223231
}

src/query/stopHooks.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ import { getTaskListId, listTasks } from '../utils/tasks.js'
3939
import { getAgentName, getTeamName, isTeammate } from '../utils/teammate.js'
4040

4141
/* eslint-disable @typescript-eslint/no-require-imports */
42-
const extractMemoriesModule = feature('EXTRACT_MEMORIES')
43-
? (require('../services/extractMemories/extractMemories.js') as typeof import('../services/extractMemories/extractMemories.js'))
44-
: null
4542
const jobClassifierModule = feature('TEMPLATES')
4643
? (require('../jobs/classifier.js') as typeof import('../jobs/classifier.js'))
4744
: null
@@ -154,12 +151,16 @@ export async function* handleStopHooks(
154151
// Fire-and-forget in both interactive and non-interactive. For -p/SDK,
155152
// print.ts drains the in-flight promise after flushing the response
156153
// but before gracefulShutdownSync (see drainPendingExtraction).
157-
void extractMemoriesModule!.executeExtractMemories(
158-
stopHookContext,
159-
toolUseContext.appendSystemMessage as
160-
| ((msg: import('../types/message.js').SystemMessage) => void)
161-
| undefined,
162-
)
154+
void import('../services/extractMemories/extractMemories.js')
155+
.then(({ executeExtractMemories }) =>
156+
executeExtractMemories(
157+
stopHookContext,
158+
toolUseContext.appendSystemMessage as
159+
| ((msg: import('../types/message.js').SystemMessage) => void)
160+
| undefined,
161+
),
162+
)
163+
.catch(() => {})
163164
}
164165
if (!toolUseContext.agentId && !poorMode) {
165166
void executeAutoDream(stopHookContext, toolUseContext.appendSystemMessage)

src/services/api/openai/__tests__/thinking.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,22 @@ describe('isOpenAIThinkingEnabled', () => {
147147
expect(isOpenAIThinkingEnabled('deepseek-coder')).toBe(true)
148148
})
149149

150+
test('returns true when model name is "mimo-v2-flash"', () => {
151+
expect(isOpenAIThinkingEnabled('mimo-v2-flash')).toBe(true)
152+
})
153+
154+
test('returns true when model name is "mimo-v2-pro"', () => {
155+
expect(isOpenAIThinkingEnabled('mimo-v2-pro')).toBe(true)
156+
})
157+
158+
test('returns true when model name is "mimo-v2.5-pro"', () => {
159+
expect(isOpenAIThinkingEnabled('mimo-v2.5-pro')).toBe(true)
160+
})
161+
162+
test('returns true when model name contains "mimo"', () => {
163+
expect(isOpenAIThinkingEnabled('MiMo-V2-Omni')).toBe(true)
164+
})
165+
150166
test('returns false when model name is "gpt-4o"', () => {
151167
expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(false)
152168
})
@@ -197,7 +213,10 @@ describe('buildOpenAIRequestBody — thinking params', () => {
197213
test('includes vLLM/self-hosted thinking format when enabled', () => {
198214
const body = buildOpenAIRequestBody({ ...baseParams, enableThinking: true })
199215
expect(body.enable_thinking).toBe(true)
200-
expect(body.chat_template_kwargs).toEqual({ thinking: true })
216+
expect(body.chat_template_kwargs).toEqual({
217+
thinking: true,
218+
enable_thinking: true,
219+
})
201220
})
202221

203222
test('includes both formats simultaneously when enabled', () => {

src/services/api/openai/requestBody.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/
77
import { isEnvTruthy, isEnvDefinedFalsy } from '../../../utils/envUtils.js'
88

99
/**
10-
* Detect whether DeepSeek-style thinking mode should be enabled.
10+
* Detect whether thinking mode should be enabled for this model.
1111
*
1212
* Enabled when:
1313
* 1. OPENAI_ENABLE_THINKING=1 is set (explicit enable), OR
14-
* 2. Model name contains "deepseek-reasoner" OR "DeepSeek-V3.2" (auto-detect, case-insensitive)
14+
* 2. Model name contains "deepseek" or "mimo" (auto-detect, case-insensitive)
1515
*
1616
* Disabled when:
1717
* - OPENAI_ENABLE_THINKING=0/false/no/off is explicitly set (overrides model detection)
@@ -23,9 +23,9 @@ export function isOpenAIThinkingEnabled(model: string): boolean {
2323
if (isEnvDefinedFalsy(process.env.OPENAI_ENABLE_THINKING)) return false
2424
// Explicit enable
2525
if (isEnvTruthy(process.env.OPENAI_ENABLE_THINKING)) return true
26-
// Auto-detect from model name (all DeepSeek models support thinking mode)
26+
// Auto-detect from model name (DeepSeek and MiMo models support thinking mode)
2727
const modelLower = model.toLowerCase()
28-
return modelLower.includes('deepseek')
28+
return modelLower.includes('deepseek') || modelLower.includes('mimo')
2929
}
3030

3131
/**
@@ -58,12 +58,12 @@ export function resolveOpenAIMaxTokens(
5858
* Build the request body for OpenAI chat.completions.create().
5959
* Extracted for testability — the thinking mode params are injected here.
6060
*
61-
* DeepSeek thinking mode: inject thinking params via request body.
62-
* Two formats are added simultaneously to support different deployments:
63-
* - Official DeepSeek API: `thinking: { type: 'enabled' }`
64-
* - Self-hosted DeepSeek-V3.2: `enable_thinking: true` + `chat_template_kwargs: { thinking: true }`
61+
* Three thinking-mode formats are sent simultaneously; each endpoint uses the
62+
* format it recognizes and ignores the others:
63+
* - Official DeepSeek API: `thinking: { type: 'enabled' }`
64+
* - Self-hosted DeepSeek: `enable_thinking: true` + `chat_template_kwargs: { thinking: true }`
65+
* - MiMo (Xiaomi): `chat_template_kwargs: { enable_thinking: true }`
6566
* OpenAI SDK passes unknown keys through to the HTTP body.
66-
* Each endpoint will use the format it recognizes and ignore the others.
6767
*/
6868
export function buildOpenAIRequestBody(params: {
6969
model: string
@@ -76,7 +76,7 @@ export function buildOpenAIRequestBody(params: {
7676
}): ChatCompletionCreateParamsStreaming & {
7777
thinking?: { type: string }
7878
enable_thinking?: boolean
79-
chat_template_kwargs?: { thinking: boolean }
79+
chat_template_kwargs?: { thinking: boolean; enable_thinking: boolean }
8080
} {
8181
const {
8282
model,
@@ -97,14 +97,15 @@ export function buildOpenAIRequestBody(params: {
9797
}),
9898
stream: true,
9999
stream_options: { include_usage: true },
100-
// DeepSeek thinking mode: enable chain-of-thought output.
101-
// When active, temperature/top_p/presence_penalty/frequency_penalty are ignored by DeepSeek.
100+
// Enable chain-of-thought output for DeepSeek and MiMo models.
101+
// When active, temperature/top_p/presence_penalty/frequency_penalty are ignored.
102102
...(enableThinking && {
103103
// Official DeepSeek API format
104104
thinking: { type: 'enabled' },
105105
// Self-hosted DeepSeek-V3.2 format
106106
enable_thinking: true,
107-
chat_template_kwargs: { thinking: true },
107+
// Both DeepSeek self-hosted and MiMo formats in chat_template_kwargs
108+
chat_template_kwargs: { thinking: true, enable_thinking: true },
108109
}),
109110
// Only send temperature when thinking mode is off (DeepSeek ignores it anyway,
110111
// but other providers may respect it)

src/utils/backgroundHousekeeping.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ import { initMagicDocs } from '../services/MagicDocs/magicDocs.js'
44
import { initSkillImprovement } from './hooks/skillImprovement.js'
55

66
/* eslint-disable @typescript-eslint/no-require-imports */
7-
const extractMemoriesModule = feature('EXTRACT_MEMORIES')
8-
? (require('../services/extractMemories/extractMemories.js') as typeof import('../services/extractMemories/extractMemories.js'))
9-
: null
107
const registerProtocolModule = feature('LODESTONE')
118
? (require('./deepLink/registerProtocol.js') as typeof import('./deepLink/registerProtocol.js'))
129
: null
@@ -32,7 +29,13 @@ export function startBackgroundHousekeeping(): void {
3229
void initMagicDocs()
3330
void initSkillImprovement()
3431
if (feature('EXTRACT_MEMORIES')) {
35-
extractMemoriesModule!.initExtractMemories()
32+
void import('../services/extractMemories/extractMemories.js')
33+
.then(({ initExtractMemories }) => {
34+
initExtractMemories()
35+
})
36+
.catch(() => {
37+
// Module load failure — non-critical, memory extraction just won't run
38+
})
3639
}
3740
initAutoDream()
3841
void autoUpdateMarketplacesAndPluginsInBackground()

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)