@mmmbuto/nexuscrew 0.9.16 → 0.9.18

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 (68) hide show
  1. package/CHANGELOG.md +157 -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-CwsGpQwK.js → index-DmpQoCsc.js} +1 -1
  22. package/frontend/dist/index.html +1 -1
  23. package/frontend/dist/version.json +1 -1
  24. package/lib/auth/middleware.js +1 -1
  25. package/lib/cells/scope-guard.js +2 -2
  26. package/lib/cli/commands.js +7 -7
  27. package/lib/cli/fleet-service.js +2 -2
  28. package/lib/cli/pidfile.js +15 -15
  29. package/lib/cli/service.js +2 -2
  30. package/lib/cli/stable-alias.js +1 -1
  31. package/lib/files/routes.js +2 -2
  32. package/lib/fleet/builtin.js +6 -6
  33. package/lib/fleet/catalogs/zai-responses.json +82 -0
  34. package/lib/fleet/cell-exec.js +4 -4
  35. package/lib/fleet/cell-lease-server.js +17 -17
  36. package/lib/fleet/definitions.js +28 -17
  37. package/lib/fleet/launch-broker.js +3 -3
  38. package/lib/fleet/lease-client.js +1 -1
  39. package/lib/fleet/managed.js +86 -26
  40. package/lib/fleet/prompt-delivery.js +1 -1
  41. package/lib/fleet/runtime.js +3 -3
  42. package/lib/live-host/bridge.js +3 -3
  43. package/lib/live-host/routes.js +1 -1
  44. package/lib/mcp/server.js +7 -8
  45. package/lib/mcp/tools.js +1 -1
  46. package/lib/nodes/commands.js +2 -2
  47. package/lib/nodes/health.js +1 -1
  48. package/lib/nodes/identity.js +10 -13
  49. package/lib/nodes/store.js +2 -2
  50. package/lib/nodes/tunnel-supervisor.js +7 -7
  51. package/lib/nodes/tunnel.js +10 -11
  52. package/lib/notify/asks.js +4 -4
  53. package/lib/notify/persist.js +1 -1
  54. package/lib/notify/push.js +4 -4
  55. package/lib/notify/routes.js +12 -12
  56. package/lib/proxy/node-proxy.js +4 -4
  57. package/lib/proxy/panel-proxy.js +2 -2
  58. package/lib/pty/attach.js +1 -1
  59. package/lib/server.js +3 -3
  60. package/lib/settings/pairing-coordinator.js +1 -1
  61. package/lib/settings/routes.js +4 -4
  62. package/lib/tmux/lifecycle.js +4 -4
  63. package/lib/tmux/preview.js +1 -1
  64. package/lib/update/runner.js +9 -9
  65. package/lib/vl-nodes/broker.js +1 -1
  66. package/lib/ws/bridge.js +1 -1
  67. package/package.json +20 -2
  68. package/frontend/index.html +0 -20
@@ -205,7 +205,7 @@ function parseModel(m) {
205
205
  // parseDefinitions(raw) -> {schemaVersion, engines, cells, models} | null
206
206
  // Accetta stringa JSON o oggetto gia' parsato. Strict + fail-closed.
207
207
  // ---------------------------------------------------------------------------
208
- function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null } = {}) {
208
+ function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null, onIssue = null } = {}) {
209
209
  const reject = (message) => {
210
210
  if (typeof onReject === 'function') onReject(message);
211
211
  return null;
@@ -233,7 +233,7 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null }
233
233
  // realta' c'e'. `models` e' opzionale: una configurazione che non lo ha
234
234
  // resta valida esattamente com'era.
235
235
  //
236
- // D2 (audit pacchetto): extraModels porta il DESCRITTORE completo (id,
236
+ // da revisione (pacchetto): extraModels porta il DESCRITTORE completo (id,
237
237
  // engine, contextWindow, maxTokens, reasoning), non solo l'id. Prima era
238
238
  // Map<engine, Set<id>>: bastava per il gate "questo id e' ammesso?"
239
239
  // (declaredFor), ma contextWindow/maxTokens/reasoning sparivano — e
@@ -262,7 +262,7 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null }
262
262
  const engineMap = new Map();
263
263
  const engines = [];
264
264
  for (const e of d.engines) {
265
- const eng = parseEngine(e, { extraModels });
265
+ const eng = parseEngine(e, { extraModels, onIssue });
266
266
  if (!eng) return null;
267
267
  if (engineIds.has(eng.id)) return null; // id engine univoco
268
268
  engineIds.add(eng.id);
@@ -276,7 +276,9 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null }
276
276
  const cellIds = new Set();
277
277
  const cells = [];
278
278
  for (const c of d.cells) {
279
- const cell = parseCell(c, engineIds, engineMap, { allowLegacyTmuxNames, extraModels, onReject: reject });
279
+ const cell = parseCell(c, engineIds, engineMap, {
280
+ allowLegacyTmuxNames, extraModels, onReject: reject, onIssue,
281
+ });
280
282
  if (!cell) return null;
281
283
  if (cellIds.has(cell.id)) return null; // id cell univoco
282
284
  cellIds.add(cell.id);
@@ -306,7 +308,7 @@ function parseDefinitions(raw, { allowLegacyTmuxNames = true, onReject = null }
306
308
  }
307
309
  }
308
310
 
309
- function parseEngine(e, { extraModels = null } = {}) {
311
+ function parseEngine(e, { extraModels = null, onIssue = null } = {}) {
310
312
  if (!e || typeof e !== 'object' || Array.isArray(e)) return null;
311
313
 
312
314
  // id
@@ -330,7 +332,7 @@ function parseEngine(e, { extraModels = null } = {}) {
330
332
  // Managed: NexusCrew conosce client/provider e compone internamente il
331
333
  // processo. Nessun command/env/argv o segreto vive nella definizione.
332
334
  if (e.managed !== undefined) {
333
- const managed = normalizeManagedSpec(e.managed, { extraModels });
335
+ const managed = normalizeManagedSpec(e.managed, { extraModels, engineId: e.id, onIssue });
334
336
  if (!managed) return null;
335
337
  for (const key of ['command', 'args', 'env', 'promptMode', 'promptFlag', 'model']) {
336
338
  if (e[key] !== undefined) return null;
@@ -340,7 +342,7 @@ function parseEngine(e, { extraModels = null } = {}) {
340
342
  // pannello, e il fallback engine->cella di cellStatus vale per entrambi.
341
343
  // Prima di questo blocco il campo veniva scartato in silenzio: un valore
342
344
  // valido spariva e uno invalido veniva accettato (fail-open) — rilievo 1
343
- // dell'audit D8.
345
+ // della revisione.
344
346
  let managedPanelUrl;
345
347
  if (e.panelUrl !== undefined) {
346
348
  if (!validPanelUrl(e.panelUrl)) return null;
@@ -426,18 +428,20 @@ function parseEngine(e, { extraModels = null } = {}) {
426
428
  // e' nell'allowlist): un peer poteva mettere un id arbitrario in una cella di
427
429
  // questa installazione, e il boot lo usava senza ricontrollarlo.
428
430
  //
429
- // Rilievo dell'audit indipendente, provato con probe su due profili distinti.
431
+ // Rilievo della revisione indipendente, provato con probe su due profili distinti.
430
432
  // Il controllo e' quello vero, non una copia: si passa dalla stessa
431
433
  // `normalizeManagedSpec` con le stesse dichiarazioni.
432
- function cellModelAllowed(engineId, model, engineMap, extraModels) {
434
+ function cellModelAllowed(engineId, model, engineMap, extraModels, onIssue) {
433
435
  const engine = engineMap.get(engineId);
434
436
  // Engine custom (non gestito): il modello e' un argomento del comando, non un
435
437
  // id di catalogo, e qui non c'e' nulla da validare.
436
438
  if (!engine || !engine.managed) return true;
437
- return !!normalizeManagedSpec({ ...engine.managed, model }, { extraModels });
439
+ return !!normalizeManagedSpec({ ...engine.managed, model }, { extraModels, engineId, onIssue });
438
440
  }
439
441
 
440
- function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames = true, extraModels = null, onReject = null } = {}) {
442
+ function parseCell(c, engineIds, engineMap = new Map(), {
443
+ allowLegacyTmuxNames = true, extraModels = null, onReject = null, onIssue = null,
444
+ } = {}) {
441
445
  const reject = (message) => {
442
446
  if (typeof onReject === 'function') onReject(message);
443
447
  return null;
@@ -476,7 +480,7 @@ function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames =
476
480
  let model;
477
481
  if (c.model !== undefined) {
478
482
  if (typeof c.model !== 'string' || c.model.length > MAX_MODEL_VAL_LEN) return null;
479
- if (!cellModelAllowed(c.engine, c.model, engineMap, extraModels)) return null;
483
+ if (!cellModelAllowed(c.engine, c.model, engineMap, extraModels, onIssue)) return null;
480
484
  model = c.model;
481
485
  }
482
486
 
@@ -493,7 +497,7 @@ function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames =
493
497
  if (!engineIds.has(engineId) || typeof value !== 'string' || !value || value.length > MAX_MODEL_VAL_LEN) return null;
494
498
  // Anche la memoria per-engine passa dal gate: e' un valore che tornera'
495
499
  // in uso appena si ritorna su quell'engine.
496
- if (!cellModelAllowed(engineId, value, engineMap, extraModels)) return null;
500
+ if (!cellModelAllowed(engineId, value, engineMap, extraModels, onIssue)) return null;
497
501
  models[engineId] = value;
498
502
  }
499
503
  }
@@ -658,7 +662,7 @@ function parseCell(c, engineIds, engineMap = new Map(), { allowLegacyTmuxNames =
658
662
  //
659
663
  // Leggi-modifica-scrivi SOTTO LOCK. `atomicWrite` garantisce che il file non
660
664
  // resti a meta' — non che nessuno lo abbia cambiato mentre lo tenevi in mano:
661
- // sono due proprieta' diverse, e la prima non implica la seconda. Un audit ha
665
+ // sono due proprieta' diverse, e la prima non implica la seconda. Una revisione ha
662
666
  // riprodotto la perdita su SETTE percorsi diversi (i backfill di avvio, la
663
667
  // riparazione desktop e la persistenza della migrazione): ognuno salvava,
664
668
  // osservava sul disco il valore scritto da un altro, e lo perdeva scrivendo un
@@ -888,7 +892,7 @@ function validateCommandTrust(command) {
888
892
  if (!st.isFile()) return { ok: false, reason: 'non e\' un file regolare (symlink o speciale)' }; // lstat: symlink -> isFile()=false
889
893
  if (!(st.mode & 0o100)) return { ok: false, reason: 'non eseguibile dall\'owner' };
890
894
  if (st.mode & 0o002) return { ok: false, reason: 'world-writable' };
891
- // Owner check (design §9a, audit impl #4): il command deve appartenere
895
+ // Owner check (design §9a, da revisione): il command deve appartenere
892
896
  // all'utente del service o a root — un owner terzo potrebbe sostituire
893
897
  // l'eseguibile mantenendo il path "trusted".
894
898
  if (typeof process.getuid === 'function') {
@@ -1034,10 +1038,17 @@ function atomicWrite(p, data) {
1034
1038
 
1035
1039
  // Le letture devono accettare store legacy per poterli migrare; le scritture
1036
1040
  // invece sono sempre tmux-safe e rifiutano qualunque nuovo nome con punto.
1037
- const parsed = parseDefinitions(data, { allowLegacyTmuxNames: false });
1041
+ // La causa viene raccolta DURANTE la validazione e riportata nel
1042
+ // messaggio finale (campo, valore, regola). Senza cause nominate il
1043
+ // messaggio resta quello storico, generico.
1044
+ const issues = [];
1045
+ const parsed = parseDefinitions(data, { allowLegacyTmuxNames: false, onIssue: (i) => issues.push(i) });
1038
1046
  if (!parsed) {
1039
1047
  backupPredecessor(p); // conserva il precedente per recovery/forensics
1040
- throw new Error('definizioni fleet non valide: validazione fallita');
1048
+ const detail = issues.length
1049
+ ? ' — ' + issues.map((i) => `${i.field} ${JSON.stringify(i.value ?? '')}: ${i.rule}`).join('; ')
1050
+ : ' — validazione fallita';
1051
+ throw new Error(`definizioni fleet non valide${detail}`);
1041
1052
  }
1042
1053
 
1043
1054
  const dir = path.dirname(p);
@@ -103,8 +103,8 @@ function createLaunchBroker(cfg = {}) {
103
103
  let raw = '';
104
104
  socket.setEncoding('utf8');
105
105
  socket.setTimeout(3000, () => socket.destroy());
106
- // Correzione (audit 2a, stesso buco trovato in cell-lease-server.js):
107
- // il broker scrive su questo socket (il payload, o dopo F-B onLease
106
+ // Correzione (da revisione, stesso buco trovato in cell-lease-server.js):
107
+ // il broker scrive su questo socket (il payload, o dopo la chiamata onLease
108
108
  // prima). Un peer che muore mentre quella write e' in volo emette
109
109
  // 'error' senza listener -> throw fatale, INTERO processo giu'.
110
110
  socket.once('error', () => { try { socket.destroy(); } catch (_) {} });
@@ -130,7 +130,7 @@ function createLaunchBroker(cfg = {}) {
130
130
  // piu' il timeout idle da 3s pensato per il one-shot.
131
131
  socket.setTimeout(0);
132
132
  socket.removeAllListeners('data');
133
- // F-B (audit 2a @ 142e272): la lease PRIMA del payload. onLease
133
+ // da revisione interna: la lease PRIMA del payload. onLease
134
134
  // associa la connessione al lease manager (attachInitial); solo se
135
135
  // committa il payload viene scritto. Se onLease ritorna false, o ha
136
136
  // gia' distrutto la socket, il payload NON parte: il client riceve
@@ -67,7 +67,7 @@ function startLeaseClient(initialSocket, info, seams = {}) {
67
67
  clearTimer(reconnectTimer);
68
68
  // R3.2: oltre la grace non si ritenta (deny o server muto non vanno in loop).
69
69
  if (reconnectDeadline != null && now() >= reconnectDeadline) {
70
- // F-B (audit 2a @ 142e272): la grace e' scaduta senza un reconnect
70
+ // da revisione interna: la grace e' scaduta senza un reconnect
71
71
  // riuscito: la lease e' PERSA per questo supervisore. Prima si desisteva
72
72
  // in silenzio e il child restava un orfano senza lease per tutta la sua
73
73
  // vita, senza che nessuno lo sapesse. Ora il supervisore viene avvisato
@@ -27,6 +27,13 @@ const OLLAMA_CLOUD_MODELS = Object.freeze([
27
27
  // glm-5.3-flash 1M corroborato 1000000 da 7 provider su models.dev);
28
28
  // altrimenti convenzione binaria del repo (deepseek 1M -> 1048576;
29
29
  // minimax 512K -> 524288, valore pre-esistente ripristinato).
30
+ // La wire Responses nativa di Z.AI (https://api.z.ai/api/v1). Gli id e le
31
+ // finestre sono misurati sul listing reale del server (catalogo Codex-style,
32
+ // 2026-09-04): glm-5.3 e glm-5.3-flash a 1M. In questa catena non c'e'
33
+ // AnthMorph e i tool restano mcp__<server>__* (namespace_tools=false era una
34
+ // necessita' del proxy, non del backend).
35
+ const ZAI_RESPONSES_MODELS = Object.freeze(['glm-5.3', 'glm-5.3-flash']);
36
+ const ZAI_RESPONSES_CONTEXT = 1048576;
30
37
  const OLLAMA_CONTEXT = Object.freeze({
31
38
  // glm-5.2: scheda «976K context» (era 1M: over-declare di 24k).
32
39
  'glm-5.2': 976000, 'kimi-k2.7-code': 262144,
@@ -92,10 +99,20 @@ const MODEL_ALIASES = Object.freeze({
92
99
  // dichiarazione e' per profilo e non per fornitore: gli elenchi di Alibaba per
93
100
  // Claude e per Codex differiscono davvero, e generalizzare creerebbe
94
101
  // combinazioni che non esistono.
95
- function declaredFor(extraModels, profileId, model) {
102
+ // `extraModels` e' indicizzata per ENGINE ID (e' la chiave con cui
103
+ // definitions.js e `extraModelsFrom` la costruiscono), non per profilo: per un
104
+ // engine custom `profile.id` e' la chiave condivisa (`codex-vl.custom`) e con
105
+ // quella sola un modello dichiarato per QUELL'engine non viene mai visto.
106
+ // La ricerca prova prima l'ID dell'engine e ricade sul profilo: un engine che
107
+ // dichiara i propri modelli li trova, e una dichiarazione legacy fatta sul
108
+ // profilo continua a valere.
109
+ function declaredFor(extraModels, profileId, model, engineId = null) {
96
110
  if (!extraModels || typeof extraModels.get !== 'function') return false;
97
- const set = extraModels.get(profileId);
98
- return !!(set && typeof set.has === 'function' && set.has(model));
111
+ const keys = engineId && engineId !== profileId ? [engineId, profileId] : [profileId];
112
+ return keys.some((k) => {
113
+ const set = extraModels.get(k);
114
+ return !!(set && typeof set.has === 'function' && set.has(model));
115
+ });
99
116
  }
100
117
 
101
118
  // Dalle definizioni alla mappa che il gate si aspetta. Sta qui e non nei
@@ -103,7 +120,7 @@ function declaredFor(extraModels, profileId, model) {
103
120
  // uno la dimentichi perche' un modello dichiarato smetta di essere valido
104
121
  // proprio nel punto che conta — l'avvio.
105
122
  //
106
- // D2: Map<engine, Map<id, model>> — porta il descrittore intero, non solo
123
+ // da revisione: Map<engine, Map<id, model>> — porta il descrittore intero, non solo
107
124
  // l'id (stesso motivo del commento gemello in definitions.js: extraModels e'
108
125
  // costruita due volte, una dentro parseDefinitions per il parsing, una qui per
109
126
  // il runtime che rilegge le definizioni gia' salvate; le due DEVONO restare
@@ -123,10 +140,23 @@ function extraModelsFrom(defs) {
123
140
  // modelli (customCatalogFor, writePiProviderExtension) chiama questa; chi ha
124
141
  // bisogno solo di validare un id (declaredFor) continua a usare extraModels
125
142
  // direttamente — due bisogni diversi sulla STESSA struttura, non due copie.
126
- function declaredModelsFor(extraModels, profileId) {
143
+ // Stessa chiave di `declaredFor` — prima l'ID dell'engine, poi il
144
+ // profilo. L'unione deduplica per `id`: se lo stesso id compaia sia sotto
145
+ // l'engine sia sotto il profilo e' lo stesso modello, e il catalogo non deve
146
+ // contenerlo due volte.
147
+ function declaredModelsFor(extraModels, profileId, engineId = null) {
127
148
  if (!extraModels || typeof extraModels.get !== 'function') return [];
128
- const byId = extraModels.get(profileId);
129
- return byId && typeof byId.values === 'function' ? [...byId.values()] : [];
149
+ const keys = engineId && engineId !== profileId ? [engineId, profileId] : [profileId];
150
+ const out = [];
151
+ const seen = new Set();
152
+ for (const k of keys) {
153
+ const byId = extraModels.get(k);
154
+ if (!byId || typeof byId.values !== 'function') continue;
155
+ for (const m of byId.values()) {
156
+ if (m && m.id != null && !seen.has(m.id)) { seen.add(m.id); out.push(m); }
157
+ }
158
+ }
159
+ return out;
130
160
  }
131
161
 
132
162
  function canonicalModel(model) {
@@ -367,6 +397,8 @@ const CATALOG = Object.freeze([
367
397
  { id: 'codex-vl.opencode-go', client: 'codex-vl', provider: 'opencode-go', label: 'OpenCode Go', auth: 'OPENCODE_API_KEY', endpoint: OPENCODE_GO_API_BASE, protocol: 'openai_responses', model: 'deepseek-v4-flash', models: OPENCODE_GO_RESPONSES_MODELS, strictModels: true, core: true },
368
398
  { id: 'codex-vl.openrouter', client: 'codex-vl', provider: 'openrouter', label: 'OpenRouter', auth: 'OPENROUTER_API_KEY', endpoint: 'https://openrouter.ai/api/v1', protocol: 'openai_responses', requiresModel: true, core: true, notice: 'codex-openrouter' },
369
399
  { id: 'codex-vl.ollama-cloud', client: 'codex-vl', provider: 'ollama-cloud', label: 'Ollama Cloud', auth: 'OLLAMA_API_KEY', endpoint: 'https://ollama.com/v1', protocol: 'openai_responses', model: 'glm-5.2', models: OLLAMA_CLOUD_MODELS, legacySecrets: true, core: true },
400
+ { id: 'codex-vl.zai-a', client: 'codex-vl', provider: 'zai-a', label: 'Z.AI A (Responses nativo)', auth: 'ZAI_API_KEY_A', endpoint: 'https://api.z.ai/api/v1', protocol: 'openai_responses', model: 'glm-5.3', models: ZAI_RESPONSES_MODELS, strictModels: true, core: true },
401
+ { id: 'codex-vl.zai-p', client: 'codex-vl', provider: 'zai-p', label: 'Z.AI P (Responses nativo)', auth: 'ZAI_API_KEY_P', endpoint: 'https://api.z.ai/api/v1', protocol: 'openai_responses', model: 'glm-5.3', models: ZAI_RESPONSES_MODELS, strictModels: true, core: true },
370
402
  { id: 'codex-vl.openai-api', client: 'codex-vl', provider: 'openai-api', label: 'OpenAI API', auth: 'OPENAI_API_KEY', endpoint: 'https://api.openai.com/v1', protocol: 'openai_responses', core: true },
371
403
  { id: 'codex-vl.ollama', client: 'codex-vl', provider: 'ollama', label: 'Ollama local', auth: 'none', endpoint: 'local provider', protocol: 'openai_responses', localProvider: 'ollama', core: true },
372
404
  { id: 'codex-vl.lmstudio', client: 'codex-vl', provider: 'lmstudio', label: 'LM Studio', auth: 'none', endpoint: 'local provider', protocol: 'openai_responses', localProvider: 'lmstudio', core: true },
@@ -508,7 +540,15 @@ function sanitizeEnvPassthrough(list) {
508
540
  return names;
509
541
  }
510
542
 
511
- function normalizeManagedSpec(value, { extraModels = null } = {}) {
543
+ function normalizeManagedSpec(value, { extraModels = null, engineId = null, onIssue = null } = {}) {
544
+ // Un rifiuto senza causa produce «validazione fallita» generica e
545
+ // l'operatore cerca a vuoto. Se il chiamante passa `onIssue`, i rifiuti
546
+ // NOMINATI riportano campo, valore e regola prima di tornare null; senza
547
+ // sink il comportamento resta identico (null, nessun messaggio).
548
+ const issue = (field, valueIssue, rule) => {
549
+ if (typeof onIssue === 'function') onIssue({ field, value: valueIssue, rule });
550
+ return null;
551
+ };
512
552
  if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
513
553
  if (Object.keys(value).some((k) => !MANAGED_KEYS.has(k))) return null;
514
554
  if (!CLIENT_LABELS[value.client]) return null;
@@ -538,7 +578,7 @@ function normalizeManagedSpec(value, { extraModels = null } = {}) {
538
578
  // resta rifiutato, ed e' cio' che impedisce di far partire una cella con un
539
579
  // modello inesistente e scoprirlo dal fallimento.
540
580
  if (profile.strictModels && !(profile.models || []).includes(model)
541
- && !declaredFor(extraModels, profile.id, model)) return null;
581
+ && !declaredFor(extraModels, profile.id, model, engineId)) return issue('model', model, 'modello non dichiarato: non e\' nel catalogo del profilo e non risulta nell\'array "models" della definizione');
542
582
  const permissionPolicy = value.permissionPolicy === undefined ? (profile.client === 'claude' ? 'unsafe' : 'standard') : value.permissionPolicy;
543
583
  if (permissionPolicy !== 'standard' && permissionPolicy !== 'unsafe') return null;
544
584
  // vl e' standard-only: il CLI Vivling non ha flag di approvazione, quindi
@@ -556,7 +596,7 @@ function normalizeManagedSpec(value, { extraModels = null } = {}) {
556
596
  const envKey = typeof value.envKey === 'string' && value.envKey.trim()
557
597
  ? value.envKey.trim()
558
598
  : profile.defaultEnvKey;
559
- if (!ENV_KEY_RE.test(envKey || '')) return null;
599
+ if (!ENV_KEY_RE.test(envKey || '')) return issue('envKey', envKey, `consentito ${ENV_KEY_RE}`);
560
600
  out.envKey = envKey;
561
601
  }
562
602
  if (profile.custom) {
@@ -571,9 +611,9 @@ function normalizeManagedSpec(value, { extraModels = null } = {}) {
571
611
  // altri client) ma per NOME via envPassthrough (D3) — un custom vl verso un
572
612
  // endpoint senza auth (Ollama remoto) non dichiara nessuna chiave, quindi
573
613
  // envKey vuota e' legittima solo per vl. Se dichiarata, resta un nome valido.
574
- if (profile.client === 'vl' ? (envKey !== '' && !ENV_KEY_RE.test(envKey)) : !ENV_KEY_RE.test(envKey)) return null;
575
- if (!PROVIDER_ID_RE.test(providerId)) return null;
576
- if (!model || !(profile.protocols || [profile.protocol]).includes(protocol)) return null;
614
+ if (profile.client === 'vl' ? (envKey !== '' && !ENV_KEY_RE.test(envKey)) : !ENV_KEY_RE.test(envKey)) return issue('envKey', envKey, `consentito ${ENV_KEY_RE} (obbligatorio; vuoto ammesso solo per il client vl)`);
615
+ if (!PROVIDER_ID_RE.test(providerId)) return issue('providerId', providerId, `consentito ${PROVIDER_ID_RE}`);
616
+ if (!model || !(profile.protocols || [profile.protocol]).includes(protocol)) return issue('protocol', protocol, `protocollo non supportato dal profilo (ammessi: ${(profile.protocols || [profile.protocol]).join(', ')})`);
577
617
  Object.assign(out, { displayName, baseUrl, ...(envKey ? { envKey } : {}), protocol, providerId });
578
618
  }
579
619
  // D3: envPassthrough e' un'allowlist di NOMI di variabili d'ambiente che il
@@ -767,7 +807,7 @@ function binaryCandidates(client, home) {
767
807
  // (non possiamo dichiararlo "trovato"); ma chi costruisce il messaggio
768
808
  // (describeManaged) puo' ora distinguere "client non trovato" da "non ho potuto
769
809
  // verificare un candidato" — il discriminante e' CHI ha fallito, non che ci sia
770
- // stata un'eccezione. Stesso principio gia' applicato in checkTermuxExec (3134d2f).
810
+ // stata un'eccezione. Stesso principio gia' applicato in checkTermuxExec.
771
811
  function findBinary(client, home, out) {
772
812
  const blocked = Array.isArray(out && out.blocked) ? out.blocked : null;
773
813
  for (const candidate of binaryCandidates(client, home)) {
@@ -1061,7 +1101,7 @@ function describeManaged(spec, cfg = {}) {
1061
1101
  const extraModels = cfg.extraModels || null;
1062
1102
  const normalized = normalizeManagedSpec(spec, { extraModels });
1063
1103
  if (!normalized) {
1064
- // D2: il rifiuto resta (i descrittori NON appartengono al profilo di una
1104
+ // da revisione: il rifiuto resta (i descrittori NON appartengono al profilo di una
1065
1105
  // cella: vengono dalla definizione dell'ENGINE, in `d.models` — due
1066
1106
  // soggetti diversi, mescolarli renderebbe ambiguo chi dichiara cosa). Ma
1067
1107
  // se la causa e' proprio questa, il messaggio generico "invalid managed
@@ -1238,7 +1278,7 @@ function sharedMcpArgs(home) {
1238
1278
  // indicizzato sul percorso assoluto — ed e' reale: su questa installazione nove
1239
1279
  // progetti ne hanno, fra cui la directory di lavoro delle celle. Dimenticarlo
1240
1280
  // significava che un elenco parziale non negava server che la sessione carica
1241
- // davvero, e l'operatore avrebbe creduto di averli esclusi. Trovato dall'audit.
1281
+ // davvero, e l'operatore avrebbe creduto di averli esclusi. Trovato da una revisione.
1242
1282
  //
1243
1283
  // RESTANO FUORI, e non sono enumerabili da qui: plugin, connettori claude.ai e
1244
1284
  // la configurazione gestita di sistema. Per quelli l'unica esclusione certa e'
@@ -1356,7 +1396,7 @@ function ensurePrivateClaudeConfig(home, profileId, label, penguinMode) {
1356
1396
  return configDir;
1357
1397
  }
1358
1398
 
1359
- // D2 audit: il file .ts generato NON e' il consumatore — Pi lo E'. Un test che
1399
+ // da revisione: il file .ts generato NON e' il consumatore — Pi lo E'. Un test che
1360
1400
  // legge solo il file resta verde su un'estensione che Pi rifiuta a runtime.
1361
1401
  // CONTRATTO REALE che Pi (@earendil-works/pi-coding-agent 0.80.10) impone a
1362
1402
  // ogni modello di `pi.registerProvider(id, {models: [...]})` — fonte:
@@ -1382,13 +1422,13 @@ function toPiModelConfig(m) {
1382
1422
  };
1383
1423
  }
1384
1424
 
1385
- // D2: `declaredModels` (opzionale) sono i descrittori dichiarati per l'ENGINE
1425
+ // da revisione: `declaredModels` (opzionale) sono i descrittori dichiarati per l'ENGINE
1386
1426
  // (via declaredModelsFor), per il ramo Pi CUSTOM — stesso motivo/stessa fonte
1387
1427
  // di customCatalogFor, mai letti da `spec.models`. Il ramo Pi NON-custom
1388
1428
  // (profile.piExtension) continua a passare `models` dentro l'oggetto spec-like
1389
1429
  // come sempre: e' un catalogo STATICO cablato nel codice (es. alibaba-token-
1390
1430
  // plan), GIA' nella forma completa che Pi si aspetta — un caso diverso, non
1391
- // toccato dal difetto D2. declaredModels, quando presente, ha priorita', ed e'
1431
+ // toccato dal difetto da revisione. declaredModels, quando presente, ha priorita', ed e'
1392
1432
  // sempre passato per toPiModelConfig (mai i descrittori grezzi cosi' come sono).
1393
1433
  function writePiProviderExtension(spec, home, declaredModels) {
1394
1434
  const dir = path.join(home, '.nexuscrew', 'pi-providers');
@@ -1524,7 +1564,7 @@ function vlSystemAppendGate(binary, cfg) {
1524
1564
  return { ok: true };
1525
1565
  }
1526
1566
 
1527
- // D2 (fix definitivo — l'audit del pacchetto aveva bocciato la prima versione:
1567
+ // (fix definitivo — una revisione del pacchetto aveva bocciato la prima versione:
1528
1568
  // il test costruiva `spec.models` a mano, e in produzione quel campo non
1529
1569
  // esiste mai). `declaredModels` NON viene da `spec`: i descrittori sono
1530
1570
  // proprieta' della definizione dell'ENGINE (`d.models` del documento), non del
@@ -1614,19 +1654,21 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
1614
1654
  // non basta: la normalizzazione che decide se la cella PARTE e' questa, e
1615
1655
  // senza l'elenco dichiarato un modello legittimo diventa «invalid managed
1616
1656
  // profile» — cioe' una cella che non si avvia per una ragione invisibile.
1617
- // E' il difetto che l'audit ha ripreso due volte: la vista diceva di si',
1657
+ // E' il difetto che la revisione ha ripreso due volte: la vista diceva di si',
1618
1658
  // il boot diceva di no, e nessun test guardava il boot.
1619
- const spec = normalizeManagedSpec(engine.managed, { extraModels: cfg.extraModels || null });
1659
+ const spec = normalizeManagedSpec(engine.managed, { extraModels: cfg.extraModels || null, engineId: engine.id });
1620
1660
  const info = describeManaged(spec, cfg);
1621
1661
  if (!spec || !info.configured) return { ok: false, reason: info.reason, info };
1622
1662
  const home = cfg.home || require('node:os').homedir();
1623
1663
  const profile = profileFor(spec.client, spec.provider, spec.credentialProfile || '');
1624
- // D2: i descrittori dichiarati per QUESTO profilo (client.provider — la
1664
+ // da revisione: i descrittori dichiarati per QUESTO profilo (client.provider — la
1625
1665
  // stessa chiave che declaredFor usa dentro normalizeManagedSpec per
1626
1666
  // validare gli id). Vengono dalla definizione dell'ENGINE (extraModels),
1627
1667
  // MAI da spec: e' il ponte che customCatalogFor/writePiProviderExtension
1628
1668
  // (rami custom) usano per emettere catalogo e finestra di contesto.
1629
- const declaredModels = declaredModelsFor(cfg.extraModels, profile.id);
1669
+ // Prima i modelli dichiarati per QUESTO engine (per un custom e' la
1670
+ // chiave che conta: 'codex-vl.custom' e' condivisa), poi il fallback profilo.
1671
+ const declaredModels = declaredModelsFor(cfg.extraModels, profile.id, engine.id);
1630
1672
  const cred = credential(profile, spec, cfg, home);
1631
1673
  // L'override PER-CELLA va canonicalizzato come lo spec: `normalizeManagedSpec`
1632
1674
  // applica l'alias a `spec.model`, ma `cell.model` lo scavalca DOPO e senza
@@ -1842,10 +1884,28 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
1842
1884
  args.push('-c', `model_catalog_json=${JSON.stringify(localCatalog)}`);
1843
1885
  args.push('-c', `model_context_window=${opencodeContext}`);
1844
1886
  }
1887
+ } else if (spec.provider === 'zai-a' || spec.provider === 'zai-p') {
1888
+ // Wire Responses nativa di Z.AI — niente AnthMorph e nessuna
1889
+ // traduzione: previous_response_id e streaming restano semantica nativa.
1890
+ // namespace_tools=false NON serve (era del proxy); i parametri di
1891
+ // robustezza sono quelli degli altri wrapper Responses misurati, e il
1892
+ // catalogo porta i metadata reali del server (context window 1M).
1893
+ const zaiId = `zai_${spec.provider.slice(-1)}`;
1894
+ const zaiEnvKey = spec.provider === 'zai-a' ? 'ZAI_API_KEY_A' : 'ZAI_API_KEY_P';
1895
+ env[zaiEnvKey] = cred.value;
1896
+ args.push(...codexProviderArgs(zaiId, 'Z.AI (Responses nativo)', profile.endpoint, zaiEnvKey));
1897
+ args.push('-c', `model_providers.${zaiId}.request_max_retries=4`);
1898
+ args.push('-c', `model_providers.${zaiId}.stream_max_retries=10`);
1899
+ args.push('-c', `model_providers.${zaiId}.stream_idle_timeout_ms=300000`);
1900
+ const zaiCatalog = path.join(__dirname, 'catalogs', 'zai-responses.json');
1901
+ if (fs.existsSync(zaiCatalog)) {
1902
+ args.push('-c', `model_catalog_json=${JSON.stringify(zaiCatalog)}`);
1903
+ args.push('-c', `model_context_window=${ZAI_RESPONSES_CONTEXT}`);
1904
+ }
1845
1905
  } else if (spec.provider === 'custom') {
1846
1906
  env[spec.envKey] = cred.value;
1847
1907
  args.push(...codexProviderArgs(spec.providerId, spec.displayName, spec.baseUrl, spec.envKey));
1848
- // D2: onora `models` (engine definition, già validato) come gli altri
1908
+ // da revisione: onora `models` (engine definition, già validato) come gli altri
1849
1909
  // provider — deriva model_catalog_json e model_context_window, così
1850
1910
  // Codex-VL non ricade sul fallback 272K. Senza `models` -> null (no regressione).
1851
1911
  const customMeta = customCatalogFor(spec, model, declaredModels, home);
@@ -2029,7 +2089,7 @@ module.exports = {
2029
2089
  defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
2030
2090
  discoverPiModels, EXTERNAL_DISCOVERY_TIMEOUT_MS, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension, customCatalogFor,
2031
2091
  writeVlCellPrompt, vlSystemAppendGate,
2032
- extraModelsFrom, declaredModelsFor,
2092
+ extraModelsFrom, declaredModelsFor, declaredFor,
2033
2093
  providerKeyPaths, parseProviderKeyFiles, credentialSources, credential, CREDENTIAL_SOURCE_VALUES,
2034
2094
  credentialEnvNeutralizeSet, applyStoreNeutralization,
2035
2095
  ensureKimiClaudeConfig, ensureAlibabaClaudeConfig, resolveInteractiveShell,
@@ -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;
@@ -51,7 +51,7 @@ function applyProbeHysteresis(key, probed, previous, now, threshold) {
51
51
  // now is simply down — reporting 'unknown' would keep a declared-dead peer
52
52
  // out of the down state forever (one probe per process, never reaching the
53
53
  // threshold unless someone keeps probing). The hysteresis only exists to
54
- // protect a measured up state from jitter, per 239e2e6.
54
+ // protect a measured up state from jitter, per una revisione precedente.
55
55
  return { ...probed, consecutiveFailures: failures };
56
56
  }
57
57