AUDIT / src /lib /agentSSEDispatch.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
27.4 kB
// agentSSEDispatch.ts — P3-5: dispatch SSE events estratto da agentSSE.ts
// handleEvent: switch su SSEEvent.event → aggiorna taskStore + emette EventRuntime.
// _maybeNotify: push notification se pagina in background.
import { useTaskStore, type TaskActionType, type PhaseBreakdown } from "@/store/taskStore";
import { eventRuntime } from "@/core/events";
import { onStepDone, onTaskEnd } from "@/lib/checkpointManager";
import { streamManager } from "@/lib/streamManager";
import { handleBuildFailure } from "@/lib/buildFailureHandler";
import { inferActionType, inferActionLabel, inferActionDetail } from "./sseActionInferer";
import type { SSEEvent } from "./agentSSETypes";
import {
_taskParams, _taskStarted, _connectAt,
_thinkingTimers, _taskTimers,
_streamingBuffer, _bufferTimestamps,
} from "./agentSSEState";
import { tgNotify } from "./agentTelegram";
import { toast } from "sonner"; // ABORT-TOAST
// [S67] Mostra notifica push se la pagina è nascosta e il permesso è concesso
function _maybeNotify(taskId: string): void {
try {
if (!document.hidden) return;
if (!("Notification" in window) || Notification.permission !== "granted") return;
navigator.serviceWorker.ready.then(reg => {
reg.showNotification("Agente AI — Task completato ✓", {
body: `Task ${taskId.slice(0, 8)}… completato con successo`,
icon: "/favicon.svg",
badge: "/favicon.svg",
tag: taskId,
});
}).catch(() => {});
} catch { /* best-effort */ }
}
// ─── Dispatch eventi → taskStore ──────────────────────────────────────────────
export function handleEvent(
ev: SSEEvent,
taskId: string,
store: ReturnType<typeof useTaskStore.getState>,
) {
switch (ev.event) {
case "task_start":
store.updateStatus(taskId, "RUNNING");
store.addLog(taskId, "info", `Task avviato: ${String(ev.goal ?? "")}`);
tgNotify.taskStart(String(ev.goal ?? ""), taskId);
store.addAction(taskId, { type: "plan", label: "Planning task execution", done: false });
// S758-P4.1: se nessun step_start arriva entro 3s, mostra "thinking" chip
{ const _thinkT = setTimeout(() => { _thinkingTimers.delete(taskId); const _cur = useTaskStore.getState().tasks.find(t => t.id === taskId); if (_cur?.status === "RUNNING" && (_cur.currentStep ?? 0) === 0) { store.addAction(taskId, { type: "thinking", label: "L’agente sta analizzando…", done: false }); store.addLog(taskId, "info", "⏳ In attesa della prima risposta…"); } }, 3_000); _thinkingTimers.set(taskId, _thinkT); }
break;
case "step_start": {
// S758-P4.1: clear thinking timer — first real step received
{ const _tt = _thinkingTimers.get(taskId); if (_tt) { clearTimeout(_tt); _thinkingTimers.delete(taskId); } }
const stepObj = ev.step as { name?: string; tool?: string } | undefined;
const name = stepObj?.name ?? String(ev.tool ?? "Step");
const type = inferActionType(name, { ...ev, tool: stepObj?.tool ?? ev.tool });
const label = inferActionLabel(name, ev);
const detail = inferActionDetail(ev);
store.addLog(taskId, "info", `▶ ${name}`);
store.addAction(taskId, { type, label, detail, done: false, toolName: name }); // S397: toolName for narrative
// [Sessione 17] Emit agent_step_start
const stepNum = useTaskStore.getState().tasks.find(t => t.id === taskId)?.currentStep ?? 0;
eventRuntime.emit({ kind: "agent_step_start", taskId, step: stepNum, action: name });
if (ev.tool) {
eventRuntime.emit({
kind: "tool_call",
taskId,
tool: String(ev.tool),
args: (ev.args as Record<string, unknown>) ?? {},
});
}
break;
}
case "step_done": {
// S396-NARR: parse rich step data from backend (action, status, title, explanation)
const stepRaw = (ev.step ?? ev) as Record<string, unknown>;
const bAction = String(stepRaw.action ?? stepRaw.name ?? "step");
// S403: SSE Visibility Guard — filtra eventi interni che non devono essere mostrati
// all'utente. Il backend assegna il campo `visibility` ad ogni step_done event.
// "internal" = mai visibile (plan, llm, fallback, reflective_debug, fast_path, smolagents)
// "debug" = solo devMode (ignorato per ora — trattato come internal)
// "progress" = visibile come StepCard (tool reali, auto-fix, meteo, ricerca…)
// Assenza del campo → backwards compat: lascia passare (eventi da versioni precedenti)
const bVisibility = String(stepRaw.visibility ?? "progress");
if (bVisibility === "internal" || bVisibility === "debug") {
// Solo log — nessuna StepCard, nessun agent_step_start/complete
store.addLog(taskId, "info", `[internal] ${bAction}`);
break;
}
const bStatus = String(stepRaw.status ?? "done");
const bTitle = String(stepRaw.title ?? bAction.replace(/_/g, " ").replace(/\b\w/g, (ch: string) => ch.toUpperCase()));
const bExpl = String(stepRaw.explanation ?? "");
const bOutput = String(stepRaw.output ?? "");
const bDurMs = (stepRaw.duration_ms ?? ev.duration_ms) as number | undefined;
// P16-B4: segnala truncation SSE al task store + log warning
const bTruncated = Boolean((stepRaw as Record<string, unknown>).truncated);
if (bTruncated) {
store.addLog(taskId, "warn", "⚠️ Risposta LLM troncata (max_tokens raggiunto) — contesto ridotto al prossimo step.");
}
const aType = inferActionType(bAction, { ...ev, tool: bAction });
if (bStatus === "running") {
// S396: Tool STARTING — aggiungi come action in corso
store.addLog(taskId, "info", `▶ ${bTitle}`);
store.addAction(taskId, {
type: aType,
toolName: bAction,
label: bTitle,
detail: bExpl.slice(0, 80) || undefined,
explanation: bExpl || undefined,
done: false,
});
} else {
// S396: Tool DONE/warning — completa pending o aggiungi completata
store.addLog(taskId, "info", `✓ ${bTitle}`);
const task = useTaskStore.getState().tasks.find(t => t.id === taskId);
if (task) {
const pending = [...task.actions].reverse().find(a => !a.done);
if (pending) {
store.completeAction(taskId, pending.id, bDurMs);
} else {
// nessuna pending — aggiungi già completata (path piano/fallback)
store.addAction(taskId, {
type: aType,
toolName: bAction,
label: bTitle,
detail: (bExpl || bOutput).slice(0, 80) || undefined,
explanation: bExpl || undefined,
done: true,
durationMs: bDurMs,
});
}
}
store.advanceStep(taskId);
const taskNow = useTaskStore.getState().tasks.find(t => t.id === taskId);
// [Sessione 17] Emit agent_step_complete
eventRuntime.emit({
kind: "agent_step_complete",
taskId,
step: taskNow?.currentStep ?? 0,
action: bAction,
success: bStatus !== "error",
});
// [Sessione 18] Checkpoint ogni N step
if (taskNow) {
onStepDone(
taskId,
taskNow.goal,
taskNow.currentStep,
taskNow.logs.map(l => l.message),
taskNow.plan ?? [],
taskNow.artifacts.map(a => a.id),
taskNow.retryCount,
);
}
}
break;
}
case "action": {
const type = (ev.type ?? "tool_call") as TaskActionType;
const label = String(ev.label ?? ev.tool ?? "Action");
const detail = (ev.detail ?? ev.path ?? ev.url) as string | undefined;
store.addAction(taskId, { type, label, detail, done: false });
break;
}
case "tool_use": {
// S758-P4.1: pre-execution chip — fires BEFORE the tool runs
{ const _tt = _thinkingTimers.get(taskId); if (_tt) { clearTimeout(_tt); _thinkingTimers.delete(taskId); } }
const _tuTool = String(ev.tool ?? ev.name ?? "Tool");
store.addAction(taskId, { type: inferActionType(_tuTool, ev), label: inferActionLabel(_tuTool, ev), detail: inferActionDetail(ev), done: false, toolName: _tuTool });
store.addLog(taskId, "info", `⚡ ${_tuTool}`);
if (ev.tool) eventRuntime.emit({ kind: "tool_call", taskId, tool: String(ev.tool), args: (ev.args as Record<string, unknown>) ?? {} });
break;
}
case "task_thinking": {
// S758-P4.1: explicit thinking event from backend
{ const _tt = _thinkingTimers.get(taskId); if (_tt) { clearTimeout(_tt); _thinkingTimers.delete(taskId); } }
const _thMsg = String(ev.message ?? "L’agente sta elaborando…");
store.addAction(taskId, { type: "thinking", label: _thMsg, done: false });
store.addLog(taskId, "info", `🧠 ${_thMsg}`);
break;
}
case "step_error": {
const errStep = String(ev.error ?? "");
store.addLog(taskId, "error", `✗ Step errore: ${errStep}`);
eventRuntime.emit({
kind: "agent_step_error",
taskId,
step: useTaskStore.getState().tasks.find(t => t.id === taskId)?.currentStep ?? 0,
action: "step",
message: errStep,
});
break;
}
case "task_done": {
{ const _tt = _thinkingTimers.get(taskId); if (_tt) { clearTimeout(_tt); _thinkingTimers.delete(taskId); } }
store.updateStatus(taskId, "SUCCESS");
const artifacts: string[] = [];
if (ev.result) {
store.addArtifact(taskId, {
id: crypto.randomUUID(),
type: "data",
name: "Risultato",
content: String(ev.result).slice(0, 10_000),
});
artifacts.push("result");
}
// [Sessione 17] Emit task_complete
eventRuntime.emit({
kind: "task_complete",
taskId,
durationMs: Date.now() - (_connectAt.get(taskId) ?? Date.now()),
artifacts,
});
// [Sessione 18] Rimuove checkpoint
// GAP-2-FIX + P16-F2: piano completato → nascondi StreamingPlanBubble
// P16-F2: ritardo 1500ms per permettere all'utente di vedere il piano completato
if (typeof window !== "undefined") {
setTimeout(() => window.dispatchEvent(new CustomEvent("agente-ai:plan-clear", { detail: {} })), 1500);
}
onTaskEnd(taskId);
// Sprint 5 ITEM 14: phase_breakdown → taskStore
if (ev.phase_breakdown && typeof ev.phase_breakdown === "object") {
store.setPhaseBreakdown(taskId, ev.phase_breakdown as PhaseBreakdown);
}
// FIX(audit): cancella stream dalla registry — evita idle timer zombie
streamManager.cancel(taskId);
// Telegram taskDone — PRIMA del cleanup per leggere _taskParams e _connectAt
// TG-ENRICH: passa stepsCount per notifica più ricca (quanti step ha eseguito)
{ const _tgTask = useTaskStore.getState().tasks.find(t => t.id === taskId);
tgNotify.taskDone(
taskId,
String(_taskParams.get(taskId)?.goal ?? ev.goal ?? ""),
Date.now() - (_connectAt.get(taskId) ?? Date.now()),
ev.result ? String(ev.result).slice(0, 400) : undefined,
undefined, // filesModified: non tracciato per action type
_tgTask?.currentStep ?? undefined, // stepsCount: numero step completati
);
}
// RT-5: cleanup Map per evitare leak e possibile rianimazione fantasma
_taskParams.delete(taskId);
_connectAt.delete(taskId);
_taskStarted.delete(taskId); // S345: cleanup — prossimo task può usare stesso ID
_streamingBuffer.delete(taskId); // S420: cleanup buffer streaming
_bufferTimestamps.delete(taskId); // S422: cleanup TTL timestamp
// S346-A: clear timeout timer — task completato normalmente
const _tt = _taskTimers.get(taskId); if (_tt) { clearTimeout(_tt); _taskTimers.delete(taskId); }
// [S67] Notifica push se la pagina è in background
_maybeNotify(taskId);
break;
}
case "task_error": {
// N3: ProblemsPanel realtime — segnala errore a diagAsync (fire-and-forget, 0 overhead)
try {
import("@/lib/vfsDb").then(({ diagAsync: _d }) => {
const _em = String(ev.error ?? "Errore sconosciuto");
_d.add({ type: "error", context: `task:${String(taskId).slice(0,8)}`, message: _em });
}).catch(() => {});
} catch { /* non-blocking */ }
{ const _tt = _thinkingTimers.get(taskId); if (_tt) { clearTimeout(_tt); _thinkingTimers.delete(taskId); } }
const errMsg = String(ev.error ?? "Errore sconosciuto");
const task = useTaskStore.getState().tasks.find(t => t.id === taskId);
if (task && task.retryCount < task.maxRetries) {
const backoffMs = Math.min(2000 * (2 ** task.retryCount), 30_000);
store.addLog(
taskId, "warn",
`Errore — retry ${task.retryCount + 1}/${task.maxRetries} in ${backoffMs / 1000}s`,
);
// S762-BUG2: buffer stale accumula contenuto del tentativo fallito;
// al task_done del retry, join produce testo duplicato → pulisci subito
_streamingBuffer.delete(taskId);
_bufferTimestamps.delete(taskId);
setTimeout(() => { store.retryTask(taskId); }, backoffMs);
} else {
store.updateStatus(taskId, "ERROR", errMsg);
// F3-S201: buildFailureHandler — auto-analisi e istruzioni recovery per build error
const _isBuildErr = /TSd{4}|build fail|esbuild|typecheck|compile/i.test(errMsg);
if (_isBuildErr) {
try {
const _bf = handleBuildFailure(errMsg, { retryCount: task?.retryCount ?? 0, taskType: "build" });
store.addLog(taskId, "warn", `🔧 Build fix: ${_bf.action}${_bf.instructions.slice(0, 120)}`);
} catch { /* non-blocking */ }
}
// FIX(audit): cancella stream dalla registry quando non c'è più retry
streamManager.cancel(taskId);
// Telegram — notifica errore definitivo PRIMA del cleanup dei Map
tgNotify.taskError(taskId, String(_taskParams.get(taskId)?.goal ?? ""), errMsg);
// RT-5: cleanup Map — evita leak e possibile rianimazione da agent:promote-queued
_taskParams.delete(taskId);
_connectAt.delete(taskId);
_taskStarted.delete(taskId); // S345: consenti fresh start su errore definitivo
_streamingBuffer.delete(taskId); // S422: cleanup buffer streaming su errore definitivo
_bufferTimestamps.delete(taskId); // S422: cleanup TTL timestamp su errore definitivo
// S346-A: clear timeout timer — task terminato con errore
const _te = _taskTimers.get(taskId); if (_te) { clearTimeout(_te); _taskTimers.delete(taskId); }
}
eventRuntime.emit({
kind: "task_error",
taskId,
message: errMsg,
retryCount: task?.retryCount ?? 0,
});
break;
}
case "text_chunk": {
// S420: accumula token in buffer locale e dispatcha evento DOM per UI real-time
const _tok = String(ev.token ?? "");
if (_tok) {
const _prev = (_streamingBuffer.get(taskId) ?? "") + _tok;
_streamingBuffer.set(taskId, _prev);
_bufferTimestamps.set(taskId, Date.now()); // S422: aggiorna TTL
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("agent:stream-chunk", {
detail: { taskId, text: _prev, token: _tok },
}));
}
}
break;
}
case "vfs_sync_complete": {
// P34: VFS Atomic Swap — tutti i file del task scritti, il frontend può committare lo staging
const _sc_files = (ev as Record<string, unknown>).files as string[] | undefined;
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("agent:vfs-sync-complete", {
detail: { taskId, files: _sc_files ?? [] },
}));
}
break;
}
case "vfs_update": {
// S723-GAP3.2: vfs_update era silenzioso — workspace non si aggiornava mai
// SYNC-1: forward content nel DOM event per vfsRemoteSync — zero fetch aggiuntivo
const _vf = String(ev.file ?? "");
const _vo = String(ev.op ?? "write");
const _vc = (ev as Record<string, unknown>).content as string | undefined; // SYNC-1: file content
store.addLog(taskId, "info", `${_vo === "delete" ? "\uD83D\uDDD1\uFE0F" : "\uD83D\uDCDD"} ${_vo}: ${_vf.split("/").pop() ?? _vf}`);
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("agent:vfs-update", { detail: { file: _vf, op: _vo, taskId, content: _vc } }));
}
break;
}
case "persona_classified": {
// P17-F5: server ha classificato la persona (auto-detection) — aggiorna badge UI
const _pc = ev as Record<string,unknown>;
const _pcPersona = String(_pc.persona ?? '');
const _pcConf = Number(_pc.confidence ?? 0);
const _pcAuto = Boolean(_pc.auto ?? false);
if (_pcPersona && typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('agent:persona-classified', {
detail: { taskId, persona: _pcPersona, confidence: _pcConf, auto: _pcAuto },
}));
store.addLog(taskId, 'info', `🎯 Persona: ${_pcPersona}${_pcAuto ? ' (auto)' : ''} ${Math.round(_pcConf * 100)}%`);
}
break;
}
case "thought": {
// SYNC-2: thought dal backend (goal esplicito dopo planning) → DOM event per ThinkingBlock
const _thText = String((ev as Record<string,unknown>).text ?? "");
if (_thText) {
store.addLog(taskId, "info", `💭 ${_thText.slice(0, 80)}`);
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("agent:thought", { detail: { taskId, text: _thText } }));
}
}
break;
}
case "plan_update": {
// SYNC-2: subtask list dal backend → aggiorna plan in taskStore + DOM event per live checkbox
const _pu = ev as Record<string,unknown>;
const _subtasks = (_pu.subtasks as Array<{ id: string|number; description: string; tool?: string; status: string }> | undefined) ?? [];
if (_subtasks.length > 0) {
store.addLog(taskId, "info", `📋 Piano: ${_subtasks.length} step`);
(store as unknown as Record<string, ((id: string, p: unknown) => void) | undefined>)["updateTask"]?.(taskId, { plan: _subtasks.map(s => String(s.description || s.id)).filter(Boolean) });
if (typeof window !== "undefined") {
// P16-F2: StreamingPlanBubble ascolta direttamente agent:plan-update (bridge rimosso)
window.dispatchEvent(new CustomEvent("agent:plan-update", { detail: { taskId, subtasks: _subtasks, goal: String(_pu.goal ?? "") } }));
}
}
break;
}
case "subtask_done": {
// SYNC-2: singolo subtask completato → DOM event per live plan checkbox update
const _sd = ev as Record<string,unknown>;
if (typeof window !== "undefined") {
// P16-F2: StreamingPlanBubble ascolta direttamente agent:subtask-done (bridge rimosso)
window.dispatchEvent(new CustomEvent("agent:subtask-done", { detail: { taskId, id: _sd.id, status: String(_sd.status ?? "done") } }));
}
break;
}
case "task_interrupted": {
// SYNC-2: backend riavviato durante esecuzione → status ERROR + toast + DOM event
{ const _tt = _thinkingTimers.get(taskId); if (_tt) { clearTimeout(_tt); _thinkingTimers.delete(taskId); } }
store.updateStatus(taskId, "ERROR", "Backend riavviato durante l'esecuzione");
store.addLog(taskId, "warn", "⚡ Backend riavviato — task interrotto");
streamManager.cancel(taskId);
const _tc2 = _taskTimers.get(taskId); if (_tc2) { clearTimeout(_tc2); _taskTimers.delete(taskId); }
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("agent:task-interrupted", { detail: { taskId, reason: String((ev as Record<string,unknown>).reason ?? "backend_restarted") } }));
}
eventRuntime.emit({ kind: "task_error", taskId, message: "Backend riavviato durante l'esecuzione", retryCount: 0 });
// GAP-2-FIX: backend crash → nascondi StreamingPlanBubble
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("agente-ai:plan-clear", { detail: {} }));
}
break;
}
case "task_cancelled":
{ const _tt = _thinkingTimers.get(taskId); if (_tt) { clearTimeout(_tt); _thinkingTimers.delete(taskId); } }
store.updateStatus(taskId, "CANCELLED");
// GAP-2-FIX: piano annullato → nascondi StreamingPlanBubble
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("agente-ai:plan-clear", { detail: {} }));
}
onTaskEnd(taskId);
// FIX(audit): cancella stream dalla registry — evita idle timer zombie
streamManager.cancel(taskId);
// S346-A: clear timeout timer su task_cancelled
{ const _tc = _taskTimers.get(taskId); if (_tc) { clearTimeout(_tc); _taskTimers.delete(taskId); } }
tgNotify.taskCancelled(taskId, String(_taskParams.get(taskId)?.goal ?? ""));
break;
case "connector_needed": {
// P39-UX: Tocco Finale Manus — agente richiede OAuth per continuare
const _provider = String(ev.provider ?? '');
const _label = String(ev.label ?? ev.provider ?? '');
const _msg = String(ev.message ?? '');
store.addLog(taskId, "warn", `🔗 Connettore richiesto: ${_label || _provider}`);
// Emit custom DOM event — ascoltato da ConnectorNeededBanner
try {
window.dispatchEvent(new CustomEvent("agent:connector-needed", {
detail: { provider: _provider, label: _label, message: _msg, taskId },
}));
} catch { /* SSR safe */ }
break;
}
case "task_aborted": {
// ABORT-DISPATCH: task fermato dal pulsante "✕ Stop" via POST /api/agent/abort.
// Auto-retry: salva params PRIMA del cleanup → riavvio automatico dopo 1200ms.
const _tt = _thinkingTimers.get(taskId); if (_tt) { clearTimeout(_tt); _thinkingTimers.delete(taskId); }
// Salva prima del cleanup — _taskParams viene cancellato sotto
const _abGoal = String(_taskParams.get(taskId)?.goal ?? "");
const _abCtx = _taskParams.get(taskId)?.context ?? [];
const _abSteps = _taskParams.get(taskId)?.maxSteps ?? 8;
store.updateStatus(taskId, "CANCELLED");
// MX16+MX18-ABORT: abort_reason enum — "user_stop" | "timeout" | "system" | "escalation" | "error_limit"
const _abortReason = String((ev as Record<string, unknown>).abort_reason ?? "user_stop");
const _isUserStop = _abortReason === "user_stop";
const _isTimeout = _abortReason === "timeout"; // MX18-ABORT
const _isEscalation = _abortReason === "escalation"; // MX18-ABORT
const _isErrorLimit = _abortReason === "error_limit"; // MX18-ABORT
// UX: messaggio e toast differenziati per tipo abort — MX18-ABORT
const _abortLogMsg = _isUserStop ? "⛔ Task fermato dall'utente"
: _isTimeout ? "⏱ Task scaduto (timeout di sistema)"
: _isEscalation ? "⚡ Provider AI esauriti — interruzione escalation"
: _isErrorLimit ? "⚠️ Limite errori raggiunto — task interrotto"
: "⛔ Task interrotto dal sistema";
store.addLog(taskId, "warn", _abortLogMsg);
if (_isUserStop) {
toast.success("✓ Task fermato", { duration: 2000, id: "task-aborted-toast" });
} else if (_isTimeout) {
toast.error("⏱ Task scaduto — riprova", { duration: 4000, id: "task-aborted-toast" });
} else if (_isEscalation) {
toast.error("⚡ Provider AI esauriti — riprovo con strategia alternativa…", { duration: 5000, id: "task-aborted-toast" });
} else {
toast.info("Task interrotto — riavvio automatico in corso…", { duration: 4000, id: "task-aborted-toast" });
}
onTaskEnd(taskId);
streamManager.cancel(taskId);
const _tc = _taskTimers.get(taskId); if (_tc) { clearTimeout(_tc); _taskTimers.delete(taskId); }
// Full Map cleanup — libera MAX_CONCURRENT_SSE per il prossimo task
_taskParams.delete(taskId);
_connectAt.delete(taskId);
_taskStarted.delete(taskId);
_streamingBuffer.delete(taskId);
_bufferTimestamps.delete(taskId);
// MX16+MX18-ABORT: task_error solo su abort di sistema, non su stop utente né timeout (già gestito)
if (!_isUserStop && !_isTimeout) {
eventRuntime.emit({
kind: "task_error",
taskId,
message: "Task interrotto dal sistema",
retryCount: useTaskStore.getState().tasks.find(t => t.id === taskId)?.retryCount ?? 0,
});
}
// MX16+MX18-ABORT: auto-retry su system/escalation/error_limit, NON su user_stop né timeout
const _shouldAutoRetry = !_isUserStop && !_isTimeout; // MX18-ABORT: timeout non retry
if (_abGoal && _shouldAutoRetry && typeof window !== "undefined") {
setTimeout(() => window.dispatchEvent(new CustomEvent("agente-ai:auto-retry", {
detail: { goal: _abGoal, context: _abCtx, maxSteps: _abSteps },
})), 1200);
}
break;
}
case "partial_output": {
// S-PARTIAL: backend delegate timeout → output parziale recuperato.
// Mostra chip "⚠ output parziale — riprendo" nella chat.
// Il chip scompare automaticamente al prossimo step_done (normale progressione).
const _po = ev as Record<string, unknown>;
const _poSteps = Number(_po.steps_done ?? 0);
const _poFiles = (_po.partial_files as string[] | undefined) ?? [];
const _poOut = String(_po.partial_output ?? _po.output ?? "").slice(0, 200);
const _poLabel = [
"⚠ output parziale — riprendo",
_poSteps > 0 ? `(${_poSteps} step completati)` : "",
_poFiles.length > 0 ? ${_poFiles.length} file parziali` : "",
].filter(Boolean).join(" ");
store.addAction(taskId, {
type: "tool_call",
label: _poLabel,
detail: _poOut || undefined,
done: false, // pending: scomparirà al prossimo completeAction
toolName: "partial_output",
});
store.addLog(taskId, "warn", `⚠ output parziale (${_poSteps} step, ${_poFiles.length} file) — il backend sta recuperando`);
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent("agent:partial-output", {
detail: { taskId, stepsDone: _poSteps, partialFiles: _poFiles, partialOutput: _poOut },
}));
}
break;
}
}
}