fluxy-bot 0.5.20 → 0.5.22

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/bin/cli.js CHANGED
@@ -270,20 +270,19 @@ function bootServer() {
270
270
  viteWarmResolve();
271
271
  }
272
272
 
273
- if (tunnelUrl && relayUrl) {
273
+ if (text.includes('__READY__')) {
274
274
  doResolve();
275
275
  return;
276
276
  }
277
277
 
278
- if (tunnelUrl && !relayUrl) {
279
- setTimeout(doResolve, 3000);
280
- }
281
-
282
278
  if (text.includes('__TUNNEL_FAILED__')) {
283
279
  doResolve();
284
280
  }
285
281
  };
286
282
 
283
+ // Safety-net timeout: resolve after 45s even if __READY__ never arrives
284
+ setTimeout(doResolve, 45_000);
285
+
287
286
  child.stdout.on('data', handleData);
288
287
  child.stderr.on('data', (data) => {
289
288
  stderrBuf += data.toString();
@@ -315,6 +314,7 @@ async function init() {
315
314
  'Installing cloudflared',
316
315
  'Starting server',
317
316
  'Connecting tunnel',
317
+ 'Verifying connection',
318
318
  'Preparing dashboard',
319
319
  ];
320
320
 
@@ -340,7 +340,8 @@ async function init() {
340
340
  process.exit(1);
341
341
  }
342
342
  const { child, tunnelUrl, relayUrl, viteWarm } = result;
343
- stepper.advance();
343
+ stepper.advance(); // Connecting tunnel done
344
+ stepper.advance(); // Verifying connection done
344
345
 
345
346
  // Wait for Vite to finish pre-transforming all modules (with timeout)
346
347
  await Promise.race([viteWarm, new Promise(r => setTimeout(r, 30_000))]);
@@ -366,7 +367,7 @@ async function start() {
366
367
 
367
368
  banner();
368
369
 
369
- const steps = ['Loading config', 'Starting server', 'Connecting tunnel', 'Preparing dashboard'];
370
+ const steps = ['Loading config', 'Starting server', 'Connecting tunnel', 'Verifying connection', 'Preparing dashboard'];
370
371
  const stepper = new Stepper(steps);
371
372
  stepper.start();
372
373
 
@@ -383,7 +384,8 @@ async function start() {
383
384
  process.exit(1);
384
385
  }
385
386
  const { child, tunnelUrl, relayUrl, viteWarm } = result;
386
- stepper.advance();
387
+ stepper.advance(); // Connecting tunnel done
388
+ stepper.advance(); // Verifying connection done
387
389
 
388
390
  // Wait for Vite to finish pre-transforming all modules (with timeout)
389
391
  await Promise.race([viteWarm, new Promise(r => setTimeout(r, 30_000))]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.5.20",
3
+ "version": "0.5.22",
4
4
  "description": "Self-hosted, self-evolving AI agent with its own dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/scripts/install CHANGED
@@ -132,6 +132,8 @@ install_fluxy() {
132
132
  NPM="npm"
133
133
  NODE="node"
134
134
  else
135
+ # Add bundled node to PATH so npm's "#!/usr/bin/env node" shebang works
136
+ export PATH="$NODE_DIR/bin:$PATH"
135
137
  NPM="$NODE_DIR/bin/npm"
136
138
  NODE="$NODE_DIR/bin/node"
137
139
  fi
@@ -132,6 +132,8 @@ install_fluxy() {
132
132
  NPM="npm"
133
133
  NODE="node"
134
134
  else
135
+ # Add bundled node to PATH so npm's "#!/usr/bin/env node" shebang works
136
+ export PATH="$NODE_DIR/bin:$PATH"
135
137
  NPM="$NODE_DIR/bin/npm"
136
138
  NODE="$NODE_DIR/bin/node"
137
139
  fi
@@ -6,6 +6,7 @@ import { log } from '../shared/logger.js';
6
6
  let child: ChildProcess | null = null;
7
7
  let restarts = 0;
8
8
  let lastSpawnTime = 0;
9
+ let intentionallyStopped = false;
9
10
  const MAX_RESTARTS = 3;
10
11
  const STABLE_THRESHOLD = 30_000; // 30s — if backend ran this long, it wasn't a crash loop
11
12
 
@@ -16,6 +17,7 @@ export function getBackendPort(basePort: number): number {
16
17
  export function spawnBackend(port: number): ChildProcess {
17
18
  const backendPath = path.join(PKG_DIR, 'workspace', 'backend', 'index.ts');
18
19
  lastSpawnTime = Date.now();
20
+ intentionallyStopped = false;
19
21
 
20
22
  child = spawn(process.execPath, ['--import', 'tsx/esm', backendPath], {
21
23
  cwd: path.join(PKG_DIR, 'workspace'),
@@ -32,19 +34,21 @@ export function spawnBackend(port: number): ChildProcess {
32
34
  });
33
35
 
34
36
  child.on('exit', (code) => {
35
- if (code !== 0 && code !== null) {
36
- log.warn(`Backend crashed (code ${code})`);
37
- // If backend was alive for >30s, it's not a crash loop — reset counter
38
- if (Date.now() - lastSpawnTime > STABLE_THRESHOLD) {
39
- restarts = 0;
40
- }
41
- if (restarts < MAX_RESTARTS) {
42
- restarts++;
43
- log.info(`Restarting backend (${restarts}/${MAX_RESTARTS})...`);
44
- setTimeout(() => spawnBackend(port), 1000 * restarts);
45
- } else {
46
- log.error('Backend failed too many times. Use Fluxy chat to debug.');
47
- }
37
+ // Supervisor called stopBackend() don't auto-restart
38
+ if (intentionallyStopped) return;
39
+
40
+ // Any unexpected exit (crash, SIGTERM, OOM, null code) — restart
41
+ log.warn(`Backend exited unexpectedly (code ${code})`);
42
+ // If backend was alive for >30s, it's not a crash loop — reset counter
43
+ if (Date.now() - lastSpawnTime > STABLE_THRESHOLD) {
44
+ restarts = 0;
45
+ }
46
+ if (restarts < MAX_RESTARTS) {
47
+ restarts++;
48
+ log.info(`Restarting backend (${restarts}/${MAX_RESTARTS})...`);
49
+ setTimeout(() => spawnBackend(port), 1000 * restarts);
50
+ } else {
51
+ log.error('Backend failed too many times. Use Fluxy chat to debug.');
48
52
  }
49
53
  });
50
54
 
@@ -53,6 +57,7 @@ export function spawnBackend(port: number): ChildProcess {
53
57
  }
54
58
 
55
59
  export function stopBackend(): void {
60
+ intentionallyStopped = true;
56
61
  child?.kill();
57
62
  child = null;
58
63
  }
@@ -497,11 +497,15 @@ export async function startSupervisor() {
497
497
  } catch {}
498
498
 
499
499
  // Start agent query
500
+ agentQueryActive = true;
500
501
  startFluxyAgentQuery(convId, content, freshConfig.ai.model, (type, eventData) => {
501
502
  // Intercept bot:done — Vite HMR handles file changes automatically
502
503
  if (type === 'bot:done') {
503
- if (eventData.usedFileTools) {
504
- console.log('[supervisor] File tools used Vite HMR handles frontend, restarting backend');
504
+ agentQueryActive = false;
505
+ // Restart if agent used file tools OR file watcher detected changes during the turn
506
+ if (eventData.usedFileTools || pendingBackendRestart) {
507
+ console.log('[supervisor] Agent turn ended — restarting backend');
508
+ pendingBackendRestart = false;
505
509
  resetBackendRestarts();
506
510
  stopBackend();
507
511
  spawnBackend(backendPort);
@@ -624,6 +628,10 @@ export async function startSupervisor() {
624
628
  log.ok(`Fluxy chat at http://localhost:${config.port}/fluxy`);
625
629
  });
626
630
 
631
+ // Track whether an agent query is active — file watcher defers to bot:done during turns
632
+ let agentQueryActive = false;
633
+ let pendingBackendRestart = false; // Set when file watcher fires during agent turn
634
+
627
635
  // Spawn worker + backend
628
636
  spawnWorker(workerPort);
629
637
  spawnBackend(backendPort);
@@ -640,20 +648,45 @@ export async function startSupervisor() {
640
648
  getModel: () => loadConfig().ai.model,
641
649
  });
642
650
 
643
- // Watch workspace/backend/ for file changes — auto-restart backend
644
- // This catches edits from Claude Code CLI, VS Code, or any external tool
645
- const backendDir = path.join(PKG_DIR, 'workspace', 'backend');
651
+ // Watch workspace files for changes — auto-restart backend
652
+ // Catches edits from VS Code, CLI, or any external tool.
653
+ // During agent turns, defers to bot:done (avoids mid-turn restarts).
654
+ const workspaceDir = path.join(PKG_DIR, 'workspace');
655
+ const backendDir = path.join(workspaceDir, 'backend');
646
656
  let backendRestartTimer: ReturnType<typeof setTimeout> | null = null;
647
- const backendWatcher = fs.watch(backendDir, { recursive: true }, (_event, filename) => {
648
- if (!filename || !filename.match(/\.(ts|js|json)$/)) return;
649
- // Debounce: wait 500ms for rapid edits to settle
657
+
658
+ function scheduleBackendRestart(reason: string) {
659
+ if (agentQueryActive) {
660
+ // Agent is working — don't restart now, flag it for bot:done
661
+ pendingBackendRestart = true;
662
+ return;
663
+ }
650
664
  if (backendRestartTimer) clearTimeout(backendRestartTimer);
651
665
  backendRestartTimer = setTimeout(() => {
652
- log.info(`[watcher] Backend file changed: ${filename} — restarting...`);
666
+ log.info(`[watcher] ${reason} — restarting backend...`);
653
667
  resetBackendRestarts();
654
668
  stopBackend();
655
669
  spawnBackend(backendPort);
656
- }, 500);
670
+ }, 1000);
671
+ }
672
+
673
+ // Watch backend/ for code changes
674
+ const backendWatcher = fs.watch(backendDir, { recursive: true }, (_event, filename) => {
675
+ if (!filename || !filename.match(/\.(ts|js|json)$/)) return;
676
+ scheduleBackendRestart(`Backend file changed: ${filename}`);
677
+ });
678
+
679
+ // Watch workspace root for .env changes and .restart trigger
680
+ const workspaceWatcher = fs.watch(workspaceDir, (_event, filename) => {
681
+ if (!filename) return;
682
+ if (filename === '.env') {
683
+ scheduleBackendRestart('.env changed');
684
+ }
685
+ if (filename === '.restart') {
686
+ // Consume the trigger file
687
+ try { fs.unlinkSync(path.join(workspaceDir, '.restart')); } catch {}
688
+ scheduleBackendRestart('.restart trigger');
689
+ }
657
690
  });
658
691
 
659
692
  // Tunnel
@@ -681,6 +714,25 @@ export async function startSupervisor() {
681
714
  log.warn(`Relay: ${err instanceof Error ? err.message : err}`);
682
715
  }
683
716
  }
717
+
718
+ // Poll until the full chain is actually working (avoid 502s)
719
+ // Cache-busting query param prevents browsers/CDN from serving stale 502s
720
+ const probeUrl = config.relay?.url || tunnelUrl;
721
+ let ready = false;
722
+ for (let i = 0; i < 20; i++) {
723
+ try {
724
+ const res = await fetch(probeUrl + `/api/health?_cb=${Date.now()}`, {
725
+ signal: AbortSignal.timeout(3000),
726
+ headers: { 'Cache-Control': 'no-cache' },
727
+ });
728
+ if (res.ok) { ready = true; break; }
729
+ } catch {}
730
+ await new Promise(r => setTimeout(r, 1000));
731
+ }
732
+ if (!ready) {
733
+ log.warn('Readiness probe timed out — URL may not be reachable yet');
734
+ }
735
+ console.log('__READY__');
684
736
  } catch (err) {
685
737
  log.warn(`Tunnel: ${err instanceof Error ? err.message : err}`);
686
738
  console.log('__TUNNEL_FAILED__');
@@ -726,6 +778,7 @@ export async function startSupervisor() {
726
778
  log.info('Shutting down...');
727
779
  stopScheduler();
728
780
  backendWatcher.close();
781
+ workspaceWatcher.close();
729
782
  if (backendRestartTimer) clearTimeout(backendRestartTimer);
730
783
  if (watchdogInterval) clearInterval(watchdogInterval);
731
784
  stopHeartbeat();
package/worker/index.ts CHANGED
@@ -63,6 +63,15 @@ initWebPush();
63
63
  const app = express();
64
64
  app.use(express.json({ limit: '10mb' }));
65
65
 
66
+ // Prevent browsers/CDN/relay from caching API responses (avoids stale 502s)
67
+ app.use('/api', (_, res, next) => {
68
+ res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
69
+ res.set('Pragma', 'no-cache');
70
+ res.set('Expires', '0');
71
+ res.set('Surrogate-Control', 'no-store');
72
+ next();
73
+ });
74
+
66
75
  app.get('/api/health', (_, res) => res.json({ status: 'ok' }));
67
76
  app.get('/api/conversations', (_, res) => res.json(listConversations()));
68
77
  app.get('/api/conversations/:id', (req, res) => {
@@ -218,6 +218,26 @@ Browser: GET /app/api/tasks → Supervisor strips prefix → Backend receives: G
218
218
  ## Build Rules
219
219
  NEVER run `npm run build`, `vite build`, or any build commands. Vite HMR handles frontend changes automatically. The backend auto-restarts when you edit files. Never look in `dist/` or `dist-fluxy/`.
220
220
 
221
+ ## Backend Lifecycle (Critical)
222
+
223
+ The supervisor manages the backend process. You don't need to manage it yourself.
224
+
225
+ **Auto-restart triggers (you don't need to do anything):**
226
+ - Editing `.ts`, `.js`, or `.json` files in `backend/` → auto-restart
227
+ - Editing `.env` → auto-restart with the new values
228
+ - Creating a `.restart` file → force restart: `touch .restart` (file is auto-deleted)
229
+ - After your turn ends, if you used Write or Edit tools → auto-restart
230
+
231
+ **During your turn:** The backend does NOT restart mid-turn. All your edits are batched — the backend restarts once when you're done. This means if you're writing multi-file changes, everything applies atomically.
232
+
233
+ **If the backend crashes:** It auto-restarts up to 3 times. If it keeps crashing, check `backend/index.ts` for syntax errors or bad imports.
234
+
235
+ **NEVER do these:**
236
+ - Never `kill` processes or run `pkill`/`killall` — you don't manage the supervisor or its children
237
+ - Never run `fluxy start` or try to restart the supervisor — only your human can do that
238
+ - Never run `npm start` or `node backend/index.ts` directly — the supervisor handles it
239
+ - If something is truly broken and won't self-heal, tell your human to restart fluxy
240
+
221
241
  ## Sacred Files — NEVER Modify
222
242
  - `supervisor/` — chat UI, proxy, process management
223
243
  - `worker/` — platform APIs and database