diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index e7cbdd28c4..be4cd5367b 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -2106,6 +2106,7 @@ export async function handleChatCore({ const allMessages = compressionBody?.messages || body?.contents || body?.request?.contents || []; let cavemanOutputModeApplied = false; let cavemanOutputModeIntensity: string | null = null; + let preCompressionBody: typeof body | null = null; if (body && Array.isArray(allMessages) && allMessages.length > 0) { let estimatedTokens = estimateTokens(JSON.stringify(allMessages)); let promptCompressionEnabled = false; @@ -2557,6 +2558,10 @@ export async function handleChatCore({ `Checking compression: ${estimatedTokens} tokens vs ${threshold} threshold (${contextLimit} limit, ${reservedTokens} reserved)` ); + // Capture pre-compression body so translators can access original message + // content even after compression alters it (e.g. stable Kiro conversationId). + preCompressionBody = body; + if (promptCompressionEnabled && estimatedTokens > threshold) { log?.info?.( "CONTEXT", @@ -2933,6 +2938,7 @@ export async function handleChatCore({ preserveDeveloperRole, preserveCacheControl, signatureNamespace: connectionId, + ...(preCompressionBody ? { preCompressionBody } : {}), } ); } diff --git a/open-sse/translator/index.ts b/open-sse/translator/index.ts index ffa9758b2a..9953a1e9cc 100644 --- a/open-sse/translator/index.ts +++ b/open-sse/translator/index.ts @@ -132,6 +132,7 @@ export function translateRequest( preserveDeveloperRole?: boolean; preserveCacheControl?: boolean; signatureNamespace?: string | null; + preCompressionBody?: Record | null; } ) { let result = body; @@ -185,12 +186,16 @@ export function translateRequest( if (targetFormat !== FORMATS.OPENAI) { const fromOpenAI = getRequestTranslator(FORMATS.OPENAI, targetFormat); if (fromOpenAI) { - const translationCredentials = options?.signatureNamespace - ? { - ...(credentials && typeof credentials === "object" ? credentials : {}), - _signatureNamespace: options.signatureNamespace, - } - : credentials; + const hasNs = options?.signatureNamespace != null; + const hasPreCompression = options?.preCompressionBody != null; + const translationCredentials = + hasNs || hasPreCompression + ? { + ...(credentials && typeof credentials === "object" ? credentials : {}), + ...(hasNs ? { _signatureNamespace: options.signatureNamespace } : {}), + ...(hasPreCompression ? { _preCompressionBody: options.preCompressionBody } : {}), + } + : credentials; result = fromOpenAI(model, result, stream, translationCredentials); } } diff --git a/open-sse/translator/request/openai-to-kiro.ts b/open-sse/translator/request/openai-to-kiro.ts index 9fb12bcb20..4483fbd98e 100644 --- a/open-sse/translator/request/openai-to-kiro.ts +++ b/open-sse/translator/request/openai-to-kiro.ts @@ -746,8 +746,33 @@ export function buildKiroPayload(model, body, stream, credentials) { // upstream Kiro/AWS conversation context, leaking prior state across // sessions. See conversionMessages() above for the `__synthetic` marker. const NAMESPACE_KIRO = "34f7193f-561d-4050-bc84-9547d953d6bf"; + + // Priority 1: Extract first user message from pre-compression body (passed by chatCore before + // compressContext runs). This keeps conversationId stable even when compression alters content. + // Priority 2: Deterministic hash from first user message in translated history (fallback). + const preCompressionBody = credentials?._preCompressionBody as + | Record + | null + | undefined; + const preCompressionMessages = Array.isArray(preCompressionBody?.messages) + ? preCompressionBody.messages + : null; + const preCompressionFirstUser = preCompressionMessages?.find( + (m: Record) => m.role === "user" + ); + const seedFromPreCompression = preCompressionFirstUser + ? typeof preCompressionFirstUser.content === "string" + ? preCompressionFirstUser.content + : Array.isArray(preCompressionFirstUser.content) + ? (preCompressionFirstUser.content as Array<{ type: string; text?: string }>) + .filter((b) => b.type === "text") + .map((b) => b.text || "") + .join(" ") + : "" + : ""; const firstRealUserTurn = history.find((h) => h?.userInputMessage?.content && !h.__synthetic); - const firstContent = firstRealUserTurn?.userInputMessage?.content || finalContent; + const firstContent = + seedFromPreCompression || firstRealUserTurn?.userInputMessage?.content || finalContent; // Use uuidv5 with the hash of the system prompt / first message to maintain AWS Builder ID context cache payload.conversationState.conversationId = uuidv5(