Dev.to · 9 min read

Perché Claude riceve un ticket di coda invece di un risultato quando avvia un Graph di osFoundry?

Perché Claude riceve un ticket di coda invece di un risultato quando avvia un Graph di osFoundry?

Questo è il tool che una chat di Claude chiama per avviare un Graph in osFoundry, così come lo elenca l'endpoint MCP pubblico. Recuperato con un tools/list non autenticato il 2026-11-08 e accorciato: {"name": "run_graph_session", "description": "Queue a Graph run on the owner's online desktop in a connected project. Retrieve progress and result with get_graph_run. The desktop can go offline, and local tool approvals still apply.", "inputSchema": { "properties": { "graph_session_id": {"type": "string"}, "arguments": {"type": "object"}, "idempotency_key": {"type": "string", "minLength": 8, "maxLength": 128}}, "required": ["graph_session_id", "idempotency_key"], "additionalProperties": false}, "annotations": {"readOnlyHint": false, "destructiveHint": true}, "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:run"]}]} Ha tre fratelli. list_graph_sessions (project_id, limit 1–50) e get_graph_run (command_id) sono in sola lettura sotto osfoundry:read. cancel_graph_run (command_id) sta sotto osfoundry:run, come la chiamata di avvio. Due risposte reali da un account collegato, ID rimossi: list_graph_sessions {"project_id": "…"} → {"project_id": "…", "sessions": []} get_graph_run {"command_id": ""} → error: Graph run not found or access removed La lista vuota è il default. Lì non compare nulla finché il proprietario non condivide una sessione dal desktop. Il verbo nella descrizione è "Queue": il tool risponde prima che il Graph sia partito, e il resto di questo post spiega perché, e quanto costa all'agente dall'altra parte. Cos'è una sessione di Graph, e perché il server non può semplicemente eseguirla? Un Graph è un programma JavaScript salvato che il motore fleet del desktop esegue: percorsi condizionali, agenti in parallelo, retry limitati, chiamate ad altri Graph. In apps/web/src-tauri/src/graph_config.rs, run_graph_now è il comando Tauri usato dalla schermata Graph. Passa lo script a run_fleet_script con un tetto rigido di token che rifiuta l'ondata successiva di agenti appena viene raggiunto. Una sessione di Graph è l'identità persistente di un Graph salvato su un dispositivo, gs_ più 32 caratteri esadecimali, nella tabella locale graph_sessions. Il motore, gli agenti che lo script avvia, i modelli locali, le cartelle e le impostazioni di approvazione per tool stanno tutti nel processo del desktop. Un runner lato server avrebbe bisogno di una copia di tutto. L'indice delle funzionalità lo dice chiaramente: leggere i Graph funziona nel browser, eseguirli richiede il desktop. Come arriva una richiesta a un desktop a cui nessuno può connettersi? Non arriva. È il desktop a chiedere lavoro. apps/web/src/hooks/useConnectedGraphRelay.ts, montato una sola volta nella finestra principale del desktop, fa questo: Ogni 20 secondi (e subito dopo una modifica alla condivisione), salta il giro se quello precedente è ancora occupato. Rinvia le completion di una outbox locale che l'ultima volta non sono arrivate al server. Per ogni progetto abilitato dal proprietario, fa POST /external-assistants/graph-relays/sync con i metadati delle sessioni spuntate per quel progetto, al massimo 100: nome, descrizione, slug, versione, stato. La risposta contiene i comandi in coda per quel progetto. Per ogni comando, ricontrolla che il progetto sia ancora abilitato, che la sessione sia ancora spuntata e che abbia ancora lo stesso slug. Altrimenti lo chiude come cancelled con "Graph session is no longer shared with this project". Altrimenti lo esegue tramite run_graph_now sul canale graph:external:, con un poll di controllo di 3 secondi per l'annullamento, poi pubblica il risultato limitato. Quindi list_graph_sessions mostra quello che ha caricato l'ultima sync, e run_graph_session scrive una riga che il desktop troverà al giro successivo. Gli ID dei comandi sono UUID. ConnectedGraphProjectBinding rifiuta qualsiasi altra cosa prima di scrivere la riga canale-progetto che archivia il run sotto il progetto collegato. La ricostruzione Su questo Mac non gira nessun binario di Claude, quindi sim.py interpreta tutte e tre le parti su un orologio simulato. La metà desktop è un port del relay qui sopra: poll di 20 s, salto se occupato, comandi in serie, ricontrollo prima dell'esecuzione, poll di controllo di 3 s, outbox, il limite di 100,000 caratteri sul risultato. La metà server è un sostituto, perché il suo codice non è pubblico. Implementa quello che dicono le descrizioni dei tool: una coda, risultati leggibili solo dall'agente che ha avviato il run, annullamento diverso per run in coda e attivi. Implementa anche il comportamento di idempotenza richiesto dalla guida CLI di osFoundry ("reuse idempotency keys after a lost response"). Il client è un Claude con script. I tempi di esecuzione dei Graph sono sintetici: lognormale, mediana 60 s, minimo 5 s. Ogni esperimento usa 500–2,000 run con seed, e la fase del relay è casuale rispetto alla richiesta. Python 3.9.6 su un Apple M1, 2.8 s di tempo reale per tutto. L'output è in code/run.out. Quanto resta in coda un run? pickup wait (queued->started), n=2000: mean 9.8 s, median 9.8 s, p95 18.9 s, max 20.0 s Fino a un intervallo di poll, metà in media. È il prezzo di un desktop che fa solo connessioni in uscita. Non tiene porte aperte né socket di lunga durata, e un portatile chiuso è solo un relay che non ha ancora fatto poll. L'altro costo è meno evidente. Il relay esegue i comandi uno dopo l'altro e salta il proprio timer finché è occupato. Cinque run messi in coda insieme finiscono in sequenza: Run in coda insieme Primo risultato (mediana) Ultimo risultato (mediana) Ultimo risultato (p95) 1 74 s 74 s 254 s 3 73 s 233 s 483 s 5 75 s 399 s 776 s Un agente che lancia cinque run di Graph in parallelo per risparmiare tempo ottiene la tabella di marcia più lenta possibile. Un desktop è un solo worker. Se vuoi parallelismo, mettilo dentro il Graph, dove il motore ha max_concurrency e un tetto di token apposta. Ogni quanto l'agente dovrebbe chiamare get_graph_run? Non c'è nemmeno un canale push verso Claude. Ogni strategia scambia chiamate contro il ritardo con cui l'agente si accorge del risultato. 2,000 run per riga: Strategia di poll del client Chiamate per run (media) p95 Ritardo nel notare il risultato (media) p95 ogni 2 s 47.1 118 1.0 s 1.9 s ogni 5 s 19.1 48 2.5 s 4.8 s ogni 15 s 6.7 16 7.5 s 14.2 s ogni 30 s 3.6 8 14.9 s 28.4 s backoff 2, 4, 8 … 30 s 6.6 11 14.8 s 28.4 s attesa di 20 s, poi ogni 10 s 8.8 23 5.0 s 9.6 s Fare poll ogni 2 s costa 47 chiamate a tool, quindi 47 passaggi nel contesto del modello, per risparmiare qualche secondo su un job che dura un minuto. Il backoff esponenziale sembra furbo e qui perde: fa tante chiamate quanto un poll fisso a 15 s e nota il risultato con il doppio del ritardo, perché quando un Graph tipico finisce è già al suo tetto di 30 s. L'ultima riga usa quello che ti dice il relay. Nel primo intervallo di poll non può partire niente comunque, quindi aspettare circa un intervallo prima del primo controllo non costa quasi nulla. Cosa ferma davvero l'annullamento? cancel at a random 0-40 s after queueing, n=2000: still queued 523, already running 1370 queued : terminal after 0.0 s (graph never started) running: terminal after mean 2.0 s, p95 3.3 s, max 3.5 s Annullare un run in coda è un cambio di stato lato server. Il desktop non vede mai il comando e non si spende nessun token. Annullare un run in esecuzione è un flag che il desktop legge al suo poll di controllo di 3 secondi, dopodiché chiama fleet_cancel_run con l'ID del run ricevuto dall'evento fleet_started. Il relay invia quell'annullamento una sola volta e mantiene l'esito come cancelled, anche se il Graph riesce a finire tra il flag e lo stop. A cosa serve la chiave di idempotenza? idempotency_key è obbligatoria, 8–128 caratteri. 200 run per ripetizione, con il 30% delle risposte perse in modo che il client ritenti: Comportamento al retry Chiamate inviate Run di Graph in coda chiave nuova a ogni retry 266 / 302 / 286 266 / 302 / 286 stessa chiave al retry 270 / 291 / 275 200 / 200 / 200 Per un Graph che scrive bozze di email o file, un duplicato non è un minuto sprecato, è una seconda serie di effetti collaterali. Per questo la chiave è obbligatoria nello schema e il tool è marcato destructiveHint: true. E se il desktop va offline? desktop offline 600 s: statuses ['queued'], executed 0 back online: first start +3 s, all 5 ['completed'] by +131 s, executed 5 completion POST lost once, outbox=on : status completed, delivered 40 s after start completion POST lost once, outbox=off: status running, never delivered Il lavoro in coda sopravvive a un desktop offline, perché la coda sta sul server. Il punto debole è il ritorno. Se il desktop ha finito un Graph e il suo POST si è perso, un relay che tenesse il risultato solo in memoria lascerebbe il run bloccato su running per sempre. Il relay vero scrive ogni completion in localStorage prima di pubblicarla e la rimuove solo dopo un 2xx, così il giro successivo, 20 s dopo, la consegna. Quanto può essere grande il risultato? Il relay pubblica {execution_id, status, result, total_tokens, reported_cost} se il suo JSON.stringify è al massimo di 100,000 caratteri. Altrimenti pubblica {execution_id, status, result_truncated: true}. Gli errori vengono tagliati a 2,000 caratteri. Il limite conta caratteri, non byte: result text that still fits (ascii): 99,899 chars = 99,899 UTF-8 bytes result text that still fits (japanese): 99,899 chars = 299,697 UTF-8 bytes Il taglio è tutto o niente. Non c'è un mezzo risultato da fraintendere, e il record completo del run resta nella cronologia del desktop sotto quell'execution_id. In ogni caso è molto testo da mettere davanti a un modello, quindi un Graph pensato per essere chiamato da un agente dovrebbe finire con un riassunto, non con un dump. Chi altro può vedere il run? another agent reads the run: {'error': 'Graph run not found or access removed'} Nel sostituto, una seconda connessione che chiede il command_id di Claude riceve lo stesso errore che il server reale ha restituito per un ID inventato. get_graph_run e cancel_graph_run sono entrambi descritti come tool che agiscono sui run "started by this Connected agent". Non distinguere tra "non è tuo" e "non esiste" significa che un ID trapelato non conferma niente. Se lo implementi tu Lascia che sia la macchina che possiede i tool ad andare a prendere il lavoro. Un poll in uscita sopravvive a NAT, sospensione e portatili chiusi. Scegli l'intervallo sapendo che ogni chiamante ne aspetta in media la metà. Restituisci un ticket dalla chiamata di avvio, e scrivi nella descrizione del tool quale tool legge il risultato. Il modello non può dedurre una coda dal nome. Metti il primo controllo di stato a circa un intervallo di presa in carico. Il poll fisso batte il backoff esponenziale quando la durata dei job si concentra intorno al minuto. Rendi obbligatoria una chiave di idempotenza sulla chiamata di avvio. Con job che hanno effetti collaterali, una richiesta ritentata senza chiave è un secondo run. Persisti le completion sul worker prima di inviarle. Il risultato è la parte del giro di andata e ritorno che non puoi ricalcolare a basso costo. Ricontrolla il consenso sul worker subito prima di eseguire, non solo quando il job entra in coda. La condivisione può cambiare nel frattempo. La definizione breve di osFoundry del protocollo sottostante è nella sua voce di glossario su MCP, e lo schema più ampio a cui appartengono i Graph nella voce sull'orchestrazione di agenti.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News