@ruvector/edge-net 0.4.1 → 0.4.3

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.
@@ -4,9 +4,15 @@
4
4
  * Uses Google Firebase as bootstrap infrastructure for WebRTC signaling
5
5
  * with migration path to full P2P DHT network.
6
6
  *
7
+ * Security Model (WASM-based, no Firebase Auth needed):
8
+ * 1. Each node generates cryptographic identity in WASM (PiKey)
9
+ * 2. All messages are signed with Ed25519 keys
10
+ * 3. Peers verify signatures before accepting connections
11
+ * 4. AdaptiveSecurity provides self-learning attack detection
12
+ *
7
13
  * Architecture:
8
14
  * 1. Firebase Firestore for signaling (offer/answer/ICE)
9
- * 2. Firebase Realtime DB for presence (who's online)
15
+ * 2. WASM cryptographic identity (no Firebase Auth)
10
16
  * 3. Gradual migration to DHT as network grows
11
17
  *
12
18
  * @module @ruvector/edge-net/firebase-signaling
@@ -19,23 +25,32 @@ import { EventEmitter } from 'events';
19
25
  // ============================================
20
26
 
21
27
  /**
22
- * Get Firebase config from environment variables or saved config
28
+ * Edge-Net Public Firebase Configuration
23
29
  *
24
- * Configuration sources (in order of priority):
25
- * 1. Environment variables: FIREBASE_API_KEY, FIREBASE_PROJECT_ID
26
- * 2. Saved config from: ~/.edge-net/firebase.json (via firebase-setup.js)
27
- * 3. Application Default Credentials (for server-side via gcloud)
30
+ * This is the PUBLIC Firebase project for edge-net P2P network.
31
+ * API keys for Firebase web apps are designed to be public - security is via:
32
+ * 1. Firestore Security Rules (only authenticated users can write)
33
+ * 2. Anonymous Authentication (anyone can join, tracked by UID)
34
+ * 3. API restrictions in Google Cloud Console
28
35
  *
29
- * SECURITY:
30
- * - Never hardcode API keys
31
- * - Use `gcloud auth application-default login` for server-side
32
- * - Restrict API keys by domain in Google Cloud Console for browser-side
36
+ * Contributors automatically join the network - no setup required!
37
+ */
38
+ export const EDGE_NET_FIREBASE_CONFIG = {
39
+ apiKey: "AIzaSyAZAJhathdnKZGzBQ8iDBFG8_OQsvb2QvA",
40
+ projectId: "ruv-dev",
41
+ authDomain: "ruv-dev.firebaseapp.com",
42
+ storageBucket: "ruv-dev.appspot.com",
43
+ };
44
+
45
+ /**
46
+ * Get Firebase config
33
47
  *
34
- * Setup:
35
- * npx edge-net-firebase-setup --project YOUR_PROJECT_ID
48
+ * Priority:
49
+ * 1. Environment variables (for custom Firebase projects)
50
+ * 2. Built-in edge-net public config (no setup required)
36
51
  */
37
52
  export function getFirebaseConfig() {
38
- // Try environment variables first (highest priority)
53
+ // Allow override via environment variables for custom projects
39
54
  const apiKey = process.env.FIREBASE_API_KEY;
40
55
  const projectId = process.env.FIREBASE_PROJECT_ID;
41
56
 
@@ -44,13 +59,12 @@ export function getFirebaseConfig() {
44
59
  apiKey,
45
60
  projectId,
46
61
  authDomain: process.env.FIREBASE_AUTH_DOMAIN || `${projectId}.firebaseapp.com`,
47
- databaseURL: process.env.FIREBASE_DATABASE_URL || `https://${projectId}-default-rtdb.firebaseio.com`,
48
62
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET || `${projectId}.appspot.com`,
49
63
  };
50
64
  }
51
65
 
52
- // Sync version only uses env vars - use getFirebaseConfigAsync for file config
53
- return null;
66
+ // Use built-in public edge-net config (no setup required!)
67
+ return EDGE_NET_FIREBASE_CONFIG;
54
68
  }
55
69
 
56
70
  /**
@@ -137,6 +151,11 @@ export class FirebaseSignaling extends EventEmitter {
137
151
  this.db = null;
138
152
  this.rtdb = null;
139
153
 
154
+ // WASM Security (replaces Firebase Auth)
155
+ /** @type {import('./secure-access.js').SecureAccessManager|null} */
156
+ this.secureAccess = options.secureAccess || null;
157
+ this.verifySignatures = options.verifySignatures !== false;
158
+
140
159
  // State
141
160
  this.isConnected = false;
142
161
  this.peers = new Map();
@@ -150,27 +169,49 @@ export class FirebaseSignaling extends EventEmitter {
150
169
  firebaseSignals: 0,
151
170
  dhtSignals: 0,
152
171
  p2pSignals: 0,
172
+ verifiedSignals: 0,
173
+ rejectedSignals: 0,
153
174
  };
154
175
  }
155
176
 
156
177
  /**
157
- * Initialize Firebase connection
178
+ * Initialize Firebase connection with WASM cryptographic security
158
179
  */
159
180
  async connect() {
160
- // SECURITY: Require valid config
181
+ // Use built-in config if not provided
182
+ if (!this.config) {
183
+ this.config = getFirebaseConfig();
184
+ }
185
+
161
186
  if (!this.config || !this.config.apiKey || !this.config.projectId) {
162
- console.log(' ⚠️ Firebase not configured (no credentials)');
163
- console.log(' 💡 Set environment variables: FIREBASE_API_KEY, FIREBASE_PROJECT_ID');
187
+ console.log(' ⚠️ Firebase not configured');
164
188
  this.emit('not-configured');
165
189
  return false;
166
190
  }
167
191
 
168
192
  try {
193
+ // Initialize WASM security if not provided
194
+ if (!this.secureAccess) {
195
+ try {
196
+ const { createSecureAccess } = await import('./secure-access.js');
197
+ this.secureAccess = await createSecureAccess({
198
+ siteId: this.room,
199
+ persistIdentity: true
200
+ });
201
+ // Use WASM-generated node ID if peerId not set
202
+ if (!this.peerId) {
203
+ this.peerId = this.secureAccess.getShortId();
204
+ }
205
+ } catch (err) {
206
+ console.log(' ⚠️ WASM security unavailable, using basic mode');
207
+ }
208
+ }
209
+
169
210
  // Dynamic import Firebase (tree-shakeable)
170
211
  const { initializeApp, getApps } = await import('firebase/app');
171
212
  const { getFirestore, collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit, serverTimestamp } = await import('firebase/firestore');
172
213
 
173
- // Store Firebase methods for later use (Firestore only - no RTDB)
214
+ // Store Firebase methods for later use
174
215
  this.firebase = {
175
216
  collection, doc, setDoc, onSnapshot, deleteDoc, query, where, orderBy, limit, serverTimestamp
176
217
  };
@@ -179,9 +220,19 @@ export class FirebaseSignaling extends EventEmitter {
179
220
  const apps = getApps();
180
221
  this.app = apps.length ? apps[0] : initializeApp(this.config);
181
222
 
182
- // Initialize Firestore (for signaling AND presence - no RTDB needed)
223
+ // Initialize Firestore
183
224
  this.db = getFirestore(this.app);
184
225
 
226
+ // WASM cryptographic identity (replaces Firebase Auth)
227
+ if (this.secureAccess) {
228
+ this.uid = this.secureAccess.getNodeId();
229
+ console.log(` 🔐 WASM crypto identity: ${this.secureAccess.getShortId()}`);
230
+ console.log(` 📦 Public key: ${this.secureAccess.getPublicKeyHex().slice(0, 16)}...`);
231
+ } else {
232
+ this.uid = this.peerId;
233
+ console.log(` ⚠️ No WASM security, using peerId: ${this.peerId?.slice(0, 8)}...`);
234
+ }
235
+
185
236
  // Register presence in Firestore
186
237
  await this.registerPresence();
187
238
 
@@ -192,7 +243,7 @@ export class FirebaseSignaling extends EventEmitter {
192
243
  this.subscribeToSignals();
193
244
 
194
245
  this.isConnected = true;
195
- console.log(' ✅ Firebase signaling connected (Firestore)');
246
+ console.log(' ✅ Firebase connected with WASM security');
196
247
 
197
248
  this.emit('connected');
198
249
  return true;
@@ -205,32 +256,54 @@ export class FirebaseSignaling extends EventEmitter {
205
256
  }
206
257
 
207
258
  /**
208
- * Register this peer's presence in Firestore
259
+ * Register this peer's presence in Firestore with WASM-signed data
209
260
  */
210
261
  async registerPresence() {
211
262
  const { doc, setDoc, serverTimestamp } = this.firebase;
212
263
 
213
264
  const presenceRef = doc(this.db, SIGNALING_PATHS.peers, this.peerId);
214
265
 
215
- // Set online status in Firestore
216
- await setDoc(presenceRef, {
266
+ // Build presence data
267
+ const presenceData = {
217
268
  peerId: this.peerId,
218
269
  room: this.room,
219
270
  online: true,
220
271
  lastSeen: serverTimestamp(),
221
272
  capabilities: ['compute', 'storage', 'verify'],
222
- }, { merge: true });
273
+ };
274
+
275
+ // Add WASM cryptographic identity if available
276
+ if (this.secureAccess) {
277
+ presenceData.publicKey = this.secureAccess.getPublicKeyHex();
278
+ // Sign the presence announcement
279
+ const signed = this.secureAccess.signMessage({
280
+ peerId: this.peerId,
281
+ room: this.room,
282
+ capabilities: presenceData.capabilities
283
+ });
284
+ presenceData.signature = signed.signature;
285
+ presenceData.signedAt = signed.timestamp;
286
+ }
287
+
288
+ // Set online status in Firestore
289
+ await setDoc(presenceRef, presenceData, { merge: true });
223
290
 
224
291
  // Set up heartbeat to maintain presence (Firestore doesn't have onDisconnect)
225
292
  this._heartbeatInterval = setInterval(async () => {
226
293
  try {
227
- await setDoc(presenceRef, { lastSeen: serverTimestamp() }, { merge: true });
294
+ const heartbeat = { lastSeen: serverTimestamp() };
295
+ // Sign heartbeat if security available
296
+ if (this.secureAccess) {
297
+ const signed = this.secureAccess.signMessage({ heartbeat: Date.now() });
298
+ heartbeat.heartbeatSig = signed.signature;
299
+ }
300
+ await setDoc(presenceRef, heartbeat, { merge: true });
228
301
  } catch (e) {
229
302
  // Ignore heartbeat errors
230
303
  }
231
304
  }, 30000);
232
305
 
233
- console.log(` 📡 Registered presence: ${this.peerId.slice(0, 8)}...`);
306
+ console.log(` 📡 Registered presence: ${this.peerId.slice(0, 8)}... (WASM-signed)`);
234
307
  }
235
308
 
236
309
  /**
@@ -303,7 +376,7 @@ export class FirebaseSignaling extends EventEmitter {
303
376
  }
304
377
 
305
378
  /**
306
- * Handle incoming signal
379
+ * Handle incoming signal with WASM signature verification
307
380
  */
308
381
  async handleSignal(signal, docId) {
309
382
  this.stats.firebaseSignals++;
@@ -312,19 +385,46 @@ export class FirebaseSignaling extends EventEmitter {
312
385
  const { doc, deleteDoc } = this.firebase;
313
386
  await deleteDoc(doc(this.db, SIGNALING_PATHS.signals, docId));
314
387
 
388
+ // Verify signature if WASM security is enabled
389
+ if (this.verifySignatures && this.secureAccess && signal.signature && signal.publicKey) {
390
+ const isValid = this.secureAccess.verifyMessage({
391
+ payload: JSON.stringify({
392
+ from: signal.from,
393
+ to: signal.to,
394
+ type: signal.type,
395
+ data: typeof signal.data === 'object' ? JSON.stringify(signal.data) : signal.data,
396
+ timestamp: signal.timestamp
397
+ }),
398
+ signature: signal.signature,
399
+ publicKey: signal.publicKey,
400
+ timestamp: signal.timestamp
401
+ });
402
+
403
+ if (!isValid) {
404
+ console.warn(` ⚠️ Invalid signature from ${signal.from?.slice(0, 8)}...`);
405
+ this.stats.rejectedSignals++;
406
+ this.emit('invalid-signature', { from: signal.from, type: signal.type });
407
+ return; // Reject the signal
408
+ }
409
+
410
+ // Register verified peer
411
+ this.secureAccess.registerPeer(signal.from, signal.publicKey);
412
+ this.stats.verifiedSignals++;
413
+ }
414
+
315
415
  // Emit appropriate event
316
416
  switch (signal.type) {
317
417
  case 'offer':
318
- this.emit('offer', { from: signal.from, offer: signal.data });
418
+ this.emit('offer', { from: signal.from, offer: signal.data, verified: !!signal.signature });
319
419
  break;
320
420
  case 'answer':
321
- this.emit('answer', { from: signal.from, answer: signal.data });
421
+ this.emit('answer', { from: signal.from, answer: signal.data, verified: !!signal.signature });
322
422
  break;
323
423
  case 'ice-candidate':
324
- this.emit('ice-candidate', { from: signal.from, candidate: signal.data });
424
+ this.emit('ice-candidate', { from: signal.from, candidate: signal.data, verified: !!signal.signature });
325
425
  break;
326
426
  default:
327
- this.emit('signal', signal);
427
+ this.emit('signal', { ...signal, verified: !!signal.signature });
328
428
  }
329
429
  }
330
430
 
@@ -350,7 +450,43 @@ export class FirebaseSignaling extends EventEmitter {
350
450
  }
351
451
 
352
452
  /**
353
- * Send signal via Firebase
453
+ * Serialize WebRTC objects to plain JSON for Firebase storage
454
+ * RTCIceCandidate and RTCSessionDescription are not directly storable
455
+ */
456
+ _serializeWebRTCData(data) {
457
+ if (!data || typeof data !== 'object') {
458
+ return data;
459
+ }
460
+
461
+ // Handle RTCIceCandidate
462
+ if (data.candidate !== undefined && data.sdpMid !== undefined) {
463
+ return {
464
+ candidate: data.candidate,
465
+ sdpMid: data.sdpMid,
466
+ sdpMLineIndex: data.sdpMLineIndex,
467
+ usernameFragment: data.usernameFragment || null,
468
+ };
469
+ }
470
+
471
+ // Handle RTCSessionDescription (offer/answer)
472
+ if (data.type !== undefined && data.sdp !== undefined) {
473
+ return {
474
+ type: data.type,
475
+ sdp: data.sdp,
476
+ };
477
+ }
478
+
479
+ // Try to convert any object with toJSON method
480
+ if (typeof data.toJSON === 'function') {
481
+ return data.toJSON();
482
+ }
483
+
484
+ // Return as-is if already plain object
485
+ return data;
486
+ }
487
+
488
+ /**
489
+ * Send signal via Firebase with WASM signature
354
490
  */
355
491
  async sendSignal(toPeerId, type, data) {
356
492
  if (!this.isConnected) {
@@ -362,14 +498,33 @@ export class FirebaseSignaling extends EventEmitter {
362
498
  const signalId = `${this.peerId}-${toPeerId}-${Date.now()}`;
363
499
  const signalRef = doc(this.db, SIGNALING_PATHS.signals, signalId);
364
500
 
365
- await setDoc(signalRef, {
501
+ // Serialize WebRTC objects to plain JSON
502
+ const serializedData = this._serializeWebRTCData(data);
503
+
504
+ const timestamp = Date.now();
505
+ const signalData = {
366
506
  from: this.peerId,
367
507
  to: toPeerId,
368
508
  type,
369
- data,
370
- timestamp: Date.now(),
509
+ data: serializedData,
510
+ timestamp,
371
511
  room: this.room,
372
- });
512
+ };
513
+
514
+ // Sign the signal with WASM cryptography
515
+ if (this.secureAccess) {
516
+ const signed = this.secureAccess.signMessage({
517
+ from: this.peerId,
518
+ to: toPeerId,
519
+ type,
520
+ data: typeof serializedData === 'object' ? JSON.stringify(serializedData) : serializedData,
521
+ timestamp
522
+ });
523
+ signalData.signature = signed.signature;
524
+ signalData.publicKey = signed.publicKey;
525
+ }
526
+
527
+ await setDoc(signalRef, signalData);
373
528
 
374
529
  return true;
375
530
  }
@@ -597,6 +752,10 @@ export class HybridBootstrap extends EventEmitter {
597
752
  this.peerId = options.peerId;
598
753
  this.config = options.firebaseConfig || DEFAULT_FIREBASE_CONFIG;
599
754
 
755
+ // WASM Security
756
+ /** @type {import('./secure-access.js').SecureAccessManager|null} */
757
+ this.secureAccess = options.secureAccess || null;
758
+
600
759
  // Components
601
760
  this.firebase = null;
602
761
  this.dht = null;
@@ -614,25 +773,63 @@ export class HybridBootstrap extends EventEmitter {
614
773
  directConnections: 0,
615
774
  firebaseSignals: 0,
616
775
  p2pSignals: 0,
776
+ verifiedPeers: 0,
617
777
  };
618
778
  }
619
779
 
620
780
  /**
621
- * Start hybrid bootstrap
781
+ * Start hybrid bootstrap with WASM security
622
782
  */
623
783
  async start(webrtc, dht) {
624
784
  this.webrtc = webrtc;
625
785
  this.dht = dht;
626
786
 
627
- // Start with Firebase
787
+ // Initialize WASM security if not provided
788
+ if (!this.secureAccess) {
789
+ try {
790
+ const { createSecureAccess } = await import('./secure-access.js');
791
+ this.secureAccess = await createSecureAccess({
792
+ siteId: 'edge-net',
793
+ persistIdentity: true
794
+ });
795
+ // Use WASM node ID if peerId not set
796
+ if (!this.peerId) {
797
+ this.peerId = this.secureAccess.getShortId();
798
+ }
799
+ } catch (err) {
800
+ console.log(' ⚠️ WASM security unavailable for bootstrap');
801
+ }
802
+ }
803
+
804
+ // Start with Firebase, passing WASM security
628
805
  this.firebase = new FirebaseSignaling({
629
806
  peerId: this.peerId,
630
807
  firebaseConfig: this.config,
808
+ secureAccess: this.secureAccess,
631
809
  });
632
810
 
633
811
  // Wire up events
634
812
  this.setupFirebaseEvents();
635
813
 
814
+ // Set up WebRTC to use Firebase for signaling
815
+ if (this.webrtc) {
816
+ this.webrtc.setExternalSignaling(async (type, toPeerId, data) => {
817
+ // Route signaling through Firebase
818
+ switch (type) {
819
+ case 'offer':
820
+ await this.firebase.sendOffer(toPeerId, data);
821
+ break;
822
+ case 'answer':
823
+ await this.firebase.sendAnswer(toPeerId, data);
824
+ break;
825
+ case 'ice-candidate':
826
+ await this.firebase.sendIceCandidate(toPeerId, data);
827
+ break;
828
+ }
829
+ this.stats.firebaseSignals++;
830
+ });
831
+ }
832
+
636
833
  // Connect to Firebase
637
834
  const connected = await this.firebase.connect();
638
835
 
@@ -693,18 +890,10 @@ export class HybridBootstrap extends EventEmitter {
693
890
  if (!this.webrtc) return;
694
891
 
695
892
  try {
696
- // Create offer
697
- const offer = await this.webrtc.createOffer(peerId);
698
-
699
- // Try P2P signaling first (if peer is directly connected)
700
- if (this.mode === 'p2p' && this.webrtc.isConnected(peerId)) {
701
- this.webrtc.sendToPeer(peerId, { type: 'offer', offer });
702
- this.stats.p2pSignals++;
703
- } else {
704
- // Fall back to Firebase
705
- await this.firebase.sendOffer(peerId, offer);
706
- this.stats.firebaseSignals++;
707
- }
893
+ // Use WebRTCPeerManager's connectToPeer method
894
+ // This handles offer creation and signaling internally
895
+ await this.webrtc.connectToPeer(peerId);
896
+ this.stats.directConnections++;
708
897
 
709
898
  } catch (error) {
710
899
  console.warn(`[HybridBootstrap] Connect to ${peerId.slice(0, 8)} failed:`, error.message);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@ruvector/edge-net",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
- "description": "Distributed compute intelligence network with AI agents and workers - contribute browser compute, spawn distributed AI agents, earn credits. Features Time Crystal coordination, Neural DAG attention, P2P swarm intelligence, ONNX inference, WebRTC signaling, CRDT ledger, and multi-agent workflows.",
5
+ "description": "Distributed compute intelligence network with WASM cryptographic security - contribute browser compute, spawn distributed AI agents, earn credits. Features Ed25519 signing, PiKey identity, Time Crystal coordination, Neural DAG attention, P2P swarm intelligence, ONNX inference, WebRTC signaling, CRDT ledger, and multi-agent workflows.",
6
6
  "main": "ruvector_edge_net.js",
7
7
  "module": "ruvector_edge_net.js",
8
8
  "types": "ruvector_edge_net.d.ts",
@@ -55,7 +55,11 @@
55
55
  "firebase",
56
56
  "gcloud",
57
57
  "firestore",
58
- "realtime-database"
58
+ "realtime-database",
59
+ "ed25519",
60
+ "wasm-cryptography",
61
+ "pikey",
62
+ "secure-access"
59
63
  ],
60
64
  "author": "RuVector Team <team@ruvector.dev>",
61
65
  "license": "MIT",
@@ -73,6 +77,8 @@
73
77
  "ruvector_edge_net.d.ts",
74
78
  "ruvector_edge_net_bg.wasm.d.ts",
75
79
  "node/",
80
+ "deploy/",
81
+ "tests/",
76
82
  "index.js",
77
83
  "cli.js",
78
84
  "join.js",
@@ -96,6 +102,7 @@
96
102
  "p2p.js",
97
103
  "firebase-signaling.js",
98
104
  "firebase-setup.js",
105
+ "secure-access.js",
99
106
  "README.md",
100
107
  "LICENSE"
101
108
  ],
@@ -160,6 +167,9 @@
160
167
  },
161
168
  "./firebase-setup": {
162
169
  "import": "./firebase-setup.js"
170
+ },
171
+ "./secure-access": {
172
+ "import": "./secure-access.js"
163
173
  }
164
174
  },
165
175
  "sideEffects": [
@@ -181,6 +191,11 @@
181
191
  "signaling": "node -e \"import('./signaling.js').then(m => new m.SignalingServer().start())\"",
182
192
  "genesis": "node genesis.js",
183
193
  "genesis:start": "node genesis.js --port 8787",
194
+ "genesis:prod": "NODE_ENV=production node deploy/genesis-prod.js",
195
+ "genesis:docker": "docker-compose -f deploy/docker-compose.yml up -d",
196
+ "genesis:docker:logs": "docker-compose -f deploy/docker-compose.yml logs -f genesis",
197
+ "genesis:docker:stop": "docker-compose -f deploy/docker-compose.yml down",
198
+ "genesis:docker:build": "docker build -t ruvector/edge-net-genesis:latest -f deploy/Dockerfile .",
184
199
  "p2p": "node -e \"import('./p2p.js').then(m => m.createP2PNetwork({ nodeId: 'test' }))\"",
185
200
  "monitor": "node -e \"import('./monitor.js').then(m => { const mon = new m.Monitor(); mon.start(); setInterval(() => console.log(JSON.stringify(mon.generateReport(), null, 2)), 5000); })\"",
186
201
  "firebase:setup": "node firebase-setup.js",
@@ -190,6 +205,7 @@
190
205
  "@ruvector/ruvllm": "^0.2.3",
191
206
  "@xenova/transformers": "^2.17.2",
192
207
  "firebase": "^10.14.1",
208
+ "wrtc": "^0.4.7",
193
209
  "ws": "^8.18.3"
194
210
  }
195
211
  }
package/real-workers.js CHANGED
@@ -295,6 +295,7 @@ export class RealWorkerPool extends EventEmitter {
295
295
  status: 'idle',
296
296
  tasksCompleted: 0,
297
297
  currentTask: null,
298
+ terminated: false, // Track intentional termination
298
299
  };
299
300
 
300
301
  worker.on('message', (msg) => {
@@ -307,13 +308,16 @@ export class RealWorkerPool extends EventEmitter {
307
308
  });
308
309
 
309
310
  worker.on('exit', (code) => {
310
- if (code !== 0) {
311
- console.error(`[Worker ${index}] Exited with code ${code}`);
311
+ // Only respawn if worker crashed unexpectedly (not terminated intentionally)
312
+ if (!workerInfo.terminated && this.status === 'ready') {
313
+ console.error(`[Worker ${index}] Exited unexpectedly with code ${code}, respawning...`);
312
314
  // Respawn worker
313
315
  const idx = this.workers.indexOf(workerInfo);
314
- if (idx >= 0 && this.status === 'ready') {
316
+ if (idx >= 0) {
315
317
  this.spawnWorker(index).then(w => {
316
318
  this.workers[idx] = w;
319
+ }).catch(err => {
320
+ console.error(`[Worker ${index}] Failed to respawn:`, err.message);
317
321
  });
318
322
  }
319
323
  }
@@ -537,8 +541,9 @@ export class RealWorkerPool extends EventEmitter {
537
541
  await new Promise(r => setTimeout(r, 100));
538
542
  }
539
543
 
540
- // Terminate workers
544
+ // Terminate workers (mark as intentionally terminated first)
541
545
  for (const workerInfo of this.workers) {
546
+ workerInfo.terminated = true;
542
547
  await workerInfo.worker.terminate();
543
548
  }
544
549
 
package/scheduler.js CHANGED
@@ -596,11 +596,15 @@ export class TaskScheduler extends EventEmitter {
596
596
  worker.allocate(task);
597
597
  this.running.set(task.id, task);
598
598
 
599
- // Calculate wait time
599
+ // Calculate wait time using running average
600
600
  const waitTime = task.startedAt - task.queuedAt;
601
- this.stats.avgWaitTime =
602
- (this.stats.avgWaitTime * this.stats.submitted + waitTime) /
603
- (this.stats.submitted + 1);
601
+ const assignedCount = this.stats.completed + this.running.size;
602
+ if (assignedCount <= 1) {
603
+ this.stats.avgWaitTime = waitTime;
604
+ } else {
605
+ this.stats.avgWaitTime =
606
+ (this.stats.avgWaitTime * (assignedCount - 1) + waitTime) / assignedCount;
607
+ }
604
608
 
605
609
  this.emit('task-assigned', {
606
610
  taskId: task.id,