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 +1 -1
- package/v3/@claude-flow/cli/dist/src/services/headless-worker-executor.d.ts +6 -0
- package/v3/@claude-flow/cli/dist/src/services/headless-worker-executor.js +14 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +35 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +134 -1
- package/v3/@claude-flow/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.7.0-alpha.
|
|
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.
|
|
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",
|