From ba200d54f4a63070e63921dcdfdef6ddfe2cb173 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 4 Jun 2026 10:45:38 +0200 Subject: [PATCH] fix(coach): keep SOS out of Coach chat history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SOS (urge.tsx) uses /api/coach/message as a stateless LLM proxy for game comments, share drafts and the stream fallback — sending SOS_BOOT + [INTERN:] prompts. The endpoint persisted the full messages array into coachSession for pro/legend users, so those internal prompts and the raw JSON replies leaked into the Coach chat history as visible bubbles. - Reactivate the sosMode flag (already sent by all three SOS call-sites): when set, the endpoint skips coachSession persistence, memory extraction and feedback detection — pure LLM proxy, no shared state. - Add a defensive filter on /api/coach/history that strips internal messages (SOS_BOOT, [INTERN:], [SYSTEM-HINT], raw JSON / [[CHIPS]] replies) so already-contaminated sessions self-heal on next load. Co-Authored-By: Claude Opus 4.8 --- backend/server/api/coach/history.get.ts | 25 ++++++++++++++++- backend/server/api/coach/message.post.ts | 35 ++++++++++++++++-------- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/backend/server/api/coach/history.get.ts b/backend/server/api/coach/history.get.ts index 8aae247..1c54f29 100644 --- a/backend/server/api/coach/history.get.ts +++ b/backend/server/api/coach/history.get.ts @@ -36,7 +36,30 @@ export default defineEventHandler(async (event) => { return { messages: [] }; } + // Defensiver Filter: Alt-Sessions können vor dem sosMode-Fix mit SOS-Resten + // kontaminiert sein (SOS_BOOT, [INTERN:]/[SYSTEM-HINT]-Prompts, rohe + // JSON-/[[CHIPS]]-Antworten). Solche Nachrichten gehören NIE in den Coach-Chat + // → beim Laden rausfiltern, damit bestehende Verläufe automatisch heilen. + const raw = + (session.content as Array<{ role: string; content: string }>) ?? []; + const isInternal = (m: { role: string; content: string }) => { + const c = (m?.content ?? "").trimStart(); + if ( + c.startsWith("[SOS-MODUS") || + c.startsWith("[INTERN:") || + c.startsWith("[SYSTEM-HINT]") + ) { + return true; + } + // Rohe Lyra-JSON-Antwort (Game-Kommentar / Share-Draft) oder Chips-Marker + if (m?.role === "assistant") { + if (c.includes("[[CHIPS]]:")) return true; + if (/^\{[\s\S]*"message"\s*:/.test(c)) return true; + } + return false; + }; + return { - messages: session.content as Array<{ role: string; content: string }>, + messages: raw.filter((m) => !isInternal(m)), }; }); diff --git a/backend/server/api/coach/message.post.ts b/backend/server/api/coach/message.post.ts index 1c82016..c66c02d 100644 --- a/backend/server/api/coach/message.post.ts +++ b/backend/server/api/coach/message.post.ts @@ -460,11 +460,17 @@ export default defineEventHandler(async (event) => { const user = await requireUser(event); const body = await readBody(event); - // sosMode ist deprecated — Coach-Page sendet es nicht mehr. - // Wird hier nur noch für Logging akzeptiert, beeinflusst kein Routing. - const { messages, locale } = body as { + // sosMode: die SOS-Page (urge.tsx) nutzt diesen Endpoint als zustandslosen + // LLM-Proxy für Game-Kommentar / Share-Draft / Stream-Fallback. Diese Calls + // tragen SOS_BOOT + [INTERN:]-Prompts in der messages-Array und DÜRFEN NICHT + // in die Coach-Chat-History (coachSession) geschrieben werden — sonst tauchen + // SOS-Reste (z.B. "[INTERN: …Snake…PB 13…]") als Bubbles im Coach-Tab auf. + // SOS und Coaching sind strikt getrennt: bei sosMode === true keine + // Persistenz, keine Memory-Extraction, keine Feedback-Detection. + const { messages, locale, sosMode } = body as { messages: Array<{ role: "user" | "assistant"; content: string }>; locale?: string; + sosMode?: boolean; }; if (!messages || !Array.isArray(messages)) { @@ -705,9 +711,12 @@ export default defineEventHandler(async (event) => { // Feedback-Detection + LLM parallel starten // (lastUserMsg ist bereits oben für Sprach-Detection berechnet) - const feedbackPromise = lastUserMsg?.content - ? detectAndSaveFeedback(lastUserMsg.content, user.id, config) - : Promise.resolve(false); + // Im sosMode ist lastUserMsg ein [INTERN:]-Prompt, kein echtes User-Feedback + // → Detection überspringen. + const feedbackPromise = + !sosMode && lastUserMsg?.content + ? detectAndSaveFeedback(lastUserMsg.content, user.id, config) + : Promise.resolve(false); let text: string | null = null; let usedModel: string | null = null; @@ -736,11 +745,13 @@ export default defineEventHandler(async (event) => { const feedbackSaved = await feedbackPromise; - // Memory: markReferenced + Extraction fire-and-forget + // Memory: markReferenced + Extraction fire-and-forget. + // sosMode überspringt Extraction — die messages-Array enthält SOS_BOOT + + // [INTERN:]-Prompts, daraus würden nur Müll-Memories entstehen. if (loadedMemoryIds.length > 0) { markReferenced(loadedMemoryIds).catch(() => {}); } - if (text) { + if (text && !sosMode) { const allMessages = [ ...messages, { role: "assistant" as const, content: text }, @@ -751,10 +762,12 @@ export default defineEventHandler(async (event) => { ); } - // Chat-Verlauf für Pro/Legend in DB speichern + // Chat-Verlauf für Pro/Legend in DB speichern. + // sosMode NICHT persistieren — SOS und Coach-Chat sind strikt getrennt; + // SOS-Reste dürfen nie in der Coach-History (coachSession) landen. // `plan` ist bereits oben deklariert (LLM-Routing-Block) - console.log("[coach/message] plan:", plan, "userId:", user.id); - if (plan === "pro" || plan === "legend") { + console.log("[coach/message] plan:", plan, "userId:", user.id, "sosMode:", !!sosMode); + if (!sosMode && (plan === "pro" || plan === "legend")) { const fullHistory = [ ...messages, { role: "assistant" as const, content: text },