fluxy-bot 0.4.15 → 0.4.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": "fluxy-bot",
3
- "version": "0.4.15",
3
+ "version": "0.4.16",
4
4
  "description": "Self-hosted, self-evolving AI agent with its own dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,24 +1,11 @@
1
1
  import { spawn, type ChildProcess } from 'child_process';
2
- import fs from 'fs';
3
2
  import path from 'path';
4
3
  import { PKG_DIR } from '../shared/paths.js';
5
4
  import { log } from '../shared/logger.js';
6
5
 
7
6
  let child: ChildProcess | null = null;
8
7
  let restarts = 0;
9
- let lastSpawnTime = 0;
10
- let pendingRestartTimer: ReturnType<typeof setTimeout> | null = null;
11
8
  const MAX_RESTARTS = 3;
12
- const STABLE_THRESHOLD_MS = 30_000; // 30s uptime = reset counter
13
-
14
- // File watcher state
15
- let watcher: fs.FSWatcher | null = null;
16
- let restartInProgress = false;
17
- let restartInProgressTimer: ReturnType<typeof setTimeout> | null = null;
18
- let debounceTimer: ReturnType<typeof setTimeout> | null = null;
19
-
20
- const IGNORED_EXTENSIONS = new Set(['.db', '.sqlite', '.db-journal', '.db-wal']);
21
- const IGNORED_DIRS = new Set(['node_modules', '.git']);
22
9
 
23
10
  export function getBackendPort(basePort: number): number {
24
11
  return basePort + 4;
@@ -33,8 +20,6 @@ export function spawnBackend(port: number): ChildProcess {
33
20
  env: { ...process.env, BACKEND_PORT: String(port) },
34
21
  });
35
22
 
36
- lastSpawnTime = Date.now();
37
-
38
23
  child.stdout?.on('data', (d) => {
39
24
  process.stdout.write(d);
40
25
  });
@@ -46,20 +31,10 @@ export function spawnBackend(port: number): ChildProcess {
46
31
  child.on('exit', (code) => {
47
32
  if (code !== 0 && code !== null) {
48
33
  log.warn(`Backend crashed (code ${code})`);
49
-
50
- // Auto-reset counter if the process was stable (alive >30s)
51
- const uptime = Date.now() - lastSpawnTime;
52
- if (uptime > STABLE_THRESHOLD_MS) {
53
- restarts = 0;
54
- }
55
-
56
34
  if (restarts < MAX_RESTARTS) {
57
35
  restarts++;
58
36
  log.info(`Restarting backend (${restarts}/${MAX_RESTARTS})...`);
59
- pendingRestartTimer = setTimeout(() => {
60
- pendingRestartTimer = null;
61
- spawnBackend(port);
62
- }, 1000);
37
+ setTimeout(() => spawnBackend(port), 1000);
63
38
  } else {
64
39
  log.error('Backend failed too many times. Use Fluxy chat to debug.');
65
40
  }
@@ -71,11 +46,6 @@ export function spawnBackend(port: number): ChildProcess {
71
46
  }
72
47
 
73
48
  export function stopBackend(): void {
74
- // Clear any pending crash-recovery restart
75
- if (pendingRestartTimer) {
76
- clearTimeout(pendingRestartTimer);
77
- pendingRestartTimer = null;
78
- }
79
49
  child?.kill();
80
50
  child = null;
81
51
  }
@@ -87,75 +57,3 @@ export function isBackendAlive(): boolean {
87
57
  export function resetBackendRestarts(): void {
88
58
  restarts = 0;
89
59
  }
90
-
91
- /**
92
- * Mark that a restart is already in progress (e.g. from Fluxy agent bot:done).
93
- * The file watcher will skip triggering a restart for the next 2 seconds.
94
- */
95
- export function markRestartInProgress(): void {
96
- restartInProgress = true;
97
- if (restartInProgressTimer) clearTimeout(restartInProgressTimer);
98
- restartInProgressTimer = setTimeout(() => {
99
- restartInProgress = false;
100
- restartInProgressTimer = null;
101
- }, 2000);
102
- }
103
-
104
- /**
105
- * Watch workspace/backend/ for file changes and auto-restart.
106
- * Returns a cleanup function.
107
- */
108
- export function startBackendWatcher(port: number, onRestart?: () => void): void {
109
- const watchDir = path.join(PKG_DIR, 'workspace', 'backend');
110
-
111
- try {
112
- watcher = fs.watch(watchDir, { recursive: true }, (_event, filename) => {
113
- if (!filename) return;
114
-
115
- // Filter out ignored files
116
- const ext = path.extname(filename);
117
- if (IGNORED_EXTENSIONS.has(ext)) return;
118
-
119
- // Filter out ignored directories
120
- const parts = filename.split(path.sep);
121
- if (parts.some((p) => IGNORED_DIRS.has(p))) return;
122
-
123
- // Debounce rapid saves (500ms)
124
- if (debounceTimer) clearTimeout(debounceTimer);
125
- debounceTimer = setTimeout(() => {
126
- debounceTimer = null;
127
-
128
- // Skip if a restart was already triggered (e.g. by Fluxy agent)
129
- if (restartInProgress) {
130
- log.info(`[watcher] File changed (${filename}) — restart already in progress, skipping`);
131
- return;
132
- }
133
-
134
- log.info(`[watcher] File changed: ${filename} — restarting backend`);
135
- resetBackendRestarts();
136
- stopBackend();
137
- spawnBackend(port);
138
- onRestart?.();
139
- }, 500);
140
- });
141
-
142
- log.ok(`Watching workspace/backend/ for changes`);
143
- } catch (err) {
144
- log.warn(`File watcher failed: ${err instanceof Error ? err.message : err}`);
145
- }
146
- }
147
-
148
- export function stopBackendWatcher(): void {
149
- if (watcher) {
150
- watcher.close();
151
- watcher = null;
152
- }
153
- if (debounceTimer) {
154
- clearTimeout(debounceTimer);
155
- debounceTimer = null;
156
- }
157
- if (restartInProgressTimer) {
158
- clearTimeout(restartInProgressTimer);
159
- restartInProgressTimer = null;
160
- }
161
- }
@@ -10,7 +10,7 @@ import { PKG_DIR } from '../shared/paths.js';
10
10
  import { log } from '../shared/logger.js';
11
11
  import { startTunnel, stopTunnel, isTunnelAlive, restartTunnel } from './tunnel.js';
12
12
  import { spawnWorker, stopWorker, getWorkerPort, isWorkerAlive } from './worker.js';
13
- import { spawnBackend, stopBackend, getBackendPort, resetBackendRestarts, startBackendWatcher, stopBackendWatcher, markRestartInProgress } from './backend.js';
13
+ import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, resetBackendRestarts } from './backend.js';
14
14
  import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
15
15
  import { startFluxyAgentQuery, stopFluxyAgentQuery, clearFluxySession } from './fluxy-agent.js';
16
16
  import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
@@ -162,44 +162,30 @@ export async function startSupervisor() {
162
162
  return;
163
163
  }
164
164
 
165
- // App API routes → proxy to user's backend server (with retry on startup)
165
+ // App API routes → proxy to user's backend server
166
166
  if (req.url?.startsWith('/app/api')) {
167
167
  const backendPath = req.url.replace(/^\/app\/api/, '') || '/';
168
168
  console.log(`[supervisor] → backend :${backendPort} | ${req.method} ${backendPath}`);
169
+ if (!isBackendAlive()) {
170
+ console.log('[supervisor] Backend down — returning 503');
171
+ res.writeHead(503, { 'Content-Type': 'application/json' });
172
+ res.end(JSON.stringify({ error: 'Backend is starting...' }));
173
+ return;
174
+ }
169
175
 
170
- // Buffer request body so we can replay it on retry
171
- const chunks: Buffer[] = [];
172
- req.on('data', (chunk) => chunks.push(chunk));
173
- req.on('end', () => {
174
- const body = Buffer.concat(chunks);
175
- let attempt = 0;
176
- const MAX_RETRIES = 3;
177
- const RETRY_DELAY = 500;
178
-
179
- function tryProxy() {
180
- const proxy = http.request(
181
- { host: '127.0.0.1', port: backendPort, path: backendPath, method: req.method, headers: req.headers },
182
- (proxyRes) => {
183
- res.writeHead(proxyRes.statusCode!, proxyRes.headers);
184
- proxyRes.pipe(res);
185
- },
186
- );
187
- proxy.on('error', (e: NodeJS.ErrnoException) => {
188
- if (e.code === 'ECONNREFUSED' && attempt < MAX_RETRIES) {
189
- attempt++;
190
- console.log(`[supervisor] Backend not ready, retry ${attempt}/${MAX_RETRIES}...`);
191
- setTimeout(tryProxy, RETRY_DELAY);
192
- } else {
193
- console.error(`[supervisor] Backend proxy error: ${req.url}`, e.message);
194
- res.writeHead(503, { 'Content-Type': 'application/json' });
195
- res.end(JSON.stringify({ error: 'Backend unavailable' }));
196
- }
197
- });
198
- proxy.end(body);
199
- }
200
-
201
- tryProxy();
176
+ const proxy = http.request(
177
+ { host: '127.0.0.1', port: backendPort, path: backendPath, method: req.method, headers: req.headers },
178
+ (proxyRes) => {
179
+ res.writeHead(proxyRes.statusCode!, proxyRes.headers);
180
+ proxyRes.pipe(res);
181
+ },
182
+ );
183
+ proxy.on('error', (e) => {
184
+ console.error(`[supervisor] Backend proxy error: ${req.url}`, e.message);
185
+ res.writeHead(503, { 'Content-Type': 'application/json' });
186
+ res.end(JSON.stringify({ error: 'Backend unavailable' }));
202
187
  });
188
+ req.pipe(proxy);
203
189
  return;
204
190
  }
205
191
 
@@ -437,7 +423,6 @@ export async function startSupervisor() {
437
423
  if (eventData.usedFileTools) {
438
424
  console.log('[supervisor] File tools used — Vite HMR will apply changes automatically');
439
425
  console.log('[supervisor] Restarting backend...');
440
- markRestartInProgress(); // prevent file watcher from double-restarting
441
426
  resetBackendRestarts();
442
427
  stopBackend();
443
428
  spawnBackend(backendPort);
@@ -579,9 +564,6 @@ export async function startSupervisor() {
579
564
  // Spawn worker + backend
580
565
  spawnWorker(workerPort);
581
566
  spawnBackend(backendPort);
582
- startBackendWatcher(backendPort, () => {
583
- broadcastFluxy('app:hmr-update');
584
- });
585
567
 
586
568
  // Tunnel
587
569
  let tunnelUrl: string | null = null;
@@ -661,7 +643,6 @@ export async function startSupervisor() {
661
643
  delete latestConfig.tunnelUrl;
662
644
  saveConfig(latestConfig);
663
645
  stopWorker();
664
- stopBackendWatcher();
665
646
  stopBackend();
666
647
  stopTunnel();
667
648
  console.log('[supervisor] Stopping Vite dev servers...');