fix(proxy): ein Bericht mitten im Turn geht nicht mehr verloren
Claude Code liefert in result.result nur den LETZTEN Textblock eines Turns.
Am 28.10. hat ARIA in einem 2-Stunden-Turn ihren Bericht geschrieben, danach
noch auf Laeufe gewartet und mit einer kurzen Notiz geendet — beim Brain kam
nur die Notiz an ("…steht im Bericht oben"), der Bericht war weg.
- Nicht-Streaming: alle Textbloecke des Turns sammeln; laengere fruehere
(>= 300 Zeichen, z.B. Berichte) kommen vor die letzte Antwort. Kurze
Zwischensaetze bleiben weg, <voice> nur aus der letzten Antwort.
- Belegprotokoll speichert jetzt auch Texte (geschwaerzt, max. 20k je
Block), damit so etwas wiederherstellbar ist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -99,7 +99,20 @@ function _parseToolCalls(text) {
|
|||||||
return { tool_calls: tcs, rest: restParts.join("").trim() };
|
return { tool_calls: tcs, rest: restParts.join("").trim() };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cliResultToOpenai(result, requestId) {
|
// Laengere Texte, die Claude im Turn VOR der letzten Antwort geschrieben hat
|
||||||
|
// (z.B. ein Bericht, danach noch Warten/Werkzeuge, dann eine kurze
|
||||||
|
// Schlussnotiz). result.result enthaelt nur den LETZTEN Textblock — der
|
||||||
|
// Bericht ging so verloren (28.10., 2-Stunden-Turn). Kurze Zwischensaetze
|
||||||
|
// ("Ich schaue nach …") bleiben weg, Sprachausgabe nur aus der letzten Antwort.
|
||||||
|
const EARLIER_TEXT_MIN = 300;
|
||||||
|
function _mergeEarlier(finalText, earlier) {
|
||||||
|
const keep = (earlier || [])
|
||||||
|
.map(t => String(t || "").replace(/<voice>[\s\S]*?<\/voice>/g, "").replace(/\[\[[A-Z_]+\]\]/g, "").trim())
|
||||||
|
.filter(t => t.length >= EARLIER_TEXT_MIN && !finalText.includes(t));
|
||||||
|
return keep.length ? keep.join("\n\n") + "\n\n" + finalText : finalText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cliResultToOpenai(result, requestId, earlierTexts = []) {
|
||||||
// modelUsage ist die Wahrheit: die CLI tragt dort den Namen ein, unter dem
|
// modelUsage ist die Wahrheit: die CLI tragt dort den Namen ein, unter dem
|
||||||
// der Request wirklich lief — bei einem Alias also die aufgeloeste Version
|
// der Request wirklich lief — bei einem Alias also die aufgeloeste Version
|
||||||
// ("opus" -> "claude-opus-5-5"). Leer bleibt es, wenn gar kein Call
|
// ("opus" -> "claude-opus-5-5"). Leer bleibt es, wenn gar kein Call
|
||||||
@@ -121,7 +134,7 @@ export function cliResultToOpenai(result, requestId) {
|
|||||||
message.content = rest || null;
|
message.content = rest || null;
|
||||||
finishReason = "tool_calls";
|
finishReason = "tool_calls";
|
||||||
} else {
|
} else {
|
||||||
message.content = rawText;
|
message.content = _mergeEarlier(rawText, earlierTexts);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+16
-3
@@ -130,7 +130,7 @@ function _emitStreamEvent(requestId, kind, fields) {
|
|||||||
// zugehoerigen Aufrufe ("Belege"). Nur start/tool_use/tool_result/end —
|
// zugehoerigen Aufrufe ("Belege"). Nur start/tool_use/tool_result/end —
|
||||||
// keine Text-Deltas. Fail-open: ein Schreibfehler stoert ARIA nie.
|
// keine Text-Deltas. Fail-open: ein Schreibfehler stoert ARIA nie.
|
||||||
const TOOL_LOG_DIR = process.env.ARIA_TOOL_LOG_DIR || "/shared/logs/aria-tools";
|
const TOOL_LOG_DIR = process.env.ARIA_TOOL_LOG_DIR || "/shared/logs/aria-tools";
|
||||||
const TOOL_LOG_KINDS = new Set(["start", "tool_use", "tool_result", "end"]);
|
const TOOL_LOG_KINDS = new Set(["start", "tool_use", "tool_result", "text", "end"]);
|
||||||
const TOOL_LOG_KEEP_DAYS = 30;
|
const TOOL_LOG_KEEP_DAYS = 30;
|
||||||
let _toolLogCleaned = "";
|
let _toolLogCleaned = "";
|
||||||
// Offensichtliche Geheimnisse nicht dauerhaft ablegen (Passwoerter, Tokens,
|
// Offensichtliche Geheimnisse nicht dauerhaft ablegen (Passwoerter, Tokens,
|
||||||
@@ -149,7 +149,8 @@ function _redact(v) {
|
|||||||
function _persistToolEvent(ev) {
|
function _persistToolEvent(ev) {
|
||||||
if (!TOOL_LOG_KINDS.has(ev.kind)) return;
|
if (!TOOL_LOG_KINDS.has(ev.kind)) return;
|
||||||
try {
|
try {
|
||||||
ev = { ...ev, input: _redact(ev.input), content: _redact(ev.content) };
|
ev = { ...ev, input: _redact(ev.input), content: _redact(ev.content),
|
||||||
|
text: typeof ev.text === "string" ? _redact(ev.text).slice(0, 20000) : ev.text };
|
||||||
const day = new Date(ev.ts).toISOString().slice(0, 10);
|
const day = new Date(ev.ts).toISOString().slice(0, 10);
|
||||||
fs.mkdirSync(TOOL_LOG_DIR, { recursive: true });
|
fs.mkdirSync(TOOL_LOG_DIR, { recursive: true });
|
||||||
fs.appendFile(`${TOOL_LOG_DIR}/${day}.jsonl`, JSON.stringify(ev) + "\n", () => {});
|
fs.appendFile(`${TOOL_LOG_DIR}/${day}.jsonl`, JSON.stringify(ev) + "\n", () => {});
|
||||||
@@ -486,6 +487,16 @@ async function handleNonStreamingResponse(res, subprocess, cliInput, requestId)
|
|||||||
subprocess.on("result", (result) => {
|
subprocess.on("result", (result) => {
|
||||||
finalResult = result;
|
finalResult = result;
|
||||||
});
|
});
|
||||||
|
// Alle Textbloecke des Turns sammeln — die letzte Antwort allein
|
||||||
|
// (result.result) verliert sonst einen frueher geschriebenen Bericht.
|
||||||
|
const turnTexts = [];
|
||||||
|
subprocess.on("assistant", (message) => {
|
||||||
|
try {
|
||||||
|
for (const b of message?.message?.content || []) {
|
||||||
|
if (b && b.type === "text" && b.text) turnTexts.push(b.text);
|
||||||
|
}
|
||||||
|
} catch (_) { }
|
||||||
|
});
|
||||||
subprocess.on("error", (error) => {
|
subprocess.on("error", (error) => {
|
||||||
console.error("[NonStreaming] Error:", error.message);
|
console.error("[NonStreaming] Error:", error.message);
|
||||||
isComplete = true;
|
isComplete = true;
|
||||||
@@ -508,7 +519,9 @@ async function handleNonStreamingResponse(res, subprocess, cliInput, requestId)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (finalResult) {
|
if (finalResult) {
|
||||||
res.json(cliResultToOpenai(finalResult, requestId));
|
const last = String(finalResult.result || "").trim();
|
||||||
|
const earlier = turnTexts.filter(t => t.trim() && t.trim() !== last);
|
||||||
|
res.json(cliResultToOpenai(finalResult, requestId, earlier));
|
||||||
}
|
}
|
||||||
else if (!res.headersSent) {
|
else if (!res.headersSent) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
|
|||||||
Reference in New Issue
Block a user