claude-flow 3.7.0-alpha.15 → 3.7.0-alpha.16

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": "claude-flow",
3
- "version": "3.7.0-alpha.15",
3
+ "version": "3.7.0-alpha.16",
4
4
  "description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -222,6 +222,12 @@ export declare class HeadlessWorkerExecutor extends EventEmitter {
222
222
  /**
223
223
  * Get pool status
224
224
  */
225
+ /**
226
+ * #1855: return the PIDs of all currently-running headless worker
227
+ * children. Used by `WorkerDaemon` to snapshot active child PIDs to
228
+ * disk so the next lifetime can reap orphans after a hard crash.
229
+ */
230
+ getActiveChildPids(): number[];
225
231
  getPoolStatus(): PoolStatus;
226
232
  /**
227
233
  * Get number of active executions
@@ -457,6 +457,20 @@ export class HeadlessWorkerExecutor extends EventEmitter {
457
457
  /**
458
458
  * Get pool status
459
459
  */
460
+ /**
461
+ * #1855: return the PIDs of all currently-running headless worker
462
+ * children. Used by `WorkerDaemon` to snapshot active child PIDs to
463
+ * disk so the next lifetime can reap orphans after a hard crash.
464
+ */
465
+ getActiveChildPids() {
466
+ const out = [];
467
+ for (const entry of this.processPool.values()) {
468
+ const pid = entry.process?.pid;
469
+ if (typeof pid === 'number' && pid > 0)
470
+ out.push(pid);
471
+ }
472
+ return out;
473
+ }
460
474
  getPoolStatus() {
461
475
  const now = Date.now();
462
476
  return {
@@ -106,6 +106,41 @@ export declare class WorkerDaemon extends EventEmitter {
106
106
  * Setup graceful shutdown handlers
107
107
  */
108
108
  private setupShutdownHandlers;
109
+ /**
110
+ * #1855: install crash handlers for uncaught exceptions and unhandled
111
+ * rejections. Without these, a thrown error from any timer callback,
112
+ * worker logic path, or transitive import crashes the daemon process
113
+ * silently — the PID file leaks and any in-flight child processes
114
+ * orphan. With these, we log a structured crash record, run stop()
115
+ * to clean up, then exit 1 so the process actually dies (otherwise
116
+ * Node would crash anyway after the handler returns).
117
+ */
118
+ private installCrashHandlers;
119
+ /**
120
+ * Append a structured crash record to .claude-flow/logs/crash.log.
121
+ * Inspectable by hand or via `ruflo daemon status` follow-ups.
122
+ */
123
+ private writeCrashRecord;
124
+ /**
125
+ * Path to the on-disk children registry — list of headless worker
126
+ * child PIDs the daemon currently owns. #1855: written on every
127
+ * execution:start / :complete / :error transition; read by the next
128
+ * lifetime to reap orphans after a hard crash.
129
+ */
130
+ private get childrenFile();
131
+ /**
132
+ * Snapshot the currently-active headless worker child PIDs to disk.
133
+ * Best-effort; failures don't propagate.
134
+ */
135
+ private writeChildrenSnapshot;
136
+ /**
137
+ * #1855: reap orphan headless worker children left behind by a
138
+ * previous crashed lifetime. Reads `.claude-flow/daemon-children.json`,
139
+ * SIGTERMs any PID still alive that doesn't belong to the current
140
+ * daemon, then truncates the file. Called at the top of `start()`
141
+ * so the next lifetime starts with a clean process tree.
142
+ */
143
+ private reapOrphanedChildren;
109
144
  /**
110
145
  * Check if system resources allow worker execution
111
146
  */
@@ -79,6 +79,9 @@ export class WorkerDaemon extends EventEmitter {
79
79
  };
80
80
  // Setup graceful shutdown handlers
81
81
  this.setupShutdownHandlers();
82
+ // #1855: install crash handlers so uncaught exceptions and unhandled
83
+ // rejections don't leak the PID file or orphan child processes.
84
+ this.installCrashHandlers();
82
85
  // Ensure directories exist
83
86
  if (!existsSync(claudeFlowDir)) {
84
87
  mkdirSync(claudeFlowDir, { recursive: true });
@@ -104,14 +107,19 @@ export class WorkerDaemon extends EventEmitter {
104
107
  this.headlessAvailable = await this.headlessExecutor.isAvailable();
105
108
  if (this.headlessAvailable) {
106
109
  this.log('info', 'Claude Code headless mode available - AI workers enabled');
107
- // Forward headless executor events
110
+ // Forward headless executor events. #1855: also snapshot the
111
+ // active child PIDs to disk on every transition so the next
112
+ // lifetime can reap orphans after a hard crash.
108
113
  this.headlessExecutor.on('execution:start', (data) => {
114
+ this.writeChildrenSnapshot();
109
115
  this.emit('headless:start', data);
110
116
  });
111
117
  this.headlessExecutor.on('execution:complete', (data) => {
118
+ this.writeChildrenSnapshot();
112
119
  this.emit('headless:complete', data);
113
120
  });
114
121
  this.headlessExecutor.on('execution:error', (data) => {
122
+ this.writeChildrenSnapshot();
115
123
  this.emit('headless:error', data);
116
124
  });
117
125
  this.headlessExecutor.on('output', (data) => {
@@ -249,6 +257,126 @@ export class WorkerDaemon extends EventEmitter {
249
257
  process.on('SIGINT', shutdown);
250
258
  process.on('SIGHUP', shutdown);
251
259
  }
260
+ /**
261
+ * #1855: install crash handlers for uncaught exceptions and unhandled
262
+ * rejections. Without these, a thrown error from any timer callback,
263
+ * worker logic path, or transitive import crashes the daemon process
264
+ * silently — the PID file leaks and any in-flight child processes
265
+ * orphan. With these, we log a structured crash record, run stop()
266
+ * to clean up, then exit 1 so the process actually dies (otherwise
267
+ * Node would crash anyway after the handler returns).
268
+ */
269
+ installCrashHandlers() {
270
+ const onCrash = (kind, err) => {
271
+ // Best-effort logging; never throw from inside the crash handler.
272
+ try {
273
+ this.writeCrashRecord(kind, err);
274
+ }
275
+ catch { /* nothing more we can do */ }
276
+ try {
277
+ // Synchronous stop — don't await; the process is dying. Just
278
+ // remove the PID file and snapshot state so the next start
279
+ // sees a clean slate.
280
+ this.removePidFile();
281
+ this.saveState();
282
+ // Snapshot any in-flight child PIDs one last time so the next
283
+ // lifetime can reap them.
284
+ this.writeChildrenSnapshot();
285
+ }
286
+ catch { /* ignore */ }
287
+ // Exit non-zero so supervisors / shells see the failure.
288
+ process.exit(1);
289
+ };
290
+ process.on('uncaughtException', (err) => onCrash('uncaughtException', err));
291
+ process.on('unhandledRejection', (err) => onCrash('unhandledRejection', err));
292
+ }
293
+ /**
294
+ * Append a structured crash record to .claude-flow/logs/crash.log.
295
+ * Inspectable by hand or via `ruflo daemon status` follow-ups.
296
+ */
297
+ writeCrashRecord(kind, err) {
298
+ const logDir = this.config.logDir;
299
+ if (!existsSync(logDir))
300
+ mkdirSync(logDir, { recursive: true });
301
+ const crashLog = join(logDir, 'crash.log');
302
+ const ts = new Date().toISOString();
303
+ const message = err instanceof Error ? err.message : String(err);
304
+ const stack = err instanceof Error && err.stack ? err.stack : '<no stack>';
305
+ const record = `[${ts}] [${kind}] pid=${process.pid} ${message}\n${stack}\n---\n`;
306
+ appendFileSync(crashLog, record, 'utf-8');
307
+ this.log('warn', `Daemon crashed (${kind}): ${message} — see ${crashLog}`);
308
+ }
309
+ /**
310
+ * Path to the on-disk children registry — list of headless worker
311
+ * child PIDs the daemon currently owns. #1855: written on every
312
+ * execution:start / :complete / :error transition; read by the next
313
+ * lifetime to reap orphans after a hard crash.
314
+ */
315
+ get childrenFile() {
316
+ return join(this.projectRoot, '.claude-flow', 'daemon-children.json');
317
+ }
318
+ /**
319
+ * Snapshot the currently-active headless worker child PIDs to disk.
320
+ * Best-effort; failures don't propagate.
321
+ */
322
+ writeChildrenSnapshot() {
323
+ if (!this.headlessExecutor)
324
+ return;
325
+ try {
326
+ const pids = this.headlessExecutor.getActiveChildPids();
327
+ const dir = join(this.projectRoot, '.claude-flow');
328
+ if (!existsSync(dir))
329
+ mkdirSync(dir, { recursive: true });
330
+ writeFileSync(this.childrenFile, JSON.stringify({ pids, daemonPid: process.pid, timestamp: new Date().toISOString() }, null, 2), 'utf-8');
331
+ }
332
+ catch { /* best-effort */ }
333
+ }
334
+ /**
335
+ * #1855: reap orphan headless worker children left behind by a
336
+ * previous crashed lifetime. Reads `.claude-flow/daemon-children.json`,
337
+ * SIGTERMs any PID still alive that doesn't belong to the current
338
+ * daemon, then truncates the file. Called at the top of `start()`
339
+ * so the next lifetime starts with a clean process tree.
340
+ */
341
+ reapOrphanedChildren() {
342
+ const file = this.childrenFile;
343
+ if (!existsSync(file))
344
+ return;
345
+ let snapshot;
346
+ try {
347
+ snapshot = JSON.parse(readFileSync(file, 'utf-8'));
348
+ }
349
+ catch {
350
+ try {
351
+ unlinkSync(file);
352
+ }
353
+ catch { /* ignore */ }
354
+ return;
355
+ }
356
+ const pids = Array.isArray(snapshot.pids) ? snapshot.pids : [];
357
+ let reaped = 0;
358
+ for (const pid of pids) {
359
+ if (typeof pid !== 'number' || pid <= 0)
360
+ continue;
361
+ if (pid === process.pid)
362
+ continue; // never our own PID
363
+ try {
364
+ process.kill(pid, 0); // is alive?
365
+ process.kill(pid, 'SIGTERM');
366
+ reaped++;
367
+ }
368
+ catch {
369
+ // already dead — fine
370
+ }
371
+ }
372
+ if (reaped > 0) {
373
+ this.log('info', `Reaped ${reaped} orphan headless worker child(ren) from previous lifetime`);
374
+ }
375
+ try {
376
+ unlinkSync(file);
377
+ }
378
+ catch { /* ignore */ }
379
+ }
252
380
  /**
253
381
  * Check if system resources allow worker execution
254
382
  */
@@ -437,6 +565,11 @@ export class WorkerDaemon extends EventEmitter {
437
565
  this.emit('warning', `Daemon already running (PID: ${existingPid})`);
438
566
  return;
439
567
  }
568
+ // #1855: reap orphan headless worker children left by a previous
569
+ // crashed lifetime, BEFORE we mark ourselves running and start
570
+ // accepting new work. The children file from the prior daemon's
571
+ // last-snapshot is the authoritative list.
572
+ this.reapOrphanedChildren();
440
573
  this.running = true;
441
574
  this.startedAt = new Date();
442
575
  this.writePidFile();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.7.0-alpha.15",
3
+ "version": "3.7.0-alpha.16",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",