Compare commits

..

4 Commits

Author SHA1 Message Date
duffyduck 86f20d3b64 clude config 2026-05-31 00:19:54 +02:00
duffyduck 78211f09ce feat(brain): Listen-API-Pagination strukturell loesen + seed-rule
Stefan-Reproduktion vom 31.05.2026: bei 'Such Playlist Prodigy raus'
hat ARIA die Spotify-Pagination drei Mal hintereinander laufen lassen,
jedes Mal eine andere Playlist-ID gefunden, am Ende falsche abgespielt.
Spotify sortiert /v1/me/playlists nach recently-played — die Reihen-
folge aendert sich zwischen Calls wenn parallel was laeuft, also
liefern aufeinanderfolgende paginierte Runs inkonsistente Snapshots.

Loesungen:

1. **spotify-Skill _all=true** (via skill_update angewendet, lebt nur
   in /data/skills/spotify/ im Container, nicht in git): Skill prueft
   _all=true im URL-Query, paginiert dann intern ueber Spotifys
   next-Field bis MAX_PAGES (20) oder fertig. Liefert konsolidiertes
   JSON {items, total, fetched_count, fetched_pages}. EIN Tool-Call,
   konsistenter Snapshot.

2. **skills.py: Stdout-Truncation entkoppeln**. Vorher: 8000-char-Cap
   sowohl fuer Disk-Log als auch fuer Return-Value an Agent.
   Konsequenz: _all=true Output (50 KB JSON) wurde fuer ARIA auf 8 KB
   gekuerzt, sie sah nur die ersten ~20 Playlists. Jetzt:
     - Disk-Log: weiterhin 8 KB pro stdout (Disk-Schoner)
     - Return-Value: ungekuerzt, agent.py macht 50 KB downstream-Cap
   Skills.py:687 — record-Dict aufgesplittet in log_record + record.

3. **seed_rule list-api-pagination-snapshot**: dokumentiert das
   Pattern fuer ARIA — bei Pagination-Resultaten IMMER vollstaendig
   laden bevor Entscheidung; _all=true bevorzugen wo verfuegbar;
   bei inkonsistenten Match-Resultaten ehrlich nachfragen statt
   raten. Mit konkreter Antipattern-Sammlung aus Stefans Test.

Deployment: brain restart noetig damit (2) und (3) greifen. Skill-
Code (1) ist schon via PATCH /skills/spotify aktiv.
2026-05-31 00:14:06 +02:00
duffyduck b2edee9adb release: bump version to 0.1.8.8 2026-05-30 23:32:27 +02:00
duffyduck bb13477ef9 fix(wake): Race zwischen endConversation und stopBargeListening killt
Wake-Word-Listener nach jeder Konversation

Aus dem Log diagnostiziert: zwei onPlaybackFinished-Listener feuern
direkt hintereinander wenn TTS endet:
  1. mein neuer Listener (Background): endConversation()
     → state=armed, OpenWakeWord.start() (idempotent)
  2. existierender Listener:           stopBargeListening()
     → bargeListening=true → OpenWakeWord.stop()  ← killt re-armed Listener

State zeigte 'armed' (UI: Ohr-Icon ausgefuellt, sieht aktiv aus), aber
das Native-Modul war gestoppt → Stefan's "Computer" verpufft.

Fix: endConversation setzt bargeListening=false BEVOR Native gerufen
wird. stopBargeListening checkt das Flag oben:
  async stopBargeListening() { if (!this.bargeListening) return; ... }
→ wird zum No-Op wenn endConversation schon gelaufen ist.

Bonus: OpenWakeWord.start() darf jetzt auch gerufen werden wenn der
Listener via barge-listening schon lief — Kotlin checkt running.get()
und resolved idempotent. Sicherer als state-vorher-Check.
2026-05-30 23:31:25 +02:00
6 changed files with 106 additions and 13 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(ssh root@172.0.2.33 \"ls -la /root/ARIA-AGENT/aria-shared/logs/\")"
]
}
}
+2 -2
View File
@@ -79,8 +79,8 @@ android {
applicationId "com.ariacockpit"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 10807
versionName "0.1.8.7"
versionCode 10808
versionName "0.1.8.8"
// Fallback fuer Libraries mit Product Flavors
missingDimensionStrategy 'react-native-camera', 'general'
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "aria-cockpit",
"version": "0.1.8.7",
"version": "0.1.8.8",
"private": true,
"scripts": {
"android": "react-native run-android",
+20 -5
View File
@@ -344,23 +344,38 @@ class WakeWordService {
/** Konversation beenden — User hat im Window nichts gesagt.
* Mit Wake-Word: zurueck zu 'armed' (Listener wieder an).
* Ohne: zurueck zu 'off'.
*
* WICHTIG: setzt bargeListening=false BEVOR OpenWakeWord.start() laeuft.
* Grund: wenn endConversation aus dem onPlaybackFinished-Handler kommt,
* feuert direkt danach ein zweiter Listener (stopBargeListening) — der
* wuerde sonst OpenWakeWord.stop() rufen weil bargeListening noch true
* ist, und unseren frisch re-armierten Listener killen.
*/
async endConversation(): Promise<void> {
if (this.state !== 'conversing') {
// Nicht in conversing — typ. nach App-Resume bevor Streaming endete.
// Trotzdem loggen damit wir's im Diagnostic sehen.
import('./logger').then(m => m.reportAppDebug('wake.end',
`endConversation called but state=${this.state} → noop`)).catch(()=>{});
return;
}
const wasBarge = this.bargeListening;
// Flag NULLEN bevor wir die Listener triggern. Sonst killt der parallele
// stopBargeListening-Listener (TTS-end) gleich danach unseren Native-
// OpenWakeWord, weil er bargeListening=true sieht und annimmt er muss
// den Listener stoppen.
this.bargeListening = false;
import('./logger').then(m => m.reportAppDebug('wake.end',
`endConversation called, nativeReady=${this.nativeReady}, calling OpenWakeWord.start()`)).catch(()=>{});
`endConversation called, wasBarge=${wasBarge}, nativeReady=${this.nativeReady}`)).catch(()=>{});
if (this.nativeReady && OpenWakeWord) {
// Wenn wakeword schon laeuft (war Barge-Listener waehrend TTS):
// OpenWakeWord.start() ist idempotent (Kotlin checkt running.get()
// und resolved sofort). Wir koennen es trotzdem rufen — billiger
// als state extra zu fragen, garantiert dass nach diesem Pfad
// Native auch wirklich an ist falls es out-of-band gestoppt wurde.
try {
await OpenWakeWord.start();
console.log('[WakeWord] Konversation zu Ende — zurueck zu armed');
console.log('[WakeWord] Konversation zu Ende — zurueck zu armed (wasBarge=%s)', wasBarge);
import('./logger').then(m => m.reportAppDebug('wake.end',
`OpenWakeWord.start() OK → state=armed, keyword=${this.keyword}`)).catch(()=>{});
`OpenWakeWord.start() OK → state=armed, wasBarge=${wasBarge}`)).catch(()=>{});
ToastAndroid.show(`Lausche wieder auf "${KEYWORD_LABELS[this.keyword]}"`, ToastAndroid.SHORT);
this.setState('armed');
return;
+55
View File
@@ -602,6 +602,61 @@ SEED_RULES: List[dict] = [
"'API Key' im Auth-Kapitel). Nicht raten."
),
},
{
"migration_key": "seed/skill-rule/list-api-pagination-snapshot",
"type": "rule",
"title": "Listen-API: einmal vollstaendig laden, DANN entscheiden",
"category": "verhalten",
"content": (
"Wenn ein Tool-Resultat ein Pagination-Schema hat (limit/offset/"
"next oder total > limit): ALLE Seiten in EINEM Tool-Call holen, "
"in EINEM Snapshot durchsuchen, ERST DANN handeln.\n"
"\n"
"Antipattern (31.05.2026, Stefan reproduziert mit 'Playlist Prodigy "
"raussuchen'):\n"
" - run_spotify path=/v1/me/playlists?limit=50\n"
"'nicht dabei'\n"
" - run_spotify path=/v1/me/playlists?limit=50&offset=50\n"
"'gefunden, ID=X' (46 Tracks)\n"
" - run_spotify path=/v1/me/player/play body={context_uri: ...:X}\n"
" → spielt aber FALSCHE Playlist\n"
" - Neue Suche, wieder paginiert → drittes Match ID=Y (15 Tracks)\n"
" - Insgesamt drei verschiedene IDs fuer dieselbe gesuchte Playlist\n"
" generiert, am Ende die falsche gespielt.\n"
"\n"
"Wurzel: Spotify sortiert /v1/me/playlists nach recently-played. "
"Zwischen aufeinanderfolgenden paginierten Calls AENDERT SICH die "
"Reihenfolge wenn parallel was abgespielt wird. Teilresultate aus "
"verschiedenen Calls vergleichen → inkonsistent.\n"
"\n"
"Richtig fuer Spotify (seit 31.05.2026 unterstuetzt):\n"
" run_spotify path=/v1/me/playlists?limit=50&_all=true\n"
" → Skill paginiert intern, liefert {items, total, fetched_count}.\n"
" → In items[] suchen, EINE ID waehlen, sofort handeln.\n"
" → Match-Logik: bevorzugt exakter Name (case-insensitive). "
"Wenn mehrere Substring-Matches: explizit nachfragen statt raten.\n"
"\n"
"Wann _all=true sinnvoll:\n"
" - /v1/me/playlists (alle eigenen Playlists)\n"
" - /v1/playlists/{id}/tracks (alle Tracks einer Playlist)\n"
" - /v1/me/tracks (Liked Songs)\n"
" - /v1/search?type=playlist&q=... (Such-Ergebnisse mit next)\n"
" - Andere Endpunkte mit items+next-Schema.\n"
"\n"
"Wann NICHT _all=true:\n"
" - /v1/me/player/currently-playing (kein Listen-Endpunkt)\n"
" - /v1/me/player/devices (kurze Liste, kein next)\n"
" - Wenn Du explizit nur 'die ersten 10' willst.\n"
"\n"
"Fuer andere Skills (yt-dlp, andere APIs) die noch kein _all "
"unterstuetzen: manuell paginieren bis total erreicht, ALLES in "
"EINEM mentalen Snapshot mergen, NIEMALS auf Teilresultaten "
"Entscheidungen treffen. Wenn zwei Pagination-Runs unterschiedliche "
"Matches liefern: ehrlich melden ('zwei verschiedene Playlists "
"namens X gefunden — welche meinst Du?') statt sich auf eine "
"festzulegen."
),
},
]
+21 -5
View File
@@ -683,8 +683,13 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) ->
timed_out = True
duration = time.time() - t0
# Log schreiben (gekuerzt damit es nicht explodiert)
record = {
# Log auf der Disk wird gekuerzt (8000 chars) — sonst sammeln sich
# logs/*.json mit MBs an grossen Skill-Outputs an. Der Return-Value
# an den Caller (Agent) bekommt aber den vollen Output, dort wird
# nochmal in agent.py auf 50000 gecappt. Stefan-Fall: spotify-Skill
# mit _all=true liefert 50+ KB JSON, das hier wurde vorher auf 8 KB
# gekappt → ARIA sah immer nur den Anfang der Liste.
log_record = {
"ts": _now(),
"args": args or {},
"exit_code": exit_code,
@@ -694,7 +699,7 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) ->
"timed_out": timed_out,
}
try:
log_path.write_text(json.dumps(record, indent=2, ensure_ascii=False), encoding="utf-8")
log_path.write_text(json.dumps(log_record, indent=2, ensure_ascii=False), encoding="utf-8")
except Exception:
pass
@@ -703,8 +708,19 @@ def run_skill(name: str, args: Optional[dict] = None, timeout_sec: int = 300) ->
manifest["use_count"] = int(manifest.get("use_count", 0)) + 1
write_manifest(name, manifest)
record["ok"] = exit_code == 0
record["log_path"] = str(log_path)
# Return-Value: nicht kuerzen (Agent kuerzt downstream selbst). Nur
# die Disk-Log-Variante war beschnitten.
record = {
"ts": log_record["ts"],
"args": log_record["args"],
"exit_code": exit_code,
"duration_sec": log_record["duration_sec"],
"stdout": out_text or "",
"stderr": err_text or "",
"timed_out": timed_out,
"ok": exit_code == 0,
"log_path": str(log_path),
}
return record