diff --git a/open-sse/utils/claudeCloaking.js b/open-sse/utils/claudeCloaking.js index f4b710ff..60e40b79 100644 --- a/open-sse/utils/claudeCloaking.js +++ b/open-sse/utils/claudeCloaking.js @@ -104,14 +104,29 @@ export function cloakClaudeTools(body) { }; } +// Strip a trailing CLAUDE_TOOL_SUFFIX from a cloaked name as a last-resort +// fallback when the name isn't in toolNameMap (e.g. map lost across a retry/ +// reconnect). Never strips decoy names — those are meant to reach the client +// unresolved so it can see "tool unavailable" instead of silently no-oping. +function stripCloakSuffix(name) { + if (typeof name !== "string" || !name.endsWith(CLAUDE_TOOL_SUFFIX)) return null; + if (CC_DEFAULT_TOOLS.has(name)) return null; + const original = name.slice(0, -CLAUDE_TOOL_SUFFIX.length); + return original.length > 0 ? original : null; +} + // Decloak tool_use names in non-streaming Claude response body (INPUT side) export function decloakToolNames(body, toolNameMap) { - if (!toolNameMap?.size || !Array.isArray(body?.content)) return body; + if (!Array.isArray(body?.content)) return body; const content = body.content.map(block => { - if (block?.type === "tool_use" && toolNameMap.has(block.name)) { + if (block?.type !== "tool_use") return block; + if (toolNameMap?.has(block.name)) { return { ...block, name: toolNameMap.get(block.name) }; } - return block; + // toolNameMap missing/stale for this name — fall back to suffix stripping + // rather than forwarding an unresolvable "_ide" name to the client. + const fallback = stripCloakSuffix(block.name); + return fallback ? { ...block, name: fallback } : block; }); return { ...body, content }; } @@ -126,19 +141,21 @@ export function decloakToolNames(body, toolNameMap) { * name appears exactly once per call — on the content_block_start event of * a tool_use block; argument deltas carry no name. * - * Unknown names (e.g. a CC decoy tool the model called anyway) pass through - * unchanged, matching the non-streaming decloak behavior. + * Falls back to stripping the literal CLAUDE_TOOL_SUFFIX when the name isn't + * in toolNameMap (map lost across a retry/reconnect), matching the + * non-streaming decloak behavior. Decoy tool names (real CC tool names) and + * anything else pass through unchanged. * * @param {object|null} chunk - Parsed SSE event (may be null on stream flush) * @param {Map|null} toolNameMap - Suffixed → original name map from cloakClaudeTools() * @returns {object|null} The chunk, with the tool_use name restored when cloaked */ export function decloakStreamChunk(chunk, toolNameMap) { - if (!toolNameMap?.size || !chunk || typeof chunk !== "object") return chunk; + if (!chunk || typeof chunk !== "object") return chunk; if (chunk.type !== "content_block_start") return chunk; const block = chunk.content_block; if (block?.type !== "tool_use" || typeof block.name !== "string") return chunk; - const original = toolNameMap.get(block.name); + const original = toolNameMap?.get(block.name) || stripCloakSuffix(block.name); if (!original) return chunk; return { ...chunk, content_block: { ...block, name: original } }; }