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 +10 -8
- package/package.json +1 -1
- package/scripts/install +2 -0
- package/scripts/install.sh +2 -0
- package/supervisor/backend.ts +18 -13
- package/supervisor/index.ts +63 -10
- package/worker/index.ts +9 -0
- package/worker/prompts/fluxy-system-prompt.txt +20 -0
package/bin/cli.js
CHANGED
|
@@ -270,20 +270,19 @@ function bootServer() {
|
|
|
270
270
|
viteWarmResolve();
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
if (
|
|
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
package/scripts/install
CHANGED
package/scripts/install.sh
CHANGED
package/supervisor/backend.ts
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
}
|
package/supervisor/index.ts
CHANGED
|
@@ -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
|
-
|
|
504
|
-
|
|
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
|
|
644
|
-
//
|
|
645
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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]
|
|
666
|
+
log.info(`[watcher] ${reason} — restarting backend...`);
|
|
653
667
|
resetBackendRestarts();
|
|
654
668
|
stopBackend();
|
|
655
669
|
spawnBackend(backendPort);
|
|
656
|
-
},
|
|
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
|