fix(coach): keep SOS out of Coach chat history

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 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-04 10:45:38 +02:00
parent 848b517d22
commit ba200d54f4
2 changed files with 48 additions and 12 deletions

View File

@ -36,7 +36,30 @@ export default defineEventHandler(async (event) => {
return { messages: [] }; 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 { return {
messages: session.content as Array<{ role: string; content: string }>, messages: raw.filter((m) => !isInternal(m)),
}; };
}); });

View File

@ -460,11 +460,17 @@ export default defineEventHandler(async (event) => {
const user = await requireUser(event); const user = await requireUser(event);
const body = await readBody(event); const body = await readBody(event);
// sosMode ist deprecated — Coach-Page sendet es nicht mehr. // sosMode: die SOS-Page (urge.tsx) nutzt diesen Endpoint als zustandslosen
// Wird hier nur noch für Logging akzeptiert, beeinflusst kein Routing. // LLM-Proxy für Game-Kommentar / Share-Draft / Stream-Fallback. Diese Calls
const { messages, locale } = body as { // 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 }>; messages: Array<{ role: "user" | "assistant"; content: string }>;
locale?: string; locale?: string;
sosMode?: boolean;
}; };
if (!messages || !Array.isArray(messages)) { if (!messages || !Array.isArray(messages)) {
@ -705,7 +711,10 @@ export default defineEventHandler(async (event) => {
// Feedback-Detection + LLM parallel starten // Feedback-Detection + LLM parallel starten
// (lastUserMsg ist bereits oben für Sprach-Detection berechnet) // (lastUserMsg ist bereits oben für Sprach-Detection berechnet)
const feedbackPromise = lastUserMsg?.content // Im sosMode ist lastUserMsg ein [INTERN:]-Prompt, kein echtes User-Feedback
// → Detection überspringen.
const feedbackPromise =
!sosMode && lastUserMsg?.content
? detectAndSaveFeedback(lastUserMsg.content, user.id, config) ? detectAndSaveFeedback(lastUserMsg.content, user.id, config)
: Promise.resolve(false); : Promise.resolve(false);
@ -736,11 +745,13 @@ export default defineEventHandler(async (event) => {
const feedbackSaved = await feedbackPromise; 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) { if (loadedMemoryIds.length > 0) {
markReferenced(loadedMemoryIds).catch(() => {}); markReferenced(loadedMemoryIds).catch(() => {});
} }
if (text) { if (text && !sosMode) {
const allMessages = [ const allMessages = [
...messages, ...messages,
{ role: "assistant" as const, content: text }, { 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) // `plan` ist bereits oben deklariert (LLM-Routing-Block)
console.log("[coach/message] plan:", plan, "userId:", user.id); console.log("[coach/message] plan:", plan, "userId:", user.id, "sosMode:", !!sosMode);
if (plan === "pro" || plan === "legend") { if (!sosMode && (plan === "pro" || plan === "legend")) {
const fullHistory = [ const fullHistory = [
...messages, ...messages,
{ role: "assistant" as const, content: text }, { role: "assistant" as const, content: text },