init claude-code
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
|
||||
import { logEvent } from '../services/analytics/index.js'
|
||||
import type {
|
||||
ConnectedMCPServer,
|
||||
MCPServerConnection,
|
||||
} from '../services/mcp/types.js'
|
||||
import type { Message } from '../types/message.js'
|
||||
import { isEnvDefinedFalsy, isEnvTruthy } from './envUtils.js'
|
||||
|
||||
export type McpInstructionsDelta = {
|
||||
/** Server names — for stateless-scan reconstruction. */
|
||||
addedNames: string[]
|
||||
/** Rendered "## {name}\n{instructions}" blocks for addedNames. */
|
||||
addedBlocks: string[]
|
||||
removedNames: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-authored instruction block to announce when a server connects,
|
||||
* in addition to (or instead of) the server's own `InitializeResult.instructions`.
|
||||
* Lets first-party servers (e.g., claude-in-chrome) carry client-side
|
||||
* context the server itself doesn't know about.
|
||||
*/
|
||||
export type ClientSideInstruction = {
|
||||
serverName: string
|
||||
block: string
|
||||
}
|
||||
|
||||
/**
|
||||
* True → announce MCP server instructions via persisted delta attachments.
|
||||
* False → prompts.ts keeps its DANGEROUS_uncachedSystemPromptSection
|
||||
* (rebuilt every turn; cache-busts on late connect).
|
||||
*
|
||||
* Env override for local testing: CLAUDE_CODE_MCP_INSTR_DELTA=true/false
|
||||
* wins over both ant bypass and the GrowthBook gate.
|
||||
*/
|
||||
export function isMcpInstructionsDeltaEnabled(): boolean {
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_MCP_INSTR_DELTA)) return true
|
||||
if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_MCP_INSTR_DELTA)) return false
|
||||
return (
|
||||
process.env.USER_TYPE === 'ant' ||
|
||||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_basalt_3kr', false)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff the current set of connected MCP servers that have instructions
|
||||
* (server-authored via InitializeResult, or client-side synthesized)
|
||||
* against what's already been announced in this conversation. Null if
|
||||
* nothing changed.
|
||||
*
|
||||
* Instructions are immutable for the life of a connection (set once at
|
||||
* handshake), so the scan diffs on server NAME, not on content.
|
||||
*/
|
||||
export function getMcpInstructionsDelta(
|
||||
mcpClients: MCPServerConnection[],
|
||||
messages: Message[],
|
||||
clientSideInstructions: ClientSideInstruction[],
|
||||
): McpInstructionsDelta | null {
|
||||
const announced = new Set<string>()
|
||||
let attachmentCount = 0
|
||||
let midCount = 0
|
||||
for (const msg of messages) {
|
||||
if (msg.type !== 'attachment') continue
|
||||
attachmentCount++
|
||||
if (msg.attachment.type !== 'mcp_instructions_delta') continue
|
||||
midCount++
|
||||
for (const n of msg.attachment.addedNames) announced.add(n)
|
||||
for (const n of msg.attachment.removedNames) announced.delete(n)
|
||||
}
|
||||
|
||||
const connected = mcpClients.filter(
|
||||
(c): c is ConnectedMCPServer => c.type === 'connected',
|
||||
)
|
||||
const connectedNames = new Set(connected.map(c => c.name))
|
||||
|
||||
// Servers with instructions to announce (either channel). A server can
|
||||
// have both: server-authored instructions + a client-side block appended.
|
||||
const blocks = new Map<string, string>()
|
||||
for (const c of connected) {
|
||||
if (c.instructions) blocks.set(c.name, `## ${c.name}\n${c.instructions}`)
|
||||
}
|
||||
for (const ci of clientSideInstructions) {
|
||||
if (!connectedNames.has(ci.serverName)) continue
|
||||
const existing = blocks.get(ci.serverName)
|
||||
blocks.set(
|
||||
ci.serverName,
|
||||
existing
|
||||
? `${existing}\n\n${ci.block}`
|
||||
: `## ${ci.serverName}\n${ci.block}`,
|
||||
)
|
||||
}
|
||||
|
||||
const added: Array<{ name: string; block: string }> = []
|
||||
for (const [name, block] of blocks) {
|
||||
if (!announced.has(name)) added.push({ name, block })
|
||||
}
|
||||
|
||||
// A previously-announced server that is no longer connected → removed.
|
||||
// There is no "announced but now has no instructions" case for a still-
|
||||
// connected server: InitializeResult is immutable, and client-side
|
||||
// instruction gates are session-stable in practice. (/model can flip
|
||||
// the model gate, but deferred_tools_delta has the same property and
|
||||
// we treat history as historical — no retroactive retractions.)
|
||||
const removed: string[] = []
|
||||
for (const n of announced) {
|
||||
if (!connectedNames.has(n)) removed.push(n)
|
||||
}
|
||||
|
||||
if (added.length === 0 && removed.length === 0) return null
|
||||
|
||||
// Same diagnostic fields as tengu_deferred_tools_pool_change — same
|
||||
// scan-fails-in-prod bug, same attachment persistence path.
|
||||
logEvent('tengu_mcp_instructions_pool_change', {
|
||||
addedCount: added.length,
|
||||
removedCount: removed.length,
|
||||
priorAnnouncedCount: announced.size,
|
||||
clientSideCount: clientSideInstructions.length,
|
||||
messagesLength: messages.length,
|
||||
attachmentCount,
|
||||
midCount,
|
||||
})
|
||||
|
||||
added.sort((a, b) => a.name.localeCompare(b.name))
|
||||
return {
|
||||
addedNames: added.map(a => a.name),
|
||||
addedBlocks: added.map(a => a.block),
|
||||
removedNames: removed.sort(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user