chahinebrini 96e1b8368c feat(lyra): deterministisches Krisen-Sicherheitsnetz (R-LYRA-01)
LLM-unabhaengiges Sicherheitsnetz fuer Lyras SOS-Pfad, schliesst das
Top-Risiko der Risiko-Akte (verpasste Krise, ISO 14971 R-LYRA-01).

Backend:
- crisis-filter.ts: deterministische Krisen-/Suizid-Erkennung (DE primaer,
  EN/FR/AR Grundabdeckung) auf den letzten User-Nachrichten, synchron, kein LLM
- sos-session.post: liefert crisisLevel sofort an die App (vor Stream-Start)
- sos-stream: sendet bei Krise zuerst 'crisis_chips' (BZgA/112/Telefonseelsorge);
  Fallback an 3 Stellen (LLM-Fehler/Abbruch/keine Chips) -> nie leerer Screen
- 43/43 Unit-Tests (crisis.json positiv, harmless.json False-Positive-Guard)

Frontend (urge.tsx):
- permanente rote Krisen-Bar oben, durch LLM-Chips nicht ueberschreibbar
  (eigener State-Slot), Hotline-Chips als tel:-Links
- neue Locale-Strings DE/EN

Risiko-Akte: R-LYRA-01 Restrisiko HOCH -> MITTEL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:56:34 +02:00

60 lines
1.6 KiB
TypeScript

/**
* In-Memory Session Store für SOS-Streaming
*
* POST /api/coach/sos-session speichert messages/locale hier,
* GET /api/coach/sos-stream lädt sie per sessionId.
*
* TTL: 5 Minuten (Auto-Cleanup)
*/
import type { CrisisLevel } from "./crisis-filter";
type SosSessionData = {
userId: string;
messages: Array<{ role: "user" | "assistant"; content: string }>;
locale: string;
/** A/B-Test: client wählt LLM via Toggle. Default openrouter-sonnet. */
llmProvider?: string;
createdAt: number;
/**
* Deterministischer Krisen-Flag: gesetzt von /api/coach/sos-session (POST)
* via crisis-filter.ts, BEVOR das LLM aufgerufen wird.
* "crisis" | "elevated" | "none" — "none" oder undefined = kein Match.
*/
crisisLevel?: CrisisLevel;
};
const sessions = new Map<string, SosSessionData>();
const SESSION_TTL = 5 * 60 * 1000; // 5min
// Cleanup-Intervall: alle 2min alte Sessions löschen
setInterval(
() => {
const now = Date.now();
for (const [id, data] of sessions.entries()) {
if (now - data.createdAt > SESSION_TTL) {
sessions.delete(id);
}
}
},
2 * 60 * 1000,
);
export function setSosSession(sessionId: string, data: SosSessionData) {
sessions.set(sessionId, data);
}
export function getSosSession(sessionId: string): SosSessionData | undefined {
const data = sessions.get(sessionId);
if (!data) return undefined;
// TTL-Check
if (Date.now() - data.createdAt > SESSION_TTL) {
sessions.delete(sessionId);
return undefined;
}
return data;
}
export function deleteSosSession(sessionId: string) {
sessions.delete(sessionId);
}