blitzwing 0.1.5 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzwing",
3
- "version": "0.1.5",
3
+ "version": "0.1.9",
4
4
  "description": "Interactive setup wizard to join a Blitzwing Petals mother swarm as a compute contributor",
5
5
  "bin": {
6
6
  "blitzwing": "bin/blitzwing.js"
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "license": "MIT",
24
24
  "config": {
25
- "discoveryUrl": "https://f44c-103-98-63-33.ngrok-free.app"
25
+ "discoveryUrl": "http://35.238.86.1:9000"
26
26
  },
27
27
  "dependencies": {
28
28
  "@clack/prompts": "^0.9.1",
package/src/config.js CHANGED
@@ -2,10 +2,13 @@
2
2
  export const DEFAULT_DISCOVERY_URL =
3
3
  process.env.BLITZWING_DISCOVERY_URL ||
4
4
  process.env.npm_package_config_discoveryUrl ||
5
- "https://f44c-103-98-63-33.ngrok-free.app";
5
+ "http://35.238.86.1:9000";
6
6
 
7
7
  export const HOME_DIR = process.env.BLITZWING_HOME || `${process.env.HOME || process.env.USERPROFILE}/.blitzwing`;
8
8
  export const STATE_PATH = `${HOME_DIR}/contributor.json`;
9
9
  export const VENV_PATH = `${HOME_DIR}/venv`;
10
- export const SHARD_PORT = Number(process.env.BLITZWING_SHARD_PORT || 8001);
11
- export const PETALS_PORT = Number(process.env.BLITZWING_PETALS_PORT || 31337);
10
+ // Windows + WSL on same PC: avoid 8001/31337 — WSL forwards those on 127.0.0.1.
11
+ const defaultShardPort = process.platform === "win32" ? 8011 : 8001;
12
+ const defaultPetalsPort = process.platform === "win32" ? 31338 : 31337;
13
+ export const SHARD_PORT = Number(process.env.BLITZWING_SHARD_PORT || defaultShardPort);
14
+ export const PETALS_PORT = Number(process.env.BLITZWING_PETALS_PORT || defaultPetalsPort);
package/src/install.js CHANGED
@@ -101,13 +101,35 @@ export function ensurePython() {
101
101
 
102
102
  function petalsImportable(python) {
103
103
  try {
104
- execFileSync(python, ["-c", "import petals"], { stdio: "ignore" });
104
+ execFileSync(
105
+ python,
106
+ ["-c", "import petals.cli.run_server"],
107
+ { stdio: "ignore" }
108
+ );
105
109
  return true;
106
110
  } catch {
107
111
  return false;
108
112
  }
109
113
  }
110
114
 
115
+ function uvicornImportable(python) {
116
+ try {
117
+ execFileSync(python, ["-c", "import uvicorn, fastapi"], { stdio: "ignore" });
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ function ensureShardManagerDeps(python, pip, env, onLog) {
125
+ if (uvicornImportable(python)) return;
126
+ onLog?.("Installing shard manager deps (uvicorn, fastapi)...");
127
+ execFileSync(python, ["-m", "pip", "install", "fastapi", "uvicorn[standard]", "pydantic"], {
128
+ stdio: "inherit",
129
+ env,
130
+ });
131
+ }
132
+
111
133
  export function ensureVenvAndPetals({ onLog }) {
112
134
  const { py } = ensurePython();
113
135
  fs.mkdirSync(HOME_DIR, { recursive: true });
@@ -125,29 +147,43 @@ export function ensureVenvAndPetals({ onLog }) {
125
147
  };
126
148
 
127
149
  if (petalsImportable(python)) {
150
+ ensureShardManagerDeps(python, pip, env, onLog);
128
151
  onLog?.("Petals already installed — skipping dependency install");
152
+ fs.writeFileSync(path.join(HOME_DIR, "python"), python + "\n");
129
153
  return { python, pip };
130
154
  }
131
155
 
156
+ if (process.platform === "win32") {
157
+ throw new Error(
158
+ "Petals cannot be installed on native Windows (hivemind requires uvloop/Linux).\n" +
159
+ "Run the contributor inside WSL instead:\n" +
160
+ " wsl bash /mnt/c/Users/MSI/Desktop/blitzwing/scripts/local_wsl_contributor_join.sh\n" +
161
+ "Or run the full E2E script:\n" +
162
+ " powershell -File scripts/run_local_e2e.ps1"
163
+ );
164
+ }
165
+
132
166
  onLog?.("Installing CPU PyTorch + Petals (may take a few minutes on first run)...");
133
- execFileSync(pip, ["install", "-U", "pip", "wheel", "setuptools<81"], {
167
+ execFileSync(python, ["-m", "pip", "install", "-U", "pip", "wheel", "setuptools<81"], {
134
168
  stdio: "inherit",
135
169
  env,
136
170
  });
137
171
  execFileSync(
138
- pip,
139
- ["install", "torch", "--index-url", "https://download.pytorch.org/whl/cpu"],
172
+ python,
173
+ ["-m", "pip", "install", "torch", "--index-url", "https://download.pytorch.org/whl/cpu"],
140
174
  { stdio: "inherit", env }
141
175
  );
142
176
  // Needed to compile hivemind protobufs during --no-build-isolation install
143
- execFileSync(pip, ["install", "grpcio", "grpcio-tools", "protobuf"], {
177
+ execFileSync(python, ["-m", "pip", "install", "grpcio", "grpcio-tools", "protobuf"], {
144
178
  stdio: "inherit",
145
179
  env,
146
180
  });
147
181
  // Hivemind first with no build isolation (needs pkg_resources from setuptools<81)
148
182
  execFileSync(
149
- pip,
183
+ python,
150
184
  [
185
+ "-m",
186
+ "pip",
151
187
  "install",
152
188
  "--no-build-isolation",
153
189
  "git+https://github.com/learning-at-home/hivemind.git@213bff98a62accb91f254e2afdccbf1d69ebdea9",
@@ -155,15 +191,16 @@ export function ensureVenvAndPetals({ onLog }) {
155
191
  { stdio: "inherit", env }
156
192
  );
157
193
  execFileSync(
158
- pip,
159
- ["install", "--no-build-isolation", "git+https://github.com/bigscience-workshop/petals.git"],
194
+ python,
195
+ ["-m", "pip", "install", "--no-build-isolation", "git+https://github.com/bigscience-workshop/petals.git"],
160
196
  { stdio: "inherit", env }
161
197
  );
162
- execFileSync(pip, ["install", "fastapi", "uvicorn[standard]", "pydantic", "httpx"], {
198
+ execFileSync(python, ["-m", "pip", "install", "fastapi", "uvicorn[standard]", "pydantic", "httpx"], {
163
199
  stdio: "inherit",
164
200
  env,
165
201
  });
166
202
 
203
+ fs.writeFileSync(path.join(HOME_DIR, "python"), python + "\n");
167
204
  return { python, pip };
168
205
  }
169
206
 
@@ -183,6 +220,7 @@ from pydantic import BaseModel, Field
183
220
 
184
221
  class ReloadRequest(BaseModel):
185
222
  block_indices: str = Field(..., pattern=r"^\\d+:\\d+$")
223
+ initial_peers: Optional[List[str]] = None
186
224
 
187
225
  class StatusResponse(BaseModel):
188
226
  running: bool
@@ -202,8 +240,12 @@ class Mgr:
202
240
  self.block_indices = os.environ["BLOCK_INDICES"]
203
241
  peers = [p.strip() for p in os.environ.get("INITIAL_PEERS", "").split(",") if p.strip()]
204
242
  self.initial_peers = peers
243
+ announce_raw = os.environ.get("ANNOUNCE_MADDRS", "")
244
+ self.announce_maddrs = [a.strip() for a in announce_raw.split(",") if a.strip()]
205
245
  self.python = os.environ.get("PETALS_PYTHON", "python")
246
+ self.petals_log = os.environ.get("PETALS_LOG", os.path.expanduser("~/.blitzwing/petals.log"))
206
247
  self._proc = None
248
+ self._log_fp = None
207
249
  self._lock = threading.Lock()
208
250
  self.last_exit_code = None
209
251
 
@@ -212,10 +254,19 @@ class Mgr:
212
254
  "--device", "cpu", "--quant_type", "none",
213
255
  "--block_indices", bi, "--port", str(self.port),
214
256
  "--identity_path", self.identity_path, "--num_handlers", "1"]
215
- if self.public_ip:
257
+ if self.announce_maddrs:
258
+ c += ["--announce_maddrs", *self.announce_maddrs]
259
+ elif self.public_ip:
216
260
  c += ["--public_ip", self.public_ip]
217
261
  if self.initial_peers:
218
262
  c += ["--initial_peers", *self.initial_peers]
263
+ use_auto_relay = os.environ.get("PETALS_USE_AUTO_RELAY", "1") not in ("0", "false", "False")
264
+ if not self.announce_maddrs and use_auto_relay:
265
+ pass
266
+ elif not use_auto_relay:
267
+ c += ["--no_auto_relay"]
268
+ if os.environ.get("PETALS_SKIP_REACHABILITY_CHECK", "1") in ("1", "true", "True"):
269
+ c += ["--skip_reachability_check"]
219
270
  return c
220
271
 
221
272
  def start(self, bi=None):
@@ -223,7 +274,15 @@ class Mgr:
223
274
  if bi: self.block_indices = bi
224
275
  if self._proc and self._proc.poll() is None:
225
276
  return
226
- self._proc = subprocess.Popen(self.cmd(self.block_indices))
277
+ if self._log_fp is None:
278
+ os.makedirs(os.path.dirname(self.petals_log) or ".", exist_ok=True)
279
+ self._log_fp = open(self.petals_log, "a", encoding="utf-8")
280
+ popen_kwargs = {"stdout": self._log_fp, "stderr": subprocess.STDOUT}
281
+ if os.name == "nt":
282
+ popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
283
+ else:
284
+ popen_kwargs["preexec_fn"] = os.setsid
285
+ self._proc = subprocess.Popen(self.cmd(self.block_indices), **popen_kwargs)
227
286
  self.last_exit_code = None
228
287
 
229
288
  def stop(self):
@@ -232,17 +291,28 @@ class Mgr:
232
291
  self._proc = None
233
292
  return
234
293
  p = self._proc
235
- p.send_signal(signal.SIGTERM)
294
+ try:
295
+ if os.name == "nt":
296
+ p.send_signal(signal.CTRL_BREAK_EVENT)
297
+ else:
298
+ os.killpg(os.getpgid(p.pid), signal.SIGTERM)
299
+ except Exception:
300
+ p.send_signal(signal.SIGTERM)
236
301
  try: p.wait(timeout=30)
237
302
  except Exception:
238
303
  p.kill(); p.wait(timeout=10)
239
304
  self.last_exit_code = p.returncode
240
305
  self._proc = None
241
306
 
242
- def reload(self, bi):
307
+ def reload(self, bi, initial_peers=None):
308
+ if initial_peers is not None:
309
+ self.initial_peers = initial_peers
243
310
  self.stop(); time.sleep(1); self.start(bi)
244
311
 
245
312
  def status(self):
313
+ if self._proc is not None and self._proc.poll() is not None:
314
+ self.last_exit_code = self._proc.returncode
315
+ self._proc = None
246
316
  running = self._proc is not None and self._proc.poll() is None
247
317
  return StatusResponse(
248
318
  running=running,
@@ -273,7 +343,7 @@ def status():
273
343
  def reload(body: ReloadRequest):
274
344
  a,b = map(int, body.block_indices.split(":"))
275
345
  if b <= a: raise HTTPException(400, "bad range")
276
- mgr.reload(body.block_indices)
346
+ mgr.reload(body.block_indices, initial_peers=body.initial_peers)
277
347
  time.sleep(0.5)
278
348
  return mgr.status()
279
349
 
@@ -292,12 +362,13 @@ export function startShardManagerProcess({
292
362
  }) {
293
363
  const appPath = writeLocalShardManager();
294
364
  const out = fs.openSync(logPath, "a");
365
+ const petalsPy = petalsImportable(venvPython()) ? venvPython() : python;
295
366
  const child = spawn(
296
- python,
367
+ petalsPy,
297
368
  ["-m", "uvicorn", `shard_manager_app:app`, "--host", "0.0.0.0", "--port", String(SHARD_PORT)],
298
369
  {
299
370
  cwd: path.dirname(appPath),
300
- env: { ...process.env, ...env, PETALS_PYTHON: python },
371
+ env: { ...process.env, ...env, PETALS_PYTHON: petalsPy, PETALS_LOG: path.join(HOME_DIR, "petals.log") },
301
372
  detached: true,
302
373
  stdio: ["ignore", out, out],
303
374
  }
@@ -306,21 +377,37 @@ export function startShardManagerProcess({
306
377
  return { pid: child.pid, logPath, appPath };
307
378
  }
308
379
 
309
- export async function waitForShardRunning({ timeoutMs = 300000 } = {}) {
380
+ export async function waitForShardRunning({ timeoutMs = 300000, statusHost } = {}) {
381
+ const host = statusHost || process.env.BLITZWING_STATUS_HOST || "127.0.0.1";
382
+ const petalsLog = path.join(HOME_DIR, "petals.log");
310
383
  const start = Date.now();
311
384
  while (Date.now() - start < timeoutMs) {
312
385
  try {
313
- const res = await fetch(`http://127.0.0.1:${SHARD_PORT}/status`);
386
+ const res = await fetch(`http://${host}:${SHARD_PORT}/status`);
314
387
  if (res.ok) {
315
388
  const body = await res.json();
316
- if (body.running) return body;
389
+ if (body.running && body.pid) return body;
390
+ if (body.last_exit_code != null) {
391
+ const tail = fs.existsSync(petalsLog)
392
+ ? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-5).join("\n")
393
+ : "";
394
+ throw new Error(
395
+ `Petals exited (code ${body.last_exit_code}). ${tail || "See " + petalsLog}`
396
+ );
397
+ }
317
398
  }
318
- } catch {
399
+ } catch (err) {
400
+ if (err instanceof Error && err.message.startsWith("Petals exited")) throw err;
319
401
  /* retry */
320
402
  }
321
403
  await new Promise((r) => setTimeout(r, 2000));
322
404
  }
323
- throw new Error("Timed out waiting for local Petals shard manager to become ready");
405
+ const tail = fs.existsSync(petalsLog)
406
+ ? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-5).join("\n")
407
+ : "";
408
+ throw new Error(
409
+ `Timed out waiting for Petals on http://${host}:${SHARD_PORT}/status. ${tail || "See " + petalsLog}`
410
+ );
324
411
  }
325
412
 
326
413
  export { SHARD_PORT, PETALS_PORT, venvPython };
package/src/net.js CHANGED
@@ -24,6 +24,59 @@ export async function detectPublicIp() {
24
24
  return "";
25
25
  }
26
26
 
27
+ /** Local LAN/WSL IP for shard_manager_url metadata (not Petals announce). */
28
+ export async function detectLocalIp() {
29
+ if (process.env.BLITZWING_LOCAL_IP) {
30
+ return process.env.BLITZWING_LOCAL_IP.trim();
31
+ }
32
+ try {
33
+ const { networkInterfaces } = await import("node:os");
34
+ const nets = networkInterfaces();
35
+ for (const name of Object.keys(nets)) {
36
+ for (const net of nets[name] || []) {
37
+ if (net.family === "IPv4" && !net.internal) {
38
+ return net.address;
39
+ }
40
+ }
41
+ }
42
+ } catch {
43
+ /* ignore */
44
+ }
45
+ return "127.0.0.1";
46
+ }
47
+
48
+ /** Optional ngrok TCP tunnel for dev; production should use VM public IP or auto-relay. */
49
+ export async function discoverNgrokTcpAnnounce(petalsPort) {
50
+ const override = process.env.BLITZWING_ANNOUNCE_MADDRS?.trim();
51
+ if (override) return override;
52
+
53
+ try {
54
+ const res = await fetch("http://127.0.0.1:4040/api/tunnels", {
55
+ signal: AbortSignal.timeout(2000),
56
+ });
57
+ if (!res.ok) return null;
58
+ const data = await res.json();
59
+ const tunnel = (data.tunnels || []).find((t) => t.proto === "tcp");
60
+ if (!tunnel?.public_url) return null;
61
+ const url = new URL(tunnel.public_url);
62
+ return `/dns4/${url.hostname}/tcp/${url.port || petalsPort}`;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ export function extractPeerMultiaddrFromLog(logPath, fs) {
69
+ if (!fs.existsSync(logPath)) return null;
70
+ const text = fs.readFileSync(logPath, "utf8");
71
+ const line = text
72
+ .split("\n")
73
+ .reverse()
74
+ .find((l) => l.includes("Running a server on"));
75
+ if (!line) return null;
76
+ const m = line.match(/Running a server on \['([^']+)'\]/);
77
+ return m ? m[1] : null;
78
+ }
79
+
27
80
  export function which(cmd) {
28
81
  try {
29
82
  const out = execFileSync(process.platform === "win32" ? "where" : "which", [cmd], {
package/src/wizard.js CHANGED
@@ -5,7 +5,12 @@ import path from "node:path";
5
5
  import { spawn } from "node:child_process";
6
6
  import { DEFAULT_DISCOVERY_URL, HOME_DIR, SHARD_PORT, PETALS_PORT } from "./config.js";
7
7
  import { listMothers, motherHosts, joinHost, readyHost, leaveHost } from "./api.js";
8
- import { detectPublicIp } from "./net.js";
8
+ import {
9
+ detectPublicIp,
10
+ detectLocalIp,
11
+ discoverNgrokTcpAnnounce,
12
+ extractPeerMultiaddrFromLog,
13
+ } from "./net.js";
9
14
  import {
10
15
  ensureVenvAndPetals,
11
16
  startShardManagerProcess,
@@ -55,6 +60,9 @@ ${color.bold("blitzwing")} — join a Blitzwing mother swarm as a compute node
55
60
 
56
61
  Env:
57
62
  BLITZWING_DISCOVERY_URL Override discovery service (default ${DEFAULT_DISCOVERY_URL})
63
+ BLITZWING_ANNOUNCE_MADDRS Explicit Petals announce multiaddr (VM with public IP)
64
+ BLITZWING_LOCAL_IP Local IP for shard manager metadata
65
+ PETALS_USE_AUTO_RELAY=0 Disable libp2p auto-relay (use with public IP / port-forward)
58
66
  `);
59
67
  }
60
68
 
@@ -130,20 +138,86 @@ async function wizard(args) {
130
138
  const layersN = Number(layers);
131
139
 
132
140
  const detected = await detectPublicIp();
133
- const publicIp = await p.text({
134
- message: "Public IP for this machine (other peers must reach you)",
135
- initialValue: detected || "",
141
+ const networkMode = await p.select({
142
+ message: "How will other peers reach your Petals node?",
143
+ options: [
144
+ {
145
+ value: "relay",
146
+ label: "Auto (recommended for home / NAT)",
147
+ hint: "Uses Petals libp2p relay — no port forwarding",
148
+ },
149
+ {
150
+ value: "public",
151
+ label: "Public IP / cloud VM",
152
+ hint: "TCP 31337 (and 8001) open on the internet",
153
+ },
154
+ {
155
+ value: "ngrok",
156
+ label: "Dev tunnel (ngrok TCP on :4040)",
157
+ hint: "Only if ngrok is already running",
158
+ },
159
+ ],
160
+ initialValue: "relay",
161
+ });
162
+ if (p.isCancel(networkMode)) {
163
+ p.cancel("Setup cancelled");
164
+ process.exit(0);
165
+ }
166
+
167
+ let publicIp = "";
168
+ let announceMaddrs = "";
169
+ let useAutoRelay = "1";
170
+
171
+ if (networkMode === "public") {
172
+ publicIp = await p.text({
173
+ message: "Public IPv4 (must be reachable on TCP " + PETALS_PORT + ")",
174
+ initialValue: detected || "",
175
+ validate(v) {
176
+ if (!v || !/^\d+\.\d+\.\d+\.\d+$/.test(String(v).trim())) {
177
+ return "Enter a valid public IPv4 address";
178
+ }
179
+ },
180
+ });
181
+ if (p.isCancel(publicIp)) {
182
+ p.cancel("Setup cancelled");
183
+ process.exit(0);
184
+ }
185
+ publicIp = String(publicIp).trim();
186
+ announceMaddrs = `/ip4/${publicIp}/tcp/${PETALS_PORT}`;
187
+ useAutoRelay = "0";
188
+ } else if (networkMode === "ngrok") {
189
+ const ngrok = await discoverNgrokTcpAnnounce(PETALS_PORT);
190
+ if (!ngrok) {
191
+ p.cancel("No ngrok TCP tunnel on http://127.0.0.1:4040. Start: ngrok tcp " + PETALS_PORT);
192
+ process.exit(1);
193
+ }
194
+ announceMaddrs = ngrok;
195
+ publicIp = ngrok.match(/dns4\/([^/]+)/)?.[1] || detected || "relay";
196
+ useAutoRelay = "0";
197
+ p.log.info(`Using ngrok announce ${announceMaddrs}`);
198
+ } else {
199
+ publicIp = detected || "relay";
200
+ p.log.info("Using Petals libp2p auto-relay (no inbound port forward required).");
201
+ }
202
+
203
+ const localIp = await detectLocalIp();
204
+
205
+ const hederaAccount = await p.text({
206
+ message: "Hedera account ID for layer payouts (e.g. 0.0.123456)",
207
+ initialValue: process.env.BLITZWING_HEDERA_ACCOUNT_ID || "",
136
208
  validate(v) {
137
- if (!v || !v.trim()) return "Public IP is required";
209
+ const s = String(v || "").trim();
210
+ if (!/^0\.0\.\d+$/.test(s)) return "Enter a Hedera account like 0.0.123456";
138
211
  },
139
212
  });
140
- if (p.isCancel(publicIp)) {
213
+ if (p.isCancel(hederaAccount)) {
141
214
  p.cancel("Setup cancelled");
142
215
  process.exit(0);
143
216
  }
217
+ const hederaAccountId = String(hederaAccount).trim();
144
218
 
145
219
  const confirm = await p.confirm({
146
- message: `Join ${selected.model} hosting ${layersN} layers from ${String(publicIp).trim()}?`,
220
+ message: `Join ${selected.model} hosting ${layersN} layers (${networkMode}) paying to ${hederaAccountId}?`,
147
221
  initialValue: true,
148
222
  });
149
223
  if (p.isCancel(confirm) || !confirm) {
@@ -166,7 +240,8 @@ async function wizard(args) {
166
240
  const python = venvPython();
167
241
  spin.stop("Environment ready");
168
242
 
169
- const shardManagerUrl = `http://${String(publicIp).trim()}:${SHARD_PORT}`;
243
+ const shardManagerUrl = `http://${localIp}:${SHARD_PORT}`;
244
+ const petalsLogPath = path.join(HOME_DIR, "petals.log");
170
245
 
171
246
  spin.start("Requesting layer assignment from mother…");
172
247
  let assignment;
@@ -176,6 +251,7 @@ async function wizard(args) {
176
251
  layers: layersN,
177
252
  public_ip: String(publicIp).trim(),
178
253
  shard_manager_url: shardManagerUrl,
254
+ hedera_account_id: hederaAccountId,
179
255
  });
180
256
  } catch (err) {
181
257
  spin.stop("Join rejected");
@@ -187,22 +263,28 @@ async function wizard(args) {
187
263
 
188
264
  const logPath = path.join(HOME_DIR, "shard_manager.log");
189
265
  spin.start("Starting local Petals server…");
190
- const { pid } = startShardManagerProcess({
191
- python,
192
- logPath,
193
- env: {
266
+ const shardEnv = {
194
267
  MODEL_NAME: selected.model,
195
- PUBLIC_IP: String(publicIp).trim(),
268
+ PUBLIC_IP: announceMaddrs ? "" : String(publicIp).trim(),
196
269
  BLOCK_INDICES: assignment.block_indices,
197
270
  INITIAL_PEERS: (assignment.initial_peers || []).join(","),
198
271
  PETALS_PORT: String(PETALS_PORT),
199
272
  IDENTITY_PATH: path.join(HOME_DIR, "petals-identity"),
200
273
  SHARD_AUTO_START: "1",
201
- },
274
+ PETALS_USE_AUTO_RELAY: useAutoRelay,
275
+ PETALS_SKIP_REACHABILITY_CHECK: "1",
276
+ };
277
+ if (announceMaddrs) {
278
+ shardEnv.ANNOUNCE_MADDRS = announceMaddrs;
279
+ }
280
+ const { pid } = startShardManagerProcess({
281
+ python,
282
+ logPath,
283
+ env: shardEnv,
202
284
  });
203
285
 
204
286
  try {
205
- await waitForShardRunning({ timeoutMs: 600000 });
287
+ await waitForShardRunning({ timeoutMs: 600000, statusHost: "127.0.0.1" });
206
288
  } catch (err) {
207
289
  spin.stop("Petals did not become ready");
208
290
  p.cancel(`${err.message}. See ${logPath}`);
@@ -211,8 +293,12 @@ async function wizard(args) {
211
293
  spin.stop("Petals is serving your layers");
212
294
 
213
295
  spin.start("Finalizing handoff with mother…");
296
+ const peerMultiaddr = extractPeerMultiaddrFromLog(petalsLogPath, fs);
214
297
  try {
215
- await readyHost(selected.mother_url, { host_id: assignment.host_id });
298
+ await readyHost(selected.mother_url, {
299
+ host_id: assignment.host_id,
300
+ peer_multiaddr: peerMultiaddr || undefined,
301
+ });
216
302
  } catch (err) {
217
303
  spin.stop("Handoff failed");
218
304
  p.cancel(err.message);
@@ -227,9 +313,12 @@ async function wizard(args) {
227
313
  block_indices: assignment.block_indices,
228
314
  layers_hosted: assignment.layers_hosted,
229
315
  public_ip: String(publicIp).trim(),
316
+ network_mode: networkMode,
317
+ announce_maddrs: announceMaddrs || null,
230
318
  shard_manager_url: shardManagerUrl,
231
319
  shard_pid: pid,
232
320
  discovery_url: args.discoveryUrl,
321
+ hedera_account_id: hederaAccountId,
233
322
  joined_at: new Date().toISOString(),
234
323
  };
235
324
  saveState(state);
@@ -281,7 +370,8 @@ async function showStatus() {
281
370
  console.log(` mother: ${state.mother_url}`);
282
371
  console.log(` public_ip: ${state.public_ip}`);
283
372
  try {
284
- const res = await fetch(`http://127.0.0.1:${SHARD_PORT}/status`);
373
+ const statusHost = "127.0.0.1";
374
+ const res = await fetch(`http://${statusHost}:${SHARD_PORT}/status`);
285
375
  if (res.ok) {
286
376
  const st = await res.json();
287
377
  console.log(` petals_running: ${st.running}`);
@@ -308,7 +398,8 @@ async function doLeave() {
308
398
  p.log.warn(`Mother leave call failed: ${err.message}`);
309
399
  }
310
400
  try {
311
- await fetch(`http://127.0.0.1:${SHARD_PORT}/stop`, { method: "POST" });
401
+ const statusHost = "127.0.0.1";
402
+ await fetch(`http://${statusHost}:${SHARD_PORT}/stop`, { method: "POST" });
312
403
  } catch {
313
404
  /* ignore */
314
405
  }