π File detail
utils/doctorContextWarnings.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes ContextWarning, ContextWarnings, and checkContextWarnings β mainly types, interfaces, or factory objects. It composes internal code from services, Tool, tools, analyzeContext, and claudemd (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { roughTokenCountEstimation } from '../services/tokenEstimation.js' import type { Tool, ToolPermissionContext } from '../Tool.js' import type { AgentDefinitionsResult } from '../tools/AgentTool/loadAgentsDir.js' import { countMcpToolTokens } from './analyzeContext.js' import {
π€ Exports (heuristic)
ContextWarningContextWarningscheckContextWarnings
π₯οΈ Source preview
import { roughTokenCountEstimation } from '../services/tokenEstimation.js'
import type { Tool, ToolPermissionContext } from '../Tool.js'
import type { AgentDefinitionsResult } from '../tools/AgentTool/loadAgentsDir.js'
import { countMcpToolTokens } from './analyzeContext.js'
import {
getLargeMemoryFiles,
getMemoryFiles,
MAX_MEMORY_CHARACTER_COUNT,
} from './claudemd.js'
import { getMainLoopModel } from './model/model.js'
import { permissionRuleValueToString } from './permissions/permissionRuleParser.js'
import { detectUnreachableRules } from './permissions/shadowedRuleDetection.js'
import { SandboxManager } from './sandbox/sandbox-adapter.js'
import {
AGENT_DESCRIPTIONS_THRESHOLD,
getAgentDescriptionsTotalTokens,
} from './statusNoticeHelpers.js'
import { plural } from './stringUtils.js'
// Thresholds (matching status notices and existing patterns)
const MCP_TOOLS_THRESHOLD = 25_000 // 15k tokens
export type ContextWarning = {
type:
| 'claudemd_files'
| 'agent_descriptions'
| 'mcp_tools'
| 'unreachable_rules'
severity: 'warning' | 'error'
message: string
details: string[]
currentValue: number
threshold: number
}
export type ContextWarnings = {
claudeMdWarning: ContextWarning | null
agentWarning: ContextWarning | null
mcpWarning: ContextWarning | null
unreachableRulesWarning: ContextWarning | null
}
async function checkClaudeMdFiles(): Promise<ContextWarning | null> {
const largeFiles = getLargeMemoryFiles(await getMemoryFiles())
// This already filters for files > 40k chars each
if (largeFiles.length === 0) {
return null
}
const details = largeFiles
.sort((a, b) => b.content.length - a.content.length)
.map(file => `${file.path}: ${file.content.length.toLocaleString()} chars`)
const message =
largeFiles.length === 1
? `Large CLAUDE.md file detected (${largeFiles[0]!.content.length.toLocaleString()} chars > ${MAX_MEMORY_CHARACTER_COUNT.toLocaleString()})`
: `${largeFiles.length} large CLAUDE.md files detected (each > ${MAX_MEMORY_CHARACTER_COUNT.toLocaleString()} chars)`
return {
type: 'claudemd_files',
severity: 'warning',
message,
details,
currentValue: largeFiles.length, // Number of files exceeding threshold
threshold: MAX_MEMORY_CHARACTER_COUNT,
}
}
/**
* Check agent descriptions token count
*/
async function checkAgentDescriptions(
agentInfo: AgentDefinitionsResult | null,
): Promise<ContextWarning | null> {
if (!agentInfo) {
return null
}
const totalTokens = getAgentDescriptionsTotalTokens(agentInfo)
if (totalTokens <= AGENT_DESCRIPTIONS_THRESHOLD) {
return null
}
// Calculate tokens for each agent
const agentTokens = agentInfo.activeAgents
.filter(a => a.source !== 'built-in')
.map(agent => {
const description = `${agent.agentType}: ${agent.whenToUse}`
return {
name: agent.agentType,
tokens: roughTokenCountEstimation(description),
}
})
.sort((a, b) => b.tokens - a.tokens)
const details = agentTokens
.slice(0, 5)
.map(agent => `${agent.name}: ~${agent.tokens.toLocaleString()} tokens`)
if (agentTokens.length > 5) {
details.push(`(${agentTokens.length - 5} more custom agents)`)
}
return {
type: 'agent_descriptions',
severity: 'warning',
message: `Large agent descriptions (~${totalTokens.toLocaleString()} tokens > ${AGENT_DESCRIPTIONS_THRESHOLD.toLocaleString()})`,
details,
currentValue: totalTokens,
threshold: AGENT_DESCRIPTIONS_THRESHOLD,
}
}
/**
* Check MCP tools token count
*/
async function checkMcpTools(
tools: Tool[],
getToolPermissionContext: () => Promise<ToolPermissionContext>,
agentInfo: AgentDefinitionsResult | null,
): Promise<ContextWarning | null> {
const mcpTools = tools.filter(tool => tool.isMcp)
// Note: MCP tools are loaded asynchronously and may not be available
// when doctor command runs, as it executes before MCP connections are established
if (mcpTools.length === 0) {
return null
}
try {
// Use the existing countMcpToolTokens function from analyzeContext
const model = getMainLoopModel()
const { mcpToolTokens, mcpToolDetails } = await countMcpToolTokens(
tools,
getToolPermissionContext,
agentInfo,
model,
)
if (mcpToolTokens <= MCP_TOOLS_THRESHOLD) {
return null
}
// Group tools by server
const toolsByServer = new Map<string, { count: number; tokens: number }>()
for (const tool of mcpToolDetails) {
// Extract server name from tool name (format: mcp__servername__toolname)
const parts = tool.name.split('__')
const serverName = parts[1] || 'unknown'
const current = toolsByServer.get(serverName) || { count: 0, tokens: 0 }
toolsByServer.set(serverName, {
count: current.count + 1,
tokens: current.tokens + tool.tokens,
})
}
// Sort servers by token count
const sortedServers = Array.from(toolsByServer.entries()).sort(
(a, b) => b[1].tokens - a[1].tokens,
)
const details = sortedServers
.slice(0, 5)
.map(
([name, info]) =>
`${name}: ${info.count} tools (~${info.tokens.toLocaleString()} tokens)`,
)
if (sortedServers.length > 5) {
details.push(`(${sortedServers.length - 5} more servers)`)
}
return {
type: 'mcp_tools',
severity: 'warning',
message: `Large MCP tools context (~${mcpToolTokens.toLocaleString()} tokens > ${MCP_TOOLS_THRESHOLD.toLocaleString()})`,
details,
currentValue: mcpToolTokens,
threshold: MCP_TOOLS_THRESHOLD,
}
} catch (_error) {
// If token counting fails, fall back to character-based estimation
const estimatedTokens = mcpTools.reduce((total, tool) => {
const chars = (tool.name?.length || 0) + tool.description.length
return total + roughTokenCountEstimation(chars.toString())
}, 0)
if (estimatedTokens <= MCP_TOOLS_THRESHOLD) {
return null
}
return {
type: 'mcp_tools',
severity: 'warning',
message: `Large MCP tools context (~${estimatedTokens.toLocaleString()} tokens estimated > ${MCP_TOOLS_THRESHOLD.toLocaleString()})`,
details: [
`${mcpTools.length} MCP tools detected (token count estimated)`,
],
currentValue: estimatedTokens,
threshold: MCP_TOOLS_THRESHOLD,
}
}
}
/**
* Check for unreachable permission rules (e.g., specific allow rules shadowed by tool-wide ask rules)
*/
async function checkUnreachableRules(
getToolPermissionContext: () => Promise<ToolPermissionContext>,
): Promise<ContextWarning | null> {
const context = await getToolPermissionContext()
const sandboxAutoAllowEnabled =
SandboxManager.isSandboxingEnabled() &&
SandboxManager.isAutoAllowBashIfSandboxedEnabled()
const unreachable = detectUnreachableRules(context, {
sandboxAutoAllowEnabled,
})
if (unreachable.length === 0) {
return null
}
const details = unreachable.flatMap(r => [
`${permissionRuleValueToString(r.rule.ruleValue)}: ${r.reason}`,
` Fix: ${r.fix}`,
])
return {
type: 'unreachable_rules',
severity: 'warning',
message: `${unreachable.length} ${plural(unreachable.length, 'unreachable permission rule')} detected`,
details,
currentValue: unreachable.length,
threshold: 0,
}
}
/**
* Check all context warnings for the doctor command
*/
export async function checkContextWarnings(
tools: Tool[],
agentInfo: AgentDefinitionsResult | null,
getToolPermissionContext: () => Promise<ToolPermissionContext>,
): Promise<ContextWarnings> {
const [claudeMdWarning, agentWarning, mcpWarning, unreachableRulesWarning] =
await Promise.all([
checkClaudeMdFiles(),
checkAgentDescriptions(agentInfo),
checkMcpTools(tools, getToolPermissionContext, agentInfo),
checkUnreachableRules(getToolPermissionContext),
])
return {
claudeMdWarning,
agentWarning,
mcpWarning,
unreachableRulesWarning,
}
}