feat(zwischenruf): Korrektur mitten in den laufenden Turn schieben

Neben Senden ein 'Zwischenruf' (App: oranger Button, nur wenn ARIA im
aktiven Kontext arbeitet; Diagnostic: Buttons neben beiden Senden). Geht
NICHT in die Queue und bricht NICHT ab — die Nachricht wird in den
laufenden claude-Subprozess geschoben; er greift sie an der naechsten
Tool-Grenze auf.

Technik:
- proxy-patches/manager.js: claude laeuft jetzt im --input-format
  stream-json-Modus, initialer Prompt als stream-json User-Message,
  stdin bleibt OFFEN; sendMessage() schiebt weitere User-Messages nach;
  bei 'result' wird stdin geschlossen (Turn endet sauber). Ersetzt die
  bisherigen sed-Patches (jetzt volle Datei via cp, docker-compose.yml).
- proxy-patches/routes.js: Side-Channel POST /interject {projectId,text}
  → subprocess.sendMessage der Kontext-Subprozesse.
- rvs: 'interject' erlaubt. bridge: RVS interject → Proxy /interject.
- App/Diagnostic: Zwischenruf-Buttons + lokale '📣 Zwischenruf'-Bubble.

Empirisch verifiziert: mid-turn injizierte Message wird an der naechsten
Tool-Grenze aufgegriffen (nicht mitten in einem blockierenden Befehl).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 05:05:13 +02:00
co-authored by Claude Opus 4.8
parent 2ad8c2f245
commit 648e3b04fd
8 changed files with 395 additions and 9 deletions
+36
View File
@@ -2434,6 +2434,20 @@ class ARIABridge:
await self._emit_activity("idle", "", project_id=cancel_pid)
return
if msg_type == "interject":
# Zwischenruf: waehrend eines laufenden Turns eine Korrektur
# reinschieben — KEIN Abbruch, keine Queue. Geht an den Proxy-
# internen /interject, der die Message in den laufenden Subprozess
# des Kontexts schreibt (claude greift sie an der naechsten Tool-
# Grenze auf).
interject_pid = str(payload.get("projectId") or "")
interject_text = str(payload.get("text") or "")
logger.info("[rvs] Zwischenruf project=%s: '%s'",
interject_pid or "(main)", interject_text[:80])
if interject_text.strip():
await self._interject_proxy_for_project(interject_pid, interject_text)
return
elif msg_type == "audio_pcm":
# Audio-PCM geht direkt von XTTS-Bridge an die App.
# Die aria-bridge darf es NICHT rebroadcasten — sonst bekommt die App
@@ -4132,6 +4146,28 @@ class ARIABridge:
logger.info("[cancel] proxy /cancel project=%s: %s %s",
project_id or "(main)", status, body)
async def _interject_proxy_for_project(self, project_id: str, text: str) -> None:
"""Zwischenruf: schiebt eine User-Message in den laufenden Turn dieses
Kontexts (proxy-internes /interject) — ohne Abbruch. claude greift sie
an der naechsten Tool-Grenze auf."""
url = os.environ.get("PROXY_INTERNAL_URL", "http://aria-proxy:3457") + "/interject"
data = json.dumps({"projectId": project_id or "", "text": text}).encode("utf-8")
def _do_request():
try:
req = urllib.request.Request(
url, method="POST", data=data,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=3) as resp:
return resp.status, resp.read().decode("utf-8", "ignore")[:200]
except Exception as e:
return f"error: {e}", ""
status, body = await asyncio.get_event_loop().run_in_executor(None, _do_request)
logger.info("[interject] proxy /interject project=%s: %s %s",
project_id or "(main)", status, body)
async def _emit_activity(self, activity: str, tool: str = "", force: bool = False,
project_id: str = "") -> None:
"""Sendet agent_activity an die App — nur wenn sich der State geaendert hat.