Compare commits
26 Commits
845a8b0020
...
v0.1.6.5
| Author | SHA1 | Date | |
|---|---|---|---|
| b182ef5ed5 | |||
| 9818dc1867 | |||
| 543ad3c46d | |||
| 408d20a087 | |||
| 0756baa2a0 | |||
| 27c9b1af96 | |||
| 70f4ff480e | |||
| c23daf14e3 | |||
| ebfde4cd1f | |||
| 5d3e3e5e8c | |||
| 0d69e211cb | |||
| 4ea13afe60 | |||
| d12bfd0302 | |||
| 8d5991f364 | |||
| 7d16a0f3e5 | |||
| 0a859f637b | |||
| 8c1476c2ca | |||
| 7d8c411f5c | |||
| fef2a32c50 | |||
| e7fd918559 | |||
| bb3c7957aa | |||
| 89cafa6251 | |||
| 1ea7ab5ab1 | |||
| 15f95ed196 | |||
| 210ce62ffe | |||
| 298b2202a1 |
@@ -37,6 +37,12 @@ aria-data/brain/qdrant/
|
||||
# Diagnostic-State (aktive Session etc.)
|
||||
aria-data/config/diag-state/
|
||||
|
||||
# ── Shared Volume (Bind-Mount statt Docker-managed) ──
|
||||
# Enthaelt User-Uploads, Voice-Cloning-Samples, OAuth-Tokens,
|
||||
# chat_backup.jsonl, Memory-Attachments, runtime-state. Hunderte MB,
|
||||
# enthaelt PRIVATE Daten. Backup via Diagnostic, nicht via Git.
|
||||
aria-shared/
|
||||
|
||||
# ── Node / npm ──────────────────────────────────
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
|
||||
@@ -388,7 +388,7 @@ Chat-Turn an die richtigen Patterns.
|
||||
|
||||
### Skill-Regeln (seed_rules)
|
||||
|
||||
`aria-brain/seed_rules.py` enthaelt 11 `type=rule, pinned=true,
|
||||
`aria-brain/seed_rules.py` enthaelt 20 `type=rule, pinned=true,
|
||||
source=seed`-Memories, die bei jedem Brain-Start idempotent in die
|
||||
Vector-DB geschrieben werden (`migration_key`-basiert). Sie tauchen in
|
||||
jedem Chat-Turn im Hot-Memory-Block auf:
|
||||
@@ -402,7 +402,7 @@ jedem Chat-Turn im Hot-Memory-Block auf:
|
||||
- **oauth-reauth-reflex** — bei 401: ZUERST `oauth_get_token` (Auto-Refresh), nur bei dessen Fehler `oauth_authorize`
|
||||
- **no-skill-drift** — kein Drift vom Skill zu Ad-hoc-Bash-Befehlen. Skill kaputt? `skill_logs` + `skill_update`. Niemals nur SAGEN „ich baue dir einen Skill", wenn `skill_create` nicht wirklich gefeuert wird
|
||||
- **runtime-topology** (architektur) — ARIA laeuft als `claude`-CLI-Subprocess IM aria-proxy Container (alpine — kein python3/jq), NICHT im aria-brain. `/data/skills/` und `BRAIN_INTERNAL_URL` existieren dort nicht. Brain-Resources via Brain-Tools (`oauth_get_token`, `memory_search`, `run_<skill>` …), nicht via Bash. SSH zur VM-Host via `ssh aria@host` (Key liegt im Proxy)
|
||||
- **scaffold-reflex** — Brain trackt cross-session welche externen Hosts via Bash-curl wiederholt (≥3× in 24h) ohne passenden Skill aufgerufen wurden. Ergebnis landet als `## API-Heuristik`-Block im System-Prompt mit konkretem `skill_scaffold(...)`-Vorschlag → ARIA scaffolded statt zu curlen. Data-Source: `agent_stream.jsonl`, Cache 5 min
|
||||
- **scaffold-reflex** — ARIA entscheidet selbst ob ein wiederkehrender Bash-Pattern Skill-würdig ist (parametrisierbar + wiederkehrend + nicht-exploratory). Im Zweifel fragt sie Stefan. **Kein Auto-Scaffold, kein Tracking, keine Pflege** — Skills werden bewusst angelegt, nicht magisch. Pentest/Audit/Recherche bleibt ad-hoc Bash, auch bei 100× derselbe Host.
|
||||
- **external-api-auth-strategy** — OAuth2 → `oauth_get_token`, sonst `config_schema`, NIEMALS hardcoden
|
||||
|
||||
### Skill-Scaffold (Templates)
|
||||
|
||||
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "com.ariacockpit"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 10603
|
||||
versionName "0.1.6.3"
|
||||
versionCode 10605
|
||||
versionName "0.1.6.5"
|
||||
// Fallback fuer Libraries mit Product Flavors
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!-- WAKE_LOCK damit Wake-Word + JS-Bridge auch bei aus-Display und Doze
|
||||
arbeiten: ohne Lock pausiert Android die CPU, Native-AudioRecord
|
||||
laeuft weiter aber JS-Bridge frisst die DeviceEvents nicht mehr ->
|
||||
Wake-Word wird erkannt aber callbacks feuern erst beim App-Resume. -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
|
||||
@@ -5,9 +5,11 @@ import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
@@ -32,15 +34,26 @@ class AriaPlaybackService : Service() {
|
||||
|
||||
private var currentReason: String = ""
|
||||
|
||||
// PARTIAL_WAKE_LOCK haelt die CPU wach solange der Foreground-Service
|
||||
// aktiv ist. Damit bleibt die JS-Bridge im Doze ansprechbar und die
|
||||
// gesamte Sprach-Pipeline (Wake → Aufnahme → POST → ARIA → TTS → wieder
|
||||
// Wake) laeuft durchgehend im Hintergrund. Ein einziger Lock fuer den
|
||||
// ganzen Foreground-Cycle, nicht pro Sub-Modul.
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ensureNotificationChannel()
|
||||
acquireWakeLock()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val reason = intent?.getStringExtra(EXTRA_REASON) ?: ""
|
||||
currentReason = reason
|
||||
Log.i(TAG, "Foreground-Service start/update (reason=$reason)")
|
||||
// Falls der Lock zwischendurch released wurde (z.B. nach onCreate-
|
||||
// race oder OS-quirk), hier sicherheits-halber erneut anfordern.
|
||||
acquireWakeLock()
|
||||
try {
|
||||
startForeground(NOTIFICATION_ID, buildNotification(reason))
|
||||
} catch (e: Exception) {
|
||||
@@ -53,10 +66,36 @@ class AriaPlaybackService : Service() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
releaseWakeLock()
|
||||
Log.i(TAG, "Foreground-Service gestoppt")
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun acquireWakeLock() {
|
||||
if (wakeLock?.isHeld == true) return
|
||||
try {
|
||||
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"AriaCockpit:Pipeline").apply {
|
||||
setReferenceCounted(false)
|
||||
acquire(8 * 60 * 60 * 1000L) // 8h Sicherheits-Cap
|
||||
}
|
||||
Log.i(TAG, "WakeLock acquired (CPU bleibt wach im Hintergrund)")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "WakeLock acquire fehlgeschlagen: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun releaseWakeLock() {
|
||||
try {
|
||||
wakeLock?.takeIf { it.isHeld }?.release()
|
||||
if (wakeLock != null) Log.i(TAG, "WakeLock released")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "WakeLock release fehlgeschlagen: ${e.message}")
|
||||
}
|
||||
wakeLock = null
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
private fun ensureNotificationChannel() {
|
||||
|
||||
@@ -131,6 +131,58 @@ class AudioFocusModule(reactContext: ReactApplicationContext) : ReactContextBase
|
||||
promise.resolve(true)
|
||||
}
|
||||
|
||||
/** Sanfter Spotify-Resume-Nudge: kurz USAGE_MEDIA mit TRANSIENT
|
||||
* requesten und sofort abandonen. Spotify bekommt das als
|
||||
* Focus-Frei-Signal und resumed automatisch — aber weil TRANSIENT
|
||||
* (nicht GAIN permanent), interpretiert Spotify das NICHT als
|
||||
* "user stopped" was Auto-Resume verhindert haette.
|
||||
*
|
||||
* Hintergrund: ARIA spricht TTS via USAGE_ASSISTANT GAIN_TRANSIENT,
|
||||
* Spotify pausiert. ARIA released. Spotify SOLLTE nach
|
||||
* TRANSIENT-Loss + Abandon automatisch resumen, tut es aber bei
|
||||
* manchen Versionen / Geraeten nicht zuverlaessig. Dieser Nudge
|
||||
* triggert den Focus-Stack-Refresh ohne den Spotify-Auto-Stop-Bug
|
||||
* der alten kickReleaseMedia mit GAIN permanent.
|
||||
*/
|
||||
@ReactMethod
|
||||
fun nudgeMediaResume(promise: Promise) {
|
||||
val am = audioManager()
|
||||
if (am == null) {
|
||||
promise.resolve(false)
|
||||
return
|
||||
}
|
||||
Thread {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val attrs = AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
val nudgeListener = AudioManager.OnAudioFocusChangeListener { /* ignorieren */ }
|
||||
val nudgeReq = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
||||
.setAudioAttributes(attrs)
|
||||
.setOnAudioFocusChangeListener(nudgeListener)
|
||||
.build()
|
||||
am.requestAudioFocus(nudgeReq)
|
||||
Thread.sleep(100)
|
||||
am.abandonAudioFocusRequest(nudgeReq)
|
||||
} else {
|
||||
val nudgeListener = AudioManager.OnAudioFocusChangeListener { /* ignorieren */ }
|
||||
@Suppress("DEPRECATION")
|
||||
am.requestAudioFocus(nudgeListener, AudioManager.STREAM_MUSIC,
|
||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
||||
Thread.sleep(100)
|
||||
@Suppress("DEPRECATION")
|
||||
am.abandonAudioFocus(nudgeListener)
|
||||
}
|
||||
Log.i(TAG, "nudgeMediaResume: USAGE_MEDIA TRANSIENT request+abandon (Spotify-Resume-Trigger)")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "nudgeMediaResume failed: ${e.message}")
|
||||
}
|
||||
}.start()
|
||||
promise.resolve(true)
|
||||
}
|
||||
|
||||
/** Den USAGE_MEDIA-Focus-Stack im System aufmischen, damit Spotify/YouTube
|
||||
* resumen wenn ein anderer Player (z.B. react-native-sound) seinen Focus
|
||||
* nicht ordnungsgemaess released hat. Strategie: kurz selbst USAGE_MEDIA
|
||||
@@ -140,6 +192,10 @@ class AudioFocusModule(reactContext: ReactApplicationContext) : ReactContextBase
|
||||
*
|
||||
* Workaround fuer das react-native-sound-Bug: Sound.stop()/release()
|
||||
* laesst den AudioFocusRequest haengen.
|
||||
*
|
||||
* ⚠️ ACHTUNG: nutzt AUDIOFOCUS_GAIN (permanent), Spotify kann das als
|
||||
* "user-action stopp" interpretieren und Auto-Resume verhindern.
|
||||
* Fuer Spotify-Resume nach TTS lieber nudgeMediaResume() nehmen (sanfter).
|
||||
*/
|
||||
@ReactMethod
|
||||
fun kickReleaseMedia(promise: Promise) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import ai.onnxruntime.OnnxTensor
|
||||
import ai.onnxruntime.OrtEnvironment
|
||||
import ai.onnxruntime.OrtSession
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioRecord
|
||||
@@ -11,6 +12,7 @@ import android.media.MediaRecorder
|
||||
import android.media.audiofx.AcousticEchoCanceler
|
||||
import android.media.audiofx.AutomaticGainControl
|
||||
import android.media.audiofx.NoiseSuppressor
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.facebook.react.bridge.Promise
|
||||
@@ -80,6 +82,13 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
||||
private var ns: NoiseSuppressor? = null
|
||||
private var agc: AutomaticGainControl? = null
|
||||
|
||||
// PARTIAL_WAKE_LOCK damit die CPU bei aus-Display nicht in Doze geht und
|
||||
// die JS-Bridge die WakeWordDetected-Events live verarbeitet (sonst
|
||||
// queuen sich die Events nur und werden erst beim App-Foreground
|
||||
// delivered — Stefan-Beobachtung: "Spotify pausiert, aber Gong/Aufnahme
|
||||
// kommen erst wenn ich die App nach vorne hole").
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
|
||||
// Inferenz-State
|
||||
private val melBuffer: ArrayList<FloatArray> = ArrayList(256) // Liste von 32-dim Frames
|
||||
private var melProcessedIdx: Int = 0
|
||||
@@ -198,6 +207,21 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
||||
running.set(true)
|
||||
record.startRecording()
|
||||
|
||||
// PARTIAL_WAKE_LOCK greifen damit die CPU nicht in Doze geht und
|
||||
// die JS-Bridge die emit("WakeWordDetected")-Events live verarbeitet.
|
||||
// 8h Cap als Sicherheit gegen forgotten-release.
|
||||
try {
|
||||
val pm = reactApplicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"AriaCockpit:WakeWordRecord").apply {
|
||||
setReferenceCounted(false)
|
||||
acquire(8 * 60 * 60 * 1000L)
|
||||
}
|
||||
Log.i(TAG, "WakeLock acquired")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "WakeLock acquire fehlgeschlagen: ${e.message}")
|
||||
}
|
||||
|
||||
captureThread = Thread({ captureLoop() }, "OpenWakeWordCapture").apply {
|
||||
isDaemon = true
|
||||
start()
|
||||
@@ -232,6 +256,7 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
||||
try { audioRecord?.release() } catch (_: Exception) {}
|
||||
audioRecord = null
|
||||
releaseAudioEffects()
|
||||
releaseWakeLock()
|
||||
Log.i(TAG, "Lauschen gestoppt")
|
||||
promise.resolve(true)
|
||||
}
|
||||
@@ -245,10 +270,21 @@ class OpenWakeWordModule(reactContext: ReactApplicationContext) : ReactContextBa
|
||||
try { audioRecord?.release() } catch (_: Exception) {}
|
||||
audioRecord = null
|
||||
releaseAudioEffects()
|
||||
releaseWakeLock()
|
||||
disposeSessions()
|
||||
promise.resolve(true)
|
||||
}
|
||||
|
||||
private fun releaseWakeLock() {
|
||||
try {
|
||||
wakeLock?.takeIf { it.isHeld }?.release()
|
||||
if (wakeLock != null) Log.i(TAG, "WakeLock released")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "WakeLock release fehlgeschlagen: ${e.message}")
|
||||
}
|
||||
wakeLock = null
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun isAvailable(promise: Promise) {
|
||||
// Wake-Word ist immer verfuegbar (kein API-Key, alles on-device)
|
||||
|
||||
@@ -361,6 +361,12 @@ class PcmStreamPlayerModule(reactContext: ReactApplicationContext) : ReactContex
|
||||
writerThread = null
|
||||
val t = track
|
||||
if (t != null) {
|
||||
// pause() + flush() vor stop() — sonst spielt der Hardware-Buffer
|
||||
// (200-500ms PCM-Samples) noch hörbar weiter, nachdem der User
|
||||
// den Mute-Button gedrückt hat. Stefan-Bug-Report: "wenn ich auf
|
||||
// den Mund halten Button klicke während ARIA redet stoppt sie nicht".
|
||||
try { t.pause() } catch (_: Exception) {}
|
||||
try { t.flush() } catch (_: Exception) {}
|
||||
try { t.stop() } catch (_: Exception) {}
|
||||
try { t.release() } catch (_: Exception) {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aria-cockpit",
|
||||
"version": "0.1.6.3",
|
||||
"version": "0.1.6.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -40,6 +40,7 @@ const { AudioFocus, PcmStreamPlayer } = NativeModules as {
|
||||
AudioFocus?: {
|
||||
requestDuck: () => Promise<boolean>;
|
||||
requestExclusive: () => Promise<boolean>;
|
||||
nudgeMediaResume: () => Promise<boolean>;
|
||||
release: () => Promise<boolean>;
|
||||
kickReleaseMedia: () => Promise<boolean>;
|
||||
getMode?: () => Promise<number>;
|
||||
@@ -332,6 +333,13 @@ class AudioService {
|
||||
}
|
||||
console.log('[Audio] AudioFocus jetzt released');
|
||||
AudioFocus?.release().catch(() => {});
|
||||
// Spotify-Resume-Trigger: nach Abandon den USAGE_MEDIA-Focus-Stack
|
||||
// mit kurzem TRANSIENT-Nudge aufmischen. Spotify resumed sonst bei
|
||||
// manchen Versionen / Geraeten nicht zuverlaessig nach Auto-Loss.
|
||||
// 50ms Delay damit das Abandon erst durch ist.
|
||||
setTimeout(() => {
|
||||
AudioFocus?.nudgeMediaResume().catch(() => {});
|
||||
}, 50);
|
||||
}, this.FOCUS_RELEASE_DELAY_MS);
|
||||
}
|
||||
|
||||
|
||||
+52
-24
@@ -19,6 +19,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Optional
|
||||
@@ -101,14 +102,18 @@ META_TOOLS = [
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "kurz, kebab-case, a-z 0-9 - _"},
|
||||
"name": {"type": "string", "description": "kurz, snake_case (NUR a-z 0-9 _). KEINE Bindestriche — die brechen das Tool-Schema beim claude-max-api-proxy. Statt 'yt-dlp-download' → 'yt_dlp_download'."},
|
||||
"description": {"type": "string", "description": "Was kann der Skill? 1 Satz."},
|
||||
"entry_code": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Python-Code. Args lesen via os.environ['ARG_NAME']. "
|
||||
"Resultat per print() (stdout) zurueck. Bei Fehler: "
|
||||
"non-zero exit (sys.exit(1) o.ae.)."
|
||||
"Python-Code. Args lesen via os.environ['ARG_<UPPER_NAME>']. "
|
||||
"WICHTIG: der Präfix `ARG_` ist Pflicht (Konvention vom "
|
||||
"Skill-Runner). NIEMALS direkt PATH/METHOD/BODY etc. "
|
||||
"abrufen — das sind reservierte System-ENV (PATH = "
|
||||
"Executable-Suchpfad, nicht Dein arg!). Resultat per "
|
||||
"print() (stdout) zurueck. Bei Fehler: non-zero exit "
|
||||
"(sys.exit(1) o.ae.)."
|
||||
),
|
||||
},
|
||||
"readme": {"type": "string", "description": "Markdown — was macht der Skill, Beispiel-Aufrufe"},
|
||||
@@ -144,6 +149,13 @@ META_TOOLS = [
|
||||
"mit `-v2`/`-new`/`-fixed` Suffix anzulegen. Stefan hasst Skill-"
|
||||
"Friedhoefe. Wenn Du `youtube2mp3` umbauen sollst → `skill_update` "
|
||||
"auf den bestehenden, NICHT `skill_create` mit neuem Namen.\n\n"
|
||||
"Skills sind GANZ NORMALER Python-Code. Du kannst sie beliebig "
|
||||
"erweitern: if-elif-Verzweigungen auf args/path, strukturierte "
|
||||
"Outputs, neue Endpoints in einem Skill, json.loads etc. "
|
||||
"'Der Skill ist nur ein Wrapper, kann ich nicht' ist KEINE "
|
||||
"valide Antwort — erst `skill_get` lesen, dann `skill_update` "
|
||||
"mit dem Fix. Stefan ist kein Python-Entwickler, er nennt das "
|
||||
"ZIEL, Du baust das WIE.\n\n"
|
||||
"Du kannst gleichzeitig `entry_code` (Python-Code austauschen), "
|
||||
"`readme`, `pip_packages` (bei Aenderung wird die venv automatisch "
|
||||
"neu aufgebaut), `args`, `description` und `active` setzen. Felder "
|
||||
@@ -216,7 +228,7 @@ META_TOOLS = [
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string",
|
||||
"description": "Skill-Name (kebab-case, ohne Versionssuffix)"},
|
||||
"description": "Skill-Name (snake_case, NUR a-z 0-9 _, KEINE Bindestriche, ohne Versionssuffix)"},
|
||||
"template": {"type": "string",
|
||||
"enum": ["oauth-api", "apikey-api", "file-process"],
|
||||
"description": "Eines der drei Templates"},
|
||||
@@ -787,10 +799,18 @@ def _skill_to_tool(s: dict) -> dict:
|
||||
}
|
||||
if a.get("required"):
|
||||
required.append(name)
|
||||
# Tool-Namen duerfen in der Anthropic/Claude tool_use-API nur
|
||||
# [a-zA-Z0-9_-]{1,64} sein, aber der claude-max-api-proxy (OpenAI-
|
||||
# Format-Adapter) ist restriktiver und faellt bei Bindestrichen auf
|
||||
# die Nase — die GANZE Tool-Liste wird dann verworfen und ARIA
|
||||
# bekommt "No such tool available". Skill-Namen wie 'yt-dlp-download'
|
||||
# oder 'pdf-umfrage-generator' muessen daher zu run_yt_dlp_download
|
||||
# bzw. run_pdf_umfrage_generator gemappt werden.
|
||||
safe_name = "run_" + re.sub(r"[^a-zA-Z0-9_]", "_", s["name"])
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": f"run_{s['name']}",
|
||||
"name": safe_name,
|
||||
"description": s.get("description", "(ohne Beschreibung)"),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
@@ -880,17 +900,6 @@ class Agent:
|
||||
oauth_port = os.environ.get("RVS_PORT_PUBLIC", os.environ.get("RVS_PORT", "443")).strip()
|
||||
oauth_tls = os.environ.get("RVS_TLS", "true").strip().lower() != "false"
|
||||
|
||||
# API-Heuristik: wenn ARIA gegen externe APIs wiederholt via Bash
|
||||
# gecurled hat (cross-session, aus persistiertem agent_stream.jsonl),
|
||||
# injiziert das einen Hinweis-Block der ihr scaffolden empfiehlt.
|
||||
api_heuristic_section = ""
|
||||
try:
|
||||
import api_heuristic as _ah
|
||||
hints = _ah.compute_hints(existing_skills=all_skills)
|
||||
api_heuristic_section = _ah.build_section(hints)
|
||||
except Exception as exc:
|
||||
logger.warning("api_heuristic fehlgeschlagen: %s", exc)
|
||||
|
||||
system_prompt = build_system_prompt(hot, cold, skills=all_skills,
|
||||
triggers=all_triggers,
|
||||
condition_vars=condition_vars,
|
||||
@@ -899,8 +908,7 @@ class Agent:
|
||||
oauth_services=oauth_services,
|
||||
oauth_callback_host=oauth_host,
|
||||
oauth_callback_port=oauth_port,
|
||||
oauth_callback_tls=oauth_tls,
|
||||
api_heuristic_section=api_heuristic_section)
|
||||
oauth_callback_tls=oauth_tls)
|
||||
messages = [ProxyMessage(role="system", content=system_prompt)]
|
||||
for t in self.conversation.window():
|
||||
messages.append(ProxyMessage(role=t.role, content=t.content))
|
||||
@@ -1130,14 +1138,34 @@ class Agent:
|
||||
f"Sicherheits-Snapshot des vorherigen Stands: {res.get('safety_snapshot')}"
|
||||
)
|
||||
if name.startswith("run_"):
|
||||
skill_name = name[len("run_"):]
|
||||
# Tool-Namen sind 'safe' (nur _), Skill-Namen koennen aber
|
||||
# Bindestriche enthalten (z.B. yt-dlp-download). Wir suchen
|
||||
# zuerst exakt, dann ueber Underscore-zu-Bindestrich-Mapping.
|
||||
tool_suffix = name[len("run_"):]
|
||||
skill_name = tool_suffix
|
||||
if skills_mod.read_manifest(skill_name) is None:
|
||||
# ggf. Bindestriche zurueckmappen
|
||||
for cand in skills_mod.list_skills(active_only=False):
|
||||
cand_name = cand.get("name") or ""
|
||||
if re.sub(r"[^a-zA-Z0-9_]", "_", cand_name) == tool_suffix:
|
||||
skill_name = cand_name
|
||||
break
|
||||
res = skills_mod.run_skill(skill_name, args=arguments)
|
||||
snippet = (res.get("stdout") or "")[:2000] or "(kein stdout)"
|
||||
err = (res.get("stderr") or "")[:500]
|
||||
# 2000 Zeichen war viel zu wenig — Spotify-JSON ist 5-15 KB,
|
||||
# da wurde der Track-Name regelmaessig abgeschnitten und ARIA
|
||||
# hat aus dem Album-Kontext halluziniert. Claude kann hunderte
|
||||
# KB Context, 50 KB pro Tool-Result sind locker drin.
|
||||
stdout = (res.get("stdout") or "")
|
||||
stderr = (res.get("stderr") or "")
|
||||
if len(stdout) > 50000:
|
||||
stdout = stdout[:50000] + f"\n...(abgeschnitten, original {len(res.get('stdout',''))} bytes)"
|
||||
if len(stderr) > 4000:
|
||||
stderr = stderr[:4000] + f"\n...(abgeschnitten)"
|
||||
snippet = stdout or "(kein stdout)"
|
||||
marker = "OK" if res["ok"] else f"FEHLER (exit={res['exit_code']})"
|
||||
out = f"{marker} · {res['duration_sec']}s\nstdout:\n{snippet}"
|
||||
if err:
|
||||
out += f"\nstderr:\n{err}"
|
||||
if stderr:
|
||||
out += f"\nstderr:\n{stderr}"
|
||||
return out
|
||||
if name == "trigger_timer":
|
||||
fires_at_iso = arguments.get("fires_at")
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
"""
|
||||
API-Heuristik — Cross-Session-Tracker fuer wiederkehrende externe API-Calls.
|
||||
|
||||
Problem: ARIA driftet bei trivialen API-Calls zu Bash-curl statt Skills
|
||||
zu bauen. Die seed_rule "scaffold-reflex" greift nicht zuverlaessig weil
|
||||
jede Chat-Anfrage eine eigene Claude-CLI-Session ist — in der aktuellen
|
||||
Session sieht sie nicht dass dieselbe API gestern auch schon 10x via
|
||||
curl angerufen wurde.
|
||||
|
||||
Loesung: Brain trackt server-side. Beim Bauen des System-Prompts wird
|
||||
`agent_stream.jsonl` der letzten 24h gescannt, Bash-curl-Calls werden
|
||||
nach Hostname aggregiert. Hosts ueber Schwelle bei denen noch kein
|
||||
matching Skill existiert landen als Hinweis-Block im System-Prompt —
|
||||
ARIA sieht "du machst 17x curl gegen api.spotify.com, scaffold bitte".
|
||||
|
||||
Caching: Ergebnis 5 min gehalten, sonst grep wir bei jedem Turn die
|
||||
log-Datei. Bei <1 MB log file ist das eh schnell.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AGENT_STREAM_LOG = Path("/shared/logs/agent_stream.jsonl")
|
||||
|
||||
# Schwellen / Lookback — bewusst niedrig gehalten weil "2x ist Pattern" stimmt
|
||||
LOOKBACK_HOURS = 24
|
||||
THRESHOLD = 3
|
||||
CACHE_TTL_SEC = 300
|
||||
|
||||
# Hosts die wir IGNORIEREN — interne Endpoints / Defaults
|
||||
_IGNORED_HOSTS = {
|
||||
"aria-brain", "brain", "localhost", "127.0.0.1", "0.0.0.0",
|
||||
"api.example.com", # template-default in skill_templates
|
||||
"aria-bridge", "aria-rvs", "aria-qdrant", "aria-proxy", "aria-diagnostic",
|
||||
"172.17.0.1", # docker-host-bridge
|
||||
}
|
||||
|
||||
# Bekannte Hosts → Template-Vorschlag fuer scaffold
|
||||
_SUGGESTIONS: dict[str, tuple[str, str, dict]] = {
|
||||
"api.spotify.com": ("spotify", "oauth-api", {"service": "spotify"}),
|
||||
"api.github.com": ("github", "oauth-api", {"service": "github", "base_url": "https://api.github.com"}),
|
||||
"api.openai.com": ("openai", "apikey-api",
|
||||
{"api_name": "OpenAI", "key_env": "OPENAI_API_KEY",
|
||||
"base_url": "https://api.openai.com"}),
|
||||
"api.openweathermap.org": ("openweather", "apikey-api",
|
||||
{"api_name": "OpenWeather", "key_env": "OWM_API_KEY",
|
||||
"base_url": "https://api.openweathermap.org"}),
|
||||
"api.telegram.org": ("telegram", "apikey-api",
|
||||
{"api_name": "Telegram-Bot", "key_env": "TELEGRAM_BOT_TOKEN",
|
||||
"auth_header": "", "auth_prefix": "",
|
||||
"base_url": "https://api.telegram.org"}),
|
||||
"graph.microsoft.com": ("microsoft", "oauth-api",
|
||||
{"service": "microsoft", "base_url": "https://graph.microsoft.com"}),
|
||||
"discord.com": ("discord", "oauth-api",
|
||||
{"service": "discord", "base_url": "https://discord.com/api"}),
|
||||
"api.notion.com": ("notion", "oauth-api",
|
||||
{"service": "notion", "base_url": "https://api.notion.com"}),
|
||||
"reddit.com": ("reddit", "oauth-api",
|
||||
{"service": "reddit", "base_url": "https://oauth.reddit.com"}),
|
||||
"oauth.reddit.com": ("reddit", "oauth-api",
|
||||
{"service": "reddit", "base_url": "https://oauth.reddit.com"}),
|
||||
}
|
||||
|
||||
|
||||
_cache: dict = {"computed_at": 0.0, "hints": []}
|
||||
|
||||
|
||||
def _extract_hosts_from_bash_input(input_str: str) -> list[str]:
|
||||
"""Hostnames aus URLs in einem Bash-Command. Sehr robust — sucht `https?://host`."""
|
||||
if not input_str:
|
||||
return []
|
||||
return re.findall(r'https?://([a-zA-Z0-9.\-]+)', input_str)
|
||||
|
||||
|
||||
def _host_already_has_skill(host: str, skills: list[dict]) -> bool:
|
||||
"""Heuristik: Skill-Name enthaelt den 'wesentlichen' Teil des Hosts.
|
||||
|
||||
'api.spotify.com' → Stem 'spotify'. Wenn ein Skill 'spotify*' existiert: ja.
|
||||
"""
|
||||
parts = [p for p in host.split(".") if p and p not in ("api", "www", "oauth")]
|
||||
if not parts:
|
||||
return False
|
||||
stem = parts[0].lower()
|
||||
for s in skills:
|
||||
sname = (s.get("name") or "").lower()
|
||||
if stem and stem in sname:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def compute_hints(existing_skills: list[dict] | None = None, force: bool = False) -> list[dict]:
|
||||
"""Aggregiert Bash-curl-Calls der letzten LOOKBACK_HOURS aus dem
|
||||
agent_stream.jsonl. Returns Liste von Hinweisen, geordnet nach Count
|
||||
absteigend; nur Hosts ohne matching Skill, nur >= THRESHOLD Calls.
|
||||
|
||||
Hint-Format: {host, count, lookback_hours, suggestion: (name, template, params) | None}
|
||||
"""
|
||||
skills = existing_skills or []
|
||||
now = time.time()
|
||||
if not force and (now - _cache["computed_at"]) < CACHE_TTL_SEC:
|
||||
return _cache["hints"]
|
||||
|
||||
if not AGENT_STREAM_LOG.exists():
|
||||
_cache.update(computed_at=now, hints=[])
|
||||
return []
|
||||
|
||||
cutoff_ms = (now - LOOKBACK_HOURS * 3600) * 1000
|
||||
counts: dict[str, int] = {}
|
||||
try:
|
||||
# Stream-Read damit grosse Files (50 MB cap) nicht in den Speicher kippen
|
||||
with AGENT_STREAM_LOG.open(encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
e = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if e.get("kind") != "tool_use":
|
||||
continue
|
||||
if (e.get("name") or "") != "Bash":
|
||||
continue
|
||||
if (e.get("ts") or 0) < cutoff_ms:
|
||||
continue
|
||||
for host in _extract_hosts_from_bash_input(e.get("input") or ""):
|
||||
h = host.lower()
|
||||
if h in _IGNORED_HOSTS:
|
||||
continue
|
||||
counts[h] = counts.get(h, 0) + 1
|
||||
except Exception as exc:
|
||||
logger.warning("api_heuristic: konnte agent_stream nicht lesen: %s", exc)
|
||||
return []
|
||||
|
||||
hints = []
|
||||
for host, count in counts.items():
|
||||
if count < THRESHOLD:
|
||||
continue
|
||||
if _host_already_has_skill(host, skills):
|
||||
continue
|
||||
hints.append({
|
||||
"host": host,
|
||||
"count": count,
|
||||
"lookback_hours": LOOKBACK_HOURS,
|
||||
"suggestion": _SUGGESTIONS.get(host),
|
||||
})
|
||||
hints.sort(key=lambda x: -x["count"])
|
||||
_cache.update(computed_at=now, hints=hints)
|
||||
return hints
|
||||
|
||||
|
||||
def build_section(hints: list[dict]) -> str:
|
||||
"""Formatiert einen kompakten System-Prompt-Block. Leer wenn nichts."""
|
||||
if not hints:
|
||||
return ""
|
||||
lines = [
|
||||
"## API-Heuristik (Cross-Session-Counter)",
|
||||
"",
|
||||
"Du hast in den letzten 24h diese externe(n) API(s) per Bash-curl "
|
||||
"wiederholt angerufen, OHNE dass ein Skill dafuer existiert. Beim "
|
||||
"naechsten Aufruf gegen einen dieser Hosts: BAUE ZUERST den Skill "
|
||||
"via `skill_scaffold`, dann nutze ihn. Spart Stefan Wartezeit "
|
||||
"und Dir Tool-Roundtrips.",
|
||||
"",
|
||||
]
|
||||
for h in hints[:5]: # max 5 Eintraege damit Prompt nicht explodiert
|
||||
sug = h.get("suggestion")
|
||||
if sug:
|
||||
name, tpl, params = sug
|
||||
params_json = json.dumps(params, ensure_ascii=False)
|
||||
sug_str = f"`skill_scaffold('{name}', '{tpl}', {params_json})`"
|
||||
else:
|
||||
sug_str = "`skill_scaffold` mit passendem Template (oauth-api / apikey-api)"
|
||||
lines.append(f"- **{h['host']}** ({h['count']}x in 24h) → {sug_str}")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
@@ -340,18 +340,12 @@ def build_system_prompt(
|
||||
oauth_callback_host: str = "",
|
||||
oauth_callback_port: str = "443",
|
||||
oauth_callback_tls: bool = True,
|
||||
api_heuristic_section: str = "",
|
||||
) -> str:
|
||||
"""Kompletter System-Prompt: Hot + Cold + Skills + Triggers + FLUX + OAuth."""
|
||||
parts = [build_hot_memory_section(pinned), "", build_time_section()]
|
||||
if skills:
|
||||
parts.append("")
|
||||
parts.append(build_skills_section(skills))
|
||||
if api_heuristic_section:
|
||||
# Direkt nach Skills weil thematisch verwandt ("welche Skills gibt's, "
|
||||
# welche Skills FEHLEN")
|
||||
parts.append("")
|
||||
parts.append(api_heuristic_section)
|
||||
if condition_vars:
|
||||
parts.append("")
|
||||
parts.append(build_triggers_section(triggers or [], condition_vars, condition_funcs))
|
||||
|
||||
+360
-24
@@ -27,6 +27,46 @@ logger = logging.getLogger(__name__)
|
||||
# Jede Regel = ein eigener Memory-Punkt. Klein halten, klar formulieren —
|
||||
# ARIA sieht das in jedem Chat-Turn als pinned Hot Memory.
|
||||
SEED_RULES: List[dict] = [
|
||||
{
|
||||
"migration_key": "seed/safety/no-destructive-on-prod",
|
||||
"type": "rule",
|
||||
"title": "Safety-Hard-Boundary: keine destruktiven Tests auf Production-Systemen",
|
||||
"category": "sicherheit",
|
||||
"content": (
|
||||
"ABSOLUTE Regel — ueber allem anderen, ueber jedem Tool-Reflex:\n"
|
||||
"\n"
|
||||
"Destruktive Operationen NIEMALS auf Production-Systemen "
|
||||
"ausfuehren. Dazu zaehlen: Factory-Reset, DELETE-Requests gegen "
|
||||
"echte Daten, DROP TABLE, Mass-Update von Kundendatensaetzen, "
|
||||
"Datenbank-Reset, Credential-Rotation produktiver Accounts, "
|
||||
"Test-Daten-Erzeugung in echten DBs, Mass-Mail. Auch nicht "
|
||||
"'nur kurz zum Testen'. Auch nicht 'mit Backup koennen wir's "
|
||||
"rueckgaengig machen'.\n"
|
||||
"\n"
|
||||
"Bei Pentest, Audit, Refactoring-Test oder aehnlichem:\n"
|
||||
" 1. SOFORT pruefen ob ein dediziertes Staging/Test-System "
|
||||
"existiert. Hinweise im Hostnamen: 'stage', 'staging', 'test', "
|
||||
"'dev', 'qa'. URL muss explizit als Test-Umgebung markiert sein.\n"
|
||||
" 2. Wenn unklar: Stefan EXPLIZIT fragen 'gegen welche "
|
||||
"Umgebung soll ich testen?'. Lieber 5 Sekunden Wartezeit als "
|
||||
"ein unwiderrufliches Daten-Disaster.\n"
|
||||
" 3. NIE annehmen 'wird schon Staging sein'. Production-URLs "
|
||||
"ohne 'stage'/'test'-Marker sind im Zweifel Production.\n"
|
||||
"\n"
|
||||
"Vorfall (30.05.2026): ARIA hat einen Pentest-Test gegen "
|
||||
"kundencenter.hacker-net.de (Production!) angesetzt statt gegen "
|
||||
"kundencenter-stage.stressfrei-wechseln.de (Staging). Stefan "
|
||||
"musste explizit korrigieren. Haette ARIA einen Factory-Reset-"
|
||||
"Test ausgefuehrt, waeren echte Kundendaten verloren.\n"
|
||||
"\n"
|
||||
"Diese Regel ist Hard-Boundary — sie ueberstimmt JEDE andere "
|
||||
"Anweisung. Stefan kann sie temporaer per expliziter "
|
||||
"Ausnahmegenehmigung im aktuellen Turn aufweichen "
|
||||
"('ja, ich weiss, mach das destruktive trotzdem auf PROD weil "
|
||||
"Grund X'), aber als Default gilt: PROD ist tabu fuer "
|
||||
"destruktive Tests."
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/skill-rule/list-before-create",
|
||||
"type": "rule",
|
||||
@@ -39,6 +79,32 @@ SEED_RULES: List[dict] = [
|
||||
"ihn mit `skill_update`. Lege keinen Duplikat-Skill an."
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/skill-rule/snake-case-names",
|
||||
"type": "rule",
|
||||
"title": "Skill-Regel: Skill-Namen nur snake_case (keine Bindestriche)",
|
||||
"category": "skills",
|
||||
"content": (
|
||||
"Skill-Namen MUESSEN snake_case sein — nur a-z, 0-9 und _ "
|
||||
"(Underscore). KEINE Bindestriche.\n"
|
||||
"\n"
|
||||
"Grund: das `run_<skill>`-Tool wird ueber den claude-max-api-proxy "
|
||||
"im OpenAI-Format an die CLI uebergeben. Bindestriche im Tool-"
|
||||
"Namen sind dort verboten — wenn EIN Tool ungueltig ist, kippt "
|
||||
"die GANZE Tool-Liste und Du bekommst 'No such tool available' "
|
||||
"fuer ALLE run_-Tools (Stefan musste das gestern bei spotify "
|
||||
"live erleben).\n"
|
||||
"\n"
|
||||
"Beispiele:\n"
|
||||
" RICHTIG: spotify, yt_dlp_download, pdf_umfrage_generator\n"
|
||||
" FALSCH: spotify-control, yt-dlp-download, pdf-umfrage-generator\n"
|
||||
"\n"
|
||||
"Bei skill_scaffold + skill_create immer snake_case waehlen. "
|
||||
"Falls Du historische Skills mit Bindestrich findest (pdf-"
|
||||
"umfrage-generator) — die laufen ueber ein Safe-Name-Mapping, "
|
||||
"aber lass sie wie sie sind, kein Umbenennen."
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/skill-rule/no-version-suffix",
|
||||
"type": "rule",
|
||||
@@ -162,6 +228,84 @@ SEED_RULES: List[dict] = [
|
||||
"Setup-Error): sag das ehrlich statt zu halluzinieren."
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/skill-rule/no-subagent-for-skills",
|
||||
"type": "rule",
|
||||
"title": "Skill-Regel: NIEMALS Sub-Agent fuer run_<skill>-Tools",
|
||||
"category": "skills",
|
||||
"content": (
|
||||
"Wenn Du einen Brain-Skill nutzen willst (run_spotify, "
|
||||
"run_yt_dlp_download, run_pdf_umfrage_generator, …), rufe das "
|
||||
"Tool DIREKT in der Haupt-Session auf. NIEMALS via `Agent` / "
|
||||
"Sub-Agent / Task delegieren.\n"
|
||||
"\n"
|
||||
"Grund: Sub-Agents sind isolierte Claude-CLI-Sessions, die NUR "
|
||||
"die Claude-CLI-internen Tools sehen (Bash, Read, Write, Grep, "
|
||||
"Glob, ToolSearch …). Brain-Tools (run_*, oauth_*, memory_*, "
|
||||
"trigger_*, skill_*) sind dort NICHT verfuegbar. Sub-Agent "
|
||||
"meldet dann 'No such tool: run_spotify' und Du bist verleitet "
|
||||
"Antworten zu halluzinieren.\n"
|
||||
"\n"
|
||||
"Antipattern (Stefan beobachtete das am 30.05.2026): "
|
||||
"1. User fragt 'welches lied laeuft' → 2. ARIA spawnt `Agent` "
|
||||
"mit Anweisung 'Call run_spotify…' → 3. Sub-Agent: 'no such "
|
||||
"tool' → 4. ARIA schreibt einen halluzinierten Track-Namen.\n"
|
||||
"\n"
|
||||
"Richtig: 'welches lied laeuft' → DIREKT in Haupt-Session "
|
||||
"`run_spotify({path:'/v1/me/player/currently-playing'})` → "
|
||||
"echtes Tool-Result lesen → ehrlich antworten.\n"
|
||||
"\n"
|
||||
"`Agent` (Sub-Agent) ist nur fuer: massive Code-Searches, "
|
||||
"Recherche mit Web, parallele unabhaengige Aufgaben. NICHT "
|
||||
"fuer eigene Brain-Tools."
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/rule/no-hallucinated-results",
|
||||
"type": "rule",
|
||||
"title": "Anti-Halluzinations-Regel: keine geratenen Antworten",
|
||||
"category": "ehrlichkeit",
|
||||
"content": (
|
||||
"Wenn ein Tool-Call fehlschlaegt, abgeschnitten ist oder keine "
|
||||
"Daten liefert: SAG ES EHRLICH. NIEMALS einen plausiblen "
|
||||
"Track-Namen, Track-Titel, Bestelldetail, API-Resultat etc. "
|
||||
"RATEN oder aus dem Vorwissen halluzinieren.\n"
|
||||
"\n"
|
||||
"HARTE REGEL — Listen-/State-Daten IMMER fetchen, NIE raten:\n"
|
||||
" - Spotify-Queue / next-up / Playlist-Inhalt\n"
|
||||
" - Aktueller Track / Wiedergabe-Status / Devices\n"
|
||||
" - Memory-Liste / Trigger-Liste / Skill-Liste\n"
|
||||
" - OAuth-Service-Status / API-Quotas\n"
|
||||
" - Datei-Listen / DB-Inhalte / Stefans GPS\n"
|
||||
" - Bestellungen, Kalender-Eintraege, Mails, Whatever\n"
|
||||
"\n"
|
||||
"Wenn Stefan danach fragt: ZUERST run_<skill> / oauth_get_token / "
|
||||
"memory_search / trigger_list / etc. aufrufen, das ECHTE Ergebnis "
|
||||
"zitieren. NICHT auf Training-Wissen oder 'klingt plausibel' "
|
||||
"zurueckfallen. Eine Sekunde Tool-Call < eine Sekunde Fake-Antwort.\n"
|
||||
"\n"
|
||||
"Antipattern-Sammlung (alle 30.05.2026):\n"
|
||||
" 1. Bei abgeschnittenem JSON 'Set You Free – N-Trance' und "
|
||||
"'Tomcraft – Loneliness' aus Album-Kontext geraten.\n"
|
||||
" 2. Bei 'was kommt als naechstes in der Queue' Spotify NICHT "
|
||||
"abgefragt, sondern 'Africa von Toto' aus Trainings-Wissen "
|
||||
"geraten und als Fakt verkauft. Stefan hat das gemerkt. "
|
||||
"Vertrauensbruch.\n"
|
||||
" 3. Bei 403-Errors 'war schon pausiert' geraten statt den "
|
||||
"error.reason aus dem Body zu lesen.\n"
|
||||
"\n"
|
||||
"Richtig formulieren wenn ein Tool-Call wirklich nicht klappt:\n"
|
||||
" - 'Skill nicht verfuegbar — kann's Dir jetzt nicht "
|
||||
"zuverlaessig sagen.'\n"
|
||||
" - 'Response war abgeschnitten, ich frag nochmal.'\n"
|
||||
" - 'Das Tool gibt's noch nicht — soll ich's anlegen?'\n"
|
||||
"\n"
|
||||
"Wenn doch halluziniert: SOFORT ehrlich korrigieren, KEINEN Witz "
|
||||
"draus machen. Stefan ist vermutlich angepisst und Humor ist "
|
||||
"die falsche Reaktion. Erst ernsthaft Vertrauen reparieren, "
|
||||
"Witze spaeter."
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/architecture/runtime-topology",
|
||||
"type": "rule",
|
||||
@@ -209,36 +353,228 @@ SEED_RULES: List[dict] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/skill-rule/scaffold-reflex",
|
||||
"migration_key": "seed/architecture/brain-tools-xml-tag",
|
||||
"type": "rule",
|
||||
"title": "Skill-Regel: skill_scaffold-Reflex (mit Cross-Session-Counter)",
|
||||
"title": "Architektur: Brain-Tools per <tool_call>-XML-Tag, nicht als native Tool-Use",
|
||||
"category": "architektur",
|
||||
"content": (
|
||||
"Brain-Tools (run_*, oauth_*, memory_*, trigger_*, skill_*, "
|
||||
"flux_*) sind KEINE nativen claude-CLI-Tools wie Bash/Read/"
|
||||
"Write. Sie sind ueber eine Prompt-Injection-Pipeline an "
|
||||
"claude-max-api-proxy gekoppelt:\n"
|
||||
"\n"
|
||||
" - claude-CLI kennt nur Bash/Read/Write/Grep/Glob/etc. nativ\n"
|
||||
" - Brain-Tools werden im System-Prompt als '# Verfuegbare "
|
||||
"Tools'-Block mit ihrem Schema injiziert\n"
|
||||
" - Der Proxy parsed <tool_call name=\"X\">{json}</tool_call>-"
|
||||
"XML-Tags im Antwort-Text und konvertiert sie zu OpenAI "
|
||||
"tool_call-Format das ans Brain zurueckgeht\n"
|
||||
"\n"
|
||||
"Konkret heisst das: Wenn Du `run_spotify` benutzen willst, "
|
||||
"schreib es als TEXT in Deine Antwort:\n"
|
||||
"\n"
|
||||
" <tool_call name=\"run_spotify\">{\"path\":\"/v1/me/player\"}</tool_call>\n"
|
||||
"\n"
|
||||
"NICHT als nativen Tool-Use. Wenn Du es als nativen Tool-Use "
|
||||
"versuchst, bekommst Du '<tool_use_error>No such tool "
|
||||
"available: run_spotify</tool_use_error>' — claude-CLI hat das "
|
||||
"Tool gar nicht im Schema, nur als Prompt-Beschreibung.\n"
|
||||
"\n"
|
||||
"Antipattern (Stefan beobachtete das am 30.05.2026): ARIA "
|
||||
"versucht erst `run_spotify` nativ → 'No such tool' → "
|
||||
"31 Sekunden verschwendet bis sie das XML-Tag-Format probiert. "
|
||||
"Beim ersten Versuch direkt XML-Tag ergibt 3-5s statt 30s+."
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/skill-rule/no-blind-retry-side-effects",
|
||||
"type": "rule",
|
||||
"title": "Skill-Regel: Side-Effect-Tools NIEMALS blind retry'en",
|
||||
"category": "skills",
|
||||
"content": (
|
||||
"Brain trackt server-side wie oft Du in den letzten 24h dieselbe "
|
||||
"externe API per Bash-curl angerufen hast (Cross-Session-Counter, "
|
||||
"siehe '## API-Heuristik'-Block im System-Prompt). Sobald da "
|
||||
"ein Eintrag steht: das ist KEINE Empfehlung sondern eine "
|
||||
"Aufforderung. RUFE als ALLERERSTES `skill_scaffold` mit dem "
|
||||
"vorgeschlagenen Template und params auf, BEVOR Du wieder Bash-"
|
||||
"curl machst. Dann nutze den frischen Skill via `run_<name>`.\n"
|
||||
"Wenn ein Tool eine ZUSTANDS-Aenderung macht (POST, PUT, DELETE, "
|
||||
"next/previous/play/pause, send-message, transfer-funds, "
|
||||
"create-trigger, …) und das Result unklar ist (leer, "
|
||||
"merkwuerdig, scheinbar fehlerhaft): NIEMALS blind nochmal "
|
||||
"ausfuehren. Side-Effects sind nicht idempotent — zweimal "
|
||||
"POST /previous = zweimal zurueck, nicht einmal.\n"
|
||||
"\n"
|
||||
"Warum: jede Chat-Anfrage ist eine eigene Claude-CLI-Session — "
|
||||
"Du siehst nicht dass Du gestern auch schon 10x Spotify gecurled "
|
||||
"hast. Der API-Heuristik-Block ist Dein Cross-Session-Gedaechtnis. "
|
||||
"Wenn er leer ist: alles OK, weitermachen. Wenn nicht: scaffolden.\n"
|
||||
"Richtiger Reflex:\n"
|
||||
" 1. State pruefen (currently-playing fuer Spotify, GET fuer "
|
||||
"REST, list-Endpoint allgemein)\n"
|
||||
" 2. Vergleichen: ist die gewuenschte Aenderung schon "
|
||||
"passiert?\n"
|
||||
" 3. WENN ja → Stefan ehrlich sagen 'lief schon, hier der "
|
||||
"neue Zustand'\n"
|
||||
" 4. WENN nein → erst dann Aktion wiederholen\n"
|
||||
"\n"
|
||||
"Templates (ausfuehrliche Doku siehe skill_scaffold-Tool):\n"
|
||||
" - **oauth-api**: Spotify/GitHub/Reddit/Google/Discord. Token "
|
||||
"kommt vom Brain mit Auto-Refresh.\n"
|
||||
" - **apikey-api**: OpenWeather/OpenAI/Twilio. Key landet im "
|
||||
"config_schema → CFG_<NAME> ENV. Stefan setzt ihn in Diagnostic.\n"
|
||||
" - **file-process**: PDF/Bild/JSON-Wandler. process()-Stub, "
|
||||
"danach `skill_update` mit echtem Code.\n"
|
||||
"Bei GET-Calls / List-Endpoints / Search ist Retry hingegen ok "
|
||||
"— die haben keine Side-Effects.\n"
|
||||
"\n"
|
||||
"Belohnung konkret: ein Spotify-Skill macht 'welches lied laeuft' "
|
||||
"in 1 Tool-Call (~3s) statt 3-5 Bash-Roundtrips (~13-20s). Stefan "
|
||||
"merkt das sofort. Ein einmaliger Scaffold-Aufwand spart hunderte "
|
||||
"Bash-Roundtrips."
|
||||
"HTTP 204 No Content ist KEIN Fehler. Bei Spotify POST/PUT "
|
||||
"(next/previous/play/pause/volume/seek) ist 204 die normale "
|
||||
"Erfolgsantwort. Wenn dein Skill bei 204 einen Parse-Error "
|
||||
"wirft: skill_update mit `if status == 204: print('OK')` "
|
||||
"VOR dem Retry, nicht erst die Aktion nochmal auslоsen.\n"
|
||||
"\n"
|
||||
"Antipattern (30.05.2026): ARIA hat POST /previous einmal "
|
||||
"gemacht (Spotify 204 OK → Skill-Parse-Error), dachte 'Skill "
|
||||
"kaputt', patchte ihn UND fuehrte das previous nochmal aus. "
|
||||
"Folge: Stefan landete zwei Lieder weiter hinten als gewollt."
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/skill-rule/arg-env-convention",
|
||||
"type": "rule",
|
||||
"title": "Skill-Regel: Args kommen als ARG_<NAME> ENV — die Konvention NIEMALS aendern",
|
||||
"category": "skills",
|
||||
"content": (
|
||||
"Skill-Args werden vom Brain-Runner als Environment-Variablen "
|
||||
"mit PRÄFIX `ARG_` ueber `os.environ` an den Skill durchgereicht. "
|
||||
"Beispiel: arg `path=\"/v1/me/player\"` → "
|
||||
"`ARG_PATH=/v1/me/player` im Skill-ENV.\n"
|
||||
"\n"
|
||||
"Beim skill_update MUSST Du diese Konvention beibehalten:\n"
|
||||
" RICHTIG: os.environ.get('ARG_PATH', '')\n"
|
||||
" RICHTIG: os.environ.get('ARG_METHOD', 'GET')\n"
|
||||
" RICHTIG: os.environ.get('ARG_BODY', '')\n"
|
||||
"\n"
|
||||
" FALSCH: os.environ.get('PATH', '') ← System-PATH "
|
||||
"(Executable-Suchpfad)!\n"
|
||||
" FALSCH: os.environ.get('METHOD', '')\n"
|
||||
" FALSCH: os.environ.get('BODY', '')\n"
|
||||
"\n"
|
||||
"Antipattern (30.05.2026): ARIA hat beim skill_update des "
|
||||
"spotify-Skills die Args von `ARG_PATH` auf `PATH` umbenannt. "
|
||||
"Folge: Skill las `/usr/local/sbin:/usr/local/bin:...` als "
|
||||
"URL-Pfad → Spotify gab 404 zurück. Stefan dachte Spotify sei "
|
||||
"kaputt. Rollback noetig.\n"
|
||||
"\n"
|
||||
"Andere reservierte ENV-Namen die Du NICHT nehmen darfst: "
|
||||
"PATH, HOME, USER, SHELL, LANG, TERM, PWD, OLDPWD, "
|
||||
"BRAIN_INTERNAL_URL, SKILL_DIR, SHARED_UPLOADS, CFG_* "
|
||||
"(letztere sind Config-Schema-Werte). Bei Skill-Args IMMER "
|
||||
"den Praefix ARG_ verwenden, dann hast Du keine Kollision."
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/skill-rule/skills-are-editable-python",
|
||||
"type": "rule",
|
||||
"title": "Skill-Regel: Skills sind beliebiger Python-Code, kein heiliger Vertrag",
|
||||
"category": "skills",
|
||||
"content": (
|
||||
"Wenn Stefan eine Skill-Anpassung wuenscht — egal wie klein oder "
|
||||
"gross — ist die Antwort fast IMMER:\n"
|
||||
" 1. `skill_get('<name>')` aufrufen, Code lesen\n"
|
||||
" 2. Ueberlegen wie sich Stefans Wunsch im Code umsetzen laesst\n"
|
||||
" 3. `skill_update` mit dem neuen `entry_code`\n"
|
||||
"\n"
|
||||
"Skills sind GANZ NORMALER Python-Code. Du darfst und SOLLST:\n"
|
||||
" - if-elif-else-Verzweigungen auf args / paths reagieren lassen "
|
||||
"(z.B. `if action == 'current': pretty_output(); else: print(json.dumps(data))`)\n"
|
||||
" - json.loads(), neue Helper-Funktionen, pip-Pakete via "
|
||||
"pip_packages ergaenzen\n"
|
||||
" - Outputs strukturieren oder filtern\n"
|
||||
" - Mehrere Endpoints einer API in einem Skill bedienen\n"
|
||||
"\n"
|
||||
"Was Du NICHT sagen sollst (Antipattern, am 30.05.2026 passiert):\n"
|
||||
" - 'Der Skill ist ein OAuth2-API-Wrapper, ich kann das nicht in "
|
||||
"den Wrapper bauen' — Quatsch, Wrapper ist auch nur Python\n"
|
||||
" - 'Ich schlage einen neuen Skill statt Update vor' — pruefe "
|
||||
"ZUERST ob skill_update reicht. Anti-Friedhof greift ohnehin "
|
||||
"wenn der Name kollidiert.\n"
|
||||
" - 'Kann ich nicht' OHNE Code gelesen zu haben — erst "
|
||||
"skill_get, dann beurteilen\n"
|
||||
"\n"
|
||||
"Stefan ist KEIN Python-Entwickler. Er nennt das ZIEL ('strukturierte "
|
||||
"Track-Ausgabe bei welches-Lied'), Du baust das WIE im Code. "
|
||||
"Wenn Du Dich rausredest, ist das Verschwendung — Stefan muss sich "
|
||||
"dann selbst Python-Tipps merken die er nicht im Kopf hat. "
|
||||
"Genau dafuer bist Du da."
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/skill-rule/scaffold-reflex",
|
||||
"type": "rule",
|
||||
"title": "Skill-Regel: Skill-Frage statt Skill-Reflex",
|
||||
"category": "skills",
|
||||
"content": (
|
||||
"Wenn Du dieselbe API mehrmals per Bash anrufst, frag Dich:\n"
|
||||
"\n"
|
||||
"1. **Parametrisierbar?** Stabile 1-5 Args (action, path, body) "
|
||||
"→ Skill-Kandidat. Jeder Aufruf anders (neuer Endpoint, "
|
||||
"modifizierter Body, neue Hypothese) → KEIN Skill.\n"
|
||||
"\n"
|
||||
"2. **Wiederkehrend?** Stefan wird das mehrfach pro Tag/Woche "
|
||||
"brauchen → ja. Einmal-Spike heute → nein.\n"
|
||||
"\n"
|
||||
"3. **Exploratory?** Pentest, Audit, Code-Review, Reverse-"
|
||||
"Engineering, Recherche → Hypothesen-Iteration. KEIN Skill, "
|
||||
"auch wenn 100x derselbe Host. Bleib bei ad-hoc Bash oder "
|
||||
"`ssh aria@host` zur VM-Host.\n"
|
||||
"\n"
|
||||
"4. **Im Zweifel: frag Stefan.** Lieber 5 Sekunden Bestaetigung "
|
||||
"als zehn unsinnige Skills im Friedhof. Beispiele:\n"
|
||||
" - 'Stefan, das ist mein 3. X-Call diese Woche — soll ich "
|
||||
"daraus einen Skill machen?'\n"
|
||||
" - 'Das hier ist Pentest-Workflow, ich bleibe bei ad-hoc "
|
||||
"Bash, ok?'\n"
|
||||
"\n"
|
||||
"Du musst NICHT automatisch scaffolden. Brain trackt NICHT mehr "
|
||||
"wer wieviele Calls gegen welchen Host gemacht hat. Du "
|
||||
"entscheidest mit Sinn und Verstand — oder fragst nach.\n"
|
||||
"\n"
|
||||
"Wenn Du einen Skill bauen willst, hast Du drei Tools:\n"
|
||||
" - `skill_scaffold` mit Template — einfachster Weg fuer "
|
||||
"Standard-Pattern (siehe oauth-api/apikey-api/file-process).\n"
|
||||
" - `skill_create` mit eigenem entry_code — fuer alles was "
|
||||
"in kein Template passt.\n"
|
||||
" - `skill_update` — wenn ein vorhandener Skill nur erweitert "
|
||||
"werden muss (was meistens der Fall ist)."
|
||||
),
|
||||
},
|
||||
{
|
||||
"migration_key": "seed/skill-rule/patch-before-diagnose",
|
||||
"type": "rule",
|
||||
"title": "Skill-Regel: vor skill_update erst skill_get lesen + API-Errors zitieren statt raten",
|
||||
"category": "skills",
|
||||
"content": (
|
||||
"Zwei Antipattern die zusammenhaengen — beide am 30.05.2026 "
|
||||
"live beobachtet:\n"
|
||||
"\n"
|
||||
"**1. Vor jedem `skill_update`: ZUERST `skill_get` lesen.** "
|
||||
"Frag Dich: ist das vermutete Problem wirklich noch im Code? "
|
||||
"Symptome != Diagnose. Vorfall: Spotify-Skill gab 403, ARIA "
|
||||
"vermutete 'der 204-Bug ist zurueck' und patchte den Skill — "
|
||||
"zweimal hintereinander. Der 204-Fix war aber laengst drin. "
|
||||
"Sie hatte das durch `skill_get` in 5 Sekunden klaeren koennen.\n"
|
||||
"\n"
|
||||
"Vor jedem skill_update also der Reflex:\n"
|
||||
" - `skill_get('<name>')` -> Code anschauen\n"
|
||||
" - Symptome durchdenken: ist mein vermuteter Bug ueberhaupt "
|
||||
"der echte? Oder ist der Fehler woanders (Spotify-API, "
|
||||
"User-Kontext, Tool-Args)?\n"
|
||||
" - Nur dann patchen wenn der Code-Befund das wirklich "
|
||||
"rechtfertigt.\n"
|
||||
"\n"
|
||||
"**2. Bei HTTP-Errors aus API-Skills (4xx/5xx): die echte "
|
||||
"Response-Body ZITIEREN, nicht die Bedeutung raten.** "
|
||||
"Vorfall: Spotify gab 403 'Restriction violated'. ARIA "
|
||||
"antwortete 'war schon pausiert, daher der 403' — das war "
|
||||
"geraten, nicht aus den Daten gelesen. 403 'Restriction "
|
||||
"violated' kann viele Sachen heissen:\n"
|
||||
" - NO_ACTIVE_DEVICE (kein Spotify-Geraet ausgewaehlt)\n"
|
||||
" - ALREADY_PAUSED / ALREADY_PLAYING\n"
|
||||
" - PREMIUM_REQUIRED\n"
|
||||
" - MARKET_RESTRICTED / DEVICE_NOT_CONTROLLABLE\n"
|
||||
"Spotify gibt die wahre Ursache als `error.reason` im JSON-"
|
||||
"Body zurueck. Lies sie aus, sag sie Stefan 1:1. Wenn die "
|
||||
"Skill-Output das verschluckt: skill_update mit error.reason-"
|
||||
"Extraktion (nach skill_get!), damit Du beim naechsten Mal "
|
||||
"die echte Info hast.\n"
|
||||
"\n"
|
||||
"Plausibel-aber-geraten ist schlimmer als 'ich weiss es nicht' "
|
||||
"— Stefan verlaesst sich auf Deine Antworten."
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1506,7 +1506,12 @@ const server = http.createServer((req, res) => {
|
||||
log("error", "server", `zip exit ${code}: ${stderr.slice(0, 200)}`);
|
||||
}
|
||||
});
|
||||
req.on("close", () => { if (!zip.killed) zip.kill("SIGTERM"); });
|
||||
// SIGTERM an zip nur wenn der Client wirklich disconnected
|
||||
// (res.close vor res.end). req.on("close") feuert auch wenn
|
||||
// der Request-Body durch ist — das wuerde zip vorzeitig killen.
|
||||
res.on("close", () => {
|
||||
if (!res.writableEnded && !zip.killed) zip.kill("SIGTERM");
|
||||
});
|
||||
});
|
||||
return;
|
||||
} else if (req.url === "/api/files-delete-batch" && req.method === "POST") {
|
||||
|
||||
+4
-7
@@ -20,7 +20,7 @@ services:
|
||||
volumes:
|
||||
- ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json)
|
||||
- ./aria-data/ssh:/root/.ssh # SSH Keys fuer VM-Zugriff (aria-wohnung, rw fuer ARIA)
|
||||
- aria-shared:/shared # Shared Volume fuer Datei-Austausch (Uploads von App)
|
||||
- ./aria-shared:/shared # Shared Volume fuer Datei-Austausch (Uploads von App)
|
||||
- ./proxy-patches:/proxy-patches:ro # Tool-Use-Adapter (ueberschreibt npm-Version, read-only)
|
||||
# Claude Code's eingebautes Auto-Memory liegt in ~/.claude/projects/.
|
||||
# Wir ueberlagern das mit tmpfs damit ARIA nicht parallel zu ARIAs eigener
|
||||
@@ -87,7 +87,7 @@ services:
|
||||
- ./aria-data/brain/data:/data # Memory-Cache + Skills + Models (bind-mount fuer Export)
|
||||
- ./aria-data/brain-import:/import:ro # Quell-MDs fuer den initialen Memory-Import (read-only)
|
||||
- ./aria-data/ssh:/root/.ssh # SSH-Keys fuer aria-wohnung (geteilt mit Proxy)
|
||||
- aria-shared:/shared # gleicher Austausch-Speicher wie Bridge
|
||||
- ./aria-shared:/shared # gleicher Austausch-Speicher wie Bridge
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- aria-net
|
||||
@@ -103,7 +103,7 @@ services:
|
||||
ports:
|
||||
- "3001:3001" # Diagnostic Web-UI (Diagnostic teilt Netzwerk mit Bridge)
|
||||
volumes:
|
||||
- aria-shared:/shared # Shared Volume fuer Datei-Austausch
|
||||
- ./aria-shared:/shared # Shared Volume fuer Datei-Austausch
|
||||
# Audio-Zugriff
|
||||
- /run/user/1000/pulse:/run/user/1000/pulse
|
||||
- /dev/snd:/dev/snd
|
||||
@@ -132,7 +132,7 @@ services:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Container Restart + Brain-Export/Import
|
||||
- ./aria-data/config/diag-state:/data # Persistenter State (aktive Session etc.)
|
||||
- aria-shared:/shared # Shared Volume (Uploads + Config + Voices)
|
||||
- ./aria-shared:/shared # Shared Volume (Uploads + Config + Voices)
|
||||
- ./aria-data/brain:/brain # Brain-Export/Import (tar.gz aus Bind-Mount)
|
||||
environment:
|
||||
- ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-}
|
||||
@@ -145,9 +145,6 @@ services:
|
||||
- RVS_TOKEN=${RVS_TOKEN:-}
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
aria-shared: # Datei-Austausch zwischen Bridge / Brain / Diagnostic
|
||||
|
||||
networks:
|
||||
aria-net:
|
||||
driver: bridge
|
||||
|
||||
@@ -912,6 +912,12 @@ async def run_loop(runner: F5Runner) -> None:
|
||||
continue
|
||||
await asyncio.sleep(min(retry_s, 30))
|
||||
retry_s = min(retry_s * 2, 30)
|
||||
# Sticky-Fallback verhindern: nach jedem Disconnect-Cycle wieder
|
||||
# mit wss anfangen. Sonst klebt der Client nach einem temporaeren
|
||||
# TLS-Hick auf ws:// fest und kommt nie mehr auf wss zurueck —
|
||||
# genau das Problem das die App + Bridge frueher schon hatten.
|
||||
use_tls = RVS_TLS
|
||||
tls_fallback_tried = False
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
|
||||
@@ -292,6 +292,12 @@ async def run_loop(runner: WhisperRunner) -> None:
|
||||
continue
|
||||
await asyncio.sleep(min(retry_s, 30))
|
||||
retry_s = min(retry_s * 2, 30)
|
||||
# Sticky-Fallback verhindern: nach jedem Disconnect-Cycle wieder
|
||||
# mit wss anfangen. Sonst klebt der Client nach einem temporaeren
|
||||
# TLS-Hick auf ws:// fest und kommt nie mehr auf wss zurueck —
|
||||
# genau das Problem das die App + Bridge frueher schon hatten.
|
||||
use_tls = RVS_TLS
|
||||
tls_fallback_tried = False
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
|
||||
Reference in New Issue
Block a user