claude-flow 3.10.42 β 3.10.43
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/README.md +0 -4
- package/package.json +1 -1
- package/v3/@claude-flow/cli/README.md +0 -4
- package/v3/@claude-flow/cli/dist/src/commands/daemon.d.ts +8 -0
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +161 -1
- package/v3/@claude-flow/cli/dist/src/commands/init.js +17 -0
- package/v3/@claude-flow/cli/dist/src/commands/neural.js +13 -3
- package/v3/@claude-flow/cli/dist/src/init/claudemd-generator.js +7 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +27 -6
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +29 -1
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +27 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +123 -0
- package/v3/@claude-flow/cli/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,10 +16,6 @@
|
|
|
16
16
|
[](https://www.npmjs.com/package/@claude-flow/codex)
|
|
17
17
|
[](https://github.com/ruvnet/ruvector)
|
|
18
18
|
|
|
19
|
-
[](https://cognitum.one/appliance)
|
|
20
|
-
|
|
21
|
-
[](https://github.com/ruvnet/ruflo/issues/1967)
|
|
22
|
-
|
|
23
19
|
# Ruflo
|
|
24
20
|
|
|
25
21
|
**Multi-agent AI harness for Claude Code and Codex**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.43",
|
|
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",
|
|
@@ -16,10 +16,6 @@
|
|
|
16
16
|
[](https://www.npmjs.com/package/@claude-flow/codex)
|
|
17
17
|
[](https://github.com/ruvnet/ruvector)
|
|
18
18
|
|
|
19
|
-
[](https://cognitum.one/appliance)
|
|
20
|
-
|
|
21
|
-
[](https://github.com/ruvnet/ruflo/issues/1967)
|
|
22
|
-
|
|
23
19
|
# Ruflo
|
|
24
20
|
|
|
25
21
|
**Multi-agent AI harness for Claude Code and Codex**
|
|
@@ -23,6 +23,14 @@ export declare function resolveWorkspaceFlag(raw: unknown): string | null;
|
|
|
23
23
|
* via the PID file.
|
|
24
24
|
*/
|
|
25
25
|
export declare function daemonCommandLineBelongsToWorkspace(commandLine: string, workspaceRoot: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* #2356: extract the workspace root from a daemon process command line for the
|
|
28
|
+
* global `daemon status --all` view. The launcher always appends
|
|
29
|
+
* `--workspace <root>` as the FINAL argv entry (see startBackgroundDaemon), so
|
|
30
|
+
* we capture everything after it to end-of-line and strip trailing quotes.
|
|
31
|
+
* Returns null for pre-#1914 daemons that never stamped a workspace.
|
|
32
|
+
*/
|
|
33
|
+
export declare function extractWorkspaceFromDaemonLine(commandLine: string): string | null;
|
|
26
34
|
export declare const daemonCommand: Command;
|
|
27
35
|
export default daemonCommand;
|
|
28
36
|
//# sourceMappingURL=daemon.d.ts.map
|
|
@@ -21,6 +21,9 @@ const startCommand = {
|
|
|
21
21
|
{ name: 'sandbox', type: 'string', description: 'Default sandbox mode for headless workers', choices: ['strict', 'permissive', 'disabled'] },
|
|
22
22
|
{ name: 'max-cpu-load', type: 'string', description: 'Override maxCpuLoad resource threshold (e.g. 4.0)' },
|
|
23
23
|
{ name: 'min-free-memory', type: 'string', description: 'Override minFreeMemoryPercent resource threshold (e.g. 15)' },
|
|
24
|
+
// #2356: self-terminating lifecycle. Caps how long a forgotten daemon can
|
|
25
|
+
// keep dispatching headless worker sweeps. Default 12h (or RUFLO_DAEMON_TTL_SECS); 0 = run until stopped.
|
|
26
|
+
{ name: 'ttl', type: 'string', description: 'Max daemon age in seconds before graceful self-shutdown (0 = run until stopped; default 43200 = 12h)' },
|
|
24
27
|
// #1914: workspace root for this daemon. Set automatically when the
|
|
25
28
|
// background launcher forks the foreground child so the daemon process
|
|
26
29
|
// carries its workspace path in argv β `killStaleDaemons` then only
|
|
@@ -71,6 +74,18 @@ const startCommand = {
|
|
|
71
74
|
config.resourceThresholds = thresholds;
|
|
72
75
|
}
|
|
73
76
|
}
|
|
77
|
+
// #2356: parse --ttl (seconds β ms). Integer-only so 0 (disable) is valid;
|
|
78
|
+
// INT_RE forbids the decimals NUMERIC_RE allows, since a TTL is whole seconds.
|
|
79
|
+
const rawTtl = ctx.flags.ttl;
|
|
80
|
+
const INT_RE = /^\d+$/;
|
|
81
|
+
if (rawTtl !== undefined) {
|
|
82
|
+
if (INT_RE.test(rawTtl)) {
|
|
83
|
+
config.ttlMs = parseInt(rawTtl, 10) * 1000;
|
|
84
|
+
}
|
|
85
|
+
else if (!quiet) {
|
|
86
|
+
output.printWarning(`Ignoring invalid --ttl value: ${sanitize(rawTtl)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
74
89
|
// Check if background daemon already running (skip if we ARE the daemon process)
|
|
75
90
|
if (!isDaemonProcess) {
|
|
76
91
|
const bgPid = getBackgroundDaemonPid(projectRoot);
|
|
@@ -95,6 +110,7 @@ const startCommand = {
|
|
|
95
110
|
workers: ctx.flags.workers,
|
|
96
111
|
headless: ctx.flags.headless,
|
|
97
112
|
sandbox: ctx.flags.sandbox,
|
|
113
|
+
ttl: rawTtl,
|
|
98
114
|
});
|
|
99
115
|
}
|
|
100
116
|
// Foreground mode: run in current process (blocks terminal)
|
|
@@ -134,6 +150,9 @@ const startCommand = {
|
|
|
134
150
|
output.printBox([
|
|
135
151
|
`PID: ${status.pid}`,
|
|
136
152
|
`Started: ${status.startedAt?.toISOString()}`,
|
|
153
|
+
status.config.ttlMs > 0
|
|
154
|
+
? `TTL: ${Math.round(status.config.ttlMs / 3600000)}h (self-shutdown)`
|
|
155
|
+
: `TTL: off (runs until stopped)`,
|
|
137
156
|
`Workers: ${status.config.workers.filter(w => w.enabled).length} enabled`,
|
|
138
157
|
`Max Concurrent: ${status.config.maxConcurrent}`,
|
|
139
158
|
`Max CPU Load: ${status.config.resourceThresholds.maxCpuLoad}`,
|
|
@@ -243,8 +262,22 @@ export function resolveWorkspaceFlag(raw) {
|
|
|
243
262
|
export function daemonCommandLineBelongsToWorkspace(commandLine, workspaceRoot) {
|
|
244
263
|
return commandLine.replace(/[\s"']+$/u, '').endsWith(`--workspace ${workspaceRoot}`);
|
|
245
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* #2356: extract the workspace root from a daemon process command line for the
|
|
267
|
+
* global `daemon status --all` view. The launcher always appends
|
|
268
|
+
* `--workspace <root>` as the FINAL argv entry (see startBackgroundDaemon), so
|
|
269
|
+
* we capture everything after it to end-of-line and strip trailing quotes.
|
|
270
|
+
* Returns null for pre-#1914 daemons that never stamped a workspace.
|
|
271
|
+
*/
|
|
272
|
+
export function extractWorkspaceFromDaemonLine(commandLine) {
|
|
273
|
+
const m = commandLine.match(/--workspace\s+(.+?)\s*$/u);
|
|
274
|
+
if (!m)
|
|
275
|
+
return null;
|
|
276
|
+
const ws = m[1].replace(/["']+$/u, '').trim();
|
|
277
|
+
return ws.length > 0 ? ws : null;
|
|
278
|
+
}
|
|
246
279
|
async function startBackgroundDaemon(projectRoot, quiet, forwarded = {}) {
|
|
247
|
-
const { maxCpuLoad, minFreeMemory, workers, headless, sandbox } = forwarded;
|
|
280
|
+
const { maxCpuLoad, minFreeMemory, workers, headless, sandbox, ttl } = forwarded;
|
|
248
281
|
// Validate and resolve project root
|
|
249
282
|
const resolvedRoot = resolve(projectRoot);
|
|
250
283
|
validatePath(resolvedRoot, 'Project root');
|
|
@@ -312,6 +345,11 @@ async function startBackgroundDaemon(projectRoot, quiet, forwarded = {}) {
|
|
|
312
345
|
if (minFreeMemory && SPAWN_NUMERIC_RE.test(minFreeMemory)) {
|
|
313
346
|
forkArgs.push('--min-free-memory', minFreeMemory);
|
|
314
347
|
}
|
|
348
|
+
// #2356: forward the TTL so the background daemon enforces it too. Integer
|
|
349
|
+
// seconds only (incl. 0 to disable) β reject anything else before it hits argv.
|
|
350
|
+
if (typeof ttl === 'string' && /^\d+$/.test(ttl)) {
|
|
351
|
+
forkArgs.push('--ttl', ttl);
|
|
352
|
+
}
|
|
315
353
|
// #1968: forward worker-selection / sandbox flags. The previous launcher
|
|
316
354
|
// dropped these, so `daemon start --workers map` ran with the default
|
|
317
355
|
// five-worker set instead of just `map`. Validate each before passing
|
|
@@ -599,6 +637,116 @@ function isProcessRunning(pid) {
|
|
|
599
637
|
return false;
|
|
600
638
|
}
|
|
601
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* #2356: enumerate every running ruflo daemon across ALL workspaces. Reuses
|
|
642
|
+
* the same `ps`/`tasklist` scan as killStaleDaemons but, instead of killing,
|
|
643
|
+
* returns each live daemon's PID + workspace so `daemon status --all` can
|
|
644
|
+
* surface daemons leaked in other projects. Best-effort: any tooling failure
|
|
645
|
+
* yields an empty list (matching the kill-stale paths).
|
|
646
|
+
*/
|
|
647
|
+
async function scanRunningDaemons() {
|
|
648
|
+
const isWin = process.platform === 'win32';
|
|
649
|
+
try {
|
|
650
|
+
const { execFileSync } = await import('child_process');
|
|
651
|
+
const out = isWin
|
|
652
|
+
? execFileSync('tasklist', ['/v', '/fo', 'csv', '/nh'], { encoding: 'utf-8', timeout: 5000 })
|
|
653
|
+
: execFileSync('ps', ['-eo', 'pid,command'], { encoding: 'utf-8', timeout: 5000 });
|
|
654
|
+
const lines = out.split(/\r?\n/);
|
|
655
|
+
const found = [];
|
|
656
|
+
for (const line of lines) {
|
|
657
|
+
if (!line.includes('daemon start --foreground'))
|
|
658
|
+
continue;
|
|
659
|
+
if (!line.includes('claude-flow') && !line.includes('@claude-flow/cli'))
|
|
660
|
+
continue;
|
|
661
|
+
let pid;
|
|
662
|
+
let cmd;
|
|
663
|
+
if (isWin) {
|
|
664
|
+
// tasklist /fo csv: quoted fields; PID is field[1], Window Title is last.
|
|
665
|
+
const fields = line.split(/","/).map(f => f.replace(/^"|"$/g, ''));
|
|
666
|
+
pid = parseInt(fields[1] ?? '', 10);
|
|
667
|
+
cmd = fields[fields.length - 1] ?? line;
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
pid = parseInt(line.trim().split(/\s+/)[0], 10);
|
|
671
|
+
cmd = line;
|
|
672
|
+
}
|
|
673
|
+
if (Number.isNaN(pid) || !isProcessRunning(pid))
|
|
674
|
+
continue;
|
|
675
|
+
found.push({ pid, workspace: extractWorkspaceFromDaemonLine(cmd) });
|
|
676
|
+
}
|
|
677
|
+
return found;
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* #2356: render the global `daemon status --all` view. For each running daemon
|
|
685
|
+
* it reads that workspace's daemon-state.json to show age + configured TTL,
|
|
686
|
+
* and flags any daemon that has outlived its TTL (or 12h when TTL is unknown)
|
|
687
|
+
* as stale β the visibility that was missing when leaked daemons ran for days.
|
|
688
|
+
*/
|
|
689
|
+
async function renderAllDaemonsStatus() {
|
|
690
|
+
const daemons = await scanRunningDaemons();
|
|
691
|
+
output.writeln();
|
|
692
|
+
if (daemons.length === 0) {
|
|
693
|
+
output.printBox('No ruflo daemons are running in any workspace.', 'RuFlo Daemons (all workspaces)');
|
|
694
|
+
return { success: true, data: { daemons: [] } };
|
|
695
|
+
}
|
|
696
|
+
const now = Date.now();
|
|
697
|
+
const TWELVE_HOURS_MS = 12 * 60 * 60 * 1000;
|
|
698
|
+
let staleCount = 0;
|
|
699
|
+
const rows = daemons.map(d => {
|
|
700
|
+
let startedAt;
|
|
701
|
+
let ttlMs;
|
|
702
|
+
if (d.workspace) {
|
|
703
|
+
try {
|
|
704
|
+
const statePath = join(d.workspace, '.claude-flow', 'daemon-state.json');
|
|
705
|
+
if (fs.existsSync(statePath)) {
|
|
706
|
+
const st = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
707
|
+
if (st?.startedAt)
|
|
708
|
+
startedAt = new Date(st.startedAt);
|
|
709
|
+
if (typeof st?.config?.ttlMs === 'number')
|
|
710
|
+
ttlMs = st.config.ttlMs;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
catch { /* unreadable/partial state β show what we have */ }
|
|
714
|
+
}
|
|
715
|
+
const ageMs = startedAt ? now - startedAt.getTime() : undefined;
|
|
716
|
+
const overTtl = ttlMs !== undefined && ttlMs > 0 && ageMs !== undefined && ageMs > ttlMs;
|
|
717
|
+
const overTwelveH = ageMs !== undefined && ageMs > TWELVE_HOURS_MS;
|
|
718
|
+
const isStale = overTtl || overTwelveH;
|
|
719
|
+
if (isStale)
|
|
720
|
+
staleCount++;
|
|
721
|
+
const ageText = ageMs !== undefined ? formatTimeAgo(startedAt).replace(' ago', '') : '?';
|
|
722
|
+
const ttlText = ttlMs !== undefined
|
|
723
|
+
? (ttlMs > 0 ? `${Math.round(ttlMs / 3600000)}h` : 'off')
|
|
724
|
+
: '?';
|
|
725
|
+
return {
|
|
726
|
+
pid: isStale ? output.warning(String(d.pid)) : String(d.pid),
|
|
727
|
+
workspace: d.workspace ?? output.dim('(unknown)'),
|
|
728
|
+
age: isStale ? output.warning(ageText) : ageText,
|
|
729
|
+
ttl: ttlText === 'off' ? output.dim('off') : ttlText,
|
|
730
|
+
};
|
|
731
|
+
});
|
|
732
|
+
output.printTable({
|
|
733
|
+
columns: [
|
|
734
|
+
{ key: 'pid', header: 'PID', width: 8 },
|
|
735
|
+
{ key: 'age', header: 'Age', width: 8 },
|
|
736
|
+
{ key: 'ttl', header: 'TTL', width: 6 },
|
|
737
|
+
{ key: 'workspace', header: 'Workspace', width: 50 },
|
|
738
|
+
],
|
|
739
|
+
data: rows,
|
|
740
|
+
});
|
|
741
|
+
output.writeln();
|
|
742
|
+
if (staleCount > 0) {
|
|
743
|
+
output.printWarning(`${staleCount} daemon(s) have outlived their TTL (or have run >12h). Stop one with: cd <workspace> && ruflo daemon stop`);
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
output.printInfo(`${daemons.length} daemon(s) running, all within their TTL.`);
|
|
747
|
+
}
|
|
748
|
+
return { success: true, data: { daemons: rows.length } };
|
|
749
|
+
}
|
|
602
750
|
// Status subcommand
|
|
603
751
|
const statusCommand = {
|
|
604
752
|
name: 'status',
|
|
@@ -606,15 +754,24 @@ const statusCommand = {
|
|
|
606
754
|
options: [
|
|
607
755
|
{ name: 'verbose', short: 'v', type: 'boolean', description: 'Show detailed worker statistics' },
|
|
608
756
|
{ name: 'show-modes', type: 'boolean', description: 'Show worker execution modes (local/headless) and sandbox settings' },
|
|
757
|
+
// #2356: the default status reads only the CURRENT workspace, so a daemon
|
|
758
|
+
// leaked in another project is invisible. --all scans every running ruflo
|
|
759
|
+
// daemon across all workspaces (the global view that surfaces leaks).
|
|
760
|
+
{ name: 'all', short: 'a', type: 'boolean', description: 'List ruflo daemons across ALL workspaces (global view β surfaces leaked daemons)' },
|
|
609
761
|
],
|
|
610
762
|
examples: [
|
|
611
763
|
{ command: 'claude-flow daemon status', description: 'Show daemon status' },
|
|
612
764
|
{ command: 'claude-flow daemon status -v', description: 'Show detailed status' },
|
|
613
765
|
{ command: 'claude-flow daemon status --show-modes', description: 'Show worker execution modes' },
|
|
766
|
+
{ command: 'claude-flow daemon status --all', description: 'List daemons across all workspaces' },
|
|
614
767
|
],
|
|
615
768
|
action: async (ctx) => {
|
|
616
769
|
const verbose = ctx.flags.verbose;
|
|
617
770
|
const showModes = ctx.flags['show-modes'];
|
|
771
|
+
// #2356: global view across every workspace, not just cwd.
|
|
772
|
+
if (ctx.flags.all) {
|
|
773
|
+
return renderAllDaemonsStatus();
|
|
774
|
+
}
|
|
618
775
|
const projectRoot = process.cwd();
|
|
619
776
|
try {
|
|
620
777
|
const daemon = getDaemon(projectRoot);
|
|
@@ -633,6 +790,9 @@ const statusCommand = {
|
|
|
633
790
|
`Status: ${statusIcon} ${statusText}${mode}`,
|
|
634
791
|
`PID: ${displayPid}`,
|
|
635
792
|
status.startedAt ? `Started: ${status.startedAt.toISOString()}` : '',
|
|
793
|
+
status.config.ttlMs > 0
|
|
794
|
+
? `TTL: ${Math.round(status.config.ttlMs / 3600000)}h (self-shutdown)`
|
|
795
|
+
: `TTL: ${output.dim('off (runs until stopped)')}`,
|
|
636
796
|
`Workers Enabled: ${status.config.workers.filter(w => w.enabled).length}`,
|
|
637
797
|
`Max Concurrent: ${status.config.maxConcurrent}`,
|
|
638
798
|
`Max CPU Load: ${status.config.resourceThresholds.maxCpuLoad}`,
|
|
@@ -190,6 +190,7 @@ const initAction = async (ctx) => {
|
|
|
190
190
|
// set still got `~/.claude/CLAUDE.md` modified. Read the real key.
|
|
191
191
|
const noGlobal = ctx.flags['no-global'] === true || ctx.flags['global'] === false;
|
|
192
192
|
const allAgents = ctx.flags['all-agents'];
|
|
193
|
+
const cloudMcp = ctx.flags['cloud-mcp'];
|
|
193
194
|
const codexMode = ctx.flags.codex;
|
|
194
195
|
const dualMode = ctx.flags.dual;
|
|
195
196
|
const cwd = ctx.cwd;
|
|
@@ -230,6 +231,12 @@ const initAction = async (ctx) => {
|
|
|
230
231
|
}
|
|
231
232
|
else if (full) {
|
|
232
233
|
options = { ...FULL_INIT_OPTIONS, targetDir: cwd, force };
|
|
234
|
+
// #2356: keep auth-gated cloud MCP servers opt-in even under --full. They
|
|
235
|
+
// require a login, get committed into .mcp.json, and add per-session MCP
|
|
236
|
+
// tool-definition token cost. --cloud-mcp restores the all-three behavior.
|
|
237
|
+
if (!cloudMcp) {
|
|
238
|
+
options.mcp = { ...options.mcp, ruvSwarm: false, flowNexus: false };
|
|
239
|
+
}
|
|
233
240
|
}
|
|
234
241
|
else {
|
|
235
242
|
options = { ...DEFAULT_INIT_OPTIONS, targetDir: cwd, force };
|
|
@@ -970,6 +977,16 @@ export const initCommand = {
|
|
|
970
977
|
type: 'boolean',
|
|
971
978
|
default: false,
|
|
972
979
|
},
|
|
980
|
+
{
|
|
981
|
+
// #2356: under --full, the auth-gated cloud MCP servers (ruv-swarm,
|
|
982
|
+
// flow-nexus) get written into a committed .mcp.json and add MCP
|
|
983
|
+
// tool-definition token cost every session. Keep them opt-in even with
|
|
984
|
+
// --full; pass --cloud-mcp to register them.
|
|
985
|
+
name: 'cloud-mcp',
|
|
986
|
+
description: 'Register auth-gated cloud MCP servers (ruv-swarm, flow-nexus) in .mcp.json (only relevant with --full)',
|
|
987
|
+
type: 'boolean',
|
|
988
|
+
default: false,
|
|
989
|
+
},
|
|
973
990
|
{
|
|
974
991
|
name: 'skip-claude',
|
|
975
992
|
description: 'Skip .claude/ directory creation (runtime only)',
|
|
@@ -411,11 +411,21 @@ const statusCommand = {
|
|
|
411
411
|
details: `${stats.patternsLearned} patterns stored`,
|
|
412
412
|
},
|
|
413
413
|
{
|
|
414
|
+
// #2356: distinguish "loaded in this process" from "installed but
|
|
415
|
+
// not yet loaded" from "not installed". Previously `neural status`
|
|
416
|
+
// always printed "Not loaded" because it never warms the lazy
|
|
417
|
+
// singleton β a false negative even when @ruvector/core is present.
|
|
414
418
|
component: 'HNSW Index',
|
|
415
|
-
status: hnswStatus.
|
|
416
|
-
|
|
419
|
+
status: hnswStatus.initialized
|
|
420
|
+
? output.success('Ready')
|
|
421
|
+
: hnswStatus.available
|
|
422
|
+
? output.info('Available')
|
|
423
|
+
: output.dim('Not installed'),
|
|
424
|
+
details: hnswStatus.initialized
|
|
417
425
|
? `${hnswStatus.entryCount} vectors, ${hnswStatus.dimensions}-dim`
|
|
418
|
-
:
|
|
426
|
+
: hnswStatus.available
|
|
427
|
+
? '@ruvector/core installed (loads on first vector search)'
|
|
428
|
+
: '@ruvector/core not available',
|
|
419
429
|
},
|
|
420
430
|
{
|
|
421
431
|
component: 'Embedding Model',
|
|
@@ -172,11 +172,15 @@ function setupAndBoundary() {
|
|
|
172
172
|
return `## Setup
|
|
173
173
|
|
|
174
174
|
\`\`\`bash
|
|
175
|
-
claude mcp add claude-flow -- npx -y @
|
|
176
|
-
npx @
|
|
177
|
-
npx @claude-flow/cli@latest doctor --fix
|
|
175
|
+
claude mcp add claude-flow -- npx -y ruflo@latest mcp start
|
|
176
|
+
npx ruflo@latest doctor --fix
|
|
178
177
|
\`\`\`
|
|
179
178
|
|
|
179
|
+
> The background \`daemon\` is optional. It runs interval workers that each spawn
|
|
180
|
+
> a headless \`claude\` session, so it consumes tokens continuously. Start it only
|
|
181
|
+
> if you want those sweeps: \`npx ruflo@latest daemon start\` (self-stops after 12h
|
|
182
|
+
> by default; \`--ttl 0\` to disable, \`daemon status --all\` to audit running daemons).
|
|
183
|
+
|
|
180
184
|
**Agent tool** handles execution (agents, files, code, git). **MCP tools** handle coordination (swarm, memory, hooks). **CLI** is the same via Bash.`;
|
|
181
185
|
}
|
|
182
186
|
function buildAndTest() {
|
|
@@ -22,6 +22,7 @@ export interface AgentRecord {
|
|
|
22
22
|
lastResult?: Record<string, unknown>;
|
|
23
23
|
}
|
|
24
24
|
export declare const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-6";
|
|
25
|
+
export declare function modelRejectsSamplingParams(model: string): boolean;
|
|
25
26
|
export interface AnthropicCallInput {
|
|
26
27
|
prompt: string;
|
|
27
28
|
systemPrompt?: string;
|
|
@@ -46,6 +46,15 @@ const MODEL_MAP = {
|
|
|
46
46
|
'opus-4.7': 'claude-opus-4-7',
|
|
47
47
|
inherit: DEFAULT_ANTHROPIC_MODEL,
|
|
48
48
|
};
|
|
49
|
+
// #2357 β the adaptive-thinking family (Fable 5, Opus 4.8, Opus 4.7) removed
|
|
50
|
+
// the sampling parameters (temperature/top_p/top_k); the Anthropic API
|
|
51
|
+
// returns 400 "Extra inputs are not permitted" when any is present.
|
|
52
|
+
// Prefix-match so dated snapshots (e.g. claude-opus-4-8-YYYYMMDD) are
|
|
53
|
+
// covered. Applies only to the direct Anthropic path β the Ollama/OpenRouter
|
|
54
|
+
// OpenAI-compat paths accept temperature and are unchanged.
|
|
55
|
+
export function modelRejectsSamplingParams(model) {
|
|
56
|
+
return /^claude-(fable-5|opus-4-8|opus-4-7)/.test(model);
|
|
57
|
+
}
|
|
49
58
|
/**
|
|
50
59
|
* Generic Anthropic Messages API call. No agent registry coupling β used
|
|
51
60
|
* by agent_execute (with the agent's configured model) and by the WASM
|
|
@@ -76,7 +85,11 @@ export async function callAnthropicMessages(input) {
|
|
|
76
85
|
apiKey: openrouterKey,
|
|
77
86
|
baseUrl: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api',
|
|
78
87
|
providerLabel: 'openrouter',
|
|
79
|
-
|
|
88
|
+
// #2357 Finding C: anthropic/claude-3.5-sonnet was retired Oct 2025.
|
|
89
|
+
// Default to the same canonical family the rest of the resolver uses
|
|
90
|
+
// (MODEL_MAP). `OPENROUTER_DEFAULT_MODEL` still wins for callers who
|
|
91
|
+
// want to pin a specific OpenRouter slug.
|
|
92
|
+
defaultModel: process.env.OPENROUTER_DEFAULT_MODEL || 'anthropic/claude-sonnet-4-6',
|
|
80
93
|
});
|
|
81
94
|
}
|
|
82
95
|
if (useOllama && ollamaKey) {
|
|
@@ -103,7 +116,13 @@ export async function callAnthropicMessages(input) {
|
|
|
103
116
|
body: JSON.stringify({
|
|
104
117
|
model,
|
|
105
118
|
max_tokens: input.maxTokens || 1024,
|
|
106
|
-
|
|
119
|
+
// #2357 β omit temperature for models that reject sampling params
|
|
120
|
+
// (Fable 5 / Opus 4.8 / Opus 4.7 β 400 "Extra inputs are not
|
|
121
|
+
// permitted"); keep the 0.7 default unchanged for models that still
|
|
122
|
+
// accept it (sonnet / haiku / opus β€4.6).
|
|
123
|
+
...(modelRejectsSamplingParams(model)
|
|
124
|
+
? {}
|
|
125
|
+
: { temperature: typeof input.temperature === 'number' ? input.temperature : 0.7 }),
|
|
107
126
|
// #8 prompt caching (hermes-agent pattern): mark the (often large,
|
|
108
127
|
// stable) system prompt as an ephemeral cache breakpoint so repeated
|
|
109
128
|
// agent_execute calls with the same system prompt hit Anthropic's
|
|
@@ -299,13 +318,15 @@ async function callOpenAICompat(input) {
|
|
|
299
318
|
function resolveOpenAICompatModel(input, fallback) {
|
|
300
319
|
if (!input)
|
|
301
320
|
return fallback;
|
|
302
|
-
// Logical Claude names β OpenRouter Anthropic-vendored names
|
|
321
|
+
// Logical Claude names β OpenRouter Anthropic-vendored names.
|
|
322
|
+
// #2357 Finding C: the 3.5 / 3-opus slugs were retired Oct 2025; align with
|
|
323
|
+
// MODEL_MAP (claude-haiku-4-5 / claude-sonnet-4-6 / claude-opus-4-8).
|
|
303
324
|
if (input === 'haiku')
|
|
304
|
-
return 'anthropic/claude-
|
|
325
|
+
return 'anthropic/claude-haiku-4-5';
|
|
305
326
|
if (input === 'sonnet' || input === 'inherit')
|
|
306
|
-
return 'anthropic/claude-
|
|
327
|
+
return 'anthropic/claude-sonnet-4-6';
|
|
307
328
|
if (input === 'opus')
|
|
308
|
-
return 'anthropic/claude-
|
|
329
|
+
return 'anthropic/claude-opus-4-8';
|
|
309
330
|
return input;
|
|
310
331
|
}
|
|
311
332
|
function resolveOllamaModel(input) {
|
|
@@ -10,7 +10,30 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
13
14
|
import { readFileMaybeEncrypted, writeFileRestricted } from '../fs-secure.js';
|
|
15
|
+
/**
|
|
16
|
+
* #2356 β cached, synchronous capability probe for @ruvector/core. `getHNSWStatus`
|
|
17
|
+
* is sync and is called by `neural status` in a fresh process that never warms
|
|
18
|
+
* the lazy HNSW singleton, so reporting availability off the warm singleton
|
|
19
|
+
* alone produced a false "Not loaded β @ruvector/core not available" even when
|
|
20
|
+
* the package is installed and exposes VectorDb. Resolving the module (without
|
|
21
|
+
* importing/initializing it) is a faithful, cheap availability signal.
|
|
22
|
+
*/
|
|
23
|
+
let _ruvectorCoreResolvable;
|
|
24
|
+
function isRuvectorCoreResolvable() {
|
|
25
|
+
if (_ruvectorCoreResolvable !== undefined)
|
|
26
|
+
return _ruvectorCoreResolvable;
|
|
27
|
+
try {
|
|
28
|
+
const req = createRequire(import.meta.url);
|
|
29
|
+
req.resolve('@ruvector/core');
|
|
30
|
+
_ruvectorCoreResolvable = true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
_ruvectorCoreResolvable = false;
|
|
34
|
+
}
|
|
35
|
+
return _ruvectorCoreResolvable;
|
|
36
|
+
}
|
|
14
37
|
/**
|
|
15
38
|
* #1854: previously every site that needed the memory directory hardcoded
|
|
16
39
|
* `getMemoryRoot()`, so the documented config entry
|
|
@@ -671,8 +694,13 @@ export function getHNSWStatus() {
|
|
|
671
694
|
dimensions: hnswIndex?.dimensions ?? 384
|
|
672
695
|
};
|
|
673
696
|
}
|
|
697
|
+
// #2356: `available` now reflects real capability (index already loaded OR
|
|
698
|
+
// @ruvector/core installed and resolvable), not merely whether the lazy
|
|
699
|
+
// singleton happens to be warm in this process. `initialized` still reports
|
|
700
|
+
// whether the in-process index is actually loaded, so callers can tell
|
|
701
|
+
// "installed but not yet loaded" apart from "loaded".
|
|
674
702
|
return {
|
|
675
|
-
available: hnswIndex !== null,
|
|
703
|
+
available: hnswIndex !== null || isRuvectorCoreResolvable(),
|
|
676
704
|
initialized: hnswIndex?.initialized ?? false,
|
|
677
705
|
entryCount: hnswIndex?.entries.size ?? 0,
|
|
678
706
|
dimensions: hnswIndex?.dimensions ?? 384
|
|
@@ -55,6 +55,8 @@ export interface DaemonConfig {
|
|
|
55
55
|
maxCpuLoad: number;
|
|
56
56
|
minFreeMemoryPercent: number;
|
|
57
57
|
};
|
|
58
|
+
ttlMs: number;
|
|
59
|
+
idleShutdownMs: number;
|
|
58
60
|
workers: WorkerConfig[];
|
|
59
61
|
}
|
|
60
62
|
/**
|
|
@@ -65,6 +67,7 @@ export declare class WorkerDaemon extends EventEmitter {
|
|
|
65
67
|
private workers;
|
|
66
68
|
private timers;
|
|
67
69
|
private queuePollTimer?;
|
|
70
|
+
private lifecycleTimer?;
|
|
68
71
|
private running;
|
|
69
72
|
private startedAt?;
|
|
70
73
|
private projectRoot;
|
|
@@ -222,6 +225,30 @@ export declare class WorkerDaemon extends EventEmitter {
|
|
|
222
225
|
* Stop the daemon and all workers
|
|
223
226
|
*/
|
|
224
227
|
stop(): Promise<void>;
|
|
228
|
+
/**
|
|
229
|
+
* #2356 β Self-terminating lifecycle monitor. A daemon with no upper bound
|
|
230
|
+
* on its lifetime is the documented root cause of multi-day token leaks:
|
|
231
|
+
* each interval worker spawns a headless `claude --print` sweep, so a daemon
|
|
232
|
+
* left running for days dispatches tens of thousands of sessions invisibly.
|
|
233
|
+
* This timer enforces a max age (`ttlMs`) and an optional idle window
|
|
234
|
+
* (`idleShutdownMs`), shutting the daemon down gracefully when either trips.
|
|
235
|
+
* Checked once a minute and `unref()`'d so it never keeps the process alive
|
|
236
|
+
* on its own. A no-op when both limits are disabled (0).
|
|
237
|
+
*/
|
|
238
|
+
private startLifecycleMonitor;
|
|
239
|
+
/**
|
|
240
|
+
* Most recent worker start/finish time across all workers (epoch ms), or
|
|
241
|
+
* null if no worker has ever started. Used for idle-shutdown detection.
|
|
242
|
+
*/
|
|
243
|
+
private lastWorkerActivityMs;
|
|
244
|
+
/**
|
|
245
|
+
* Graceful self-shutdown triggered by the lifecycle monitor. Mirrors the
|
|
246
|
+
* signal-handler path (`stop()` then `process.exit(0)`) because the
|
|
247
|
+
* foreground keep-alive in the daemon command is a *ref'd* `setInterval`
|
|
248
|
+
* that would otherwise hold the process open after `stop()` clears the
|
|
249
|
+
* service timers β leaving a zombie that reports stopped but never exits.
|
|
250
|
+
*/
|
|
251
|
+
private selfShutdown;
|
|
225
252
|
/**
|
|
226
253
|
* Get daemon status
|
|
227
254
|
*/
|
|
@@ -27,6 +27,30 @@ const DEFAULT_WORKERS = [
|
|
|
27
27
|
// Worker timeout β must exceed the longest per-worker headless timeout (15 min for audit/refactor).
|
|
28
28
|
// Previously 5 min, which caused orphan processes when daemon timeout fired before executor timeout (#1117).
|
|
29
29
|
const DEFAULT_WORKER_TIMEOUT_MS = 16 * 60 * 1000;
|
|
30
|
+
// #2356 β Self-terminating lifecycle defaults. A background daemon with no
|
|
31
|
+
// upper bound on its lifetime runs until the box reboots; in the field this
|
|
32
|
+
// leaked tens of thousands of headless `claude --print` sweeps over many days
|
|
33
|
+
// (one observed daemon ran 19 days). A 12h default age cap (matching the
|
|
34
|
+
// pacphi/ruflo-machine-ref kit's proven value) heals a forgotten daemon within
|
|
35
|
+
// half a day; set RUFLO_DAEMON_TTL_SECS=0 (or `--ttl 0`) to opt out. Idle
|
|
36
|
+
// shutdown is opt-in (0 = disabled) since a legitimately quiet daemon is not a leak.
|
|
37
|
+
const DEFAULT_DAEMON_TTL_MS = 12 * 60 * 60 * 1000;
|
|
38
|
+
const DEFAULT_DAEMON_IDLE_SHUTDOWN_MS = 0;
|
|
39
|
+
/**
|
|
40
|
+
* Read a non-negative seconds value from an env var and return it as ms.
|
|
41
|
+
* Unlike the `parseInt(x) || default` idiom used elsewhere, an explicit `0`
|
|
42
|
+
* is honored (it disables the corresponding limit) rather than falling back
|
|
43
|
+
* to the default. Invalid / negative / absent values fall back.
|
|
44
|
+
*/
|
|
45
|
+
function readEnvSecsAsMs(name, defaultMs) {
|
|
46
|
+
const raw = process.env[name];
|
|
47
|
+
if (raw === undefined || raw.trim() === '')
|
|
48
|
+
return defaultMs;
|
|
49
|
+
const secs = Number.parseInt(raw, 10);
|
|
50
|
+
if (!Number.isFinite(secs) || secs < 0)
|
|
51
|
+
return defaultMs;
|
|
52
|
+
return secs * 1000;
|
|
53
|
+
}
|
|
30
54
|
/**
|
|
31
55
|
* Worker Daemon - Manages background workers with Node.js
|
|
32
56
|
*/
|
|
@@ -37,6 +61,9 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
37
61
|
// #1845: separate timer for the MCP-dispatch queue poller. Kept off
|
|
38
62
|
// the per-worker map so stop() clears both kinds without confusion.
|
|
39
63
|
queuePollTimer;
|
|
64
|
+
// #2356: separate timer that enforces the daemon's max-age TTL + idle
|
|
65
|
+
// shutdown. Cleared in stop() alongside the worker/queue timers.
|
|
66
|
+
lifecycleTimer;
|
|
40
67
|
running = false;
|
|
41
68
|
startedAt;
|
|
42
69
|
projectRoot;
|
|
@@ -92,6 +119,11 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
92
119
|
maxCpuLoad: config?.resourceThresholds?.maxCpuLoad ?? fileConfig.maxCpuLoad ?? smartMaxCpuLoad,
|
|
93
120
|
minFreeMemoryPercent: config?.resourceThresholds?.minFreeMemoryPercent ?? fileConfig.minFreeMemoryPercent ?? defaultMinFreeMemory,
|
|
94
121
|
},
|
|
122
|
+
// #2356 β precedence: constructor arg > config.json (daemon.ttlSecs) >
|
|
123
|
+
// env (RUFLO_DAEMON_TTL_SECS) > built-in default. readEnvSecsAsMs folds
|
|
124
|
+
// env-or-default and honors an explicit 0 (disable).
|
|
125
|
+
ttlMs: config?.ttlMs ?? fileConfig.ttlMs ?? readEnvSecsAsMs('RUFLO_DAEMON_TTL_SECS', DEFAULT_DAEMON_TTL_MS),
|
|
126
|
+
idleShutdownMs: config?.idleShutdownMs ?? fileConfig.idleShutdownMs ?? readEnvSecsAsMs('RUFLO_DAEMON_IDLE_SECS', DEFAULT_DAEMON_IDLE_SHUTDOWN_MS),
|
|
95
127
|
workers: config?.workers ?? DEFAULT_WORKERS,
|
|
96
128
|
};
|
|
97
129
|
// Setup graceful shutdown handlers
|
|
@@ -274,12 +306,19 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
274
306
|
const rawMinMem = cfg['daemon.resourceThresholds.minFreeMemoryPercent'] ?? raw['daemon.resourceThresholds.minFreeMemoryPercent'];
|
|
275
307
|
const rawMaxConcurrent = cfg['daemon.maxConcurrent'] ?? raw['daemon.maxConcurrent'];
|
|
276
308
|
const rawTimeout = cfg['daemon.workerTimeoutMs'] ?? raw['daemon.workerTimeoutMs'];
|
|
309
|
+
// #2356 β lifecycle limits are configured in SECONDS in config.json
|
|
310
|
+
// (`daemon.ttlSecs` / `daemon.idleSecs`) for parity with the CLI flag
|
|
311
|
+
// and env var; stored internally as ms. An explicit 0 disables.
|
|
312
|
+
const rawTtl = cfg['daemon.ttlSecs'] ?? raw['daemon.ttlSecs'];
|
|
313
|
+
const rawIdle = cfg['daemon.idleSecs'] ?? raw['daemon.idleSecs'];
|
|
277
314
|
return {
|
|
278
315
|
autoStart: typeof raw['daemon.autoStart'] === 'boolean' ? raw['daemon.autoStart'] : undefined,
|
|
279
316
|
maxConcurrent: (typeof rawMaxConcurrent === 'number' && rawMaxConcurrent > 0) ? rawMaxConcurrent : undefined,
|
|
280
317
|
workerTimeoutMs: (typeof rawTimeout === 'number' && rawTimeout > 0) ? rawTimeout : undefined,
|
|
281
318
|
maxCpuLoad: (typeof rawCpuLoad === 'number' && rawCpuLoad > 0 && rawCpuLoad < 1000) ? rawCpuLoad : undefined,
|
|
282
319
|
minFreeMemoryPercent: (typeof rawMinMem === 'number' && rawMinMem >= 0 && rawMinMem <= 100) ? rawMinMem : undefined,
|
|
320
|
+
ttlMs: (typeof rawTtl === 'number' && rawTtl >= 0) ? rawTtl * 1000 : undefined,
|
|
321
|
+
idleShutdownMs: (typeof rawIdle === 'number' && rawIdle >= 0) ? rawIdle * 1000 : undefined,
|
|
283
322
|
};
|
|
284
323
|
}
|
|
285
324
|
catch {
|
|
@@ -668,6 +707,9 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
668
707
|
if (typeof this.queuePollTimer.unref === 'function') {
|
|
669
708
|
this.queuePollTimer.unref();
|
|
670
709
|
}
|
|
710
|
+
// #2356: self-terminating lifecycle. Without an upper bound on lifetime a
|
|
711
|
+
// forgotten daemon keeps dispatching headless worker sweeps for days.
|
|
712
|
+
this.startLifecycleMonitor();
|
|
671
713
|
// Save state
|
|
672
714
|
this.saveState();
|
|
673
715
|
this.log('info', `Daemon started (PID: ${process.pid}, CPUs: ${cpus().length}, workers: ${this.config.workers.filter(w => w.enabled).length}, maxCpuLoad: ${this.config.resourceThresholds.maxCpuLoad}, minFreeMemoryPercent: ${this.config.resourceThresholds.minFreeMemoryPercent}%)`);
|
|
@@ -762,12 +804,93 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
762
804
|
clearInterval(this.queuePollTimer);
|
|
763
805
|
this.queuePollTimer = undefined;
|
|
764
806
|
}
|
|
807
|
+
// #2356: stop the TTL/idle lifecycle monitor.
|
|
808
|
+
if (this.lifecycleTimer) {
|
|
809
|
+
clearInterval(this.lifecycleTimer);
|
|
810
|
+
this.lifecycleTimer = undefined;
|
|
811
|
+
}
|
|
765
812
|
this.running = false;
|
|
766
813
|
this.removePidFile();
|
|
767
814
|
this.saveState();
|
|
768
815
|
this.emit('stopped', { stoppedAt: new Date() });
|
|
769
816
|
this.log('info', 'Daemon stopped');
|
|
770
817
|
}
|
|
818
|
+
/**
|
|
819
|
+
* #2356 β Self-terminating lifecycle monitor. A daemon with no upper bound
|
|
820
|
+
* on its lifetime is the documented root cause of multi-day token leaks:
|
|
821
|
+
* each interval worker spawns a headless `claude --print` sweep, so a daemon
|
|
822
|
+
* left running for days dispatches tens of thousands of sessions invisibly.
|
|
823
|
+
* This timer enforces a max age (`ttlMs`) and an optional idle window
|
|
824
|
+
* (`idleShutdownMs`), shutting the daemon down gracefully when either trips.
|
|
825
|
+
* Checked once a minute and `unref()`'d so it never keeps the process alive
|
|
826
|
+
* on its own. A no-op when both limits are disabled (0).
|
|
827
|
+
*/
|
|
828
|
+
startLifecycleMonitor() {
|
|
829
|
+
const ttlMs = this.config.ttlMs;
|
|
830
|
+
const idleMs = this.config.idleShutdownMs;
|
|
831
|
+
if ((!ttlMs || ttlMs <= 0) && (!idleMs || idleMs <= 0)) {
|
|
832
|
+
return; // both limits disabled β preserve legacy run-until-stopped behavior
|
|
833
|
+
}
|
|
834
|
+
const CHECK_INTERVAL_MS = 60_000;
|
|
835
|
+
this.lifecycleTimer = setInterval(() => {
|
|
836
|
+
if (!this.running)
|
|
837
|
+
return;
|
|
838
|
+
const now = Date.now();
|
|
839
|
+
const startedMs = this.startedAt?.getTime() ?? now;
|
|
840
|
+
if (ttlMs > 0 && now - startedMs >= ttlMs) {
|
|
841
|
+
void this.selfShutdown(`max age ${Math.round(ttlMs / 1000)}s reached`);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (idleMs > 0) {
|
|
845
|
+
const lastActivity = this.lastWorkerActivityMs() ?? startedMs;
|
|
846
|
+
if (now - lastActivity >= idleMs) {
|
|
847
|
+
void this.selfShutdown(`idle for ${Math.round(idleMs / 1000)}s (no worker activity)`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}, CHECK_INTERVAL_MS);
|
|
851
|
+
if (typeof this.lifecycleTimer.unref === 'function') {
|
|
852
|
+
this.lifecycleTimer.unref();
|
|
853
|
+
}
|
|
854
|
+
const parts = [];
|
|
855
|
+
if (ttlMs > 0)
|
|
856
|
+
parts.push(`ttl=${Math.round(ttlMs / 1000)}s`);
|
|
857
|
+
if (idleMs > 0)
|
|
858
|
+
parts.push(`idle=${Math.round(idleMs / 1000)}s`);
|
|
859
|
+
this.log('info', `Lifecycle monitor active (${parts.join(', ')})`);
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Most recent worker start/finish time across all workers (epoch ms), or
|
|
863
|
+
* null if no worker has ever started. Used for idle-shutdown detection.
|
|
864
|
+
*/
|
|
865
|
+
lastWorkerActivityMs() {
|
|
866
|
+
let latest = null;
|
|
867
|
+
for (const state of this.workers.values()) {
|
|
868
|
+
for (const t of [state.lastRun, state.lastStartedAt]) {
|
|
869
|
+
if (t) {
|
|
870
|
+
const ms = t.getTime();
|
|
871
|
+
if (latest === null || ms > latest)
|
|
872
|
+
latest = ms;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return latest;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Graceful self-shutdown triggered by the lifecycle monitor. Mirrors the
|
|
880
|
+
* signal-handler path (`stop()` then `process.exit(0)`) because the
|
|
881
|
+
* foreground keep-alive in the daemon command is a *ref'd* `setInterval`
|
|
882
|
+
* that would otherwise hold the process open after `stop()` clears the
|
|
883
|
+
* service timers β leaving a zombie that reports stopped but never exits.
|
|
884
|
+
*/
|
|
885
|
+
async selfShutdown(reason) {
|
|
886
|
+
this.log('info', `Daemon self-shutdown: ${reason}`);
|
|
887
|
+
this.emit('self-shutdown', { reason });
|
|
888
|
+
try {
|
|
889
|
+
await this.stop();
|
|
890
|
+
}
|
|
891
|
+
catch { /* best-effort β we are exiting regardless */ }
|
|
892
|
+
process.exit(0);
|
|
893
|
+
}
|
|
771
894
|
/**
|
|
772
895
|
* Get daemon status
|
|
773
896
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.43",
|
|
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",
|