claude-flow 3.7.0-alpha.14 → 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/.claude/settings.local.json +10 -1
- package/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +65 -10
- 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 +37 -3
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +44 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +147 -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",
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module v3/cli/memory-initializer
|
|
10
10
|
*/
|
|
11
|
+
export declare function getMemoryRoot(): string;
|
|
12
|
+
/** For tests + the `memory configure` flow that mutates the config at runtime. */
|
|
13
|
+
export declare function _resetMemoryRootCache(): void;
|
|
11
14
|
/**
|
|
12
15
|
* Enhanced schema with pattern confidence, temporal decay, versioning
|
|
13
16
|
* Vector embeddings enabled for semantic search
|
|
@@ -11,6 +11,61 @@
|
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { readFileMaybeEncrypted, writeFileRestricted } from '../fs-secure.js';
|
|
14
|
+
/**
|
|
15
|
+
* #1854: previously every site that needed the memory directory hardcoded
|
|
16
|
+
* `getMemoryRoot()`, so the documented config entry
|
|
17
|
+
* points (`memory.persistPath` config field, `memory configure --path`,
|
|
18
|
+
* `CLAUDE_FLOW_MEMORY_PATH` env var) all silently no-op'd. This helper
|
|
19
|
+
* is the single source of truth — every `.swarm/memory.db` resolution in
|
|
20
|
+
* this file flows through it.
|
|
21
|
+
*
|
|
22
|
+
* Precedence (highest → lowest):
|
|
23
|
+
* 1. CLAUDE_FLOW_MEMORY_PATH env var
|
|
24
|
+
* 2. memory.persistPath / memory.path in claude-flow.config.json (cwd or
|
|
25
|
+
* the directory the CLI was invoked from)
|
|
26
|
+
* 3. Default: cwd/.swarm
|
|
27
|
+
*
|
|
28
|
+
* Cached per-process so repeated lookups are cheap; reset only by spawning
|
|
29
|
+
* a fresh process (which is how config changes already propagate).
|
|
30
|
+
*/
|
|
31
|
+
let _memoryRootCache;
|
|
32
|
+
export function getMemoryRoot() {
|
|
33
|
+
if (_memoryRootCache !== undefined)
|
|
34
|
+
return _memoryRootCache;
|
|
35
|
+
// 1. Env var
|
|
36
|
+
const envPath = process.env.CLAUDE_FLOW_MEMORY_PATH;
|
|
37
|
+
if (envPath && envPath.trim().length > 0) {
|
|
38
|
+
_memoryRootCache = path.resolve(envPath);
|
|
39
|
+
return _memoryRootCache;
|
|
40
|
+
}
|
|
41
|
+
// 2. Config file (claude-flow.config.json)
|
|
42
|
+
const configCandidates = [
|
|
43
|
+
path.resolve(process.cwd(), 'claude-flow.config.json'),
|
|
44
|
+
path.resolve(process.cwd(), '.claude-flow', 'config.json'),
|
|
45
|
+
];
|
|
46
|
+
for (const configPath of configCandidates) {
|
|
47
|
+
if (!fs.existsSync(configPath))
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
51
|
+
const fromConfig = raw?.memory?.persistPath ?? raw?.memory?.path;
|
|
52
|
+
if (typeof fromConfig === 'string' && fromConfig.trim().length > 0) {
|
|
53
|
+
_memoryRootCache = path.resolve(fromConfig);
|
|
54
|
+
return _memoryRootCache;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* malformed config — fall through to default */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// 3. Default
|
|
62
|
+
_memoryRootCache = path.resolve(process.cwd(), '.swarm');
|
|
63
|
+
return _memoryRootCache;
|
|
64
|
+
}
|
|
65
|
+
/** For tests + the `memory configure` flow that mutates the config at runtime. */
|
|
66
|
+
export function _resetMemoryRootCache() {
|
|
67
|
+
_memoryRootCache = undefined;
|
|
68
|
+
}
|
|
14
69
|
// ADR-053: Lazy import of AgentDB v3 bridge
|
|
15
70
|
let _bridge;
|
|
16
71
|
async function getBridge() {
|
|
@@ -358,7 +413,7 @@ export async function getHNSWIndex(options) {
|
|
|
358
413
|
}
|
|
359
414
|
const { VectorDb } = ruvectorCore;
|
|
360
415
|
// Persistent storage paths — resolve to absolute to survive CWD changes
|
|
361
|
-
const swarmDir =
|
|
416
|
+
const swarmDir = getMemoryRoot();
|
|
362
417
|
if (!fs.existsSync(swarmDir)) {
|
|
363
418
|
fs.mkdirSync(swarmDir, { recursive: true });
|
|
364
419
|
}
|
|
@@ -459,7 +514,7 @@ function saveHNSWMetadata() {
|
|
|
459
514
|
if (!hnswIndex?.entries)
|
|
460
515
|
return;
|
|
461
516
|
try {
|
|
462
|
-
const swarmDir =
|
|
517
|
+
const swarmDir = getMemoryRoot();
|
|
463
518
|
const metadataPath = path.join(swarmDir, 'hnsw.metadata.json');
|
|
464
519
|
const metadata = Array.from(hnswIndex.entries.entries());
|
|
465
520
|
fs.writeFileSync(metadataPath, JSON.stringify(metadata));
|
|
@@ -974,7 +1029,7 @@ async function activateControllerRegistry(dbPath, verbose) {
|
|
|
974
1029
|
*/
|
|
975
1030
|
export async function initializeMemoryDatabase(options) {
|
|
976
1031
|
const { backend = 'hybrid', dbPath: customPath, force = false, verbose = false, migrate = true } = options;
|
|
977
|
-
const swarmDir =
|
|
1032
|
+
const swarmDir = getMemoryRoot();
|
|
978
1033
|
const dbPath = customPath || path.join(swarmDir, 'memory.db');
|
|
979
1034
|
const dbDir = path.dirname(dbPath);
|
|
980
1035
|
try {
|
|
@@ -1162,7 +1217,7 @@ export async function initializeMemoryDatabase(options) {
|
|
|
1162
1217
|
* Check if memory database is properly initialized
|
|
1163
1218
|
*/
|
|
1164
1219
|
export async function checkMemoryInitialization(dbPath) {
|
|
1165
|
-
const swarmDir =
|
|
1220
|
+
const swarmDir = getMemoryRoot();
|
|
1166
1221
|
const path_ = dbPath || path.join(swarmDir, 'memory.db');
|
|
1167
1222
|
if (!fs.existsSync(path_)) {
|
|
1168
1223
|
return { initialized: false };
|
|
@@ -1211,7 +1266,7 @@ export async function checkMemoryInitialization(dbPath) {
|
|
|
1211
1266
|
* Reduces confidence of patterns that haven't been used recently
|
|
1212
1267
|
*/
|
|
1213
1268
|
export async function applyTemporalDecay(dbPath) {
|
|
1214
|
-
const swarmDir =
|
|
1269
|
+
const swarmDir = getMemoryRoot();
|
|
1215
1270
|
const path_ = dbPath || path.join(swarmDir, 'memory.db');
|
|
1216
1271
|
try {
|
|
1217
1272
|
const initSqlJs = (await import('sql.js')).default;
|
|
@@ -1737,7 +1792,7 @@ export async function storeEntry(options) {
|
|
|
1737
1792
|
}
|
|
1738
1793
|
// Fallback: raw sql.js
|
|
1739
1794
|
const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert = false } = options;
|
|
1740
|
-
const swarmDir =
|
|
1795
|
+
const swarmDir = getMemoryRoot();
|
|
1741
1796
|
const dbPath = customPath ? path.resolve(customPath) : path.join(swarmDir, 'memory.db');
|
|
1742
1797
|
try {
|
|
1743
1798
|
if (!fs.existsSync(dbPath)) {
|
|
@@ -1830,7 +1885,7 @@ export async function searchEntries(options) {
|
|
|
1830
1885
|
// Fallback: raw sql.js
|
|
1831
1886
|
const { query, namespace, limit = 10, threshold = 0.3, dbPath: customPath } = options;
|
|
1832
1887
|
const effectiveNamespace = namespace || 'all';
|
|
1833
|
-
const swarmDir =
|
|
1888
|
+
const swarmDir = getMemoryRoot();
|
|
1834
1889
|
const dbPath = customPath ? path.resolve(customPath) : path.join(swarmDir, 'memory.db');
|
|
1835
1890
|
const startTime = Date.now();
|
|
1836
1891
|
try {
|
|
@@ -2001,7 +2056,7 @@ export async function listEntries(options) {
|
|
|
2001
2056
|
}
|
|
2002
2057
|
// Fallback: raw sql.js
|
|
2003
2058
|
const { namespace, limit = 20, offset = 0, dbPath: customPath } = options;
|
|
2004
|
-
const swarmDir =
|
|
2059
|
+
const swarmDir = getMemoryRoot();
|
|
2005
2060
|
const dbPath = customPath || path.join(swarmDir, 'memory.db');
|
|
2006
2061
|
try {
|
|
2007
2062
|
if (!fs.existsSync(dbPath)) {
|
|
@@ -2086,7 +2141,7 @@ export async function getEntry(options) {
|
|
|
2086
2141
|
}
|
|
2087
2142
|
// Fallback: raw sql.js
|
|
2088
2143
|
const { key, namespace = 'default', dbPath: customPath } = options;
|
|
2089
|
-
const swarmDir =
|
|
2144
|
+
const swarmDir = getMemoryRoot();
|
|
2090
2145
|
const dbPath = customPath || path.join(swarmDir, 'memory.db');
|
|
2091
2146
|
try {
|
|
2092
2147
|
if (!fs.existsSync(dbPath)) {
|
|
@@ -2190,7 +2245,7 @@ export async function deleteEntry(options) {
|
|
|
2190
2245
|
}
|
|
2191
2246
|
// Fallback: raw sql.js
|
|
2192
2247
|
const { key, namespace = 'default', dbPath: customPath } = options;
|
|
2193
|
-
const swarmDir =
|
|
2248
|
+
const swarmDir = getMemoryRoot();
|
|
2194
2249
|
const dbPath = customPath || path.join(swarmDir, 'memory.db');
|
|
2195
2250
|
try {
|
|
2196
2251
|
if (!fs.existsSync(dbPath)) {
|
|
@@ -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 {
|
|
@@ -831,13 +845,33 @@ Analyze the above codebase context and provide your response following the forma
|
|
|
831
845
|
// Set model
|
|
832
846
|
// Resolve model: user env override > config override > default alias
|
|
833
847
|
env.ANTHROPIC_MODEL = process.env.ANTHROPIC_MODEL || MODEL_IDS[options.model];
|
|
834
|
-
// Spawn claude CLI process
|
|
835
|
-
|
|
848
|
+
// Spawn claude CLI process. #1852: previously the prompt was passed
|
|
849
|
+
// as a positional CLI arg. On Windows `claude` resolves to
|
|
850
|
+
// `claude.cmd`, which Node refuses to exec directly (CVE-2024-27980
|
|
851
|
+
// mitigation) — it routes through `cmd.exe /d /s /c`, which then
|
|
852
|
+
// re-tokenizes the entire command line including the prompt.
|
|
853
|
+
// Source-code prompts contain `>` `<` `&` `|` (arrow functions,
|
|
854
|
+
// comparisons, redirections) — cmd.exe parses those as redirects
|
|
855
|
+
// and creates zero-byte files in cwd named after the next token
|
|
856
|
+
// (`controller.abort()`, `{const`, `0`, `HTTP`, etc.).
|
|
857
|
+
//
|
|
858
|
+
// Fix: pipe the prompt via stdin instead. `child.stdin.end(prompt)`
|
|
859
|
+
// writes the prompt and closes stdin atomically — the EOF still
|
|
860
|
+
// unblocks `claude --print` (the original concern in #1395) but no
|
|
861
|
+
// shell tokenization touches the prompt.
|
|
862
|
+
const child = spawn('claude', ['--print'], {
|
|
836
863
|
cwd: this.projectRoot,
|
|
837
864
|
env,
|
|
838
|
-
stdio: ['
|
|
865
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
839
866
|
windowsHide: true, // Prevent phantom console windows on Windows
|
|
840
867
|
});
|
|
868
|
+
try {
|
|
869
|
+
child.stdin?.end(prompt);
|
|
870
|
+
}
|
|
871
|
+
catch {
|
|
872
|
+
// stdin already closed (e.g. spawn failed) — `error` handler below
|
|
873
|
+
// will surface the real cause.
|
|
874
|
+
}
|
|
841
875
|
// Setup timeout
|
|
842
876
|
const timeoutHandle = setTimeout(() => {
|
|
843
877
|
if (this.processPool.has(options.executionId)) {
|
|
@@ -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
|
*/
|
|
@@ -128,6 +163,15 @@ export declare class WorkerDaemon extends EventEmitter {
|
|
|
128
163
|
/**
|
|
129
164
|
* Check if another daemon instance is already running.
|
|
130
165
|
* Returns the existing PID if alive, or null if no daemon is running.
|
|
166
|
+
*
|
|
167
|
+
* #1853: ignore self-PID matches. The detached-spawn path in
|
|
168
|
+
* `commands/daemon.ts` writes the child's PID into the file as a
|
|
169
|
+
* fallback after a 500ms wait. If the child reaches `start()` slower
|
|
170
|
+
* than the parent's 500ms wait (observed on Node 25 / macOS 26), the
|
|
171
|
+
* child reads its own PID back from the file and concludes "another
|
|
172
|
+
* daemon is already running" — so it exits before scheduling workers
|
|
173
|
+
* and `daemon status` reports STOPPED forever. A daemon process is
|
|
174
|
+
* never "another instance" of itself; treat self-match as absence.
|
|
131
175
|
*/
|
|
132
176
|
private checkExistingDaemon;
|
|
133
177
|
/**
|
|
@@ -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
|
*/
|
|
@@ -370,6 +498,15 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
370
498
|
/**
|
|
371
499
|
* Check if another daemon instance is already running.
|
|
372
500
|
* Returns the existing PID if alive, or null if no daemon is running.
|
|
501
|
+
*
|
|
502
|
+
* #1853: ignore self-PID matches. The detached-spawn path in
|
|
503
|
+
* `commands/daemon.ts` writes the child's PID into the file as a
|
|
504
|
+
* fallback after a 500ms wait. If the child reaches `start()` slower
|
|
505
|
+
* than the parent's 500ms wait (observed on Node 25 / macOS 26), the
|
|
506
|
+
* child reads its own PID back from the file and concludes "another
|
|
507
|
+
* daemon is already running" — so it exits before scheduling workers
|
|
508
|
+
* and `daemon status` reports STOPPED forever. A daemon process is
|
|
509
|
+
* never "another instance" of itself; treat self-match as absence.
|
|
373
510
|
*/
|
|
374
511
|
checkExistingDaemon() {
|
|
375
512
|
if (!existsSync(this.pidFile))
|
|
@@ -378,6 +515,10 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
378
515
|
const pid = parseInt(readFileSync(this.pidFile, 'utf-8').trim(), 10);
|
|
379
516
|
if (isNaN(pid))
|
|
380
517
|
return null;
|
|
518
|
+
// #1853: a PID file containing our own PID is not "another daemon".
|
|
519
|
+
// Treat as absent so the start() path proceeds normally.
|
|
520
|
+
if (pid === process.pid)
|
|
521
|
+
return null;
|
|
381
522
|
// Check if process is alive (signal 0 = existence check)
|
|
382
523
|
process.kill(pid, 0);
|
|
383
524
|
return pid; // Process is alive
|
|
@@ -424,6 +565,11 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
424
565
|
this.emit('warning', `Daemon already running (PID: ${existingPid})`);
|
|
425
566
|
return;
|
|
426
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();
|
|
427
573
|
this.running = true;
|
|
428
574
|
this.startedAt = new Date();
|
|
429
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",
|