feat(multitask): kontext-getaggte Activity + kontext-scoped Cancel (Restpunkte 1+2)
Beide Restpunkte teilen eine Verkabelung: die projectId fliesst jetzt bis zum
Proxy und zurueck in die agent_activity-Events.
Restpunkt 1 — per-Kontext Activity-Indikator:
- Brain: proxy_client.chat_full(project_id) → payload.aria_project_id;
agent.py gibt active_project_id mit.
- Proxy routes.js: liest aria_project_id, taggt Tool-/Stream-Hooks damit,
trackt Subprozesse pro Kontext.
- Bridge: _emit_activity(project_id) + payload.projectId; /internal/agent-activity
reicht projectId durch; send_to_core/_process_core_response taggen thinking/idle
mit dem Turn-Kontext.
- App: agentActivityByCtx-Map; „ARIA denkt"-Indikator zeigt nur den
fokussierten Kontext statt global zu flackern.
Restpunkt 2 — kontext-scoped Cancel (Barge-In):
- Proxy: neuer /cancel {projectId} killt NUR die Subprozesse eines Kontexts
(_cancelByProject); /cancel-all bleibt fuers NOT-AUS.
- Bridge: soft cancel_request → _cancel_proxy_for_project(projectId) statt des
toten Diagnostic /api/cancel. Hard bleibt /cancel-all.
- App: cancel_request + Abbrechen-Button tragen die fokussierte projectId.
Damit laufen Kontexte in der App echt parallel: Arbeit in Projekt A wird durch
Senden/Abbrechen in Hauptchat oder Projekt B nicht mehr abgewuergt, und der
Indikator gehoert zum sichtbaren Chat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+54
-12
@@ -70,9 +70,9 @@ function _postJson(url, body) {
|
||||
/**
|
||||
* Pusht einen Tool-Use-Event an die Bridge (alter Gedanken-Stream-Pfad).
|
||||
*/
|
||||
function _emitToolEvent(toolName) {
|
||||
function _emitToolEvent(toolName, projectId) {
|
||||
if (!toolName) return;
|
||||
_postJson(TOOL_HOOK_URL, { tool: String(toolName) });
|
||||
_postJson(TOOL_HOOK_URL, { tool: String(toolName), projectId: projectId || "" });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,9 +92,11 @@ function _truncate(str, 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.
|
||||
// Wert: { subprocess, projectId }. projectId erlaubt kontext-scoped Cancel
|
||||
// (nur die Subprozesse EINES Projekts killen statt aller).
|
||||
const _activeSubprocesses = new Map();
|
||||
function _trackSubprocess(requestId, subprocess) {
|
||||
_activeSubprocesses.set(requestId, subprocess);
|
||||
function _trackSubprocess(requestId, subprocess, projectId) {
|
||||
_activeSubprocesses.set(requestId, { subprocess, projectId: projectId || "" });
|
||||
const cleanup = () => _activeSubprocesses.delete(requestId);
|
||||
subprocess.on("close", cleanup);
|
||||
subprocess.on("error", cleanup);
|
||||
@@ -149,24 +151,25 @@ function _attachIdleWatchdog(subprocess, requestId) {
|
||||
* - 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) {
|
||||
function _attachToolHook(subprocess, requestId, projectId) {
|
||||
subprocess.on("assistant", (message) => {
|
||||
try {
|
||||
const blocks = message?.message?.content || [];
|
||||
for (const b of blocks) {
|
||||
if (!b) continue;
|
||||
if (b.type === "tool_use") {
|
||||
if (b.name) _emitToolEvent(b.name);
|
||||
if (b.name) _emitToolEvent(b.name, projectId);
|
||||
const inputStr = b.input ? JSON.stringify(b.input) : "";
|
||||
const inp = _truncate(inputStr, TOOL_INPUT_MAX_CHARS);
|
||||
_emitStreamEvent(requestId, "tool_use", {
|
||||
projectId: projectId || "",
|
||||
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 });
|
||||
_emitStreamEvent(requestId, "text", { projectId: projectId || "", 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.
|
||||
@@ -227,15 +230,18 @@ export async function handleChatCompletions(req, res) {
|
||||
}
|
||||
// Convert to CLI input format
|
||||
const cliInput = openaiToCli(body);
|
||||
// ARIA: Projekt-Kontext (vom Brain via aria_project_id). Fuer
|
||||
// kontext-getaggte Activity-/Stream-Events + kontext-scoped Cancel.
|
||||
const ariaProjectId = String(body.aria_project_id || "");
|
||||
const subprocess = new ClaudeSubprocess();
|
||||
// ARIA-Patch: Tool-Use-Events + voller Live-Stream an die Bridge.
|
||||
// Plus: Subprocess fuer Not-Aus tracken (Hard-Kill via /v1/cancel-all).
|
||||
// Plus: Idle-Watchdog — Subprocess darf ewig laufen solange Events
|
||||
// kommen, wird aber gekillt nach IDLE_TIMEOUT_MS Inaktivitaet.
|
||||
_attachToolHook(subprocess, requestId);
|
||||
_trackSubprocess(requestId, subprocess);
|
||||
_attachToolHook(subprocess, requestId, ariaProjectId);
|
||||
_trackSubprocess(requestId, subprocess, ariaProjectId);
|
||||
_attachIdleWatchdog(subprocess, requestId);
|
||||
_emitStreamEvent(requestId, "start", { model: body.model || null });
|
||||
_emitStreamEvent(requestId, "start", { model: body.model || null, projectId: ariaProjectId });
|
||||
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) }));
|
||||
@@ -497,9 +503,9 @@ const INTERNAL_HOST = "0.0.0.0"; // im aria-net erreichbar, nicht nach extern e
|
||||
function _cancelAll() {
|
||||
const ids = Array.from(_activeSubprocesses.keys());
|
||||
let killed = 0;
|
||||
for (const [id, subp] of _activeSubprocesses) {
|
||||
for (const [id, entry] of _activeSubprocesses) {
|
||||
try {
|
||||
subp.kill();
|
||||
entry.subprocess.kill();
|
||||
killed++;
|
||||
} catch (e) {
|
||||
console.error("[aria-not-aus] kill failed for", id, e?.message);
|
||||
@@ -509,6 +515,27 @@ function _cancelAll() {
|
||||
return { killed, requestIds: ids };
|
||||
}
|
||||
|
||||
// Kontext-scoped Cancel: killt NUR die Subprozesse eines Projekts (leer =
|
||||
// Hauptchat). Fuer Barge-In in einem Kontext ohne die parallele Arbeit in
|
||||
// anderen Kontexten abzuwuergen.
|
||||
function _cancelByProject(projectId) {
|
||||
const pid = String(projectId || "");
|
||||
const ids = [];
|
||||
let killed = 0;
|
||||
for (const [id, entry] of Array.from(_activeSubprocesses)) {
|
||||
if (entry.projectId !== pid) continue;
|
||||
ids.push(id);
|
||||
try {
|
||||
entry.subprocess.kill();
|
||||
killed++;
|
||||
} catch (e) {
|
||||
console.error("[aria-cancel] kill failed for", id, e?.message);
|
||||
}
|
||||
_activeSubprocesses.delete(id);
|
||||
}
|
||||
return { killed, requestIds: ids, projectId: pid };
|
||||
}
|
||||
|
||||
try {
|
||||
const internalServer = http.createServer((req, res) => {
|
||||
if (req.method === "POST" && req.url === "/cancel-all") {
|
||||
@@ -518,6 +545,21 @@ try {
|
||||
res.end(JSON.stringify({ ok: true, ...result }));
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST" && req.url === "/cancel") {
|
||||
// Body: {projectId}. Kontext-scoped Barge-In — killt nur die
|
||||
// Subprozesse dieses Kontexts (leer = Hauptchat).
|
||||
let raw = "";
|
||||
req.on("data", (c) => { raw += c; if (raw.length > 4096) req.destroy(); });
|
||||
req.on("end", () => {
|
||||
let projectId = "";
|
||||
try { projectId = String((JSON.parse(raw || "{}")).projectId || ""); } catch (_) {}
|
||||
const result = _cancelByProject(projectId);
|
||||
console.warn("[aria-cancel] /cancel project=%s — killed %d", projectId || "(main)", result.killed);
|
||||
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 }));
|
||||
|
||||
Reference in New Issue
Block a user