@mmmbuto/nexuscrew 0.9.2 → 0.9.4
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.
- package/CHANGELOG.md +157 -0
- package/frontend/dist/assets/index-CbMytNmg.css +32 -0
- package/frontend/dist/assets/index-DFI1J-u5.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +11 -3
- package/lib/cli/init.js +18 -3
- package/lib/cli/pidfile.js +27 -1
- package/lib/cli/runtime-lifecycle.js +31 -1
- package/lib/files/telemetry.js +89 -0
- package/lib/fleet/builtin.js +218 -64
- package/lib/fleet/definitions.js +226 -0
- package/lib/live-host/bridge.js +82 -22
- package/lib/nodes/peer-transitions.js +81 -0
- package/lib/proxy/panel-proxy.js +30 -3
- package/lib/server.js +15 -0
- package/lib/update/runner.js +32 -31
- package/package.json +1 -1
- package/skills/live/SKILL.md +8 -0
- package/frontend/dist/assets/index-0vuhL1YP.css +0 -32
- package/frontend/dist/assets/index-uRh_hSop.js +0 -93
package/lib/live-host/bridge.js
CHANGED
|
@@ -17,10 +17,20 @@
|
|
|
17
17
|
// fallimento, la risposta è `none` col motivo: la Live parte senza
|
|
18
18
|
// puntamento, comportamento standard. Un `none` non è un errore HTTP.
|
|
19
19
|
// - MC2: il prompt per-cella (LIVE_PROMPT.md accanto ai canonici della
|
|
20
|
-
// cella) viaggia su developerInstructions di thread/start
|
|
21
|
-
//
|
|
22
|
-
// rev5 MC2).
|
|
23
|
-
//
|
|
20
|
+
// cella) viaggia su developerInstructions di thread/start e SOSTITUISCE
|
|
21
|
+
// le developer instructions della config per quella Live (rev4 LC2
|
|
22
|
+
// emendata da rev5 MC2). La riga che decide è in codex-rs
|
|
23
|
+
// core/src/config/mod.rs: `developer_instructions.or(cfg.developer_
|
|
24
|
+
// instructions)` — l'override Some scarta il valore di config. R2
|
|
25
|
+
// (2026-08-16, verso corretto dopo audit pre-release): l'identità della
|
|
26
|
+
// cella designata viaggia SEMPRE come intestazione anteposta al campo,
|
|
27
|
+
// anche senza prompt. Il campo NON è additivo: una cella senza
|
|
28
|
+
// LIVE_PROMPT.md, che prima non passava nulla e riceveva le developer
|
|
29
|
+
// instructions della config, ora passa la sola intestazione e QUELLE
|
|
30
|
+
// NON le riceve più. Restano fuori da questa sostituzione AGENTS.md e
|
|
31
|
+
// il world state (fragment user, canale separato) e il prompt base.
|
|
32
|
+
// La via designata per le istruzioni di lavoro della Live è il
|
|
33
|
+
// LIVE_PROMPT.md della cella: viaggia nello stesso campo.
|
|
24
34
|
// - MC3: il ponte crea le proprie conversazioni con thread/start e non
|
|
25
35
|
// tocca MAI la thread di una TUI — né turn/start né thread/resume: chi
|
|
26
36
|
// guarda i metodi visti dal server deve vedere solo initialize,
|
|
@@ -62,21 +72,41 @@ const CLIENT_NAME = 'nexuscrew-live-bridge';
|
|
|
62
72
|
const ORPHAN_GRACE_MS = 1500;
|
|
63
73
|
|
|
64
74
|
// —— Prompt per-cella (rev4 LC2, nome fisso confermato da Dev 2026-08-15) ——
|
|
65
|
-
// Collocazione: filesRoot
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
75
|
+
// Collocazione: filesRoot/<tmuxSession>/LIVE_PROMPT.md — la sessione tmux
|
|
76
|
+
// ESATTA che il roster dichiara per la cella designata, la stessa fonte gia'
|
|
77
|
+
// usata per l'intestazione R2 (identityHeader). NON un prefisso ricostruito a
|
|
78
|
+
// mano: fino al 2026-08-16 questa funzione anteponeva 'cloud-' come default
|
|
79
|
+
// universale quando il cellId non ce l'aveva gia' — su un device che chiama
|
|
80
|
+
// le proprie sessioni con un prefisso diverso il file non veniva MAI trovato,
|
|
81
|
+
// e l'esito era 'missing' ("assenza legittima"): il bug si mascherava
|
|
82
|
+
// esattamente nel ramo che avrebbe dovuto segnalarlo. Bug trovato scrivendo
|
|
83
|
+
// docs/LIVE_PROMPT.md, corretto qui.
|
|
84
|
+
//
|
|
85
|
+
// Quattro esiti DISTINTI, perché «non so nemmeno dove cercare», «ho cercato
|
|
86
|
+
// e non c'è» e «c'è ma non si può leggere» portano chi indaga in posti
|
|
87
|
+
// diversi:
|
|
88
|
+
// applied:true → il testo va su developerInstructions
|
|
89
|
+
// applied:false, reason session-unknown → il roster non dichiara la sessione
|
|
90
|
+
// tmux per questa cella: NESSUN path viene
|
|
91
|
+
// costruito (mai un prefisso indovinato),
|
|
92
|
+
// quindi non si tenta nemmeno la lettura
|
|
93
|
+
// applied:false, reason missing → ENOENT sul path dichiarato: assenza
|
|
94
|
+
// legittima (LC2.3), si procede senza
|
|
95
|
+
// PROMPT (R2: l'intestazione identità
|
|
96
|
+
// viaggia comunque). ATTENZIONE: il campo
|
|
97
|
+
// developerInstructions viene comunque
|
|
98
|
+
// inviato per via dell'intestazione, e il
|
|
99
|
+
// consumer lo SOSTITUISCE alla propria
|
|
100
|
+
// configurazione invece di sommarlo — chi
|
|
101
|
+
// non ha prompt per-cella non riceve le
|
|
102
|
+
// developer instructions globali che
|
|
103
|
+
// riceveva prima (vedi MC2, verificato
|
|
104
|
+
// sulla riga che decide)
|
|
72
105
|
// applied:false, reason unreadable|empty → presente ma inutilizzabile: va
|
|
73
106
|
// dichiarato, mai silenziato
|
|
74
|
-
function readCellPrompt(filesRoot,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// qui, una volta sola.
|
|
78
|
-
const dirName = String(cellId).startsWith('cloud-') ? String(cellId) : `cloud-${cellId}`;
|
|
79
|
-
const file = path.join(filesRoot, dirName, 'LIVE_PROMPT.md');
|
|
107
|
+
function readCellPrompt(filesRoot, tmuxSession) {
|
|
108
|
+
if (typeof tmuxSession !== 'string' || !tmuxSession) return { applied: false, reason: 'session-unknown' };
|
|
109
|
+
const file = path.join(filesRoot, tmuxSession, 'LIVE_PROMPT.md');
|
|
80
110
|
let raw;
|
|
81
111
|
try {
|
|
82
112
|
raw = fs.readFileSync(file, 'utf8');
|
|
@@ -89,6 +119,22 @@ function readCellPrompt(filesRoot, cellId) {
|
|
|
89
119
|
return { applied: true, source: 'LIVE_PROMPT.md', text };
|
|
90
120
|
}
|
|
91
121
|
|
|
122
|
+
// —— Identità della Live (R2, 2026-08-16): la porta il ponte, non il prompt ——
|
|
123
|
+
// Il ponte ha la designazione IN MANO (MC3.4: è la sua condizione di
|
|
124
|
+
// funzionamento) e un prompt può legittimamente mancare (MC2.4): se
|
|
125
|
+
// l'identità dipendesse dal prompt, l'assenza del prompt diventerebbe assenza
|
|
126
|
+
// di identità — è esattamente il difetto visto sul campo (la voce andava a
|
|
127
|
+
// leggere tmux per capire dove si trovava).
|
|
128
|
+
//
|
|
129
|
+
// Il fatto, niente di più: quale cella (id Fleet) e, se il roster la dichiara,
|
|
130
|
+
// la sessione tmux esatta — quella con cui la voce raggiunge i canonici della
|
|
131
|
+
// cella in ~/NexusFiles/<tmuxSession>/. Non un'instruzione di lavoro: quelle
|
|
132
|
+
// vivono nel prompt per-cella, che questa intestazione PRECEDE sempre.
|
|
133
|
+
function identityHeader(cellId, tmuxSession) {
|
|
134
|
+
const sessione = tmuxSession ? ` (sessione tmux ${tmuxSession})` : '';
|
|
135
|
+
return `Live NexusCrew agganciata alla cella ${cellId}${sessione}.`;
|
|
136
|
+
}
|
|
137
|
+
|
|
92
138
|
// —— Client on-demand del socket di controllo (sezione protocollo sopra) ——
|
|
93
139
|
// Una sola richiesta per connessione: aperta, handshake, thread/start, chiusa.
|
|
94
140
|
// Le eventuali notifiche broadcast che arrivano nel frattempo vengono ignorate
|
|
@@ -308,7 +354,9 @@ function createLiveBridge({
|
|
|
308
354
|
// qualunque altro engine la Live lavora ATTRAVERSO la cella e il ponte non
|
|
309
355
|
// ha nulla da creare qui.
|
|
310
356
|
const engine = String(cell.engine || '');
|
|
311
|
-
|
|
357
|
+
// Stessa fonte dell'intestazione qui sotto: la sessione tmux che il
|
|
358
|
+
// roster dichiara, mai il cellId ricostruito con un prefisso indovinato.
|
|
359
|
+
const prompt = readCellPrompt(root, cell.tmuxSession);
|
|
312
360
|
|
|
313
361
|
if (!engine.startsWith('codex-vl')) {
|
|
314
362
|
const out = {
|
|
@@ -322,12 +370,21 @@ function createLiveBridge({
|
|
|
322
370
|
return out;
|
|
323
371
|
}
|
|
324
372
|
|
|
373
|
+
// R2: l'identità viaggia SEMPRE, anteposta al prompt quando c'è. Il campo
|
|
374
|
+
// non è mai più assente: senza LIVE_PROMPT.md porta la sola intestazione
|
|
375
|
+
// — e poiché il campo SOSTITUISCE le developer instructions della config
|
|
376
|
+
// (vedi MC2: la .or() in config/mod.rs), quella cella non le riceve più.
|
|
377
|
+
const intestazione = identityHeader(snap.hostCell, cell.tmuxSession);
|
|
378
|
+
const developerInstructions = prompt.applied
|
|
379
|
+
? `${intestazione}\n\n${prompt.text}`
|
|
380
|
+
: intestazione;
|
|
381
|
+
|
|
325
382
|
let started;
|
|
326
383
|
try {
|
|
327
384
|
started = await startThreadOnControlSocket({
|
|
328
385
|
socketPath: cfg.liveBridgeSocketPath,
|
|
329
386
|
cwd: cell.cwd,
|
|
330
|
-
developerInstructions
|
|
387
|
+
developerInstructions,
|
|
331
388
|
timeoutMs: cfg.liveBridgeTimeoutMs,
|
|
332
389
|
WebSocket,
|
|
333
390
|
log,
|
|
@@ -347,12 +404,15 @@ function createLiveBridge({
|
|
|
347
404
|
at: now(),
|
|
348
405
|
};
|
|
349
406
|
// LC1.4: il puntamento è visibile lato nostro — log con cella, thread e
|
|
350
|
-
// prompt applicato. È il "dirottamento dichiarato" del contratto.
|
|
351
|
-
|
|
407
|
+
// prompt applicato. È il "dirottamento dichiarato" del contratto. Il
|
|
408
|
+
// campo SOSTITUISCE le developer instructions della config (MC2, la
|
|
409
|
+
// .or() in config/mod.rs): il log lo dichiara, perché chi lo legge sappia
|
|
410
|
+
// cosa quella Live NON riceve.
|
|
411
|
+
log(`[live-bridge] Live puntata su ${snap.hostCell}: thread ${started.threadId} (cwd ${started.cwd}, identità nell'intestazione, prompt ${prompt.applied ? 'per-cella applicato' : `non applicato (${promptEcho.reason})`}, sostituisce le developer instructions di config)`);
|
|
352
412
|
return out;
|
|
353
413
|
}
|
|
354
414
|
|
|
355
|
-
return { resolveForLive, readCellPrompt: (
|
|
415
|
+
return { resolveForLive, readCellPrompt: (tmuxSession) => { const { text, ...rest } = readCellPrompt(root, tmuxSession); return rest; } };
|
|
356
416
|
}
|
|
357
417
|
|
|
358
418
|
let cachedVersion = null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// lib/nodes/peer-transitions.js — registra SOLO le transizioni di stato dei
|
|
3
|
+
// peer sul registro diagnostico GIA' esistente (lib/diagnostics/store.js):
|
|
4
|
+
// nessun nuovo store, nessun flusso nuovo, nessun polling in piu'. La salute
|
|
5
|
+
// federata (lib/nodes/health.js) viene gia' sondata a ogni richiesta della UI
|
|
6
|
+
// (/api/nodes, /api/peers) e il risultato veniva buttato via.
|
|
7
|
+
//
|
|
8
|
+
// Misurato il 2026-08-07: un nodo giu' per 25 minuti produceva ZERO record
|
|
9
|
+
// (nextSeq: 1, retained: 0). Senza storia delle transizioni, quando qualcosa
|
|
10
|
+
// cade non si puo' sapere QUANDO e' caduto ne' QUANTE VOLTE.
|
|
11
|
+
//
|
|
12
|
+
// TUNNEL e SERVIZIO restano DUE FATTI DISTINTI, mai un booleano «online»: il
|
|
13
|
+
// caso reale che ha fatto perdere quattro ore era tunnel su + servizio giu'
|
|
14
|
+
// (ECONNRESET, listener vivo ma NexusCrew morto sul dispositivo) contro
|
|
15
|
+
// nessun listener affatto (ECONNREFUSED) — due guasti, due rimedi diversi,
|
|
16
|
+
// e un valore unico li avrebbe fatti cercare nel posto sbagliato.
|
|
17
|
+
//
|
|
18
|
+
// SOLO SULLA TRANSIZIONE: un peer stabile per N sonde produce ZERO record,
|
|
19
|
+
// indipendentemente da quante volte viene sondato. Scrivere a ogni sonda
|
|
20
|
+
// trasformerebbe il registro in rumore e la storia si perderebbe nel volume
|
|
21
|
+
// — peggio di oggi, perche' sembrerebbe di avere i dati.
|
|
22
|
+
//
|
|
23
|
+
// warn, non info: il registro diagnostico scarta 'info'/'debug' quando il
|
|
24
|
+
// verbose non e' acceso (lib/diagnostics/store.js) — e il verbose non si puo'
|
|
25
|
+
// accendere retroattivamente su un incidente gia' successo. Una transizione
|
|
26
|
+
// di peer deve restare visibile anche senza averlo previsto in anticipo, sia
|
|
27
|
+
// che vada verso un guasto sia che torni a posto: solo cosi' si puo'
|
|
28
|
+
// ricostruire QUANDO e' caduto e QUANDO e' tornato, per contare gli episodi.
|
|
29
|
+
const last = new Map(); // nodeName -> { tunnel, service }
|
|
30
|
+
|
|
31
|
+
function tunnelStateOf(health) {
|
|
32
|
+
return health && typeof health.transport === 'string' ? health.transport : 'unknown';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Il servizio si valuta SOLO quando il tunnel e' su: senza trasporto, auth e
|
|
36
|
+
// reachability restano 'unknown' per costruzione (probeHealth non li tocca
|
|
37
|
+
// nel ramo down, vedi lib/proxy/federation.js) — dichiarare qui un servizio
|
|
38
|
+
// "giu'" sarebbe un secondo guasto INVENTATO, non misurato: il tunnel giu'
|
|
39
|
+
// spiega gia' da solo perche' il servizio non e' raggiungibile.
|
|
40
|
+
function serviceStateOf(health) {
|
|
41
|
+
if (!health || health.transport !== 'up') return 'unknown';
|
|
42
|
+
if (health.auth === 'ok' && health.reachability === 'ok') return 'ok';
|
|
43
|
+
if (health.auth === 'failed') return 'auth-failed';
|
|
44
|
+
if (health.reachability === 'failed') return 'unreachable';
|
|
45
|
+
return 'unknown';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// nodeName: chiave stabile del peer (node.name in lib/nodes/store.js).
|
|
49
|
+
// health: l'oggetto {transport, auth, reachability, ...} gia' calcolato da
|
|
50
|
+
// lib/nodes/health.js — questa funzione non sonda nulla, consuma soltanto.
|
|
51
|
+
// diagnostics: {record(level, component, code, message, meta)} — vedi
|
|
52
|
+
// lib/diagnostics/store.js. Chiamata ripetutamente con lo stesso stato: dopo
|
|
53
|
+
// la prima volta, silenzio.
|
|
54
|
+
function recordPeerTransition(nodeName, health, diagnostics) {
|
|
55
|
+
if (!nodeName || !diagnostics || typeof diagnostics.record !== 'function') return;
|
|
56
|
+
const tunnel = tunnelStateOf(health);
|
|
57
|
+
const service = serviceStateOf(health);
|
|
58
|
+
const previous = last.get(nodeName);
|
|
59
|
+
last.set(nodeName, { tunnel, service });
|
|
60
|
+
if (!previous) return; // primo probe di questo peer: nessuna "transizione" da niente.
|
|
61
|
+
|
|
62
|
+
const tunnelChanged = previous.tunnel !== tunnel;
|
|
63
|
+
if (tunnelChanged) {
|
|
64
|
+
diagnostics.record('warn', 'peer-health', 'TUNNEL_TRANSITION',
|
|
65
|
+
`${nodeName}: tunnel ${previous.tunnel} -> ${tunnel}`,
|
|
66
|
+
{ node: nodeName, state: tunnel });
|
|
67
|
+
}
|
|
68
|
+
// Testabile solo quando il tunnel e' su ORA: se e' appena caduto, il
|
|
69
|
+
// servizio e' passato a 'unknown' come artefatto del tunnel (gia' segnalato
|
|
70
|
+
// sopra), non come un fatto nuovo — quindi qui non si scrive nulla.
|
|
71
|
+
if (tunnel === 'up' && previous.service !== service) {
|
|
72
|
+
diagnostics.record('warn', 'peer-health', 'SERVICE_TRANSITION',
|
|
73
|
+
`${nodeName}: service ${previous.service} -> ${service}`,
|
|
74
|
+
{ node: nodeName, state: service });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Per i test: azzera la memoria delle transizioni fra un caso e l'altro.
|
|
79
|
+
function clearPeerTransitions() { last.clear(); }
|
|
80
|
+
|
|
81
|
+
module.exports = { recordPeerTransition, clearPeerTransitions, tunnelStateOf, serviceStateOf };
|
package/lib/proxy/panel-proxy.js
CHANGED
|
@@ -89,9 +89,23 @@ function stripLocalTokenQuery(search) {
|
|
|
89
89
|
// sessione del control plane, esattamente cio' che l'Authorization rimossa
|
|
90
90
|
// doveva impedire. E `x-forwarded-*` da un client sono valori che un pannello
|
|
91
91
|
// potrebbe credere veri. Rilievo di un audit indipendente.
|
|
92
|
-
function isStrippedRequestHeader(key) {
|
|
92
|
+
function isStrippedRequestHeader(key, value) {
|
|
93
93
|
if (HOP_BY_HOP.has(key)) return true;
|
|
94
|
-
if (key === '
|
|
94
|
+
if (key === 'cookie' || key === 'host') return true;
|
|
95
|
+
// `authorization` NON si strippa in blocco: lo schema dice di chi e'.
|
|
96
|
+
//
|
|
97
|
+
// `Bearer` e' il nostro token del control plane e non deve mai raggiungere il
|
|
98
|
+
// container — e' la ragione per cui questo strip esiste. Ma un pannello
|
|
99
|
+
// protetto da password risponde `401 WWW-Authenticate: Basic`, il browser
|
|
100
|
+
// chiede le credenziali all'utente e le rimanda: buttando via ANCHE quelle,
|
|
101
|
+
// il pannello ripresentava il prompt all'infinito e il login era impossibile
|
|
102
|
+
// per costruzione. Sono credenziali che l'utente ha inserito PER il pannello,
|
|
103
|
+
// in risposta a una sua richiesta: appartengono a lui.
|
|
104
|
+
//
|
|
105
|
+
// Dichiarato: se qualcuno mette una basic auth propria davanti a NexusCrew,
|
|
106
|
+
// quella credenziale raggiungerebbe il pannello. Non e' il nostro schema di
|
|
107
|
+
// autenticazione e non e' una configurazione che produciamo.
|
|
108
|
+
if (key === 'authorization') return !credenzialeDelPannello(value);
|
|
95
109
|
// `referer` porterebbe il ticket d'ingresso (viaggiava in query sulla prima
|
|
96
110
|
// richiesta dell'iframe) fino al pannello: al container non deve arrivare
|
|
97
111
|
// NESSUNA credenziale, nemmeno di seconda mano.
|
|
@@ -101,10 +115,23 @@ function isStrippedRequestHeader(key) {
|
|
|
101
115
|
return false;
|
|
102
116
|
}
|
|
103
117
|
|
|
118
|
+
// ELENCO CHIUSO di cio' che si inoltra, non di cio' che si toglie: un valore
|
|
119
|
+
// sconosciuto non deve poter passare per omissione. Sono gli schemi con cui un
|
|
120
|
+
// server web chiede le credenziali al browser — quelli che l'utente puo' aver
|
|
121
|
+
// inserito in risposta a un `401` del pannello. Tutto il resto, `Bearer`
|
|
122
|
+
// compreso, resta di qua.
|
|
123
|
+
const SCHEMI_DEL_PANNELLO = /^(basic|digest|ntlm|negotiate)\s/i;
|
|
124
|
+
|
|
125
|
+
function credenzialeDelPannello(value) {
|
|
126
|
+
const primo = Array.isArray(value) ? value[0] : value;
|
|
127
|
+
if (typeof primo !== 'string') return false;
|
|
128
|
+
return SCHEMI_DEL_PANNELLO.test(primo.trim());
|
|
129
|
+
}
|
|
130
|
+
|
|
104
131
|
function forwardHeaders(headers, targetHost) {
|
|
105
132
|
const out = {};
|
|
106
133
|
for (const [k, v] of Object.entries(headers || {})) {
|
|
107
|
-
if (isStrippedRequestHeader(k.toLowerCase())) continue;
|
|
134
|
+
if (isStrippedRequestHeader(k.toLowerCase(), v)) continue;
|
|
108
135
|
out[k] = v;
|
|
109
136
|
}
|
|
110
137
|
// L'host deve essere quello della destinazione: un pannello che genera
|
package/lib/server.js
CHANGED
|
@@ -19,6 +19,7 @@ const { loadOrCreateToken, verify } = require('./auth/token.js');
|
|
|
19
19
|
const { requireToken, bearerFrom } = require('./auth/middleware.js');
|
|
20
20
|
const { filesRoutes } = require('./files/routes.js');
|
|
21
21
|
const { createOutboxWatcher } = require('./files/watcher.js');
|
|
22
|
+
const { leggiTelemetria } = require('./files/telemetry.js');
|
|
22
23
|
const VERSION = require('../package.json').version;
|
|
23
24
|
const { transcribe } = require('./voice/transcribe.js');
|
|
24
25
|
const { selectProvider } = require('./fleet/provider.js');
|
|
@@ -32,6 +33,7 @@ const { fsRoutes } = require('./fs/routes.js');
|
|
|
32
33
|
const nodesStore = require('./nodes/store.js');
|
|
33
34
|
const nodesTunnel = require('./nodes/tunnel.js');
|
|
34
35
|
const nodesHealth = require('./nodes/health.js');
|
|
36
|
+
const { recordPeerTransition } = require('./nodes/peer-transitions.js');
|
|
35
37
|
const nodesInventory = require('./nodes/inventory.js');
|
|
36
38
|
const { createReverseSlotListeners } = require('./nodes/reverse-slot-listeners.js');
|
|
37
39
|
const reverseRotation = require('./nodes/reverse-rotation.js');
|
|
@@ -753,6 +755,10 @@ function createServer(opts = {}) {
|
|
|
753
755
|
status: working ? (s.status || sample?.status || '') : '',
|
|
754
756
|
outbox: sum[s.name] || { count: 0, latest: 0 },
|
|
755
757
|
preview: sample?.preview ?? null,
|
|
758
|
+
// Contesto libero e tier usati, dove la cella li pubblica (celle
|
|
759
|
+
// Claude via statusline). Null per tutte le altre — assenza
|
|
760
|
+
// legittima, non un trattino: la riga resta com'era.
|
|
761
|
+
telemetry: leggiTelemetria(cfg.filesRoot, s.name),
|
|
756
762
|
};
|
|
757
763
|
}));
|
|
758
764
|
res.json({ sessions: enriched });
|
|
@@ -976,6 +982,11 @@ function createServer(opts = {}) {
|
|
|
976
982
|
});
|
|
977
983
|
const nodes = view.nodes.map((n, i) => {
|
|
978
984
|
const h = healths[i] || null;
|
|
985
|
+
// La sonda gia' avviene per servire questa risposta: qui si registra
|
|
986
|
+
// SOLO se il risultato e' diverso dall'ultimo noto per questo peer
|
|
987
|
+
// (lib/nodes/peer-transitions.js) — nessun probe in piu', nessun
|
|
988
|
+
// record se il peer resta invariato.
|
|
989
|
+
recordPeerTransition(n.name, h, diagnostics);
|
|
979
990
|
return { ...n, tunnel: nodesHealth.tunnelFromHealth(h), health: h };
|
|
980
991
|
});
|
|
981
992
|
res.json({ nodeId: view.nodeId, nodes });
|
|
@@ -994,6 +1005,10 @@ function createServer(opts = {}) {
|
|
|
994
1005
|
});
|
|
995
1006
|
const extras = new Map(direct.map((node, index) => {
|
|
996
1007
|
const health = healths[index] || null;
|
|
1008
|
+
// Stessa registrazione di /nodes: la Map di stato di
|
|
1009
|
+
// peer-transitions.js e' condivisa per nome peer, quindi una
|
|
1010
|
+
// transizione gia' vista da /nodes non produce un secondo record qui.
|
|
1011
|
+
recordPeerTransition(node.name, health, diagnostics);
|
|
997
1012
|
return [node.name, { health, tunnel: nodesHealth.tunnelFromHealth(health) }];
|
|
998
1013
|
}));
|
|
999
1014
|
const topology = await federation.collectLocalTopology({
|
package/lib/update/runner.js
CHANGED
|
@@ -6,7 +6,7 @@ const os = require('node:os');
|
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const { execFileSync } = require('node:child_process');
|
|
8
8
|
const {
|
|
9
|
-
PACKAGE_NAME, parseVersion, scrubError,
|
|
9
|
+
PACKAGE_NAME, parseVersion, scrubError, adoptUpdateLock, releaseUpdateLock, readState, writeState,
|
|
10
10
|
stableRuntimeDir,
|
|
11
11
|
} = require('./core.js');
|
|
12
12
|
|
|
@@ -36,44 +36,45 @@ async function restartRuntime(opts = {}) {
|
|
|
36
36
|
}
|
|
37
37
|
mode = 'service';
|
|
38
38
|
} else {
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
39
|
+
// IL PUNTO UNICO DEL RIAVVIO, attraversato anche qui. Fino a qui questo
|
|
40
|
+
// ramo aveva una sua copia di ogni passo: tunnel fermati con un
|
|
41
|
+
// try/copy proprio, abbattimento con killPidfile diretto (senza l'attesa
|
|
42
|
+
// dell'uscita che il riavvio manuale fa), attesa porta inline. Due
|
|
43
|
+
// copie divergono: la stessa divergenza è stata trovata TRE volte, una
|
|
44
|
+
// per guasto — l'ultima misurata sul campo (2026-08-07): riavvii a mano
|
|
45
|
+
// → peer su 2/2, riavvii per aggiornamento → peer giù 2/2, la differenza
|
|
46
|
+
// era UNA riga. Ora i tunnel si fermano PRIMA dallo STESSO ingresso
|
|
47
|
+
// (`fermaTunnelPrimaDiRiavviare`, con la policy best-effort di questo
|
|
48
|
+
// chiamante: il compito principale è aggiornare) e il vecchio processo
|
|
49
|
+
// si abbatte con la STESSA `stopPortableRuntime` del manuale — kill e
|
|
50
|
+
// attesa dell'uscita incluse.
|
|
44
51
|
//
|
|
45
|
-
// COSA
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// percorsi era esattamente questa riga.
|
|
57
|
-
//
|
|
58
|
-
// Best-effort: un errore nel fermare i tunnel non deve impedire
|
|
59
|
-
// l'aggiornamento, che e' il compito principale. Ma tacerlo lo
|
|
60
|
-
// renderebbe irrintracciabile la prossima volta.
|
|
61
|
-
try {
|
|
62
|
-
(opts.stopTunnelsImpl || commands.stopManagedTunnels)({ home, platform });
|
|
63
|
-
} catch (e) {
|
|
64
|
-
if (opts.log) opts.log(`restart portatile: stop tunnel non riuscito — ${e && e.message ? e.message : e}`);
|
|
65
|
-
}
|
|
52
|
+
// COSA PRODUCEVA non fermarli, osservato sul campo: il servizio nuovo
|
|
53
|
+
// trovava un supervisore ancora vivo che non riusciva ad attribuirsi e
|
|
54
|
+
// non lo fermava né ne avviava uno suo (`lib/nodes/tunnel.js`:
|
|
55
|
+
// «unattributable existing supervisor»): il canale inverso restava
|
|
56
|
+
// appeso a un orfano e il peer risultava giù finché qualcuno non
|
|
57
|
+
// interveniva a mano.
|
|
58
|
+
commands.fermaTunnelPrimaDiRiavviare({
|
|
59
|
+
home, platform,
|
|
60
|
+
...(opts.stopTunnelsImpl ? { stopTunnelsImpl: opts.stopTunnelsImpl } : {}),
|
|
61
|
+
...(opts.log ? { log: opts.log } : {}),
|
|
62
|
+
});
|
|
66
63
|
const pidPath = pidf.defaultPidfilePath(home);
|
|
67
64
|
const meta = pidf.readPidfile(pidPath);
|
|
68
65
|
if (meta && pidf.isAlive(meta)) {
|
|
69
|
-
const stopped =
|
|
66
|
+
const stopped = (opts.stopPortableImpl || commands.stopPortableRuntime)({ home, platform });
|
|
70
67
|
if (!stopped.killed) throw new Error(`restart portatile fallito: ${stopped.reason || 'processo non arrestato'}`);
|
|
68
|
+
// Il pid è già stato atteso DENTRO stopPortableRuntime (waitForPidExit);
|
|
69
|
+
// resta la PORTA, che il kernel può trattenere oltre la morte del
|
|
70
|
+
// processo: un bind nuovo su porta occupata fa cadere il runtime sul
|
|
71
|
+
// fallback (server.js EADDRINUSE) — una porta DIVERSA da quella
|
|
72
|
+
// configurata, con i peer accoppiati rifiutati. Si aspetta la vera
|
|
73
|
+
// liberazione, non la morte del pid.
|
|
71
74
|
const wait = opts.waitImpl || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
72
75
|
let released = false;
|
|
73
76
|
for (let i = 0; i < 60; i += 1) {
|
|
74
|
-
|
|
75
|
-
const free = await (opts.portAvailableImpl || commands.portAvailable)(port);
|
|
76
|
-
if (dead && free) { released = true; break; }
|
|
77
|
+
if (await (opts.portAvailableImpl || commands.portAvailable)(port)) { released = true; break; }
|
|
77
78
|
await wait(100);
|
|
78
79
|
}
|
|
79
80
|
if (!released) throw new Error(`restart portatile fallito: porta ${port} non liberata`);
|
package/package.json
CHANGED
package/skills/live/SKILL.md
CHANGED
|
@@ -83,6 +83,14 @@ owning node, not a retry from the requester.
|
|
|
83
83
|
with nothing designated still answers `GET` with `hostCell: null` — a valid
|
|
84
84
|
state, distinct from "not permitted to ask".
|
|
85
85
|
|
|
86
|
+
## The per-cell voice prompt
|
|
87
|
+
|
|
88
|
+
Once a session lands on a host cell, a native Live (engine `codex-vl*`)
|
|
89
|
+
always sends `thread/start` an identity header naming the designated cell
|
|
90
|
+
and its exact tmux session, and appends a per-cell prompt file's text after
|
|
91
|
+
it when one exists. See [The Live per-cell prompt](../../docs/LIVE_PROMPT.md)
|
|
92
|
+
for where the file goes and the ready-to-copy IT/EN/ES templates.
|
|
93
|
+
|
|
86
94
|
## Dependencies
|
|
87
95
|
|
|
88
96
|
**Bundled:** this is a NexusCrew core feature. No external MCP companion or
|