@mmmbuto/nexuscrew 0.8.52 → 0.8.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@
11
11
  <meta name="apple-mobile-web-app-title" content="NexusCrew" />
12
12
  <link rel="manifest" href="/manifest.json" />
13
13
  <title>NexusCrew</title>
14
- <script type="module" crossorigin src="/assets/index-Cxwpa2Y_.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-BJ-_5bxw.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-CYi_lhCg.css">
16
16
  </head>
17
17
  <body>
@@ -1 +1 @@
1
- {"version":"0.8.52"}
1
+ {"version":"0.8.53"}
@@ -7,6 +7,7 @@ const { execFileSync, spawn } = require('node:child_process');
7
7
  const fs = require('node:fs');
8
8
  const path = require('node:path');
9
9
  const net = require('node:net');
10
+ const crypto = require('node:crypto');
10
11
  const { detectPlatform, nodeBin, repoRoot, uid } = require('./platform.js');
11
12
  const { installPath: serviceInstallPath, ensureLinuxTmuxSurvival } = require('./service.js');
12
13
  const { fleetInstallPath } = require('./fleet-service.js');
@@ -37,7 +38,8 @@ Usage:
37
38
  nexuscrew boot enable startup at boot (use: boot off|status)
38
39
  nexuscrew status show service, port, roles and node status
39
40
  nexuscrew stop stop the background service
40
- nexuscrew restart restart the background service
41
+ nexuscrew restart restart the background service (verifies it came back)
42
+ nexuscrew autoupdate turn automatic updates on or off (use: autoupdate on|off|status)
41
43
  nexuscrew doctor run local diagnostics
42
44
  nexuscrew nodes list and manage connected peers (use: nodes help)
43
45
  nexuscrew help show this help
@@ -544,6 +546,25 @@ async function probeNexusCrew(port, token, opts = {}) {
544
546
  } catch (_) { return false; }
545
547
  }
546
548
 
549
+ // probeNexusCrew restituisce un BOOLEANO e collassa ogni non-200 su `false`:
550
+ // per i suoi molti chiamanti va bene, e cambiarne il contratto ripercuoterebbe
551
+ // ovunque. Ma «non risponde» e «risponde 401» sono due cose opposte — un 401
552
+ // dice che il servizio E' IN PIEDI e che il token locale non vale — e chi deve
553
+ // spiegare un riavvio ha bisogno di distinguerle, altrimenti dichiara fallito
554
+ // un riavvio riuscito e manda a cercare un processo morto che e' vivo.
555
+ // Rilievo dell'audit: il caso token-assente era gia' coperto, questo no.
556
+ async function probeNexusCrewStatus(port, token, opts = {}) {
557
+ const fetchImpl = opts.fetchImpl || globalThis.fetch;
558
+ if (typeof fetchImpl !== 'function') return null;
559
+ try {
560
+ const r = await fetchImpl(`http://127.0.0.1:${port}/api/config`, {
561
+ headers: { authorization: `Bearer ${token || ''}` },
562
+ signal: AbortSignal.timeout?.(700),
563
+ });
564
+ return r.status;
565
+ } catch (_) { return null; } // nessuna risposta: e' un altro guasto
566
+ }
567
+
547
568
  async function waitForNexusCrew(port, token, opts = {}) {
548
569
  const probe = opts.probeImpl || probeNexusCrew;
549
570
  const attempts = opts.waitAttempts === undefined ? 30 : opts.waitAttempts;
@@ -811,6 +832,113 @@ function logs(opts = {}) {
811
832
  return { platform, bin, args, follow, keepAlive: true };
812
833
  }
813
834
 
835
+ // autoupdate on|off|status — l'aggiornamento automatico si spegne anche da qui.
836
+ //
837
+ // PERCHE' NEL CLI, visto che la casella esiste gia' in Settings. Perche' il
838
+ // momento in cui serve spegnerlo e' quello in cui la PWA non si apre: il nodo
839
+ // si e' aggiornato, il servizio non e' tornato su, e la riga di comando e'
840
+ // l'unica superficie rimasta. Un controllo che vive solo dove il guasto lo
841
+ // rende irraggiungibile e' mezzo assente.
842
+ //
843
+ // A SERVIZIO ACCESO PASSA DALL'API, e non e' un dettaglio: scrivere il file
844
+ // mentre il processo vive lascerebbe il manager in memoria con il vecchio
845
+ // valore, e continuerebbe ad aggiornare all'ora prevista. Un interruttore che
846
+ // risulta spento e non spegne e' peggio di un interruttore che manca. La route
847
+ // invece persiste E chiama `setEnabled`, quindi vale subito.
848
+ function autoUpdateCommand(args, opts = {}) {
849
+ const log = opts.log || console.log;
850
+ const azione = String(args[0] || 'status').toLowerCase();
851
+ if (!['on', 'off', 'status'].includes(azione)) {
852
+ log('uso: nexuscrew autoupdate on|off|status');
853
+ return { code: 1 };
854
+ }
855
+ const { configPath, tokenPath } = urlmod.resolvePaths(opts);
856
+ const port = urlmod.loadPort(opts);
857
+ const token = urlmod.readToken(tokenPath);
858
+ const attivo = (opts.isServiceRunningImpl || isServiceRunning)({ ...opts });
859
+ const fetchImpl = opts.fetchImpl || fetch;
860
+
861
+ const daFile = () => {
862
+ try {
863
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
864
+ return cfg.autoUpdate !== false;
865
+ } catch (_) { return true; } // default ON, come la configurazione
866
+ };
867
+
868
+ if (azione === 'status') {
869
+ // A servizio acceso vince cio' che il processo sta DAVVERO facendo, non
870
+ // cio' che il file dice: se i due divergono, e' il primo a spegnere o
871
+ // accendere gli aggiornamenti.
872
+ if (!attivo) {
873
+ log(`autoupdate: ${daFile() ? 'on' : 'off'} (da configurazione; servizio non attivo)`);
874
+ return { code: 0 };
875
+ }
876
+ return fetchImpl(`http://127.0.0.1:${port}/api/settings`, {
877
+ headers: { authorization: `Bearer ${token}` },
878
+ }).then(async (r) => {
879
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
880
+ const j = await r.json();
881
+ const acceso = j.autoUpdate !== false;
882
+ log(`autoupdate: ${acceso ? 'on' : 'off'}`);
883
+ if (j.update && j.update.latest) log(` ultima versione vista su npm latest: ${j.update.latest}`);
884
+ return { code: 0 };
885
+ }).catch((e) => {
886
+ log(`autoupdate: ${daFile() ? 'on' : 'off'} (da configurazione; servizio non interrogabile: ${e.message})`);
887
+ return { code: 0 };
888
+ });
889
+ }
890
+
891
+ const acceso = azione === 'on';
892
+ if (!attivo) {
893
+ // Servizio spento: si scrive il file, e si DICE che vale al prossimo avvio.
894
+ // Tacerlo lascerebbe credere che abbia gia' effetto.
895
+ let cfg = {};
896
+ try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch (_) { /* file nuovo */ }
897
+ // Scrittura atomica e rifiuto dei symlink, come fa la route che scrive lo
898
+ // stesso file: tmp nella stessa directory, 0600, rename. Un `writeFileSync`
899
+ // diretto puo' lasciare un config.json troncato se il processo muore a
900
+ // meta' — e un config.json illeggibile e' un nodo che non riparte, cioe'
901
+ // proprio il guasto che questo comando serve a evitare. Rilievo dell'audit.
902
+ try {
903
+ if (fs.lstatSync(configPath).isSymbolicLink()) {
904
+ log('autoupdate: config.json e\' un symlink, non lo scrivo');
905
+ return { code: 1 };
906
+ }
907
+ } catch (e) { if (e.code !== 'ENOENT') throw e; }
908
+ const dir = path.dirname(configPath);
909
+ fs.mkdirSync(dir, { recursive: true });
910
+ const tmp = path.join(dir, `.${path.basename(configPath)}.${crypto.randomBytes(6).toString('hex')}.tmp`);
911
+ try {
912
+ fs.writeFileSync(tmp, `${JSON.stringify({ ...cfg, autoUpdate: acceso }, null, 2)}\n`, { mode: 0o600 });
913
+ fs.chmodSync(tmp, 0o600);
914
+ fs.renameSync(tmp, configPath);
915
+ } catch (e) {
916
+ try { fs.unlinkSync(tmp); } catch (_) { /* best-effort */ }
917
+ throw e;
918
+ }
919
+ log(`autoupdate: ${azione} (scritto in configurazione; vale al prossimo avvio del servizio)`);
920
+ return { code: 0 };
921
+ }
922
+ return fetchImpl(`http://127.0.0.1:${port}/api/settings/config`, {
923
+ method: 'POST',
924
+ headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
925
+ body: JSON.stringify({ autoUpdate: acceso }),
926
+ }).then(async (r) => {
927
+ if (!r.ok) {
928
+ const j = await r.json().catch(() => ({}));
929
+ log(`autoupdate: non applicato — ${j.error || `HTTP ${r.status}`}`);
930
+ return { code: 1 };
931
+ }
932
+ log(`autoupdate: ${azione} (applicato subito e salvato)`);
933
+ return { code: 0 };
934
+ }).catch((e) => {
935
+ log(`autoupdate: non applicato — ${e.message}`);
936
+ log(' il servizio risulta attivo ma non risponde: non scrivo il file, perche\' resterebbe');
937
+ log(' un valore che il processo vivo non conosce.');
938
+ return { code: 1 };
939
+ });
940
+ }
941
+
814
942
  // update: npm i -g @latest + restart se attivo. Fallimento npm -> messaggio chiaro, code 1.
815
943
  function update(opts = {}) {
816
944
  const execImpl = opts.execImpl || execFileSync;
@@ -1158,9 +1286,134 @@ function dispatch(argv, opts = {}) {
1158
1286
  const result = stop({ ...opts, log });
1159
1287
  return { code: result.stopped || ['not running', 'stale pidfile'].includes(result.reason) ? 0 : 1 };
1160
1288
  }
1289
+ if (cmd === 'autoupdate') {
1290
+ return autoUpdateCommand(rest.slice(1), { ...opts, log });
1291
+ }
1161
1292
  if (cmd === 'restart') {
1162
- const result = restart({ ...opts, log });
1163
- return { code: result.restarted ? 0 : 1 };
1293
+ // Seam come negli altri punti del file (`opts.restartImpl || restart`):
1294
+ // serve a poter dichiarare in prova QUALE runtime e' in piedi, perche' il
1295
+ // ritentativo si comporta in modo diverso fra gestito e portatile — e senza
1296
+ // il seam un test finisce per esercitare il ramo che non intendeva.
1297
+ const result = (opts.restartImpl || restart)({ ...opts, log });
1298
+ if (!result.restarted) return { code: 1 };
1299
+ // IL RIAVVIO ORA VERIFICA DI ESSERE TORNATO SU, e prima no. `restart`
1300
+ // dichiarava successo appena `systemctl restart` (o launchctl, o l'avvio
1301
+ // portatile) ritornava: quella e' la conferma che il COMANDO e' partito,
1302
+ // non che il servizio risponda. Su Termux, il 2026-08-07, ha restituito 0
1303
+ // con il servizio morto, e il nodo e' rimasto giu' oltre quattro ore senza
1304
+ // che nessuno lo sapesse — da fuori si vedeva solo un KO generico.
1305
+ //
1306
+ // La verifica esisteva gia' e veniva usata dal percorso di auto-update e
1307
+ // dal bootstrap Fleet («serve un restart verificato»); mancava proprio sul
1308
+ // comando che una persona digita a mano. Con l'auto-update acceso questo
1309
+ // riavvio avviene DA SOLO su ogni nodo, quindi un esito non verificato si
1310
+ // moltiplica per la flotta.
1311
+ //
1312
+ // Il ramo restituisce una PROMESSA e `dispatch` resta sincrona: e' il
1313
+ // disegno che il file ha gia' (dispatchNodes fa lo stesso) e il chiamante
1314
+ // avvolge in `Promise.resolve`. Cambiare la firma di dispatch avrebbe
1315
+ // rotto ottanta test che la chiamano in modo sincrono, per una verifica
1316
+ // che riguarda un comando solo.
1317
+ const { tokenPath } = urlmod.resolvePaths(opts);
1318
+ const port = urlmod.loadPort(opts);
1319
+ const token = urlmod.readToken(tokenPath);
1320
+ // SENZA TOKEN NON SI PUO' DIRE NIENTE SULLA SALUTE, e soprattutto non si
1321
+ // deve dire qualcosa di sbagliato: la sonda fallirebbe per autenticazione e
1322
+ // il messaggio incolperebbe la porta o il processo, mandando a cercare dove
1323
+ // il problema non e'. Rilievo dell'audit. Il riavvio e' comunque partito,
1324
+ // quindi non e' un fallimento: e' una verifica che non si e' potuta fare, e
1325
+ // si dice cosi'.
1326
+ if (!token) {
1327
+ log('restart: comando eseguito, ma la salute NON e\' verificabile senza token locale.');
1328
+ log(' Controlla a mano che il servizio risponda.');
1329
+ return { code: 0 };
1330
+ }
1331
+ const attesa = (sano) => {
1332
+ if (sano) {
1333
+ log(`restart: servizio verificato su 127.0.0.1:${port}`);
1334
+ return { code: 0 };
1335
+ }
1336
+ // NON E' ANCORA UN FALLIMENTO: si prova a capire PERCHE' e, se e' la
1337
+ // causa nota, si rimedia una volta sola.
1338
+ //
1339
+ // LA CAUSA NOTA. Sul percorso portatile — quello di Termux, dove non c'e'
1340
+ // un gestore di servizi che rialzi il processo — `restart` avvia il
1341
+ // nuovo processo SUBITO dopo aver fermato il vecchio, senza aspettare che
1342
+ // muoia ne' che la porta si liberi. Il nuovo non riesce ad ascoltare,
1343
+ // esce, e nessuno se ne accorge: servizio giu', tunnel su (e' un
1344
+ // processo SSH separato), nessun auto-recupero. Misurato il 2026-08-07:
1345
+ // oltre venti minuti in quello stato.
1346
+ //
1347
+ // Il percorso dell'AUTO-UPDATE fa gia' la cosa giusta — aspetta che il
1348
+ // processo sia morto E che la porta sia libera, fino a sei secondi, e
1349
+ // solo allora avvia. Le due strade erano divergenti, e quella che una
1350
+ // persona digita a mano era la meno prudente.
1351
+ //
1352
+ // PRIMA DI DARE LA COLPA A QUALCUNO, si guarda se il servizio risponde
1353
+ // affatto. Un 401 significa che E' IN PIEDI e che il token locale non
1354
+ // vale: il riavvio e' riuscito, e dichiararlo fallito manderebbe a
1355
+ // cercare un processo morto che invece e' vivo.
1356
+ return (opts.probeStatusImpl || probeNexusCrewStatus)(port, token, opts).then((stato) => {
1357
+ if (stato === 401 || stato === 403) {
1358
+ log(`restart: il servizio RISPONDE su 127.0.0.1:${port}, ma il token locale non e' valido.`);
1359
+ log(' Il riavvio e\' riuscito; e\' la credenziale a non funzionare.');
1360
+ log(' Rigenera il token locale, poi riprova a collegarti.');
1361
+ return { code: 1 };
1362
+ }
1363
+ return continua();
1364
+ });
1365
+ };
1366
+ const continua = () => {
1367
+ // SOLO SUL RUNTIME PORTATILE. E' li' che manca un supervisore — ed e'
1368
+ // esattamente la ragione per cui il difetto esiste: su Termux nessuno
1369
+ // rialza il processo. Dove il servizio e' gestito (systemd, launchd) il
1370
+ // supervisore c'e' ed e' suo il compito: avviare noi un processo
1371
+ // portatile accanto significherebbe metterne in piedi uno che il gestore
1372
+ // non conosce, mentre il gestore puo' rialzare la propria unita' — due
1373
+ // processi sulla stessa porta, e il nostro sopravvivrebbe allo stop del
1374
+ // servizio. Li' si riferisce e basta.
1375
+ //
1376
+ // Difetto mio, trovato rileggendo prima dell'audit: la prima stesura
1377
+ // chiamava `startPortable` in ogni caso.
1378
+ if (result.runtimeOwner === 'managed') {
1379
+ log(`restart: il servizio NON risponde su 127.0.0.1:${port} dopo il riavvio.`);
1380
+ log(' Il runtime e\' gestito dal servizio di sistema: non avvio un processo accanto.');
1381
+ log(' Controlla lo stato e i log dell\'unita\' di sistema.');
1382
+ return { code: 1 };
1383
+ }
1384
+ // UN SOLO RITENTATIVO, e solo a porta libera. Se la porta e' ancora
1385
+ // occupata il problema e' un altro (un processo che non muore) e
1386
+ // riprovare lo nasconderebbe; se e' libera e il servizio non c'e', il
1387
+ // nuovo processo e' uscito e riavviarlo e' esattamente il rimedio.
1388
+ // Ripetere all'infinito trasformerebbe un guasto in un ciclo.
1389
+ return (opts.portAvailableImpl || portAvailable)(port, '127.0.0.1').then((libera) => {
1390
+ if (!libera) {
1391
+ log(`restart: il servizio NON risponde su 127.0.0.1:${port} e la porta e' ancora occupata.`);
1392
+ log(' Qualcosa tiene la porta senza servire: non riavvio a vuoto.');
1393
+ log(' Controlla i log del servizio e i processi rimasti.');
1394
+ return { code: 1 };
1395
+ }
1396
+ log(`restart: nessun servizio su ${port} e porta libera — il processo e' uscito. Riprovo una volta.`);
1397
+ (opts.startPortableImpl || startPortable)({ ...opts, spawnImpl: opts.spawnImpl });
1398
+ return (opts.waitForRuntimeImpl || waitForNexusCrew)(port, token, {
1399
+ ...opts, waitAttempts: opts.waitAttempts === undefined ? 60 : opts.waitAttempts,
1400
+ waitDelayMs: opts.waitDelayMs === undefined ? 250 : opts.waitDelayMs,
1401
+ }).then((sanoOra) => {
1402
+ if (sanoOra) {
1403
+ log(`restart: servizio verificato su 127.0.0.1:${port} al secondo tentativo`);
1404
+ return { code: 0 };
1405
+ }
1406
+ log(`restart: il servizio NON risponde su 127.0.0.1:${port} nemmeno dopo un secondo avvio.`);
1407
+ log(' Il comando di riavvio e\' partito, il processo non resta su.');
1408
+ log(' Controlla i log del servizio; su Termux puo\' servire riavviare il dispositivo.');
1409
+ return { code: 1 };
1410
+ });
1411
+ });
1412
+ };
1413
+ return (opts.waitForRuntimeImpl || waitForNexusCrew)(port, token, {
1414
+ ...opts, waitAttempts: opts.waitAttempts === undefined ? 60 : opts.waitAttempts,
1415
+ waitDelayMs: opts.waitDelayMs === undefined ? 250 : opts.waitDelayMs,
1416
+ }).then(attesa);
1164
1417
  }
1165
1418
  // Internal runtime commands used by service managers and MCP clients. They
1166
1419
  // are intentionally omitted from HELP and are not configuration surfaces.
package/lib/mcp/server.js CHANGED
@@ -215,10 +215,44 @@ function createMcpServer(opts = {}) {
215
215
  });
216
216
  } catch (e) { throw transportError(baseUrl, e); }
217
217
  const j = await r.json().catch(() => ({}));
218
- if (!r.ok) throw new Error(j.error ? `API ${r.status}: ${j.error}` : `API ${r.status}`);
218
+ if (!r.ok) {
219
+ const base = j.error ? `API ${r.status}: ${j.error}` : `API ${r.status}`;
220
+ throw new Error(base + await disallineamentoDiVersione());
221
+ }
219
222
  return j;
220
223
  }
221
224
 
225
+ // NC-R. Aggiornare NexusCrew NON aggiorna il bridge MCP delle celle gia' in
226
+ // piedi: quel processo e' partito col codice di prima e ci resta fino al
227
+ // riavvio della cella. Il sintomo e' crudele — si installa una correzione, si
228
+ // riprova, e si riceve l'errore VECCHIO — e chi lo subisce conclude che la
229
+ // correzione non funziona. E' successo il 2026-08-07 su rc.26, a me, e ci ho
230
+ // messo un giro intero a capirlo.
231
+ //
232
+ // Il momento in cui serve saperlo e' esattamente quello in cui qualcosa
233
+ // fallisce, quindi la verifica sta SOLO sul ramo d'errore: a regime non costa
234
+ // niente, e non si puo' nemmeno mettere in cache all'avvio — la versione che
235
+ // cambia e' quella dell'hub, e cambia proprio mentre questo processo vive.
236
+ //
237
+ // Non trasforma mai un errore in un altro: se la verifica fallisce, l'errore
238
+ // originale esce come sarebbe uscito comunque.
239
+ async function disallineamentoDiVersione() {
240
+ try {
241
+ const r = await fetchImpl(`${baseUrl}/api/config`, {
242
+ headers: { authorization: `Bearer ${readToken()}` },
243
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
244
+ });
245
+ if (!r.ok) return '';
246
+ const cfg = await r.json();
247
+ if (typeof cfg.version !== 'string' || cfg.version === VERSION) return '';
248
+ return `\n\nNOTA: questo bridge MCP e' la versione ${VERSION}, l'hub e' la ${cfg.version}.`
249
+ + ' Aggiornare NexusCrew non aggiorna il bridge di una cella gia\' avviata:'
250
+ + ' riavvia questa cella se ti aspettavi un comportamento diverso.';
251
+ } catch (_) {
252
+ return '';
253
+ }
254
+ }
255
+
222
256
  const ctx = {
223
257
  session,
224
258
  identity,
@@ -351,8 +351,35 @@ async function probeHealth({ port, token, expectedInstanceId = null, fetchImpl =
351
351
  } catch (e) {
352
352
  out.transport = 'down';
353
353
  out.status = 'down';
354
- out.detail = (e && (e.name === 'AbortError' || e.code === 'ETIMEDOUT'))
355
- ? `peer non raggiungibile (timeout ${timeoutMs}ms)` : 'peer non raggiungibile (tcp refused/down)';
354
+ // DUE GUASTI DIVERSI, e prima avevano lo stesso messaggio. Con un canale
355
+ // inverso SSH il listener sull'hub e' sshd: se il dispositivo non e'
356
+ // connesso non c'e' nessun listener e la connessione viene RIFIUTATA; se
357
+ // invece il tunnel regge ma NexusCrew sul dispositivo e' morto, sshd
358
+ // accetta, inoltra, e la connessione viene AZZERATA dall'altro capo.
359
+ //
360
+ // MISURATO il 2026-08-07 sui tre casi reali contemporaneamente presenti:
361
+ // tunnel su + servizio giu' -> ECONNRESET
362
+ // nessun listener -> ECONNREFUSED
363
+ // tutto su -> HTTP (401)
364
+ //
365
+ // Perche' vale la pena distinguerli: «peer non raggiungibile» ha mandato
366
+ // l'indagine nella federazione e nel pairing per quattro ore, mentre il
367
+ // difetto era un servizio che non era ripartito sul dispositivo. Sono due
368
+ // guasti con due rimedi diversi — uno si risolve sulla rete, l'altro
369
+ // andando sul dispositivo — e un messaggio unico li fa cercare nel posto
370
+ // sbagliato la meta' delle volte.
371
+ const codice = (e && (e.code || (e.cause && e.cause.code))) || '';
372
+ if (e && (e.name === 'AbortError' || codice === 'ETIMEDOUT')) {
373
+ out.detail = `peer non raggiungibile (timeout ${timeoutMs}ms)`;
374
+ } else if (codice === 'ECONNREFUSED') {
375
+ out.detail = `canale inverso non attivo sulla porta ${port}: il dispositivo non e' connesso`;
376
+ out.layer = 'tunnel';
377
+ } else if (codice === 'ECONNRESET' || codice === 'ECONNABORTED' || codice === 'EPIPE') {
378
+ out.detail = `canale inverso attivo sulla porta ${port}, ma NexusCrew non risponde sul dispositivo`;
379
+ out.layer = 'service';
380
+ } else {
381
+ out.detail = 'peer non raggiungibile (tcp refused/down)';
382
+ }
356
383
  return out;
357
384
  } finally {
358
385
  if (timer) clearTimeout(timer);
@@ -157,7 +157,39 @@ async function submitToSession(tmuxBin, session, text, opts = {}) {
157
157
  // inside the composer. C-e is sent as its own tmux command after the paste,
158
158
  // then the pane is revalidated before the separate Enter. Other clients
159
159
  // still receive a short separation between paste and submit.
160
- await delay(codexComposer ? 400 : 150);
160
+ //
161
+ // NC-Q: L'ATTESA SCALA COL PAYLOAD, e prima era una costante. Il tempo che
162
+ // un TUI impiega a ingerire un bracketed paste cresce con la lunghezza —
163
+ // Claude Code sopra una certa taglia lo COLLASSA («paste again to expand»)
164
+ // — mentre l'attesa era fissa. Oltre quella soglia l'Enter arriva mentre il
165
+ // client sta ancora digerendo e viene mangiato: il testo resta nel
166
+ // composer, la cella non si muove, e il mittente riceve `submitted`. E'
167
+ // una perdita silenziosa, ed e' la peggiore: chi manda crede di aver
168
+ // consegnato.
169
+ //
170
+ // MISURATO il 2026-08-07, stesso bersaglio a 11 minuti di distanza: ~2900
171
+ // caratteri -> mai elaborato per NOVE ORE; ~60 caratteri -> in lavorazione
172
+ // dopo dodici secondi. Lo stesso era successo con `claude.zai-a` a 150 ms e
173
+ // con `codex-vl` a 400: la costante piu' alta non bastava, perche' il
174
+ // problema non e' quale costante ma che sia una costante.
175
+ //
176
+ // I numeri sono un'EURISTICA, non una misura del tempo di ingestione: 150
177
+ // ms ogni 500 caratteri oltre i primi 500, col vecchio valore come minimo.
178
+ // Il gradino usa `floor` di proposito: sotto i 500 caratteri non cambia
179
+ // NIENTE, perche' quei messaggi erano gia' affidabili e rallentarli sarebbe
180
+ // stato un costo pagato da tutti per un difetto che non li riguarda.
181
+ //
182
+ // Nessun tetto esplicito: il testo e' gia' limitato a MAX_SUBMIT, quindi
183
+ // l'attesa non puo' superare i ~2,5 s per costruzione. Un `Math.min` qui
184
+ // sarebbe un ramo che non si esegue mai — e un ramo irraggiungibile e' un
185
+ // pezzo di codice che promette di proteggere da qualcosa che non puo'
186
+ // accadere.
187
+ //
188
+ // Riduce di molto la finestra; non la chiude. `submitted` continua a
189
+ // significare paste+Enter, non accettazione — e resta vero che dopo un
190
+ // messaggio lungo conviene verificare che la cella si sia mossa.
191
+ const attesaBase = codexComposer ? 400 : 150;
192
+ await delay(attesaBase + Math.floor(text.length / 500) * 150);
161
193
  if (!(await paneAlive())) return { submitted: false, reason: 'sessione terminata durante la consegna' };
162
194
  if (codexComposer) {
163
195
  const flush = await execTmux(execFileImpl, tmuxBin, ['send-keys', '-t', paneId, 'C-e']);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.52",
3
+ "version": "0.8.53",
4
4
  "description": "Faithful browser tmux client — attach to live sessions over a real PTY, localhost-only, mobile-easy",
5
5
  "main": "lib/server.js",
6
6
  "bin": {