Spaces:
Sleeping
Sleeping
| /** | |
| * adaptiveSolver.ts — Adaptive Problem Solver (S50) | |
| * | |
| * Pattern: quando un'azione fallisce per un BLOCCO SPECIFICO, | |
| * non si ripete ne si chiede aiuto. Si IDENTIFICA il blocco | |
| * e si ESEGUE un'alternativa nota automaticamente. | |
| * | |
| * Esempio reale che ispira questa feature: | |
| * Operazione push bloccata → rilevato blocco "permission" → | |
| * strategia "permission_rest" → usa API REST invece → successo. | |
| * | |
| * Strategie built-in (7): | |
| * 1. cors_proxy — fetch CORS bloccato → 3 proxy in ordine | |
| * 2. file_fuzzy — file not found → ricerca fuzzy VFS | |
| * 3. rate_limit_wait — 429 → backoff esponenziale + retry | |
| * 4. chunk_payload — payload troppo grande → suggerimento chunking | |
| * 5. permission_rest — operazione negata → equivalente REST API | |
| * 6. network_retry — errore rete transitorio → retry timeout aumentato | |
| * 7. syntax_hint — errore sintattico → analisi riga + hint preciso | |
| * | |
| * Invarianti: | |
| * - Safari-safe: no SharedArrayBuffer, no Worker | |
| * - DI via _setTestDeps() per unit test senza vi.mock | |
| * - Max 2 tentativi per blocco (no loop infiniti) | |
| * - Ogni strategia e IDEMPOTENTE: fallire non peggiora lo stato | |
| * - Emette sempre un messaggio leggibile su cosa sta facendo | |
| */ | |
| import { vfsAsync } from "@/lib/vfsDb"; | |
| import { agentMemory } from "@/lib/agentMemory"; | |
| // ─── Tipi pubblici (estratti in adaptiveSolverTypes.ts — S552 — re-esportati per backward compat) ── | |
| import type { BlockadeKind, Blockade, SolveResult } from "./adaptiveSolverTypes"; | |
| export type { BlockadeKind, Blockade, SolveResult } from "./adaptiveSolverTypes"; | |
| interface Strategy { | |
| id: string; | |
| label: string; | |
| handles: BlockadeKind[]; | |
| tryFix( | |
| blockade: Blockade, | |
| originalInput: string, | |
| context: string, | |
| ): Promise<string | null>; | |
| } | |
| // ─── DI per test ───────────────────────────────────────────────────────────── | |
| interface SolverDeps { | |
| vfsListFn: () => Promise<{ path: string; content: string }[]>; | |
| memorySetFn: (key: string, val: string, cat: string) => void; | |
| /** Gap-5: lettura last_winner e win-count dal ring buffer di agentMemory. | |
| * Separato da memorySetFn per DI pulita nei test — il reader può essere mockato | |
| * indipendentemente dallo writer (pattern read-write separation). */ | |
| memoryGetFn: (key: string) => string | undefined; | |
| fetchFn: (url: string, opts?: RequestInit) => Promise<Response>; | |
| waitFn: (ms: number) => Promise<void>; | |
| } | |
| let _deps: SolverDeps = { | |
| vfsListFn: () => vfsAsync.list() as Promise<{ path: string; content: string }[]>, | |
| memorySetFn: (key, val, cat) => agentMemory.set(key, val, cat), | |
| memoryGetFn: (key) => agentMemory.get(key) ?? undefined, // Fix-5a: agentMemory.get()→string|null; interfaccia dichiara string|undefined → null coerced | |
| fetchFn: (url, opts) => fetch(url, opts), | |
| waitFn: (ms) => new Promise(res => setTimeout(res, ms)), | |
| }; | |
| /** Dependency injection per unit test */ | |
| export function _setTestDeps(deps: Partial<SolverDeps>): void { | |
| _deps = { ..._deps, ...deps }; | |
| } | |
| // ─── Rilevamento blocco ─────────────────────────────────────────────────────── | |
| const BLOCKADE_PATTERNS: Array<{ re: RegExp; kind: BlockadeKind; hint: string }> = [ | |
| { re: /cors|cross.?origin|blocked by cors|no.*access-control/i, kind: "cors", hint: "Richiesta bloccata da policy CORS" }, | |
| { re: /404|not found|no such file|cannot find|enoent|file non trovato/i, kind: "not_found", hint: "Risorsa non trovata — path errato o file mancante" }, | |
| { re: /429|rate limit|too many requests/i, kind: "rate_limit", hint: "Rate limit raggiunto" }, | |
| { re: /403|401|access denied|permission denied|not allowed|unauthorized/i, kind: "permission", hint: "Operazione non consentita" }, | |
| { re: /timeout|timed out|aborted|etimedout/i, kind: "timeout", hint: "Timeout — operazione troppo lenta" }, | |
| { re: /quota|storage full|no space|exceeded.*storage/i, kind: "quota", hint: "Storage quota superata" }, | |
| { re: /syntaxerror|unexpected token|unterminated|parse error/i, kind: "syntax", hint: "Errore di sintassi" }, | |
| { re: /network error|failed to fetch|net::err|econnrefused/i, kind: "network", hint: "Errore di rete" }, | |
| // Fix-1.1: import_error sblocca dependency_probe (riga ~332) — era mai attivata | |
| { re: /ModuleNotFoundError|No module named|ImportError|Cannot find module|Module not found/i, kind: "import_error", hint: "Libreria mancante — necessita installazione" }, | |
| ]; | |
| export function detectBlockade(errorMessage: string): Blockade { | |
| for (const { re, kind, hint } of BLOCKADE_PATTERNS) { | |
| if (re.test(errorMessage)) { | |
| return { kind, message: errorMessage.slice(0, 300), hint }; | |
| } | |
| } | |
| return { kind: "unknown", message: errorMessage.slice(0, 300), hint: "Errore non riconosciuto" }; | |
| } | |
| // ─── Helper: Proxy CORS ─────────────────────────────────────────────────────── | |
| // S320-FIX: corsproxy.io rimosso (403 pagamento obbligatorio — test 2026-06-01) | |
| const CORS_PROXIES: Array<(u: string) => string> = [ | |
| u => "https://api.allorigins.win/get?url=" + encodeURIComponent(u), | |
| u => "https://api.codetabs.com/v1/proxy?quest=" + encodeURIComponent(u), | |
| u => "https://api.allorigins.win/raw?url=" + encodeURIComponent(u), | |
| ]; | |
| async function viaProxy(url: string, timeoutMs = 8000): Promise<string> { | |
| for (const make of CORS_PROXIES) { | |
| try { | |
| const ctrl = new AbortController(); | |
| const tid = setTimeout(() => ctrl.abort(), timeoutMs); | |
| const res = await _deps.fetchFn(make(url), { signal: ctrl.signal }); | |
| clearTimeout(tid); | |
| if (!res.ok) continue; | |
| const raw = await res.text(); | |
| if (raw.startsWith('{"contents"')) { | |
| const d = JSON.parse(raw) as { contents?: string }; | |
| if ((d.contents?.length ?? 0) > 20) return d.contents!; | |
| } else if (raw.length > 20) { | |
| return raw; | |
| } | |
| } catch { continue; } | |
| } | |
| throw new Error("Tutti i proxy CORS falliti"); | |
| } | |
| // ─── Helper: fuzzy match VFS ────────────────────────────────────────────────── | |
| async function fuzzyFindPath(target: string): Promise<string | null> { | |
| const files = await _deps.vfsListFn(); | |
| if (!files.length) return null; | |
| const t = target.toLowerCase().replace(/^\/+/, ""); | |
| const exact = files.find(f => f.path === target || f.path === "/" + t); | |
| if (exact) return exact.path; | |
| const fname = t.split("/").pop() ?? t; | |
| const byName = files.find(f => f.path.toLowerCase().endsWith("/" + fname)); | |
| if (byName) return byName.path; | |
| const bySub = files.find(f => f.path.toLowerCase().includes(fname)); | |
| return bySub?.path ?? null; | |
| } | |
| // ─── Strategie ──────────────────────────────────────────────────────────────── | |
| const STRATEGIES: Strategy[] = [ | |
| // 1. CORS → proxy automatico | |
| { | |
| id: "cors_proxy", label: "Proxy CORS automatico", handles: ["cors", "network"], | |
| async tryFix(_blockade, originalInput) { | |
| const m = originalInput.match(/https?:\/\/[^\s"'`\)]+/); | |
| if (!m) return null; | |
| const url = m[0].replace(/[,;)]+$/, ""); | |
| try { | |
| const content = await viaProxy(url); | |
| return "✅ [proxy CORS] " + url + "\n" + content.slice(0, 3000); | |
| } catch { return null; } | |
| }, | |
| }, | |
| // 2. Not found → fuzzy search VFS | |
| { | |
| id: "file_fuzzy", label: "Ricerca fuzzy file nel workspace", handles: ["not_found"], | |
| async tryFix(_blockade, originalInput) { | |
| const m = originalInput.match(/['"]([/a-zA-Z0-9_\-\.]+\.[a-zA-Z]{1,5})['"]/); | |
| if (!m) return null; | |
| const searchPath = m[1]; | |
| const found = await fuzzyFindPath(searchPath); | |
| if (!found) { | |
| const list = (await _deps.vfsListFn()).slice(0, 10).map(f => f.path).join(", "); | |
| return "❌ File \"" + searchPath + "\" non trovato.\nFile disponibili nel workspace: " + (list || "nessuno"); | |
| } | |
| const file = (await _deps.vfsListFn()).find(f => f.path === found); | |
| const content = (file?.content ?? "").slice(0, 3000); | |
| return "✅ [file trovato] " + found + "\n" + content; | |
| }, | |
| }, | |
| // 3. Rate limit → backoff esponenziale | |
| { | |
| id: "rate_limit_wait", label: "Backoff esponenziale e retry", handles: ["rate_limit"], | |
| async tryFix(_blockade, originalInput) { | |
| const m = originalInput.match(/https?:\/\/[^\s"'`\)]+/); | |
| if (!m) return null; | |
| const url = m[0].replace(/[,;)]+$/, ""); | |
| // S324: cap a 2s iniziale (era 2s), abort dopo 4s — evita watchdog kill su iOS Safari | |
| // (JS fermo >5s può essere terminato dal watchdog; 2+2=4s totale di attesa massima) | |
| await _deps.waitFn(2000); | |
| try { | |
| const ctrl = new AbortController(); | |
| setTimeout(() => ctrl.abort(), 4000); // S324: 10000→4000ms, Safari watchdog safe | |
| const res = await _deps.fetchFn(url, { signal: ctrl.signal }); | |
| if (res.status === 429) { await _deps.waitFn(2000); return null; } // S324: 5000→2000ms | |
| if (!res.ok) return null; | |
| return "✅ [retry dopo backoff] " + url + "\n" + (await res.text()).slice(0, 2000); | |
| } catch { return null; } | |
| }, | |
| }, | |
| // 4. Quota/timeout → suggerimento chunking | |
| { | |
| id: "chunk_payload", label: "Suddivisione in chunk", handles: ["quota", "timeout"], | |
| async tryFix(_blockade, originalInput) { | |
| if (originalInput.length < 5000) return null; | |
| const CHUNK = 3000; | |
| const parts = Math.ceil(originalInput.length / CHUNK); | |
| return ( | |
| "⚠️ Input troppo grande (" + originalInput.length + " chars) — strategia chunk.\n" + | |
| "Dividi in " + parts + " chunk da " + CHUNK + " chars ciascuno.\n" + | |
| "Chunk 1:\n" + originalInput.slice(0, CHUNK) | |
| ); | |
| }, | |
| }, | |
| // 5. Permission negata → alternativa REST API | |
| { | |
| id: "permission_rest", label: "Alternativa via REST API", handles: ["permission"], | |
| async tryFix(blockade, originalInput, context) { | |
| const lc = (originalInput + " " + context).toLowerCase(); | |
| if (lc.includes("push") || lc.includes("commit")) { | |
| return [ | |
| "⚠️ Operazione bloccata. Alternativa: usa API REST.", | |
| "Per operazioni su repository:", | |
| " 1. POST /repos/OWNER/REPO/git/blobs → carica contenuto file", | |
| " 2. POST /repos/OWNER/REPO/git/trees → crea albero con i blob", | |
| " 3. POST /repos/OWNER/REPO/git/commits → crea commit", | |
| " 4. PATCH /repos/OWNER/REPO/git/refs/heads/main → aggiorna branch", | |
| "Usa fetch con Authorization: token TOKEN_VALUE", | |
| ].join("\n"); | |
| } | |
| if (lc.includes("localstorage") || lc.includes("storage")) { | |
| return "⚠️ Storage bloccato. Alternativa: IndexedDB via Dexie (vfsDb.ts) o sessionStorage."; | |
| } | |
| if (lc.includes("clipboard")) { | |
| return "⚠️ Clipboard API bloccata. Alternativa: mostra testo in <textarea> con selectAll() per copia manuale."; | |
| } | |
| return "⚠️ " + blockade.hint + "\nCerca API equivalente o workaround browser-safe."; | |
| }, | |
| }, | |
| // 6. Errore di rete → retry con timeout aumentato | |
| { | |
| id: "network_retry", label: "Retry con timeout aumentato", handles: ["network", "timeout"], | |
| async tryFix(_blockade, originalInput) { | |
| const m = originalInput.match(/https?:\/\/[^\s"'`\)]+/); | |
| if (!m) return null; | |
| const url = m[0].replace(/[,;)]+$/, ""); | |
| await _deps.waitFn(1500); | |
| try { | |
| const ctrl = new AbortController(); | |
| setTimeout(() => ctrl.abort(), 15000); | |
| const res = await _deps.fetchFn(url, { signal: ctrl.signal }); | |
| if (!res.ok) return null; | |
| return "✅ [retry rete] " + url + "\n" + (await res.text()).slice(0, 2000); | |
| } catch { return null; } | |
| }, | |
| }, | |
| // 7. Errore di sintassi → analisi riga + hint preciso | |
| { | |
| id: "syntax_hint", label: "Analisi errore sintattico", handles: ["syntax"], | |
| async tryFix(blockade, originalInput) { | |
| const lines = originalInput.split("\n"); | |
| const m = blockade.message.match(/line (\d+)|:(\d+):/); | |
| const lineNum = m ? parseInt(m[1] ?? m[2]) - 1 : -1; | |
| const hint = (lineNum >= 0 && lineNum < lines.length) | |
| ? "Riga " + (lineNum + 1) + ": \"" + (lines[lineNum]?.trim().slice(0, 150) ?? "") + "\"" // S609: 80→150 | |
| : "Posizione non determinata"; | |
| return ( | |
| "⚠️ Errore sintattico — " + hint + "\n" + | |
| blockade.message.slice(0, 300) + "\n" + // S609: 200→300 | |
| "Controlla: parentesi bilanciate, virgole mancanti, backtick non chiusi, import mancanti." | |
| ); | |
| }, | |
| }, | |
| // S697-8: Arg mutation — cache-bust URL, shorter query, minimal args | |
| { | |
| id: "arg_mutate", label: "Mutazione argomenti (cache-bust, query corta)", handles: ["rate_limit", "timeout", "not_found"], | |
| async tryFix(_blockade: Blockade, originalInput: string) { | |
| const urlM = originalInput.match(/https?:\/\/[^\s"'`\)]+/); | |
| if (urlM) { | |
| const url = urlM[0].replace(/[,;)]+$/, ""); | |
| const busted = url + (url.includes("?") ? "&" : "?") + "_cb=" + Date.now(); | |
| try { | |
| const ctrl = new AbortController(); | |
| setTimeout(() => ctrl.abort(), 6000); | |
| const r = await _deps.fetchFn(busted, { signal: ctrl.signal }); | |
| if (r.ok) return "✅ [cache-bust] " + busted + "\n" + (await r.text()).slice(0, 2000); | |
| } catch { /* continua */ } | |
| const base = url.split("?")[0]; | |
| if (base !== url) { | |
| try { | |
| const ctrl2 = new AbortController(); | |
| setTimeout(() => ctrl2.abort(), 5000); | |
| const r2 = await _deps.fetchFn(base, { signal: ctrl2.signal }); | |
| if (r2.ok) return "✅ [no-query] " + base + "\n" + (await r2.text()).slice(0, 2000); | |
| } catch { /* continua */ } | |
| } | |
| } | |
| const qM = originalInput.match(/query[s:"']+([^"'\n]{10,})/i); | |
| if (qM) { | |
| const short = qM[1].trim().split(" ").slice(0, 6).join(" "); | |
| return `💡 Riprova con query più corta: "${short}"`; | |
| } | |
| return null; | |
| }, | |
| }, | |
| // S697-9: Tool substitute — mappa 20+ tool con alternative funzionali | |
| { | |
| id: "tool_substitute", label: "Sostituzione tool con alternativa funzionale", handles: ["timeout", "network", "cors", "permission", "rate_limit"], | |
| async tryFix(_blockade: Blockade, _originalInput: string, context: string) { | |
| const SUBS: Record<string, { alt: string; hint: string }> = { | |
| "web_search": { alt: "fetch_url", hint: "Usa fetch_url con URL diretto" }, | |
| "fetch_url": { alt: "web_search", hint: "Usa web_search con la query" }, | |
| "get_weather": { alt: "fetch_url", hint: "Usa fetch_url su wttr.in" }, | |
| "get_news": { alt: "web_search", hint: "Usa web_search site:ansa.it" }, | |
| "get_stock": { alt: "fetch_url", hint: "Usa fetch_url su Yahoo Finance API" }, | |
| "run_code": { alt: "run_python", hint: "Prova run_python come alternativo" }, | |
| "run_python": { alt: "run_code", hint: "Prova run_code (Node.js) come alternativo" }, | |
| "translate": { alt: "web_search", hint: "Usa web_search per traduzione" }, | |
| "screenshot_url":{ alt: "fetch_url", hint: "Usa fetch_url + parse HTML" }, | |
| }; | |
| const toolM = context.match(/tool[:s"']+([a-z_]+)/i); | |
| if (!toolM) return null; | |
| const sub = SUBS[toolM[1]]; | |
| if (!sub) return null; | |
| return "🔄 Tool fallito. Alternativa: " + sub.alt + ". " + sub.hint + "."; | |
| }, | |
| }, | |
| // S697-10: Dependency probe — verifica disponibilità su npm/PyPI | |
| { | |
| id: "dependency_probe", label: "Verifica dipendenza su npm/PyPI", handles: ["dependency", "import_error", "not_found"], | |
| async tryFix(blockade: Blockade, originalInput: string) { | |
| const modM = blockade.message.match(/(?:module|package|import|require)s+["']?([a-zA-Z0-9@/._-]+)["']?/i); | |
| if (!modM) return null; | |
| const mod = modM[1]; | |
| const isPy = /python|.py/.test(originalInput.toLowerCase()); | |
| const url = isPy | |
| ? "https://pypi.org/pypi/" + encodeURIComponent(mod) + "/json" | |
| : "https://registry.npmjs.org/" + encodeURIComponent(mod) + "/latest"; | |
| try { | |
| const ctrl = new AbortController(); | |
| setTimeout(() => ctrl.abort(), 5000); | |
| const res = await _deps.fetchFn(url, { signal: ctrl.signal }); | |
| if (res.ok) { | |
| const d = await res.json() as Record<string, unknown>; | |
| const v = isPy ? ((d["info"] as Record<string,string>)?.["version"]) : ((d as Record<string,string>)["version"]); | |
| return "✅ " + mod + " disponibile" + (v ? " (v" + v + ")" : "") + ". Installa: " + (isPy ? "pip install " + mod : "npm install " + mod); | |
| } | |
| if (res.status === 404) return "❌ " + mod + " non trovato su " + (isPy ? "PyPI" : "npm") + "."; | |
| } catch { /* timeout non-blocking */ } | |
| return null; | |
| }, | |
| }, | |
| ]; | |
| // ─── AdaptiveSolver ─────────────────────────────────────────────────────────── | |
| class AdaptiveSolverImpl { | |
| private readonly _maxAttempts = 2; | |
| /** | |
| * Dato un errore e l'input originale che lo ha causato: | |
| * 1. Rileva il tipo di blocco | |
| * 2. Trova le strategie applicabili | |
| * 3. Prova in ordine finché una riesce | |
| * 4. Memorizza il vincitore in agentMemory | |
| */ | |
| async solve( | |
| errorMessage: string, | |
| originalInput = "", | |
| context = "", | |
| _triedIds: string[] = [], // S697: already-tried strategy IDs for anti-repetition | |
| ): Promise<SolveResult> { | |
| const blockade = detectBlockade(errorMessage); | |
| if (blockade.kind === "unknown") { | |
| return { | |
| success: false, | |
| output: errorMessage, | |
| strategy: "none", | |
| description: "Nessuna strategia adattiva disponibile.", | |
| }; | |
| } | |
| // Gap-5: score-based reorder — last_winner prioritizzato, poi win-count storico, poi ordine fisso. | |
| // Evita di sprecare il budget di 2 tentativi (maxAttempts) su strategie storicamente perdenti. | |
| const _winnerKey = "adaptive:" + blockade.kind + ":last_winner"; | |
| const _countKey = "adaptive:" + blockade.kind + ":win_counts"; | |
| const _lastWinner = (() => { try { return _deps.memoryGetFn(_winnerKey); } catch { return undefined; } })(); | |
| const _winCounts: Record<string, number> = (() => { | |
| try { const raw = _deps.memoryGetFn(_countKey); return raw ? (JSON.parse(raw) as Record<string,number>) : {}; } catch { return {}; } | |
| })(); | |
| const strategies = STRATEGIES | |
| .filter(s => s.handles.includes(blockade.kind) && !_triedIds.includes(s.id)) | |
| .map(s => ({ | |
| s, | |
| score: s.id === _lastWinner ? 100 // L1: confirmed last winner | |
| : (_winCounts[s.id] ?? 0) > 0 ? _winCounts[s.id] // L2: historical wins | |
| : 0, // L3: no history | |
| })) | |
| .sort((a, b) => b.score - a.score || 0) // stable: ties keep STRATEGIES insertion order | |
| .map(x => x.s); | |
| for (let i = 0; i < Math.min(strategies.length, this._maxAttempts); i++) { | |
| const strategy = strategies[i]; | |
| try { | |
| const result = await strategy.tryFix(blockade, originalInput, context); | |
| if (result !== null) { | |
| try { | |
| // Gap-5: scrivi last_winner + incrementa win_count per scoring persistente | |
| _deps.memorySetFn("adaptive:" + blockade.kind + ":last_winner", strategy.id, "adaptive_solver"); | |
| const _prevCounts: Record<string,number> = (() => { | |
| try { const r = _deps.memoryGetFn("adaptive:" + blockade.kind + ":win_counts"); return r ? JSON.parse(r) as Record<string,number> : {}; } catch { return {}; } | |
| })(); | |
| _prevCounts[strategy.id] = (_prevCounts[strategy.id] ?? 0) + 1; | |
| _deps.memorySetFn("adaptive:" + blockade.kind + ":win_counts", JSON.stringify(_prevCounts), "adaptive_solver"); | |
| } catch { /* fire-and-forget */ } | |
| return { | |
| success: !result.startsWith("❌"), | |
| output: result, | |
| strategy: strategy.id, | |
| description: blockade.hint + " → " + strategy.label, | |
| }; | |
| } | |
| } catch { continue; } | |
| } | |
| return { | |
| success: false, | |
| output: "⚠️ " + blockade.hint + "\nNessuna strategia alternativa ha avuto successo.", | |
| strategy: "exhausted", | |
| description: "Blocco \"" + blockade.kind + "\" — strategie esaurite.", | |
| }; | |
| } | |
| detect(errorMessage: string): Blockade { | |
| return detectBlockade(errorMessage); | |
| } | |
| strategiesFor(kind: BlockadeKind): string[] { | |
| return STRATEGIES.filter(s => s.handles.includes(kind)).map(s => s.label); | |
| } | |
| } | |
| export const adaptiveSolver = new AdaptiveSolverImpl(); | |
| export default adaptiveSolver; | |
| // ─── System prompt snippet ──────────────────────────────────────────────────── | |
| /** | |
| * Insegna il pattern adattivo all'LLM. | |
| * Viene iniettato nel system prompt di agentLoop.ts (S50). | |
| */ | |
| export const ADAPTIVE_SOLVER_SYSTEM_HINT = ` | |
| ## PROBLEM SOLVING ADATTIVO — Regola fondamentale | |
| Quando un'azione fallisce per un blocco specifico, NON ripetere la stessa azione. | |
| Identifica il blocco e usa l'alternativa: | |
| | Blocco rilevato | Alternativa da usare | | |
| |---|---| | |
| | Operazione push bloccata | API REST: fetch su api.github.com (blobs → trees → commits → refs) | | |
| | fetch CORS bloccato | proxy: allorigins.win, codetabs.com (corsproxy.io rimosso: 403) | | |
| | file not found (404/ENOENT) | usa read_file con path esatto da vfs.list() | | |
| | rate limit (429) | aspetta 2s, riprova o cambia endpoint | | |
| | operazione negata (403/401) | cerca API equivalente browser-safe o REST alternativa | | |
| | storage quota superata | chunking — dividi dati in parti da max 3000 chars | | |
| | errore di sintassi | identifica riga esatta, correggi solo quella, non riscrivere tutto | | |
| | errore di rete generico | retry dopo 1.5s con timeout 15s, poi usa proxy | | |
| Comunica sempre: "X bloccato → uso Y invece" prima di eseguire l'alternativa. | |
| `.trim(); | |