diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx
index 62d2012..d6d1c84 100644
--- a/android/src/screens/ChatScreen.tsx
+++ b/android/src/screens/ChatScreen.tsx
@@ -2444,6 +2444,24 @@ const ChatScreen: React.FC = () => {
}
}, [inputText, pendingAttachments, sendPendingAttachments, getCtxState, setCtxState, setCtxQueue, actuallySend]);
+ // Zwischenruf: waehrend ARIA arbeitet eine Korrektur MITTEN in den laufenden
+ // Turn schieben — NICHT in die Queue, KEIN Abbruch. Geht als 'interject' ueber
+ // RVS an die Bridge → Proxy → laufender Subprozess (greift es an der naechsten
+ // Tool-Grenze auf). Sichtbar nur, wenn der aktive Kontext gerade arbeitet.
+ const sendInterject = useCallback(() => {
+ const text = inputText.trim();
+ if (!text) return;
+ const activePid = focusedProjectIdRef.current;
+ rvs.send('interject' as any, { projectId: activePid, text });
+ // Lokale Bubble zur Rueckmeldung (laeuft NICHT durch Send/Queue).
+ setMessages(prev => capMessages([...prev, {
+ id: nextId(), sender: 'user', text: `📣 Zwischenruf: ${text}`,
+ timestamp: Date.now(), projectId: activePid,
+ }]));
+ projectDraftsRef.current = { ...projectDraftsRef.current, [activePid]: '' };
+ setInputText('');
+ }, [inputText]);
+
// --- Rendering ---
const renderMessage = ({ item }: { item: ChatMessage }) => {
@@ -3218,9 +3236,19 @@ const ChatScreen: React.FC = () => {
{/* Senden oder Sprache */}
{inputText.trim() || pendingAttachments.length > 0 ? (
-
- {'\u2B06\uFE0F'}
-
+ <>
+ {/* Zwischenruf: nur wenn ARIA im aktiven Kontext gerade arbeitet und
+ Text da ist. Schiebt die Korrektur in den laufenden Turn statt
+ sie anzustellen. */}
+ {inputText.trim() && (agentActivityByCtx[focusedProjectId]?.activity || 'idle') !== 'idle' ? (
+
+ {'\uD83D\uDCE3'}
+
+ ) : null}
+
+ {'\u2B06\uFE0F'}
+
+ >
) : (
<>
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.
diff --git a/diagnostic/index.html b/diagnostic/index.html
index 27ce6ed..bf5464a 100644
--- a/diagnostic/index.html
+++ b/diagnostic/index.html
@@ -328,6 +328,7 @@
+
@@ -345,6 +346,7 @@
+
@@ -2244,6 +2246,18 @@
diagCtxStates[pid] = 'running';
renderDiagQueue();
}
+ // Zwischenruf: waehrend ARIA arbeitet eine Korrektur in den laufenden Turn
+ // schieben — NICHT anstellen, NICHT abbrechen.
+ function interjectDiag(inputId) {
+ const input = document.getElementById(inputId || 'chat-input');
+ const text = (input.value || '').trim();
+ if (!text) return;
+ send({ action: 'interject', text, projectId: focusedContextId });
+ addChat('sent', '📣 Zwischenruf: ' + text, 'interject', { projectId: focusedContextId });
+ diagDrafts[focusedContextId] = '';
+ localStorage.setItem('diag_ctx_drafts', JSON.stringify(diagDrafts));
+ input.value = '';
+ }
function advanceDiagQueue(pid) {
const q = diagCtxQueues[pid] || [];
if (q.length === 0) { diagCtxStates[pid] = 'idle'; renderDiagQueue(); return; }
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 51cd314..74c9d52 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -2455,6 +2455,14 @@ wss.on("connection", (ws) => {
} else if (msg.action === "test_rvs") {
traceStart("RVS", msg.text || "aria lebst du noch?");
sendToRVS(msg.text || "aria lebst du noch?", true, msg.projectId || "");
+ } else if (msg.action === "interject") {
+ // Zwischenruf: in den laufenden Turn schieben (kein Abbruch, keine
+ // Queue) → RVS interject → Bridge → Proxy /interject.
+ const t = String(msg.text || "");
+ if (t.trim()) {
+ sendToRVS_raw({ type: "interject", payload: { projectId: msg.projectId || "", text: t }, timestamp: Date.now() });
+ log("info", "server", "Zwischenruf an RVS (project=" + (msg.projectId || "(main)") + "): " + t.slice(0, 60));
+ }
} else if (msg.action === "reconnect_gateway") {
connectGateway();
} else if (msg.action === "reconnect_rvs") {
diff --git a/docker-compose.yml b/docker-compose.yml
index 292be7b..384733f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,11 +11,7 @@ services:
npm install -g @anthropic-ai/claude-code claude-max-api-proxy &&
DIST=$$(find /usr/local/lib -path '*/claude-max-api-proxy/dist' -type d | head -1) &&
sed -i 's/startServer({ port })/startServer({ port, host: process.env.HOST || \"127.0.0.1\" })/' $$DIST/server/standalone.js &&
- sed -i 's/\"--no-session-persistence\",/\"--no-session-persistence\",\"--dangerously-skip-permissions\",/' $$DIST/subprocess/manager.js &&
- sed -i 's/\"--dangerously-skip-permissions\",/\"--dangerously-skip-permissions\",\"--system-prompt\",options.systemPrompt,/' $$DIST/subprocess/manager.js &&
- sed -i 's/const DEFAULT_TIMEOUT = 300000;/const DEFAULT_TIMEOUT = 86400000;/' $$DIST/subprocess/manager.js &&
- sed -i '/prompt, \\/\\/ Pass prompt as argument/d' $$DIST/subprocess/manager.js &&
- sed -i 's|this\\.process\\.stdin?\\.end();|this.process.stdin?.end(prompt);|' $$DIST/subprocess/manager.js &&
+ cp /proxy-patches/manager.js $$DIST/subprocess/manager.js &&
cp /proxy-patches/openai-to-cli.js $$DIST/adapter/openai-to-cli.js &&
cp /proxy-patches/cli-to-openai.js $$DIST/adapter/cli-to-openai.js &&
cp /proxy-patches/routes.js $$DIST/server/routes.js &&
diff --git a/proxy-patches/manager.js b/proxy-patches/manager.js
new file mode 100644
index 0000000..2ad1181
--- /dev/null
+++ b/proxy-patches/manager.js
@@ -0,0 +1,256 @@
+/**
+ * Claude Code CLI Subprocess Manager — ARIA-Patch
+ *
+ * Basis: claude-max-api-proxy dist/subprocess/manager.js, plus die bisher per
+ * sed in docker-compose.yml eingespielten Anpassungen (dangerously-skip-
+ * permissions, system-prompt, 24h-Timeout, Prompt via stdin) — hier fest im
+ * File, damit der groessere Zwischenruf-Umbau nicht per sed gefrickelt werden
+ * muss. Wird per `cp` ueber die npm-Version gelegt (siehe docker-compose.yml).
+ *
+ * ZWISCHENRUF (interject): Statt den Prompt als Text zu schreiben und stdin
+ * sofort zu schliessen (--print/text), laeuft claude jetzt im
+ * `--input-format stream-json`-Modus. Der initiale Prompt geht als
+ * stream-json User-Message rein, stdin bleibt OFFEN — so kann waehrend des
+ * laufenden Turns per sendMessage() eine weitere User-Message reingeschoben
+ * werden, die claude an der naechsten Tool-Grenze aufgreift (kein Abbruch).
+ * Bei 'result' (Turn fertig) wird stdin geschlossen, damit claude sauber
+ * beendet und die HTTP-Response (in routes.js an 'close' gebunden) rausgeht.
+ */
+import { spawn } from "child_process";
+import { EventEmitter } from "events";
+import { isAssistantMessage, isResultMessage, isContentDelta } from "../types/claude-cli.js";
+const DEFAULT_TIMEOUT = 86400000; // 24h — lange Agent-Loops (Pentests etc.)
+export class ClaudeSubprocess extends EventEmitter {
+ process = null;
+ buffer = "";
+ timeoutId = null;
+ isKilled = false;
+ _stdinClosed = false;
+ /**
+ * Start the Claude CLI subprocess with the given prompt
+ */
+ async start(prompt, options) {
+ const args = this.buildArgs(prompt, options);
+ const timeout = options.timeout || DEFAULT_TIMEOUT;
+ return new Promise((resolve, reject) => {
+ try {
+ // Use spawn() for security - no shell interpretation
+ this.process = spawn("claude", args, {
+ cwd: options.cwd || process.cwd(),
+ env: { ...process.env },
+ stdio: ["pipe", "pipe", "pipe"],
+ });
+ // Set timeout
+ this.timeoutId = setTimeout(() => {
+ if (!this.isKilled) {
+ this.isKilled = true;
+ this.process?.kill("SIGTERM");
+ this.emit("error", new Error(`Request timed out after ${timeout}ms`));
+ }
+ }, timeout);
+ // Handle spawn errors (e.g., claude not found)
+ this.process.on("error", (err) => {
+ this.clearTimeout();
+ if (err.message.includes("ENOENT")) {
+ reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
+ }
+ else {
+ reject(err);
+ }
+ });
+ // stdin BLEIBT OFFEN: initialen Prompt als stream-json User-
+ // Message schreiben; spaetere Zwischenrufe kommen via
+ // sendMessage(). Geschlossen wird bei 'result' (s. processBuffer).
+ this._writeUserMessage(prompt);
+ // Falls stdin (z.B. EPIPE) frueh stirbt: nicht crashen.
+ this.process.stdin?.on("error", () => {});
+ console.error(`[Subprocess] Process spawned with PID: ${this.process.pid}`);
+ // Parse JSON stream from stdout
+ this.process.stdout?.on("data", (chunk) => {
+ const data = chunk.toString();
+ console.error(`[Subprocess] Received ${data.length} bytes of stdout`);
+ this.buffer += data;
+ this.processBuffer();
+ });
+ // Capture stderr for debugging
+ this.process.stderr?.on("data", (chunk) => {
+ const errorText = chunk.toString().trim();
+ if (errorText) {
+ // Don't emit as error unless it's actually an error
+ // Claude CLI may write debug info to stderr
+ console.error("[Subprocess stderr]:", errorText.slice(0, 200));
+ }
+ });
+ // Handle process close
+ this.process.on("close", (code) => {
+ console.error(`[Subprocess] Process closed with code: ${code}`);
+ this.clearTimeout();
+ // Process any remaining buffer
+ if (this.buffer.trim()) {
+ this.processBuffer();
+ }
+ this.emit("close", code);
+ });
+ // Resolve immediately since we're streaming
+ resolve();
+ }
+ catch (err) {
+ this.clearTimeout();
+ reject(err);
+ }
+ });
+ }
+ /**
+ * Build CLI arguments array
+ */
+ buildArgs(prompt, options) {
+ const args = [
+ "--print", // Non-interactive mode
+ "--output-format",
+ "stream-json", // JSON streaming output
+ "--verbose", // Required for stream-json
+ "--include-partial-messages", // Enable streaming chunks
+ "--input-format",
+ "stream-json", // ARIA: User-Messages via stdin (Zwischenruf)
+ "--model",
+ options.model, // Model alias (opus/sonnet/haiku)
+ "--no-session-persistence", "--dangerously-skip-permissions", "--system-prompt", options.systemPrompt, "--safe-mode",
+ ];
+ if (options.sessionId) {
+ args.push("--session-id", options.sessionId);
+ }
+ return args;
+ }
+ /**
+ * Eine User-Message im stream-json-Input-Format an stdin schreiben.
+ * Genutzt fuer den initialen Prompt UND fuer Zwischenrufe (sendMessage).
+ */
+ _writeUserMessage(text) {
+ const p = this.process;
+ if (!p || !p.stdin || p.stdin.destroyed || this._stdinClosed)
+ return false;
+ try {
+ p.stdin.write(JSON.stringify({ type: "user", message: { role: "user", content: String(text) } }) + "\n");
+ return true;
+ }
+ catch (_) {
+ return false;
+ }
+ }
+ /**
+ * Zwischenruf: waehrend eines laufenden Turns eine weitere User-Message
+ * reinschieben. claude greift sie an der naechsten Tool-Grenze auf, ohne
+ * den Turn abzubrechen. Kein Effekt, wenn stdin schon geschlossen ist
+ * (Turn praktisch fertig) — dann ist der Zwischenruf schlicht zu spaet.
+ */
+ sendMessage(text) {
+ return this._writeUserMessage(text);
+ }
+ /**
+ * stdin schliessen → claude beendet den stream-json-Input und exit't.
+ */
+ _closeStdin() {
+ if (this._stdinClosed)
+ return;
+ this._stdinClosed = true;
+ try {
+ this.process?.stdin?.end();
+ }
+ catch (_) { }
+ }
+ /**
+ * Process the buffer and emit parsed messages
+ */
+ processBuffer() {
+ const lines = this.buffer.split("\n");
+ this.buffer = lines.pop() || ""; // Keep incomplete line
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed)
+ continue;
+ try {
+ const message = JSON.parse(trimmed);
+ this.emit("message", message);
+ if (isContentDelta(message)) {
+ // Emit content delta for streaming
+ this.emit("content_delta", message);
+ }
+ else if (isAssistantMessage(message)) {
+ this.emit("assistant", message);
+ }
+ else if (isResultMessage(message)) {
+ this.emit("result", message);
+ // Turn fertig → stdin schliessen, sonst wartet claude im
+ // stream-json-Input auf weitere Messages und der Prozess
+ // (und damit die HTTP-Response) haengt fuer immer.
+ this._closeStdin();
+ }
+ }
+ catch {
+ // Non-JSON output, emit as raw
+ this.emit("raw", trimmed);
+ }
+ }
+ }
+ /**
+ * Clear the timeout timer
+ */
+ clearTimeout() {
+ if (this.timeoutId) {
+ clearTimeout(this.timeoutId);
+ this.timeoutId = null;
+ }
+ }
+ /**
+ * Kill the subprocess
+ */
+ kill(signal = "SIGTERM") {
+ if (!this.isKilled && this.process) {
+ this.isKilled = true;
+ this.clearTimeout();
+ this.process.kill(signal);
+ }
+ }
+ /**
+ * Check if the process is still running
+ */
+ isRunning() {
+ return this.process !== null && !this.isKilled && this.process.exitCode === null;
+ }
+}
+/**
+ * Verify that Claude CLI is installed and accessible
+ */
+export async function verifyClaude() {
+ return new Promise((resolve) => {
+ const proc = spawn("claude", ["--version"], { stdio: "pipe" });
+ let output = "";
+ proc.stdout?.on("data", (chunk) => {
+ output += chunk.toString();
+ });
+ proc.on("error", () => {
+ resolve({
+ ok: false,
+ error: "Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code",
+ });
+ });
+ proc.on("close", (code) => {
+ if (code === 0) {
+ resolve({ ok: true, version: output.trim() });
+ }
+ else {
+ resolve({
+ ok: false,
+ error: "Claude CLI returned non-zero exit code",
+ });
+ }
+ });
+ });
+}
+/**
+ * Check if Claude CLI is authenticated
+ */
+export async function verifyAuth() {
+ return { ok: true };
+}
+//# sourceMappingURL=manager.js.map
diff --git a/proxy-patches/routes.js b/proxy-patches/routes.js
index 49bcb35..8aabc67 100644
--- a/proxy-patches/routes.js
+++ b/proxy-patches/routes.js
@@ -621,6 +621,28 @@ function _cancelByProject(projectId) {
return { killed, requestIds: ids, projectId: pid };
}
+// Zwischenruf: schiebt eine User-Message in den/die laufenden Subprozess(e)
+// eines Kontexts, OHNE sie zu killen. claude greift sie an der naechsten Tool-
+// Grenze auf (stream-json-Input, s. manager.js). Kein Treffer / stdin schon
+// zu (Turn quasi fertig) → delivered=0.
+function _interjectByProject(projectId, text) {
+ const pid = String(projectId || "");
+ const ids = [];
+ let delivered = 0;
+ for (const [id, entry] of Array.from(_activeSubprocesses)) {
+ if (entry.projectId !== pid) continue;
+ try {
+ if (typeof entry.subprocess.sendMessage === "function" && entry.subprocess.sendMessage(text)) {
+ delivered++;
+ ids.push(id);
+ }
+ } catch (e) {
+ console.error("[aria-interject] sendMessage failed for", id, e?.message);
+ }
+ }
+ return { delivered, requestIds: ids, projectId: pid };
+}
+
try {
const internalServer = http.createServer((req, res) => {
if (req.method === "POST" && req.url === "/cancel-all") {
@@ -645,6 +667,20 @@ try {
});
return;
}
+ if (req.method === "POST" && req.url === "/interject") {
+ // Body: {projectId, text}. Zwischenruf in den laufenden Turn.
+ let raw = "";
+ req.on("data", (c) => { raw += c; if (raw.length > 65536) req.destroy(); });
+ req.on("end", () => {
+ let projectId = "", text = "";
+ try { const b = JSON.parse(raw || "{}"); projectId = String(b.projectId || ""); text = String(b.text || ""); } catch (_) {}
+ const result = text ? _interjectByProject(projectId, text) : { delivered: 0, requestIds: [], projectId };
+ console.warn("[aria-interject] /interject project=%s — delivered %d", projectId || "(main)", result.delivered);
+ res.writeHead(200, { "Content-Type": "application/json" });
+ res.end(JSON.stringify({ ok: true, ...result }));
+ });
+ return;
+ }
if (req.method === "GET" && req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, active: _activeSubprocesses.size }));
diff --git a/rvs/server.js b/rvs/server.js
index be25f0e..d42023d 100644
--- a/rvs/server.js
+++ b/rvs/server.js
@@ -17,7 +17,7 @@ const ALLOWED_TYPES = new Set([
"file_request", "file_response", "file_saved", "stt_result", "config", "tts_request",
"xtts_request", "xtts_response", "xtts_list_voices", "xtts_voices_list", "voice_upload", "xtts_voice_saved",
"update_check", "update_available", "update_download", "update_data",
- "agent_activity", "cancel_request",
+ "agent_activity", "cancel_request", "interject",
"audio_pcm",
"file_from_aria",
"container_restart",