From 2cf6c23fdc9586fe03841489e22c66a0258df22a Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Fri, 3 Apr 2026 14:32:31 +0800 Subject: [PATCH] Remove local context from model requests --- src/constants/prompts.ts | 65 +++----------- src/context.ts | 179 ++------------------------------------- src/query.ts | 7 +- src/utils/api.ts | 110 +----------------------- 4 files changed, 23 insertions(+), 338 deletions(-) diff --git a/src/constants/prompts.ts b/src/constants/prompts.ts index 9eb49b3..92e329d 100644 --- a/src/constants/prompts.ts +++ b/src/constants/prompts.ts @@ -448,9 +448,7 @@ export async function getSystemPrompt( mcpClients?: MCPServerConnection[], ): Promise { if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { - return [ - `You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`, - ] + return [`You are Claude Code, Anthropic's official CLI for Claude.`] } const cwd = getCwd() @@ -607,8 +605,6 @@ export async function computeEnvInfo( modelId: string, additionalWorkingDirectories?: string[], ): Promise { - const [isGit, unameSR] = await Promise.all([getIsGit(), getUnameSR()]) - // Undercover: keep ALL model names/IDs out of the system prompt so nothing // internal can leak into public commits/PRs. This includes the public // FRONTIER_MODEL_* constants — if those ever point at an unannounced model, @@ -627,33 +623,20 @@ export async function computeEnvInfo( : `You are powered by the model ${modelId}.` } - const additionalDirsInfo = - additionalWorkingDirectories && additionalWorkingDirectories.length > 0 - ? `Additional working directories: ${additionalWorkingDirectories.join(', ')}\n` - : '' - const cutoff = getKnowledgeCutoff(modelId) const knowledgeCutoffMessage = cutoff ? `\n\nAssistant knowledge cutoff is ${cutoff}.` : '' - return `Here is useful information about the environment you are running in: - -Working directory: ${getCwd()} -Is directory a git repo: ${isGit ? 'Yes' : 'No'} -${additionalDirsInfo}Platform: ${env.platform} -${getShellInfoLine()} -OS Version: ${unameSR} - -${modelDescription}${knowledgeCutoffMessage}` + return [`# Environment`, `You are Claude Code.`, modelDescription, knowledgeCutoffMessage] + .filter(Boolean) + .join('\n') } export async function computeSimpleEnvInfo( modelId: string, additionalWorkingDirectories?: string[], ): Promise { - const [isGit, unameSR] = await Promise.all([getIsGit(), getUnameSR()]) - // Undercover: strip all model name/ID references. See computeEnvInfo. // DCE: inline the USER_TYPE check at each site — do NOT hoist to a const. let modelDescription: string | null = null @@ -671,42 +654,14 @@ export async function computeSimpleEnvInfo( ? `Assistant knowledge cutoff is ${cutoff}.` : null - const cwd = getCwd() - const isWorktree = getCurrentWorktreeSession() !== null - - const envItems = [ - `Primary working directory: ${cwd}`, - isWorktree - ? `This is a git worktree — an isolated copy of the repository. Run all commands from this directory. Do NOT \`cd\` to the original repository root.` - : null, - [`Is a git repository: ${isGit}`], - additionalWorkingDirectories && additionalWorkingDirectories.length > 0 - ? `Additional working directories:` - : null, - additionalWorkingDirectories && additionalWorkingDirectories.length > 0 - ? additionalWorkingDirectories - : null, - `Platform: ${env.platform}`, - getShellInfoLine(), - `OS Version: ${unameSR}`, - modelDescription, - knowledgeCutoffMessage, - process.env.USER_TYPE === 'ant' && isUndercover() - ? null - : `The most recent Claude model family is Claude 4.5/4.6. Model IDs — Opus 4.6: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.opus}', Sonnet 4.6: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.sonnet}', Haiku 4.5: '${CLAUDE_4_5_OR_4_6_MODEL_IDS.haiku}'. When building AI applications, default to the latest and most capable Claude models.`, - process.env.USER_TYPE === 'ant' && isUndercover() - ? null - : `Claude Code is available as a CLI in the terminal, desktop app (Mac/Windows), web app (claude.ai/code), and IDE extensions (VS Code, JetBrains).`, - process.env.USER_TYPE === 'ant' && isUndercover() - ? null - : `Fast mode for Claude Code uses the same ${FRONTIER_MODEL_NAME} model with faster output. It does NOT switch to a different model. It can be toggled with /fast.`, - ].filter(item => item !== null) - return [ `# Environment`, - `You have been invoked in the following environment: `, - ...prependBullets(envItems), - ].join(`\n`) + `You are Claude Code.`, + modelDescription, + knowledgeCutoffMessage, + ] + .filter(Boolean) + .join(`\n`) } // @[MODEL LAUNCH]: Add a knowledge cutoff date for the new model. diff --git a/src/context.ts b/src/context.ts index 423414d..5053bff 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,25 +1,7 @@ -import { feature } from 'bun:bundle' import memoize from 'lodash-es/memoize.js' -import { - getAdditionalDirectoriesForClaudeMd, - setCachedClaudeMdContent, -} from './bootstrap/state.js' -import { getLocalISODate } from './constants/common.js' -import { - filterInjectedMemoryFiles, - getClaudeMds, - getMemoryFiles, -} from './utils/claudemd.js' -import { logForDiagnosticsNoPII } from './utils/diagLogs.js' -import { isBareMode, isEnvTruthy } from './utils/envUtils.js' -import { execFileNoThrow } from './utils/execFileNoThrow.js' -import { getBranch, getDefaultBranch, getIsGit, gitExe } from './utils/git.js' -import { shouldIncludeGitInstructions } from './utils/gitSettings.js' -import { logError } from './utils/log.js' +import { setCachedClaudeMdContent } from './bootstrap/state.js' -const MAX_STATUS_CHARS = 2000 - -// System prompt injection for cache breaking (ant-only, ephemeral debugging state) +// System prompt injection remains a local cache-busting hook only. let systemPromptInjection: string | null = null export function getSystemPromptInjection(): string | null { @@ -28,162 +10,17 @@ export function getSystemPromptInjection(): string | null { export function setSystemPromptInjection(value: string | null): void { systemPromptInjection = value - // Clear context caches immediately when injection changes getUserContext.cache.clear?.() getSystemContext.cache.clear?.() } -export const getGitStatus = memoize(async (): Promise => { - if (process.env.NODE_ENV === 'test') { - // Avoid cycles in tests - return null - } +export const getGitStatus = memoize(async (): Promise => null) - const startTime = Date.now() - logForDiagnosticsNoPII('info', 'git_status_started') - - const isGitStart = Date.now() - const isGit = await getIsGit() - logForDiagnosticsNoPII('info', 'git_is_git_check_completed', { - duration_ms: Date.now() - isGitStart, - is_git: isGit, - }) - - if (!isGit) { - logForDiagnosticsNoPII('info', 'git_status_skipped_not_git', { - duration_ms: Date.now() - startTime, - }) - return null - } - - try { - const gitCmdsStart = Date.now() - const [branch, mainBranch, status, log, userName] = await Promise.all([ - getBranch(), - getDefaultBranch(), - execFileNoThrow(gitExe(), ['--no-optional-locks', 'status', '--short'], { - preserveOutputOnError: false, - }).then(({ stdout }) => stdout.trim()), - execFileNoThrow( - gitExe(), - ['--no-optional-locks', 'log', '--oneline', '-n', '5'], - { - preserveOutputOnError: false, - }, - ).then(({ stdout }) => stdout.trim()), - execFileNoThrow(gitExe(), ['config', 'user.name'], { - preserveOutputOnError: false, - }).then(({ stdout }) => stdout.trim()), - ]) - - logForDiagnosticsNoPII('info', 'git_commands_completed', { - duration_ms: Date.now() - gitCmdsStart, - status_length: status.length, - }) - - // Check if status exceeds character limit - const truncatedStatus = - status.length > MAX_STATUS_CHARS - ? status.substring(0, MAX_STATUS_CHARS) + - '\n... (truncated because it exceeds 2k characters. If you need more information, run "git status" using BashTool)' - : status - - logForDiagnosticsNoPII('info', 'git_status_completed', { - duration_ms: Date.now() - startTime, - truncated: status.length > MAX_STATUS_CHARS, - }) - - return [ - `This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.`, - `Current branch: ${branch}`, - `Main branch (you will usually use this for PRs): ${mainBranch}`, - ...(userName ? [`Git user: ${userName}`] : []), - `Status:\n${truncatedStatus || '(clean)'}`, - `Recent commits:\n${log}`, - ].join('\n\n') - } catch (error) { - logForDiagnosticsNoPII('error', 'git_status_failed', { - duration_ms: Date.now() - startTime, - }) - logError(error) - return null - } -}) - -/** - * This context is prepended to each conversation, and cached for the duration of the conversation. - */ export const getSystemContext = memoize( - async (): Promise<{ - [k: string]: string - }> => { - const startTime = Date.now() - logForDiagnosticsNoPII('info', 'system_context_started') - - // Skip git status in CCR (unnecessary overhead on resume) or when git instructions are disabled - const gitStatus = - isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) || - !shouldIncludeGitInstructions() - ? null - : await getGitStatus() - - // Include system prompt injection if set (for cache breaking, ant-only) - const injection = feature('BREAK_CACHE_COMMAND') - ? getSystemPromptInjection() - : null - - logForDiagnosticsNoPII('info', 'system_context_completed', { - duration_ms: Date.now() - startTime, - has_git_status: gitStatus !== null, - has_injection: injection !== null, - }) - - return { - ...(gitStatus && { gitStatus }), - ...(feature('BREAK_CACHE_COMMAND') && injection - ? { - cacheBreaker: `[CACHE_BREAKER: ${injection}]`, - } - : {}), - } - }, + async (): Promise> => ({}), ) -/** - * This context is prepended to each conversation, and cached for the duration of the conversation. - */ -export const getUserContext = memoize( - async (): Promise<{ - [k: string]: string - }> => { - const startTime = Date.now() - logForDiagnosticsNoPII('info', 'user_context_started') - - // CLAUDE_CODE_DISABLE_CLAUDE_MDS: hard off, always. - // --bare: skip auto-discovery (cwd walk), BUT honor explicit --add-dir. - // --bare means "skip what I didn't ask for", not "ignore what I asked for". - const shouldDisableClaudeMd = - isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) || - (isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0) - // Await the async I/O (readFile/readdir directory walk) so the event - // loop yields naturally at the first fs.readFile. - const claudeMd = shouldDisableClaudeMd - ? null - : getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles())) - // Cache for the auto-mode classifier (yoloClassifier.ts reads this - // instead of importing claudemd.ts directly, which would create a - // cycle through permissions/filesystem → permissions → yoloClassifier). - setCachedClaudeMdContent(claudeMd || null) - - logForDiagnosticsNoPII('info', 'user_context_completed', { - duration_ms: Date.now() - startTime, - claudemd_length: claudeMd?.length ?? 0, - claudemd_disabled: Boolean(shouldDisableClaudeMd), - }) - - return { - ...(claudeMd && { claudeMd }), - currentDate: `Today's date is ${getLocalISODate()}.`, - } - }, -) +export const getUserContext = memoize(async (): Promise> => { + setCachedClaudeMdContent(null) + return {} +}) diff --git a/src/query.ts b/src/query.ts index 07e8b6f..224043e 100644 --- a/src/query.ts +++ b/src/query.ts @@ -55,7 +55,6 @@ import { stripSignatureBlocks, } from './utils/messages.js' import { generateToolUseSummary } from './services/toolUseSummary/toolUseSummaryGenerator.js' -import { prependUserContext, appendSystemContext } from './utils/api.js' import { createAttachmentMessage, filterDuplicateMemoryAttachments, @@ -446,9 +445,7 @@ async function* queryLoop( messagesForQuery = collapseResult.messages } - const fullSystemPrompt = asSystemPrompt( - appendSystemContext(systemPrompt, systemContext), - ) + const fullSystemPrompt = asSystemPrompt(systemPrompt) queryCheckpoint('query_autocompact_start') const { compactionResult, consecutiveFailures } = await deps.autocompact( @@ -657,7 +654,7 @@ async function* queryLoop( let streamingFallbackOccured = false queryCheckpoint('query_api_streaming_start') for await (const message of deps.callModel({ - messages: prependUserContext(messagesForQuery, userContext), + messages: messagesForQuery, systemPrompt: fullSystemPrompt, thinkingConfig: toolUseContext.options.thinkingConfig, tools: toolUseContext.options.tools, diff --git a/src/utils/api.ts b/src/utils/api.ts index 9b66fd7..c7d12b0 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -438,39 +438,14 @@ export function appendSystemContext( systemPrompt: SystemPrompt, context: { [k: string]: string }, ): string[] { - return [ - ...systemPrompt, - Object.entries(context) - .map(([key, value]) => `${key}: ${value}`) - .join('\n'), - ].filter(Boolean) + return systemPrompt } export function prependUserContext( messages: Message[], context: { [k: string]: string }, ): Message[] { - if (process.env.NODE_ENV === 'test') { - return messages - } - - if (Object.entries(context).length === 0) { - return messages - } - - return [ - createUserMessage({ - content: `\nAs you answer the user's questions, you can use the following context:\n${Object.entries( - context, - ) - .map(([key, value]) => `# ${key}\n${value}`) - .join('\n')} - - IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n\n`, - isMeta: true, - }), - ...messages, - ] + return messages } /** @@ -480,86 +455,7 @@ export async function logContextMetrics( mcpConfigs: Record, toolPermissionContext: ToolPermissionContext, ): Promise { - // Early return if logging is disabled - if (isAnalyticsDisabled()) { - return - } - const [{ tools: mcpTools }, tools, userContext, systemContext] = - await Promise.all([ - prefetchAllMcpResources(mcpConfigs), - getTools(toolPermissionContext), - getUserContext(), - getSystemContext(), - ]) - // Extract individual context sizes and calculate total - const gitStatusSize = systemContext.gitStatus?.length ?? 0 - const claudeMdSize = userContext.claudeMd?.length ?? 0 - - // Calculate total context size - const totalContextSize = gitStatusSize + claudeMdSize - - // Get file count using ripgrep (rounded to nearest power of 10 for privacy) - const currentDir = getCwd() - const ignorePatternsByRoot = getFileReadIgnorePatterns(toolPermissionContext) - const normalizedIgnorePatterns = normalizePatternsToPath( - ignorePatternsByRoot, - currentDir, - ) - const fileCount = await countFilesRoundedRg( - currentDir, - AbortSignal.timeout(1000), - normalizedIgnorePatterns, - ) - - // Calculate tool metrics - let mcpToolsCount = 0 - let mcpServersCount = 0 - let mcpToolsTokens = 0 - let nonMcpToolsCount = 0 - let nonMcpToolsTokens = 0 - - const nonMcpTools = tools.filter(tool => !tool.isMcp) - mcpToolsCount = mcpTools.length - nonMcpToolsCount = nonMcpTools.length - - // Extract unique server names from MCP tool names (format: mcp__servername__toolname) - const serverNames = new Set() - for (const tool of mcpTools) { - const parts = tool.name.split('__') - if (parts.length >= 3 && parts[1]) { - serverNames.add(parts[1]) - } - } - mcpServersCount = serverNames.size - - // Estimate tool tokens locally for analytics (avoids N API calls per session) - // Use inputJSONSchema (plain JSON Schema) when available, otherwise convert Zod schema - for (const tool of mcpTools) { - const schema = - 'inputJSONSchema' in tool && tool.inputJSONSchema - ? tool.inputJSONSchema - : zodToJsonSchema(tool.inputSchema) - mcpToolsTokens += roughTokenCountEstimation(jsonStringify(schema)) - } - for (const tool of nonMcpTools) { - const schema = - 'inputJSONSchema' in tool && tool.inputJSONSchema - ? tool.inputJSONSchema - : zodToJsonSchema(tool.inputSchema) - nonMcpToolsTokens += roughTokenCountEstimation(jsonStringify(schema)) - } - - logEvent('tengu_context_size', { - git_status_size: gitStatusSize, - claude_md_size: claudeMdSize, - total_context_size: totalContextSize, - project_file_count_rounded: fileCount, - mcp_tools_count: mcpToolsCount, - mcp_servers_count: mcpServersCount, - mcp_tools_tokens: mcpToolsTokens, - non_mcp_tools_count: nonMcpToolsCount, - non_mcp_tools_tokens: nonMcpToolsTokens, - }) + return } // TODO: Generalize this to all tools