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:
@@ -2444,6 +2444,24 @@ const ChatScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [inputText, pendingAttachments, sendPendingAttachments, getCtxState, setCtxState, setCtxQueue, actuallySend]);
|
}, [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 ---
|
// --- Rendering ---
|
||||||
|
|
||||||
const renderMessage = ({ item }: { item: ChatMessage }) => {
|
const renderMessage = ({ item }: { item: ChatMessage }) => {
|
||||||
@@ -3218,9 +3236,19 @@ const ChatScreen: React.FC = () => {
|
|||||||
|
|
||||||
{/* Senden oder Sprache */}
|
{/* Senden oder Sprache */}
|
||||||
{inputText.trim() || pendingAttachments.length > 0 ? (
|
{inputText.trim() || pendingAttachments.length > 0 ? (
|
||||||
<TouchableOpacity style={styles.sendButton} onPress={sendTextMessage}>
|
<>
|
||||||
<Text style={styles.sendIcon}>{'\u2B06\uFE0F'}</Text>
|
{/* Zwischenruf: nur wenn ARIA im aktiven Kontext gerade arbeitet und
|
||||||
</TouchableOpacity>
|
Text da ist. Schiebt die Korrektur in den laufenden Turn statt
|
||||||
|
sie anzustellen. */}
|
||||||
|
{inputText.trim() && (agentActivityByCtx[focusedProjectId]?.activity || 'idle') !== 'idle' ? (
|
||||||
|
<TouchableOpacity style={styles.interjectButton} onPress={sendInterject} accessibilityLabel="Zwischenruf">
|
||||||
|
<Text style={styles.interjectIcon}>{'\uD83D\uDCE3'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : null}
|
||||||
|
<TouchableOpacity style={styles.sendButton} onPress={sendTextMessage}>
|
||||||
|
<Text style={styles.sendIcon}>{'\u2B06\uFE0F'}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<VoiceButton
|
<VoiceButton
|
||||||
@@ -3738,6 +3766,18 @@ const styles = StyleSheet.create({
|
|||||||
sendIcon: {
|
sendIcon: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
},
|
},
|
||||||
|
interjectButton: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: '#FF9500', // orange — Zwischenruf, klar vom blauen Senden getrennt
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 6,
|
||||||
|
},
|
||||||
|
interjectIcon: {
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
wakeWordBtn: {
|
wakeWordBtn: {
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
|
|||||||
@@ -2434,6 +2434,20 @@ class ARIABridge:
|
|||||||
await self._emit_activity("idle", "", project_id=cancel_pid)
|
await self._emit_activity("idle", "", project_id=cancel_pid)
|
||||||
return
|
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":
|
elif msg_type == "audio_pcm":
|
||||||
# Audio-PCM geht direkt von XTTS-Bridge an die App.
|
# Audio-PCM geht direkt von XTTS-Bridge an die App.
|
||||||
# Die aria-bridge darf es NICHT rebroadcasten — sonst bekommt 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",
|
logger.info("[cancel] proxy /cancel project=%s: %s %s",
|
||||||
project_id or "(main)", status, body)
|
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,
|
async def _emit_activity(self, activity: str, tool: str = "", force: bool = False,
|
||||||
project_id: str = "") -> None:
|
project_id: str = "") -> None:
|
||||||
"""Sendet agent_activity an die App — nur wenn sich der State geaendert hat.
|
"""Sendet agent_activity an die App — nur wenn sich der State geaendert hat.
|
||||||
|
|||||||
@@ -328,6 +328,7 @@
|
|||||||
<input type="file" id="diag-file-input" multiple accept="image/*,application/pdf,.doc,.docx,.txt" style="display:none;" onchange="handleDiagFileSelect(this.files)">
|
<input type="file" id="diag-file-input" multiple accept="image/*,application/pdf,.doc,.docx,.txt" style="display:none;" onchange="handleDiagFileSelect(this.files)">
|
||||||
</label>
|
</label>
|
||||||
<textarea id="chat-input" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" onpaste="handleDiagPaste(event)" oninput="autoResizeTextarea(this)"></textarea>
|
<textarea id="chat-input" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" onpaste="handleDiagPaste(event)" oninput="autoResizeTextarea(this)"></textarea>
|
||||||
|
<button class="btn secondary" onclick="interjectDiag('chat-input')" title="Zwischenruf — Korrektur in den laufenden Turn (kein Abbruch, keine Queue)" style="border-color:#FF9500;color:#FF9500;">📣</button>
|
||||||
<button class="btn" id="btn-rvs" onclick="testRVS()">Senden</button>
|
<button class="btn" id="btn-rvs" onclick="testRVS()">Senden</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -345,6 +346,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="input-row" style="margin-top:8px;">
|
<div class="input-row" style="margin-top:8px;">
|
||||||
<textarea id="chat-input-fs" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" oninput="autoResizeTextarea(this)"></textarea>
|
<textarea id="chat-input-fs" placeholder="Nachricht an ARIA... (Enter sendet, Shift+Enter neue Zeile)" rows="2" oninput="autoResizeTextarea(this)"></textarea>
|
||||||
|
<button class="btn secondary" onclick="interjectDiag('chat-input-fs')" title="Zwischenruf — Korrektur in den laufenden Turn (kein Abbruch, keine Queue)" style="border-color:#FF9500;color:#FF9500;">📣</button>
|
||||||
<button class="btn" onclick="testRVSFS()">Senden</button>
|
<button class="btn" onclick="testRVSFS()">Senden</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2244,6 +2246,18 @@
|
|||||||
diagCtxStates[pid] = 'running';
|
diagCtxStates[pid] = 'running';
|
||||||
renderDiagQueue();
|
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) {
|
function advanceDiagQueue(pid) {
|
||||||
const q = diagCtxQueues[pid] || [];
|
const q = diagCtxQueues[pid] || [];
|
||||||
if (q.length === 0) { diagCtxStates[pid] = 'idle'; renderDiagQueue(); return; }
|
if (q.length === 0) { diagCtxStates[pid] = 'idle'; renderDiagQueue(); return; }
|
||||||
|
|||||||
@@ -2455,6 +2455,14 @@ wss.on("connection", (ws) => {
|
|||||||
} else if (msg.action === "test_rvs") {
|
} else if (msg.action === "test_rvs") {
|
||||||
traceStart("RVS", msg.text || "aria lebst du noch?");
|
traceStart("RVS", msg.text || "aria lebst du noch?");
|
||||||
sendToRVS(msg.text || "aria lebst du noch?", true, msg.projectId || "");
|
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") {
|
} else if (msg.action === "reconnect_gateway") {
|
||||||
connectGateway();
|
connectGateway();
|
||||||
} else if (msg.action === "reconnect_rvs") {
|
} else if (msg.action === "reconnect_rvs") {
|
||||||
|
|||||||
+1
-5
@@ -11,11 +11,7 @@ services:
|
|||||||
npm install -g @anthropic-ai/claude-code claude-max-api-proxy &&
|
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) &&
|
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/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 &&
|
cp /proxy-patches/manager.js $$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/openai-to-cli.js $$DIST/adapter/openai-to-cli.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/cli-to-openai.js $$DIST/adapter/cli-to-openai.js &&
|
||||||
cp /proxy-patches/routes.js $$DIST/server/routes.js &&
|
cp /proxy-patches/routes.js $$DIST/server/routes.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
|
||||||
@@ -621,6 +621,28 @@ function _cancelByProject(projectId) {
|
|||||||
return { killed, requestIds: ids, projectId: pid };
|
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 {
|
try {
|
||||||
const internalServer = http.createServer((req, res) => {
|
const internalServer = http.createServer((req, res) => {
|
||||||
if (req.method === "POST" && req.url === "/cancel-all") {
|
if (req.method === "POST" && req.url === "/cancel-all") {
|
||||||
@@ -645,6 +667,20 @@ try {
|
|||||||
});
|
});
|
||||||
return;
|
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") {
|
if (req.method === "GET" && req.url === "/health") {
|
||||||
res.writeHead(200, { "Content-Type": "application/json" });
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
res.end(JSON.stringify({ ok: true, active: _activeSubprocesses.size }));
|
res.end(JSON.stringify({ ok: true, active: _activeSubprocesses.size }));
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ const ALLOWED_TYPES = new Set([
|
|||||||
"file_request", "file_response", "file_saved", "stt_result", "config", "tts_request",
|
"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",
|
"xtts_request", "xtts_response", "xtts_list_voices", "xtts_voices_list", "voice_upload", "xtts_voice_saved",
|
||||||
"update_check", "update_available", "update_download", "update_data",
|
"update_check", "update_available", "update_download", "update_data",
|
||||||
"agent_activity", "cancel_request",
|
"agent_activity", "cancel_request", "interject",
|
||||||
"audio_pcm",
|
"audio_pcm",
|
||||||
"file_from_aria",
|
"file_from_aria",
|
||||||
"container_restart",
|
"container_restart",
|
||||||
|
|||||||
Reference in New Issue
Block a user