@mmmbuto/nexuscrew 0.9.15 → 0.9.17

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +132 -3
  2. package/docs/ALIBABA_TOKEN_PLAN.md +70 -0
  3. package/docs/AUDIO_SHARE.md +119 -0
  4. package/docs/CELL_PANEL.md +156 -0
  5. package/docs/CONFIGURATION.md +122 -0
  6. package/docs/FLEET.md +222 -0
  7. package/docs/INSTALLATION.md +128 -0
  8. package/docs/LIVE_PROMPT.md +110 -0
  9. package/docs/MCP.md +151 -0
  10. package/docs/NODES.md +214 -0
  11. package/docs/NOTIFICATIONS.md +101 -0
  12. package/docs/OPERATIONS.md +114 -0
  13. package/docs/README.md +55 -0
  14. package/docs/SECURITY.md +185 -0
  15. package/docs/VL_MICRO_NODES.md +107 -0
  16. package/docs/img/fleet-deck-desktop.png +0 -0
  17. package/docs/img/session-mobile.png +0 -0
  18. package/docs/live-prompt-templates/LIVE_PROMPT.en.md +67 -0
  19. package/docs/live-prompt-templates/LIVE_PROMPT.es.md +70 -0
  20. package/docs/live-prompt-templates/LIVE_PROMPT.it.md +67 -0
  21. package/frontend/dist/assets/{index-ToJ5arRI.js → index-jBYBPXhB.js} +29 -29
  22. package/frontend/dist/index.html +1 -1
  23. package/frontend/dist/version.json +1 -1
  24. package/lib/audio/receipt.js +1 -1
  25. package/lib/auth/middleware.js +1 -1
  26. package/lib/cells/routes.js +26 -9
  27. package/lib/cells/scope-guard.js +2 -2
  28. package/lib/cli/commands.js +7 -7
  29. package/lib/cli/fleet-service.js +2 -2
  30. package/lib/cli/pidfile.js +15 -15
  31. package/lib/cli/service.js +2 -2
  32. package/lib/cli/stable-alias.js +1 -1
  33. package/lib/diagnostics/store.js +4 -1
  34. package/lib/files/routes.js +2 -2
  35. package/lib/fleet/builtin.js +59 -23
  36. package/lib/fleet/catalogs/opencode-go.json +3 -3
  37. package/lib/fleet/cell-exec.js +4 -4
  38. package/lib/fleet/cell-lease-server.js +17 -17
  39. package/lib/fleet/definitions.js +34 -15
  40. package/lib/fleet/launch-broker.js +3 -3
  41. package/lib/fleet/lease-client.js +1 -1
  42. package/lib/fleet/managed.js +97 -26
  43. package/lib/fleet/prompt-delivery.js +1 -1
  44. package/lib/fleet/runtime.js +3 -3
  45. package/lib/live-host/bridge.js +3 -3
  46. package/lib/live-host/routes.js +1 -1
  47. package/lib/mcp/server.js +7 -8
  48. package/lib/mcp/tools.js +1 -1
  49. package/lib/nodes/commands.js +2 -2
  50. package/lib/nodes/health.js +7 -11
  51. package/lib/nodes/identity.js +10 -13
  52. package/lib/nodes/store.js +2 -2
  53. package/lib/nodes/tunnel-supervisor.js +7 -7
  54. package/lib/nodes/tunnel.js +10 -11
  55. package/lib/notify/asks.js +4 -4
  56. package/lib/notify/persist.js +1 -1
  57. package/lib/notify/push.js +4 -4
  58. package/lib/notify/routes.js +12 -12
  59. package/lib/proxy/node-proxy.js +4 -4
  60. package/lib/proxy/panel-proxy.js +2 -2
  61. package/lib/pty/attach.js +1 -1
  62. package/lib/server.js +16 -4
  63. package/lib/settings/pairing-coordinator.js +1 -1
  64. package/lib/settings/routes.js +4 -4
  65. package/lib/tmux/lifecycle.js +4 -4
  66. package/lib/tmux/preview.js +1 -1
  67. package/lib/update/runner.js +9 -9
  68. package/lib/vl-nodes/broker.js +1 -1
  69. package/lib/ws/bridge.js +56 -3
  70. package/lib/ws/drop-counter.js +37 -0
  71. package/package.json +20 -2
  72. package/skills/nexuscrew/SKILL.md +1 -1
  73. package/frontend/index.html +0 -20
@@ -13,12 +13,57 @@ const { readCredentialStore, safePrivateDir } = require('./credentials.js');
13
13
  const OLLAMA_CLOUD_MODELS = Object.freeze([
14
14
  'glm-5.2', 'kimi-k2.7-code', 'deepseek-v4-pro', 'minimax-m3',
15
15
  'qwen3.5:397b', 'deepseek-v4-flash', 'mistral-large-3:675b', 'gemma4:31b',
16
+ // glm-5.3-flash: disponibile su ollama.com/library (2026-08-27; glm-5.3
17
+ // non-flash invece 404 su Ollama e NON va elencata qui). Gia` usata da
18
+ // celle esistenti su questo engine: senza voce in OLLAMA_CONTEXT il launch
19
+ // ricadeva sul fallback 200000 in silenzio.
20
+ 'glm-5.3-flash',
16
21
  ]);
22
+ // Autorita' per OLLAMA_CONTEXT = IL CAMPO STRUTTURATO «Context» della scheda
23
+ // canale (ollama.com/library, fetch 2026-08-27) — NON la descrizione, che
24
+ // racconta il modello in astratto (su minimax-m3 dice 1M mentre il campo dice
25
+ // 512K). models.dev vale dove la riga provider esiste ed e' aggiornata.
26
+ // Criterio suffissi: numero esatto disponibile vince (glm-5.2 976K -> 976000;
27
+ // glm-5.3-flash 1M corroborato 1000000 da 7 provider su models.dev);
28
+ // altrimenti convenzione binaria del repo (deepseek 1M -> 1048576;
29
+ // minimax 512K -> 524288, valore pre-esistente ripristinato).
17
30
  const OLLAMA_CONTEXT = Object.freeze({
18
- 'glm-5.2': 1000000, 'kimi-k2.7-code': 262144, 'deepseek-v4-pro': 524288,
19
- 'minimax-m3': 524288, 'qwen3.5:397b': 262144, 'deepseek-v4-flash': 1048576,
31
+ // glm-5.2: scheda «976K context» (era 1M: over-declare di 24k).
32
+ 'glm-5.2': 976000, 'kimi-k2.7-code': 262144,
33
+ // deepseek-v4-pro: scheda «1M context» (era 524288: META' del reale).
34
+ 'deepseek-v4-pro': 1048576,
35
+ 'minimax-m3': 524288,
36
+ 'qwen3.5:397b': 262144, 'deepseek-v4-flash': 1048576,
20
37
  'mistral-large-3:675b': 262144, 'gemma4:31b': 262144,
38
+ // glm-5.3-flash: scheda «1M context» + concorde zhipuai/zai/opencode-go.
39
+ 'glm-5.3-flash': 1000000,
21
40
  });
41
+ // Capacita' per modello OSSERVATE dalla scheda canale (ollama.com/library,
42
+ // 2026-08-27). Assenza = default conservativo (comportamento odierno, nessuna
43
+ // regressione). glm-5.3-flash e' nativamente multimodale con tools e thinking:
44
+ // senza queste voci il catalogo generato spegne vision e parallel tools anche
45
+ // a finestra corretta (il gating Image e' su model_info.input_modalities).
46
+ const OLLAMA_MODEL_CAPABILITIES = Object.freeze({
47
+ // vision/thinking/tools: dichiarati dalla scheda. PARALLEL NON dichiarato:
48
+ // default conservativo false finche' non misurato su device (i tool restano
49
+ // attivi; resta spento solo l'invocarli in parallelo). Accendere dopo smoke.
50
+ 'glm-5.3-flash': Object.freeze({ input: ['text', 'image'], reasoning: true, supportsParallelToolCalls: false }),
51
+ });
52
+ // Descrittori dell'engine ollama-cloud per la generazione del catalogo client:
53
+ // stessa forma dei descrittori custom/PI (id, label, contextWindow, input,
54
+ // reasoning, supportsParallelToolCalls).
55
+ function ollamaCloudCatalogModels() {
56
+ return OLLAMA_CLOUD_MODELS.map((id) => {
57
+ const cap = OLLAMA_MODEL_CAPABILITIES[id] || {};
58
+ return Object.freeze({
59
+ id, label: id,
60
+ contextWindow: ollamaContextFor(id),
61
+ input: Object.freeze([...(cap.input || ['text'])]),
62
+ reasoning: cap.reasoning === true,
63
+ supportsParallelToolCalls: cap.supportsParallelToolCalls === true,
64
+ });
65
+ });
66
+ }
22
67
  // Lo stesso modello si scrive con o senza tag ('deepseek-v4-flash' e
23
68
  // 'deepseek-v4-flash:0731'). Si prova prima la chiave esatta, perche' nella
24
69
  // mappa ci sono nomi in cui il tag fa parte dell'identita' del modello
@@ -58,7 +103,7 @@ function declaredFor(extraModels, profileId, model) {
58
103
  // uno la dimentichi perche' un modello dichiarato smetta di essere valido
59
104
  // proprio nel punto che conta — l'avvio.
60
105
  //
61
- // D2: Map<engine, Map<id, model>> — porta il descrittore intero, non solo
106
+ // da revisione: Map<engine, Map<id, model>> — porta il descrittore intero, non solo
62
107
  // l'id (stesso motivo del commento gemello in definitions.js: extraModels e'
63
108
  // costruita due volte, una dentro parseDefinitions per il parsing, una qui per
64
109
  // il runtime che rilegge le definizioni gia' salvate; le due DEVONO restare
@@ -151,7 +196,9 @@ const ALIBABA_PI_MODELS = Object.freeze([
151
196
  // xiaomi/mimo-v2.5*"). hy3-preview: "Model is unavailable".
152
197
  // Il catalogo live li pubblicizza comunque; qui non entrano.
153
198
  // - grok-4.5 solo su Responses: su Chat risponde 503 e Messages lo rifiuta
154
- // esplicitamente ("not supported for format anthropic").
199
+ // esplicitamente ("not supported for format anthropic"). Dal 2026-08-27
200
+ // l'id in elenco e` grok-4.6 (stessi limiti misurati su models.dev); il
201
+ // commento sopra conserva la misura storica fatta su 4.5.
155
202
  // - deepseek-v4-flash-vision-exp: MISURATO 2026-08-24 sul gateway
156
203
  // opencode.ai/zen/go -> 200 su tutti e tre i wire (Responses
157
204
  // status=completed, Messages stop_reason=end_turn, Chat finish_reason=stop).
@@ -168,7 +215,7 @@ const OPENCODE_GO_MESSAGES_MODELS = Object.freeze([
168
215
  'qwen3.8-max', 'qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-plus', 'qwen3.5-plus',
169
216
  ]);
170
217
  const OPENCODE_GO_RESPONSES_MODELS = Object.freeze([
171
- 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'gpt-5.6-luna', 'grok-4.5', 'glm-5.2', 'glm-5.1', 'glm-5',
218
+ 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'gpt-5.6-luna', 'grok-4.6', 'glm-5.2', 'glm-5.1', 'glm-5',
172
219
  ]);
173
220
  const OPENCODE_GO_CHAT_MODELS = Object.freeze([
174
221
  'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'glm-5.2', 'glm-5.1', 'glm-5',
@@ -212,7 +259,9 @@ const OPENCODE_GO_LIMITS = Object.freeze({
212
259
  'mimo-v2.5-pro': { context: 1048576, output: 128000 },
213
260
  hy3: { context: 256000, output: 64000 },
214
261
  'gpt-5.6-luna': { context: 1050000, output: 128000 },
215
- 'grok-4.5': { context: 500000, output: 500000 },
262
+ // grok-4.5 -> grok-4.6 (2026-08-27): id aggiornato al modello reale;
263
+ // limiti confermati da models.dev (xai e opencode-go: 500000/500000).
264
+ 'grok-4.6': { context: 500000, output: 500000 },
216
265
  });
217
266
 
218
267
  function opencodeGoContextFor(model) {
@@ -296,7 +345,7 @@ const CATALOG = Object.freeze([
296
345
  // deve dire la verita' su cosa stiamo usando. Il suffisso `[1m]` resta: e' un
297
346
  // flag di finestra del CLI e viene tolto prima della richiesta HTTP, dove
298
347
  // `glm-5.3[1m]` letterale darebbe 400 (code 1214, misurato).
299
- { id: 'claude.zai', client: 'claude', provider: 'zai', label: 'Z.AI', auth: 'dynamic', credentialEnv: true, defaultEnvKey: 'ZAI_API_KEY', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]'], core: true },
348
+ { id: 'claude.zai', client: 'claude', provider: 'zai', label: 'Z.AI', auth: 'dynamic', credentialEnv: true, defaultEnvKey: 'ZAI_API_KEY', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]', 'glm-5.3-flash'], core: true },
300
349
  // OpenCode Go su Claude parla Anthropic Messages, e la wire accetta SOLO
301
350
  // `x-api-key`: con `Authorization: Bearer` risponde 401 AuthError. Per questo
302
351
  // l'endpoint e' la root senza `/v1` (il client aggiunge `/v1/messages`) e il
@@ -433,8 +482,8 @@ const CATALOG = Object.freeze([
433
482
  // --- Sezione legacy --------------------------------------------------------
434
483
  // Compatibilita' sola lettura/launch per configurazioni 0.8.0: mai nel catalogo
435
484
  // UI (publicCatalog filtra `legacy`). Risolti solo da profileFor/normalizeManagedSpec.
436
- { id: 'claude.zai-a', client: 'claude', provider: 'zai', credentialProfile: 'a', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_A', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]'], legacySecrets: true, legacyProvider: 'zai-a', legacy: true },
437
- { id: 'claude.zai-p', client: 'claude', provider: 'zai', credentialProfile: 'p', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_P', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]'], legacySecrets: true, legacyProvider: 'zai-p', legacy: true },
485
+ { id: 'claude.zai-a', client: 'claude', provider: 'zai', credentialProfile: 'a', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_A', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]', 'glm-5.3-flash'], legacySecrets: true, legacyProvider: 'zai-a', legacy: true },
486
+ { id: 'claude.zai-p', client: 'claude', provider: 'zai', credentialProfile: 'p', label: 'Z.AI legacy profile', auth: 'ZAI_API_KEY_P', endpoint: 'https://api.z.ai/api/anthropic', protocol: 'anthropic_messages', model: 'glm-5.3[1m]', models: ['glm-5.3[1m]', 'glm-5.2[1m]', 'glm-5.3-flash'], legacySecrets: true, legacyProvider: 'zai-p', legacy: true },
438
487
  ]);
439
488
 
440
489
  function profileFor(client, provider, credentialProfile) {
@@ -718,7 +767,7 @@ function binaryCandidates(client, home) {
718
767
  // (non possiamo dichiararlo "trovato"); ma chi costruisce il messaggio
719
768
  // (describeManaged) puo' ora distinguere "client non trovato" da "non ho potuto
720
769
  // verificare un candidato" — il discriminante e' CHI ha fallito, non che ci sia
721
- // stata un'eccezione. Stesso principio gia' applicato in checkTermuxExec (3134d2f).
770
+ // stata un'eccezione. Stesso principio gia' applicato in checkTermuxExec.
722
771
  function findBinary(client, home, out) {
723
772
  const blocked = Array.isArray(out && out.blocked) ? out.blocked : null;
724
773
  for (const candidate of binaryCandidates(client, home)) {
@@ -1012,7 +1061,7 @@ function describeManaged(spec, cfg = {}) {
1012
1061
  const extraModels = cfg.extraModels || null;
1013
1062
  const normalized = normalizeManagedSpec(spec, { extraModels });
1014
1063
  if (!normalized) {
1015
- // D2: il rifiuto resta (i descrittori NON appartengono al profilo di una
1064
+ // da revisione: il rifiuto resta (i descrittori NON appartengono al profilo di una
1016
1065
  // cella: vengono dalla definizione dell'ENGINE, in `d.models` — due
1017
1066
  // soggetti diversi, mescolarli renderebbe ambiguo chi dichiara cosa). Ma
1018
1067
  // se la causa e' proprio questa, il messaggio generico "invalid managed
@@ -1189,7 +1238,7 @@ function sharedMcpArgs(home) {
1189
1238
  // indicizzato sul percorso assoluto — ed e' reale: su questa installazione nove
1190
1239
  // progetti ne hanno, fra cui la directory di lavoro delle celle. Dimenticarlo
1191
1240
  // significava che un elenco parziale non negava server che la sessione carica
1192
- // davvero, e l'operatore avrebbe creduto di averli esclusi. Trovato dall'audit.
1241
+ // davvero, e l'operatore avrebbe creduto di averli esclusi. Trovato da una revisione.
1193
1242
  //
1194
1243
  // RESTANO FUORI, e non sono enumerabili da qui: plugin, connettori claude.ai e
1195
1244
  // la configurazione gestita di sistema. Per quelli l'unica esclusione certa e'
@@ -1307,7 +1356,7 @@ function ensurePrivateClaudeConfig(home, profileId, label, penguinMode) {
1307
1356
  return configDir;
1308
1357
  }
1309
1358
 
1310
- // D2 audit: il file .ts generato NON e' il consumatore — Pi lo E'. Un test che
1359
+ // da revisione: il file .ts generato NON e' il consumatore — Pi lo E'. Un test che
1311
1360
  // legge solo il file resta verde su un'estensione che Pi rifiuta a runtime.
1312
1361
  // CONTRATTO REALE che Pi (@earendil-works/pi-coding-agent 0.80.10) impone a
1313
1362
  // ogni modello di `pi.registerProvider(id, {models: [...]})` — fonte:
@@ -1333,13 +1382,13 @@ function toPiModelConfig(m) {
1333
1382
  };
1334
1383
  }
1335
1384
 
1336
- // D2: `declaredModels` (opzionale) sono i descrittori dichiarati per l'ENGINE
1385
+ // da revisione: `declaredModels` (opzionale) sono i descrittori dichiarati per l'ENGINE
1337
1386
  // (via declaredModelsFor), per il ramo Pi CUSTOM — stesso motivo/stessa fonte
1338
1387
  // di customCatalogFor, mai letti da `spec.models`. Il ramo Pi NON-custom
1339
1388
  // (profile.piExtension) continua a passare `models` dentro l'oggetto spec-like
1340
1389
  // come sempre: e' un catalogo STATICO cablato nel codice (es. alibaba-token-
1341
1390
  // plan), GIA' nella forma completa che Pi si aspetta — un caso diverso, non
1342
- // toccato dal difetto D2. declaredModels, quando presente, ha priorita', ed e'
1391
+ // toccato dal difetto da revisione. declaredModels, quando presente, ha priorita', ed e'
1343
1392
  // sempre passato per toPiModelConfig (mai i descrittori grezzi cosi' come sono).
1344
1393
  function writePiProviderExtension(spec, home, declaredModels) {
1345
1394
  const dir = path.join(home, '.nexuscrew', 'pi-providers');
@@ -1475,7 +1524,7 @@ function vlSystemAppendGate(binary, cfg) {
1475
1524
  return { ok: true };
1476
1525
  }
1477
1526
 
1478
- // D2 (fix definitivo — l'audit del pacchetto aveva bocciato la prima versione:
1527
+ // (fix definitivo — una revisione del pacchetto aveva bocciato la prima versione:
1479
1528
  // il test costruiva `spec.models` a mano, e in produzione quel campo non
1480
1529
  // esiste mai). `declaredModels` NON viene da `spec`: i descrittori sono
1481
1530
  // proprieta' della definizione dell'ENGINE (`d.models` del documento), non del
@@ -1510,6 +1559,8 @@ function customCatalogFor(spec, model, declaredModels, home) {
1510
1559
  : [{ effort: 'low', description: 'Fast responses with lighter reasoning' },
1511
1560
  { effort: 'medium', description: 'Balanced reasoning depth' },
1512
1561
  { effort: 'high', description: 'Greater reasoning depth for complex problems' }],
1562
+ input_modalities: m.input || ['text'],
1563
+ supports_parallel_tool_calls: m.supportsParallelToolCalls === true,
1513
1564
  shell_type: 'default',
1514
1565
  visibility: 'list',
1515
1566
  supported_in_api: true,
@@ -1524,12 +1575,10 @@ function customCatalogFor(spec, model, declaredModels, home) {
1524
1575
  apply_patch_tool_type: null,
1525
1576
  web_search_tool_type: 'text',
1526
1577
  truncation_policy: { mode: 'tokens', limit: m.maxTokens || m.contextWindow || 128000 },
1527
- supports_parallel_tool_calls: false,
1528
1578
  supports_image_detail_original: false,
1529
1579
  context_window: m.contextWindow || 128000,
1530
1580
  effective_context_window_percent: 95,
1531
1581
  experimental_supported_tools: [],
1532
- input_modalities: ['text'],
1533
1582
  supports_search_tool: false,
1534
1583
  };
1535
1584
  }),
@@ -1543,8 +1592,12 @@ function customCatalogFor(spec, model, declaredModels, home) {
1543
1592
  else throw e;
1544
1593
  }
1545
1594
  fs.chmodSync(dir, 0o700);
1546
- const target = path.join(dir, `${spec.providerId}.json`);
1547
- const tmp = path.join(dir, `.${spec.providerId}.${crypto.randomBytes(6).toString('hex')}.tmp`);
1595
+ // Nome generico per engine gestito: i custom hanno providerId proprio, gli
1596
+ // altri usano client.provider cosi' QUALSIASI engine che dichiara modelli
1597
+ // ottiene il suo catalogo senza casi speciali per provider.
1598
+ const catalogId = spec.providerId || `${spec.client}.${spec.provider}`;
1599
+ const target = path.join(dir, `${catalogId}.json`);
1600
+ const tmp = path.join(dir, `.${catalogId}.${crypto.randomBytes(6).toString('hex')}.tmp`);
1548
1601
  try {
1549
1602
  fs.writeFileSync(tmp, JSON.stringify(cat), { mode: 0o600 });
1550
1603
  fs.chmodSync(tmp, 0o600);
@@ -1561,14 +1614,14 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
1561
1614
  // non basta: la normalizzazione che decide se la cella PARTE e' questa, e
1562
1615
  // senza l'elenco dichiarato un modello legittimo diventa «invalid managed
1563
1616
  // profile» — cioe' una cella che non si avvia per una ragione invisibile.
1564
- // E' il difetto che l'audit ha ripreso due volte: la vista diceva di si',
1617
+ // E' il difetto che la revisione ha ripreso due volte: la vista diceva di si',
1565
1618
  // il boot diceva di no, e nessun test guardava il boot.
1566
1619
  const spec = normalizeManagedSpec(engine.managed, { extraModels: cfg.extraModels || null });
1567
1620
  const info = describeManaged(spec, cfg);
1568
1621
  if (!spec || !info.configured) return { ok: false, reason: info.reason, info };
1569
1622
  const home = cfg.home || require('node:os').homedir();
1570
1623
  const profile = profileFor(spec.client, spec.provider, spec.credentialProfile || '');
1571
- // D2: i descrittori dichiarati per QUESTO profilo (client.provider — la
1624
+ // da revisione: i descrittori dichiarati per QUESTO profilo (client.provider — la
1572
1625
  // stessa chiave che declaredFor usa dentro normalizeManagedSpec per
1573
1626
  // validare gli id). Vengono dalla definizione dell'ENGINE (extraModels),
1574
1627
  // MAI da spec: e' il ponte che customCatalogFor/writePiProviderExtension
@@ -1734,8 +1787,26 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
1734
1787
  env.OPENAI_API_KEY = cred.value;
1735
1788
  args.push(...codexProviderArgs('ollama_cloud', 'Ollama Cloud', profile.endpoint, 'OPENAI_API_KEY'));
1736
1789
  args.push('-c', 'model_providers.ollama_cloud.stream_idle_timeout_ms=600000', '-c', `model_context_window=${ollamaContextFor(model) ?? 200000}`);
1737
- const localCatalog = path.join(home, '.codex', 'ollama_cloud_model_catalog.json');
1738
- if (fs.existsSync(localCatalog)) args.push('-c', `model_catalog_json="${localCatalog}"`);
1790
+ // Catalogo generato dagli id DICHIARATI DELL'ENGINE: senza entry il
1791
+ // client cade sul descrittore fallback (272000 fisso, parallel tools
1792
+ // false, nessun reasoning, nessuna modalita' visiva — guasto misurato
1793
+ // su glm-5.3-flash, controllo 2026-08-27). Il file
1794
+ // utente resta come fallback se la generazione non e' possibile.
1795
+ let catalogMeta = null;
1796
+ try {
1797
+ catalogMeta = customCatalogFor(spec, model, ollamaCloudCatalogModels(), home);
1798
+ } catch (_) {
1799
+ // Generazione impossibile (es. path occupato da una directory):
1800
+ // NON deve far fallire il launch — si ricade sul file utente sotto.
1801
+ catalogMeta = null;
1802
+ }
1803
+ if (catalogMeta) {
1804
+ args.push('-c', `model_catalog_json=${JSON.stringify(catalogMeta.catalogPath)}`);
1805
+ if (catalogMeta.contextWindow) args.push('-c', `model_context_window=${catalogMeta.contextWindow}`);
1806
+ } else {
1807
+ const localCatalog = path.join(home, '.codex', 'ollama_cloud_model_catalog.json');
1808
+ if (fs.existsSync(localCatalog)) args.push('-c', `model_catalog_json="${localCatalog}"`);
1809
+ }
1739
1810
  } else if (spec.provider === 'openrouter') {
1740
1811
  env.OPENROUTER_API_KEY = cred.value;
1741
1812
  const authHelper = path.join(__dirname, 'openrouter-auth-helper.js');
@@ -1774,7 +1845,7 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
1774
1845
  } else if (spec.provider === 'custom') {
1775
1846
  env[spec.envKey] = cred.value;
1776
1847
  args.push(...codexProviderArgs(spec.providerId, spec.displayName, spec.baseUrl, spec.envKey));
1777
- // D2: onora `models` (engine definition, già validato) come gli altri
1848
+ // da revisione: onora `models` (engine definition, già validato) come gli altri
1778
1849
  // provider — deriva model_catalog_json e model_context_window, così
1779
1850
  // Codex-VL non ricade sul fallback 272K. Senza `models` -> null (no regressione).
1780
1851
  const customMeta = customCatalogFor(spec, model, declaredModels, home);
@@ -1950,7 +2021,7 @@ function publicCatalog() {
1950
2021
 
1951
2022
  module.exports = {
1952
2023
  knownMcpServerNames,
1953
- CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, ALIBABA_TOKEN_PLAN_MODELS,
2024
+ CATALOG, OLLAMA_CLOUD_MODELS, OLLAMA_CONTEXT, OLLAMA_MODEL_CAPABILITIES, ollamaCloudCatalogModels, ALIBABA_TOKEN_PLAN_MODELS,
1954
2025
  ALIBABA_CODEX_MODELS, ALIBABA_TOKEN_PLAN_CONTEXT, ALIBABA_PI_MODELS,
1955
2026
  OPENCODE_GO_MESSAGES_MODELS, OPENCODE_GO_RESPONSES_MODELS, OPENCODE_GO_CHAT_MODELS, OPENCODE_GO_LIMITS,
1956
2027
  OPENCODE_GO_ANTHROPIC_ROOT, OPENCODE_GO_API_BASE,
@@ -4,7 +4,7 @@
4
4
  // managed conservano il prompt su argv (finding separato) e i custom send-keys
5
5
  // conservano injectPrompt legacy senza Enter.
6
6
  //
7
- // Contratto (design audit 0.8.47):
7
+ // Contratto (design della revisione 0.8.47):
8
8
  // - Readiness REALE prima della consegna: il pane viene classificato sul SOLO
9
9
  // viewport visibile (capture-pane -p, mai scrollback -S: su generation>0 lo
10
10
  // scrollback puo' contenere vecchi prompt e marker stale). Auth, consenso
@@ -80,7 +80,7 @@ function createBuiltinRuntime(ctx) {
80
80
  // solo — il ponte Live, che la chiede esplicitamente e in-processo — quindi
81
81
  // e' lui a doverla chiedere, non tutti gli altri a doversene ricordare.
82
82
  // Il backup vieta gia' le cwd assolute per la stessa ragione: sono specifiche
83
- // del dispositivo. Rilievo di un audit indipendente.
83
+ // del dispositivo. Rilievo di una revisione indipendente.
84
84
  async function cellStatus({ includeCwd = false } = {}) {
85
85
  if (Date.now() - cache.at > STATUS_TTL_MS) {
86
86
  reloadDefs(); // pick-up di edit esterne/file
@@ -100,7 +100,7 @@ function createBuiltinRuntime(ctx) {
100
100
  const effectivePolicy = ['pi', 'shell'].includes(engineDef?.managed?.client)
101
101
  ? 'standard'
102
102
  : (remembered || engineDefault || '');
103
- // D8: panelUrl per-cella vince su quello precompilato dall'engine (es.
103
+ // da revisione: panelUrl per-cella vince su quello precompilato dall'engine (es.
104
104
  // desktop.local), che a sua volta e' il default. Stessa forma di
105
105
  // engineDef gia' usata sopra per la permission policy: un valore
106
106
  // presente qui e' gia' passato da validPanelUrl a monte (parseEngine/
@@ -222,7 +222,7 @@ function createBuiltinRuntime(ctx) {
222
222
  // broker-backed supervisor: credentials never enter tmux state/argv and a
223
223
  // client that exits after readiness is restarted with bounded backoff.
224
224
  // '-P -F #{pane_id}': tmux stampa il pane id della sessione appena creata,
225
- // cosi' l'iniezione del prompt bersaglia ESATTAMENTE quel pane (audit impl
225
+ // cosi' l'iniezione del prompt bersaglia ESATTAMENTE quel pane (da revisione impl
226
226
  // #5: elimina la race di riuso del nome sessione tra waitAlive e paste).
227
227
  const readyMs = cfg.launchReadyMs != null ? cfg.launchReadyMs : 500;
228
228
  const child = composeClientInvocation(launchEngine, cell);
@@ -22,7 +22,7 @@
22
22
  // emendata da rev5 MC2). La riga che decide è in codex-rs
23
23
  // core/src/config/mod.rs: `developer_instructions.or(cfg.developer_
24
24
  // instructions)` — l'override Some scarta il valore di config. R2
25
- // (2026-08-16, verso corretto dopo audit pre-release): l'identità della
25
+ // (verso corretto dopo revisione pre-release): l'identità della
26
26
  // cella designata viaggia SEMPRE come intestazione anteposta al campo,
27
27
  // anche senza prompt. Il campo NON è additivo: una cella senza
28
28
  // LIVE_PROMPT.md, che prima non passava nulla e riceveva le developer
@@ -227,7 +227,7 @@ function readCellPrompt(filesRoot, tmuxSession) {
227
227
  // il ponte: il nome esatto della sessione, che il server MCP accetta da
228
228
  // NEXUSCREW_MCP_SESSION via stdio. Il valore NON si mette in systemd
229
229
  // Environment= (una identità statica condivisa = impersonare una cella
230
- // fissa, demolito in audit v1): lo dice l'intestazione, per-cella.
230
+ // fissa, demolito in revisione v1): lo dice l'intestazione, per-cella.
231
231
  //
232
232
  // Il fatto, niente di più: quale cella (id Fleet) e, se il roster la dichiara,
233
233
  // la sessione tmux esatta — quella con cui la voce raggiunge i canonici della
@@ -497,7 +497,7 @@ function createLiveBridge({
497
497
  // Tre condizioni diverse, tre nomi: la designazione dichiarata non
498
498
  // eleggibile dall'hub, la cella che non esiste piu' nel roster, e la cella
499
499
  // che c'e' ma e' spenta. Un nome solo mandava a guardare l'hub anche quando
500
- // il problema era una sessione chiusa. Rilievo di un audit indipendente.
500
+ // il problema era una sessione chiusa. Rilievo di una revisione indipendente.
501
501
  if (snap.eligible !== true) return none('host-ineligible');
502
502
 
503
503
  let cell;
@@ -39,7 +39,7 @@ function isActive(cell) {
39
39
  return !!(cell && cell.active === true && cell.tmux !== false);
40
40
  }
41
41
 
42
- // --- Seam lease↔designazione (2026-08-15, decisione presa in revisione: grace = false) -----
42
+ // --- Seam lease↔designazione (decisione presa in revisione: grace = false) -----
43
43
  //
44
44
  // L'idoneita' dell'host designato non e' piu' solo «sessione tmux viva»: con
45
45
  // remain-on-exit la sessione sopravvive alla morte del supervisore, e la
package/lib/mcp/server.js CHANGED
@@ -162,7 +162,7 @@ function resolveIdentity({ env, tmuxBin, execFileImpl }) {
162
162
  return settle(missing);
163
163
  }
164
164
  const name = String(stdout || '').trim();
165
- // tmux 3.4, misura dell'audit (probe A1/A2): pane morto con -t ->
165
+ // tmux 3.4, misura della revisione (probe doppia): pane morto con -t ->
166
166
  // rc=0 e stdout VUOTO. Il vuoto è il segnale dello stantio.
167
167
  if (!name) return settle(stalePane);
168
168
  if (isValidSession(name)) {
@@ -288,12 +288,11 @@ function createMcpServer(opts = {}) {
288
288
  return j;
289
289
  }
290
290
 
291
- // NC-R. Aggiornare NexusCrew NON aggiorna il bridge MCP delle celle gia' in
292
- // piedi: quel processo e' partito col codice di prima e ci resta fino al
293
- // riavvio della cella. Il sintomo e' crudele — si installa una correzione, si
294
- // riprova, e si riceve l'errore VECCHIO e chi lo subisce conclude che la
295
- // correzione non funziona. E' successo il 2026-08-07 su rc.26, a me, e ci ho
296
- // messo un giro intero a capirlo.
291
+ // NC-R. Aggiornare NexusCrew NON cambia il codice gia' caricato dai bridge
292
+ // MCP delle celle attive: ogni processo resta sulla versione con cui e' stato
293
+ // avviato fino al riavvio della cella. Dopo una correzione installata, un
294
+ // bridge ancora attivo puo' quindi restituire l'errore precedente; prima di
295
+ // valutare la correzione vanno confrontate le versioni di hub e bridge.
297
296
  //
298
297
  // Il momento in cui serve saperlo e' esattamente quello in cui qualcosa
299
298
  // fallisce, quindi la verifica sta SOLO sul ramo d'errore: a regime non costa
@@ -367,7 +366,7 @@ function createMcpServer(opts = {}) {
367
366
  return replyError(null, INVALID_REQUEST, 'richiesta non valida (atteso oggetto JSON-RPC)');
368
367
  }
369
368
  const id = (typeof msg.id === 'string' || typeof msg.id === 'number') ? msg.id : undefined;
370
- // F6 (audit): SOLO JSON-RPC 2.0 — versione assente/errata -> -32600 anche
369
+ // da revisione: SOLO JSON-RPC 2.0 — versione assente/errata -> -32600 anche
371
370
  // per i messaggi senza id (il no-op vale solo per notification 2.0 valide).
372
371
  if (msg.jsonrpc !== '2.0') {
373
372
  return replyError(id !== undefined ? id : null, INVALID_REQUEST, 'jsonrpc "2.0" richiesto');
package/lib/mcp/tools.js CHANGED
@@ -345,7 +345,7 @@ const TOOLS = [
345
345
  // Restituirlo cosi' evita di far leggere un successo dove c'e' un rifiuto.
346
346
  // Nessun conteggio: il dispatcher non lo propaga, e un ramo che non puo'
347
347
  // mai essere vero e' codice morto travestito da informazione
348
- // (rilievo R1 di un audit indipendente su rc.14). Se un giorno servira' il dettaglio,
348
+ // (rilievo di una revisione indipendente). Se un giorno servira' il dettaglio,
349
349
  // va fatto propagare da forward(), non dedotto qui.
350
350
  if (target) return { status: j.status, ...(j.reason ? { reason: j.reason } : {}) };
351
351
  return { delivered: j.delivered };
@@ -204,7 +204,7 @@ function nodesAdd(opts) {
204
204
 
205
205
  log(`nodes add: nodo "${name}" aggiunto (ssh ${ssh}${sshPort ? `:${sshPort}` : ''}, nexus remoto ${remotePort} -> locale ${localPort})`);
206
206
  log('Incolla nel ~/.ssh/authorized_keys del NODO (lato forward, chiave dedicata):');
207
- // R19: permitopen con le destinazioni EXPLICITE dei -L. Qui si conosce solo
207
+ // da revisione: permitopen con le destinazioni EXPLICITE dei -L. Qui si conosce solo
208
208
  // la porta nexus: la porta pannello del peer la annuncia il JOIN, non
209
209
  // esiste ancora — quando apparirà, la riga completa (due destinazioni) la
210
210
  // emette il pairing. Mai un permesso generico: è ciò che impedisce a questa
@@ -392,7 +392,7 @@ function nodesRemove(opts) {
392
392
  let next;
393
393
  try { next = store.removeNode(st, name); }
394
394
  catch (e) { log(`nodes remove: ${e.message}`); return { code: 1 }; }
395
- // Ferma un eventuale forward tunnel ATTIVO prima di togliere la config (audit F4):
395
+ // Ferma un eventuale forward tunnel ATTIVO prima di togliere la config (da revisione):
396
396
  // senza questo il ssh resterebbe orfano — porta locale aperta verso un nodo che non
397
397
  // e' piu' in config, irraggiungibile e senza piu' uno stop pulito via CLI/API.
398
398
  let stopped = false;
@@ -46,17 +46,13 @@ function applyProbeHysteresis(key, probed, previous, now, threshold) {
46
46
  detail: `health probe transitorio (${failures}/${threshold})`,
47
47
  };
48
48
  }
49
- return {
50
- ...probed,
51
- transport: 'unknown',
52
- auth: 'unknown',
53
- reachability: 'unknown',
54
- status: 'unknown',
55
- at: now,
56
- probePending: true,
57
- consecutiveFailures: failures,
58
- detail: `health probe transitorio (${failures}/${threshold})`,
59
- };
49
+ // No previous authoritative 'up' to protect: the first measurement is the
50
+ // real one. A node that has never been seen answering and does not answer
51
+ // now is simply down — reporting 'unknown' would keep a declared-dead peer
52
+ // out of the down state forever (one probe per process, never reaching the
53
+ // threshold unless someone keeps probing). The hysteresis only exists to
54
+ // protect a measured up state from jitter, per una revisione precedente.
55
+ return { ...probed, consecutiveFailures: failures };
60
56
  }
61
57
 
62
58
  // Compatibilita' tunnel per il frontend attuale (che legge tunnel.status): deriva
@@ -28,7 +28,7 @@
28
28
  // secondo scrittore — la proprieta' regge per assenza, non per controllo, e la
29
29
  // differenza va detta invece che lasciata credere.
30
30
  //
31
- // Rilievo dell'audit indipendente: una funzione provata e senza chiamanti e' il
31
+ // Rilievo della revisione indipendente: una funzione provata e senza chiamanti e' il
32
32
  // segnale piu' forte che una garanzia esista, ed e' il piu' facile da dare per
33
33
  // sbaglio.
34
34
  const fs = require('node:fs');
@@ -79,14 +79,11 @@ function readKeyFile(p) {
79
79
  + 'la chiave privata e\' leggibile oltre il proprietario, va rigenerata a mano');
80
80
  }
81
81
  // UN FILE CHE ESISTE MA NON E' USABILE FA RUMORE, NON SI SOSTITUISCE.
82
- // La prima stesura tornava `null` per uno schema sbagliato cioe' «genera
83
- // pure una chiave nuova» e LANCIAVA su un JSON corrotto, perche' il parse
84
- // stava fuori dal try. Due esiti opposti per due modi di essere illeggibile,
85
- // e quello comodo era il piu' pericoloso: rigenerare cambia l'identita' di
86
- // questo nodo, e ogni peer che aveva legato la vecchia si ritrova per sempre
87
- // con una chiave che non corrisponde. Meglio fermarsi e farlo sapere.
88
- // Rilievo dell'audit indipendente; risolto nel verso opposto a quello
89
- // proposto, perche' la coerenza va cercata sul ramo sicuro.
82
+ // Uno schema errato e un JSON corrotto sono due forme dello stesso stato non
83
+ // usabile: entrambi devono fare rumore e nessuno deve autorizzare una nuova
84
+ // chiave. Rigenerare cambia l'identita' del nodo e lascia ogni peer gia'
85
+ // associato con una chiave che non corrisponde. Il ramo sicuro si ferma e
86
+ // rende esplicita la causa senza sostituire il file.
90
87
  let parsed;
91
88
  try {
92
89
  parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
@@ -136,10 +133,10 @@ function writeKeyFile(p, privateKeyPem) {
136
133
  // ensureNodeKey: idempotente. La prima chiamata genera, le successive leggono.
137
134
  // `created` distingue i due casi per chi vuole registrarlo una volta sola.
138
135
  // `afterWriteSeam` e' un seam di prova, come `sessionExistsSeam` e `fleetSeam`
139
- // altrove: viene chiamato subito dopo la scrittura e serve a far accadere la
140
- // corsa in modo deterministico. Senza, il test della corsa e' verde per
141
- // costruzione il file esiste gia' e il percorso di CREAZIONE non viene mai
142
- // eseguito. L'ho scoperto perche' il controllo negativo non falliva.
136
+ // altrove: viene chiamato subito dopo la scrittura e rende deterministico il
137
+ // punto della corsa. Senza il seam, il test puo' osservare un file gia'
138
+ // esistente e non esercitare mai il percorso di CREAZIONE; il controllo
139
+ // negativo deve quindi fissare esplicitamente quel punto.
143
140
  function ensureNodeKey({ home, keyPath, afterWriteSeam } = {}) {
144
141
  const p = keyPath || keyPathFor(home);
145
142
  const existing = readKeyFile(p);
@@ -80,7 +80,7 @@ function isAbsPath(p) {
80
80
  // token remoto: segreto opaco, single-line, cap. Vuoto -> assente (non salvato).
81
81
  // Charset ristretto a header-safe (VCHAR + spazio/tab): il token viene iniettato
82
82
  // in `Authorization: Bearer <t>` verso l'upstream; un char fuori range farebbe
83
- // lanciare setHeader in modo sincrono (ERR_INVALID_CHAR). (hardening audit).
83
+ // lanciare setHeader in modo sincrono (ERR_INVALID_CHAR). (hardening).
84
84
  function validToken(t) {
85
85
  return typeof t === 'string' && t.length > 0 && t.length <= MAX_TOKEN_LEN
86
86
  && /^[\x20-\x7e\t]+$/.test(t);
@@ -589,7 +589,7 @@ function initStore(p) {
589
589
  }
590
590
 
591
591
  // Compatibilita' API interna: il vecchio nome non inizializza piu' a runtime.
592
- // Tenerlo strict impedisce che un call-site dimenticato reintroduca F1.
592
+ // Tenerlo strict impedisce che un call-site dimenticato reintroduca quel difetto.
593
593
  function loadOrInitStore(p) { return loadStoreStrict(p); }
594
594
 
595
595
  // --- Mutazioni pure (ritornano un nuovo store; il caller lo scrive) ---------
@@ -21,7 +21,7 @@ const ownershipGraceMs = Number.isFinite(ownershipGraceRaw) && ownershipGraceRaw
21
21
  const reverseFailureMaxRaw = Number(process.env.NEXUSCREW_TUNNEL_REVERSE_FAILURE_MAX || 8);
22
22
  const reverseFailureMax = Number.isInteger(reverseFailureMaxRaw) && reverseFailureMaxRaw >= 1
23
23
  ? Math.min(reverseFailureMaxRaw, 32) : 8;
24
- // R19 seguito — contratto per la sonda del canale -L quando non conclude MAI:
24
+ // da revisione — contratto per la sonda del canale -L quando non conclude MAI:
25
25
  // continuare a sondare ogni 250ms e' giusto per la finestra transitoria (il
26
26
  // servizio remoto non e' ancora su dopo un restart/aggiornamento: la prossima
27
27
  // sonda e' una connessione FRESCA, si qualifica da sola appena il servizio
@@ -35,7 +35,7 @@ const reverseFailureMax = Number.isInteger(reverseFailureMaxRaw) && reverseFailu
35
35
  // quella soglia il canale entra in degraded, ESATTAMENTE come il fallimento
36
36
  // del forward inverso — stesso stato, stessa auto-guarigione a cadenza fissa,
37
37
  // mai un "pronto" dichiarato senza averlo verificato (sarebbe la stessa bugia
38
- // che R19 ha tolto, solo con una bandiera "non verificato" appesa sopra).
38
+ // che una revisione ha tolto, solo con una bandiera "non verificato" appesa sopra).
39
39
  const channelProbeMaxRaw = Number(process.env.NEXUSCREW_TUNNEL_CHANNEL_PROBE_MAX || 60);
40
40
  const channelProbeMax = Number.isInteger(channelProbeMaxRaw) && channelProbeMaxRaw >= 1
41
41
  ? Math.min(channelProbeMaxRaw, 240) : 60;
@@ -112,7 +112,7 @@ function clearForwardProbe() {
112
112
  // forces OpenSSH to establish the real forward channel. Only that event may
113
113
  // advertise transport-ready or reset retry backoff.
114
114
  //
115
- // R19: connect NON è prova di canale. OpenSSH accetta la TCP sul listener
115
+ // da revisione: connect NON è prova di canale. OpenSSH accetta la TCP sul listener
116
116
  // locale SUBITO e chiede il canale al server DOPO: se il server lo nega
117
117
  // (permitopen senza quella destinazione) il socket viene CHIUSO nel giro di
118
118
  // millisecondi — bind locale riuscito, canale morto. Una finestra di grazia
@@ -159,7 +159,7 @@ function probeForwardChannels({ ports, graceMs = CHANNEL_GRACE_MS, connect = net
159
159
  });
160
160
  }
161
161
 
162
- // R19 punto 3: chi ha installato la chiave quando il pannello non esisteva è
162
+ // da revisione punto 3: chi ha installato la chiave quando il pannello non esisteva è
163
163
  // rotto e NON lo sapeva. Il prodotto lo DICE, con la riga da sostituire: le
164
164
  // destinazioni arrivano dagli `-L`, la pubblica si DERIVA dalla privata
165
165
  // indicata da `-i`. Quando non si riesce a derivarla, dice comunque COSA
@@ -359,7 +359,7 @@ function enterDegraded(diagnosis) {
359
359
  retryTimer = setTimeout(run, steadyRetryMs);
360
360
  }
361
361
 
362
- // R19 seguito, secondo difetto (2026-08-17, audit su develop@437d29f):
362
+ // secondo difetto della stessa famiglia (da revisione interna):
363
363
  // enterDegraded per il canale -L riusava la macchina del forward inverso
364
364
  // senza la sua precondizione implicita. Nel reverse failure `child` e' GIA'
365
365
  // null quando enterDegraded gira (handleFailure lo azzera PRIMA, perche' e'
@@ -369,7 +369,7 @@ function enterDegraded(diagnosis) {
369
369
  // generazione. run() faceva `child = spawn(...)` incondizionatamente: la
370
370
  // vecchia generazione, ancora viva, restava senza piu' nessuna variabile che
371
371
  // la referenzi — irraggiungibile da stop(), orfana, titolare dei suoi bind
372
- // per sempre. Misurato dall'auditor: due fake-ssh vivi dopo un degraded,
372
+ // per sempre. Misurato in revisione: due fake-ssh vivi dopo un degraded,
373
373
  // SIGTERM al supervisor ne ferma solo l'ultimo.
374
374
  //
375
375
  // La correzione non e' locale (un kill dentro enterDegraded): e' che
@@ -525,6 +525,6 @@ function acquireGeneration() {
525
525
  }
526
526
  if (require.main === module) acquireGeneration();
527
527
 
528
- // Esportate per prova diretta (R19): il main resta argv/env-driven e NON parte
528
+ // Esportate per prova diretta (da revisione): il main resta argv/env-driven e NON parte
529
529
  // al require.
530
530
  module.exports = { probeForwardChannels, refusalHint, refusalDetails, CHANNEL_GRACE_MS };