feat(diagnostic): ARIA-Live (read-only Terminal-Mirror) + Not-Aus statt SSH-Tab
SSH-Tab raus — funktionierte eh nicht zuverlaessig und war konzeptionell falsch. Stattdessen Live-Mirror der Claude-Code-Session: - proxy-patches/routes.js: assistant + user Events parsed → POSTed Tool- Inputs (truncated 2 KB) + Tool-Results (truncated 4 KB) + Assistant-Text an aria-bridge:8090/internal/agent-stream. start/end Marker pro Session. Subprocess-Tracking (_activeSubprocesses Map) + interner Side-Channel auf Port 3457 mit POST /cancel-all fuer Hard-Kill. - bridge: neuer /internal/agent-stream Endpoint pusht 1:1 als RVS agent_stream. cancel_request Handler nimmt optional 'hard'-Flag — triggert dann zusaetzlich _cancel_proxy_subprocesses() das den Proxy- Side-Channel ruft. - rvs: agent_stream whitelisted. - diagnostic: SSH-Tab → 'ARIA Live'. Monospace-Stream, farbcodiert (text=hell, tool_use=cyan, tool_result=gruen/rot, thinking=gelb-italic), Auto-Scroll, max 2000 Zeilen Backlog. Roter ⛔ Not-Aus-Button mit Confirm → aria_panic_stop action → diagnostic-server broadcastet cancel_request mit hard:true. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+155
-16
@@ -7,6 +7,10 @@
|
||||
* (ARIA_TOOL_HOOK_URL, default http://aria-bridge:8090/internal/agent-activity).
|
||||
* Bridge spiegelt das als RVS `agent_activity` an App+Diagnostic →
|
||||
* Gedanken-Stream zeigt live was ARIA gerade tool-maessig macht.
|
||||
* - Voller Live-Stream (assistant_text, tool_use mit input, tool_result)
|
||||
* geht an ARIA_STREAM_HOOK_URL → Bridge → RVS `agent_stream` → Diagnostic
|
||||
* "ARIA Live"-View (TeamViewer-mäßiger Mirror der Claude-Code-Session).
|
||||
* - Subprocess-Tracking + POST /v1/cancel-all fuer Not-Aus (Hard-Kill).
|
||||
* - Fire-and-forget, fail-open. Wenn die Bridge nicht antwortet, bricht
|
||||
* der Brain-Call NICHT ab.
|
||||
*
|
||||
@@ -21,42 +25,121 @@ import { cliResultToOpenai, createDoneChunk, } from "../adapter/cli-to-openai.js
|
||||
|
||||
const TOOL_HOOK_URL = process.env.ARIA_TOOL_HOOK_URL
|
||||
|| "http://aria-bridge:8090/internal/agent-activity";
|
||||
const STREAM_HOOK_URL = process.env.ARIA_STREAM_HOOK_URL
|
||||
|| "http://aria-bridge:8090/internal/agent-stream";
|
||||
|
||||
// Tool-Output kann sehr lang werden (git log -p, find /). Wir truncaten
|
||||
// hart auf 4 KB pro Event — der User sieht weiterhin den Anfang und einen
|
||||
// "...(N bytes truncated)" Hinweis. Vollstaendiger Output bleibt im Brain
|
||||
// und wird normal verarbeitet, das hier ist NUR fuer den Live-Mirror.
|
||||
const TOOL_RESULT_MAX_CHARS = 4096;
|
||||
const TOOL_INPUT_MAX_CHARS = 2048;
|
||||
|
||||
/**
|
||||
* Pusht einen Tool-Use-Event an die Bridge. Fire-and-forget — keine Awaits,
|
||||
* keine Fehler nach oben. Logged Fehler still.
|
||||
* Generic Fire-and-forget POST an die Bridge. Keine Awaits, keine Fehler
|
||||
* nach oben. Eingesetzt fuer Tool-Hook + Stream-Hook.
|
||||
*/
|
||||
function _emitToolEvent(toolName) {
|
||||
if (!toolName) return;
|
||||
function _postJson(url, body) {
|
||||
try {
|
||||
const u = new URL(TOOL_HOOK_URL);
|
||||
const body = JSON.stringify({ tool: String(toolName) });
|
||||
const u = new URL(url);
|
||||
const data = JSON.stringify(body);
|
||||
const req = http.request({
|
||||
method: "POST",
|
||||
hostname: u.hostname,
|
||||
port: u.port || 80,
|
||||
path: u.pathname,
|
||||
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
||||
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) },
|
||||
timeout: 2000,
|
||||
}, (res) => { res.resume(); });
|
||||
req.on("error", () => {});
|
||||
req.on("timeout", () => req.destroy());
|
||||
req.write(body);
|
||||
req.write(data);
|
||||
req.end();
|
||||
} catch (_) { /* niemals weiterwerfen */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hookt die `assistant`-Events des Subprozesses. Jedes assistant-Message
|
||||
* kann mehrere content-Bloecke haben — tool_use-Bloecke pushen wir live.
|
||||
* Pusht einen Tool-Use-Event an die Bridge (alter Gedanken-Stream-Pfad).
|
||||
*/
|
||||
function _attachToolHook(subprocess) {
|
||||
function _emitToolEvent(toolName) {
|
||||
if (!toolName) return;
|
||||
_postJson(TOOL_HOOK_URL, { tool: String(toolName) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Pusht ein Stream-Event an die Bridge (neuer "ARIA Live"-Pfad).
|
||||
* kind: "start" | "text" | "tool_use" | "tool_result" | "end"
|
||||
*/
|
||||
function _emitStreamEvent(requestId, kind, fields) {
|
||||
_postJson(STREAM_HOOK_URL, { requestId, kind, ts: Date.now(), ...fields });
|
||||
}
|
||||
|
||||
function _truncate(str, max) {
|
||||
if (typeof str !== "string") str = String(str ?? "");
|
||||
if (str.length <= max) return { text: str, truncatedBytes: 0 };
|
||||
return { text: str.slice(0, max), truncatedBytes: str.length - max };
|
||||
}
|
||||
|
||||
// ── Subprocess-Tracking fuer Not-Aus ──────────────────────────
|
||||
// requestId → ClaudeSubprocess. Eintraege werden beim close/result-Event
|
||||
// wieder entfernt. /v1/cancel-all iteriert und ruft .kill() auf jeden.
|
||||
const _activeSubprocesses = new Map();
|
||||
function _trackSubprocess(requestId, subprocess) {
|
||||
_activeSubprocesses.set(requestId, subprocess);
|
||||
const cleanup = () => _activeSubprocesses.delete(requestId);
|
||||
subprocess.on("close", cleanup);
|
||||
subprocess.on("error", cleanup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hookt assistant + user Events und pusht beides an Bridge:
|
||||
* - Alt-API: nur Tool-Namen an /internal/agent-activity (Gedanken-Stream)
|
||||
* - Neu-API: voller Stream (text/tool_use/tool_result) an /internal/agent-stream
|
||||
*/
|
||||
function _attachToolHook(subprocess, requestId) {
|
||||
subprocess.on("assistant", (message) => {
|
||||
try {
|
||||
const blocks = message?.message?.content || [];
|
||||
for (const b of blocks) {
|
||||
if (b && b.type === "tool_use" && b.name) {
|
||||
_emitToolEvent(b.name);
|
||||
if (!b) continue;
|
||||
if (b.type === "tool_use") {
|
||||
if (b.name) _emitToolEvent(b.name);
|
||||
const inputStr = b.input ? JSON.stringify(b.input) : "";
|
||||
const inp = _truncate(inputStr, TOOL_INPUT_MAX_CHARS);
|
||||
_emitStreamEvent(requestId, "tool_use", {
|
||||
id: b.id || null,
|
||||
name: b.name || "",
|
||||
input: inp.text,
|
||||
inputTruncatedBytes: inp.truncatedBytes,
|
||||
});
|
||||
} else if (b.type === "text" && b.text) {
|
||||
_emitStreamEvent(requestId, "text", { text: b.text });
|
||||
} else if (b.type === "thinking" && b.thinking) {
|
||||
// Wenn das Modell Extended Thinking emittiert — selten in
|
||||
// Claude Code CLI, aber moeglich. Markieren wir extra.
|
||||
_emitStreamEvent(requestId, "thinking", { text: b.thinking });
|
||||
}
|
||||
}
|
||||
} catch (_) { /* fail-open */ }
|
||||
});
|
||||
// user-Events enthalten tool_result-Blocks
|
||||
subprocess.on("user", (message) => {
|
||||
try {
|
||||
const blocks = message?.message?.content || [];
|
||||
for (const b of blocks) {
|
||||
if (b && b.type === "tool_result") {
|
||||
let content = "";
|
||||
if (typeof b.content === "string") content = b.content;
|
||||
else if (Array.isArray(b.content)) {
|
||||
content = b.content.map(c => (c && c.type === "text" && c.text) ? c.text : "").join("");
|
||||
}
|
||||
const out = _truncate(content, TOOL_RESULT_MAX_CHARS);
|
||||
_emitStreamEvent(requestId, "tool_result", {
|
||||
id: b.tool_use_id || null,
|
||||
content: out.text,
|
||||
truncatedBytes: out.truncatedBytes,
|
||||
isError: b.is_error === true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (_) { /* fail-open */ }
|
||||
@@ -86,9 +169,14 @@ export async function handleChatCompletions(req, res) {
|
||||
// Convert to CLI input format
|
||||
const cliInput = openaiToCli(body);
|
||||
const subprocess = new ClaudeSubprocess();
|
||||
// ARIA-Patch: Tool-Use-Events live an die Bridge weiterleiten.
|
||||
// Greift fuer beide Branches (stream + non-stream).
|
||||
_attachToolHook(subprocess);
|
||||
// ARIA-Patch: Tool-Use-Events + voller Live-Stream an die Bridge.
|
||||
// Plus: Subprocess fuer Not-Aus tracken (Hard-Kill via /v1/cancel-all).
|
||||
_attachToolHook(subprocess, requestId);
|
||||
_trackSubprocess(requestId, subprocess);
|
||||
_emitStreamEvent(requestId, "start", { model: body.model || null });
|
||||
subprocess.on("result", () => _emitStreamEvent(requestId, "end", { reason: "result" }));
|
||||
subprocess.on("close", (code) => _emitStreamEvent(requestId, "end", { reason: "close", code }));
|
||||
subprocess.on("error", (err) => _emitStreamEvent(requestId, "end", { reason: "error", error: String(err?.message || err) }));
|
||||
if (stream) {
|
||||
await handleStreamingResponse(req, res, subprocess, cliInput, requestId);
|
||||
}
|
||||
@@ -306,4 +394,55 @@ export function handleHealth(_req, res) {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// ── Not-Aus Side-Channel ───────────────────────────────────
|
||||
//
|
||||
// claude-max-api-proxy steuert seine eigene Route-Registrierung — wir
|
||||
// koennen da nicht reinpatchen ohne sed-Operationen am npm-Paket. Saubrer:
|
||||
// ein dedizierter kleiner HTTP-Listener nur fuer den Not-Aus, auf einem
|
||||
// internen Port im aria-net. Bridge ruft den, killt alle aktiven Claude-
|
||||
// Subprocesses. App + Diagnostic sehen den Stream sofort enden.
|
||||
const INTERNAL_PORT = parseInt(process.env.ARIA_PROXY_INTERNAL_PORT || "3457", 10);
|
||||
const INTERNAL_HOST = "0.0.0.0"; // im aria-net erreichbar, nicht nach extern exposed
|
||||
|
||||
function _cancelAll() {
|
||||
const ids = Array.from(_activeSubprocesses.keys());
|
||||
let killed = 0;
|
||||
for (const [id, subp] of _activeSubprocesses) {
|
||||
try {
|
||||
subp.kill();
|
||||
killed++;
|
||||
} catch (e) {
|
||||
console.error("[aria-not-aus] kill failed for", id, e?.message);
|
||||
}
|
||||
}
|
||||
_activeSubprocesses.clear();
|
||||
return { killed, requestIds: ids };
|
||||
}
|
||||
|
||||
try {
|
||||
const internalServer = http.createServer((req, res) => {
|
||||
if (req.method === "POST" && req.url === "/cancel-all") {
|
||||
const result = _cancelAll();
|
||||
console.warn("[aria-not-aus] /cancel-all — killed", result.killed, "subprocess(es)");
|
||||
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 }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(404).end();
|
||||
});
|
||||
internalServer.on("error", (err) => {
|
||||
console.error("[aria-not-aus] internal listener error:", err.message);
|
||||
});
|
||||
internalServer.listen(INTERNAL_PORT, INTERNAL_HOST, () => {
|
||||
console.log("[aria-not-aus] internal listener on", INTERNAL_HOST + ":" + INTERNAL_PORT);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[aria-not-aus] startup failed:", e?.message);
|
||||
}
|
||||
//# sourceMappingURL=routes.js.map
|
||||
Reference in New Issue
Block a user