fluxy-bot 0.12.1 → 0.12.3
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
package/supervisor/backend.ts
CHANGED
|
@@ -97,31 +97,47 @@ export function spawnBackend(port: number): ChildProcess {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/** Stop the backend and wait for the process to fully exit before resolving.
|
|
100
|
-
* This prevents port collisions when restarting (old process must release the port first).
|
|
100
|
+
* This prevents port collisions when restarting (old process must release the port first).
|
|
101
|
+
* Concurrent calls return the same promise to avoid double-spawn races. */
|
|
102
|
+
let stopPromise: Promise<void> | null = null;
|
|
103
|
+
|
|
101
104
|
export function stopBackend(): Promise<void> {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
resolve();
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
intentionallyStopped = true;
|
|
109
|
-
const dying = child;
|
|
105
|
+
if (stopPromise) return stopPromise;
|
|
106
|
+
|
|
107
|
+
if (!child || child.exitCode !== null) {
|
|
110
108
|
child = null;
|
|
111
|
-
|
|
109
|
+
return Promise.resolve();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
intentionallyStopped = true;
|
|
113
|
+
const dying = child;
|
|
114
|
+
child = null;
|
|
115
|
+
|
|
116
|
+
stopPromise = new Promise<void>((resolve) => {
|
|
117
|
+
dying.once('exit', () => {
|
|
118
|
+
stopPromise = null;
|
|
119
|
+
resolve();
|
|
120
|
+
});
|
|
112
121
|
dying.kill();
|
|
113
122
|
// Safety: force kill after 3s if SIGTERM doesn't work
|
|
114
123
|
setTimeout(() => {
|
|
115
124
|
try { dying.kill('SIGKILL'); } catch {}
|
|
125
|
+
stopPromise = null;
|
|
116
126
|
resolve();
|
|
117
127
|
}, 3000);
|
|
118
128
|
});
|
|
129
|
+
|
|
130
|
+
return stopPromise;
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
export function isBackendAlive(): boolean {
|
|
122
134
|
return child !== null && child.exitCode === null;
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
export function isBackendStopping(): boolean {
|
|
138
|
+
return stopPromise !== null;
|
|
139
|
+
}
|
|
140
|
+
|
|
125
141
|
export function resetBackendRestarts(): void {
|
|
126
142
|
restarts = 0;
|
|
127
143
|
}
|
package/supervisor/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { log } from '../shared/logger.js';
|
|
|
11
11
|
import { startTunnel, stopTunnel, isTunnelAlive, restartTunnel, startNamedTunnel, restartNamedTunnel } from './tunnel.js';
|
|
12
12
|
import { createWorkerApp } from '../worker/index.js';
|
|
13
13
|
import { closeDb, getSession, getSetting } from '../worker/db.js';
|
|
14
|
-
import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, resetBackendRestarts } from './backend.js';
|
|
14
|
+
import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, isBackendStopping, resetBackendRestarts } from './backend.js';
|
|
15
15
|
import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
|
|
16
16
|
import { startFluxyAgentQuery, stopFluxyAgentQuery, type RecentMessage } from './fluxy-agent.js';
|
|
17
17
|
import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
|
|
@@ -341,7 +341,7 @@ export async function startSupervisor() {
|
|
|
341
341
|
|
|
342
342
|
// App API routes → proxy to user's backend server
|
|
343
343
|
if (req.url?.startsWith('/app/api')) {
|
|
344
|
-
const backendPath = req.url.replace(/^\/app
|
|
344
|
+
const backendPath = req.url.replace(/^\/app/, '');
|
|
345
345
|
console.log(`[supervisor] → backend :${backendPort} | ${req.method} ${backendPath}`);
|
|
346
346
|
if (!isBackendAlive()) {
|
|
347
347
|
console.log('[supervisor] Backend down — returning 503');
|
|
@@ -473,7 +473,7 @@ export async function startSupervisor() {
|
|
|
473
473
|
if (msg.type !== 'app:api' || !msg.data) return;
|
|
474
474
|
|
|
475
475
|
const { id, method, path: reqPath, headers: reqHeaders, body } = msg.data;
|
|
476
|
-
const backendPath = (reqPath || '').replace(/^\/app
|
|
476
|
+
const backendPath = (reqPath || '').replace(/^\/app/, '');
|
|
477
477
|
|
|
478
478
|
console.log(`[supervisor] App WS → backend :${backendPort} | ${method} ${backendPath} (${id})`);
|
|
479
479
|
|
|
@@ -860,6 +860,7 @@ export async function startSupervisor() {
|
|
|
860
860
|
if (eventData.usedFileTools || pendingBackendRestart) {
|
|
861
861
|
console.log('[supervisor] Agent turn ended — restarting backend');
|
|
862
862
|
pendingBackendRestart = false;
|
|
863
|
+
if (backendRestartTimer) { clearTimeout(backendRestartTimer); backendRestartTimer = null; }
|
|
863
864
|
resetBackendRestarts();
|
|
864
865
|
stopBackend().then(() => spawnBackend(backendPort));
|
|
865
866
|
}
|
|
@@ -1058,8 +1059,11 @@ export async function startSupervisor() {
|
|
|
1058
1059
|
pendingBackendRestart = true;
|
|
1059
1060
|
return;
|
|
1060
1061
|
}
|
|
1062
|
+
// Skip if a stop/restart is already in progress (bot:done handler owns the restart)
|
|
1063
|
+
if (isBackendStopping()) return;
|
|
1061
1064
|
if (backendRestartTimer) clearTimeout(backendRestartTimer);
|
|
1062
1065
|
backendRestartTimer = setTimeout(async () => {
|
|
1066
|
+
if (isBackendStopping()) return; // re-check after delay
|
|
1063
1067
|
log.info(`[watcher] ${reason} — restarting backend...`);
|
|
1064
1068
|
resetBackendRestarts();
|
|
1065
1069
|
await stopBackend();
|
package/vite.config.ts
CHANGED
|
@@ -260,16 +260,16 @@ Your working directory is the `workspace/` folder. This is your full-stack works
|
|
|
260
260
|
|
|
261
261
|
## Backend Routing (Critical)
|
|
262
262
|
|
|
263
|
-
A supervisor process sits in front of everything on port 3000. It strips the `/app
|
|
263
|
+
A supervisor process sits in front of everything on port 3000. It strips the `/app` prefix before forwarding to the backend, preserving the `/api/` path.
|
|
264
264
|
|
|
265
265
|
```
|
|
266
|
-
Browser: GET /app/api/tasks → Supervisor strips
|
|
266
|
+
Browser: GET /app/api/tasks → Supervisor strips /app → Backend receives: GET /api/tasks
|
|
267
267
|
```
|
|
268
268
|
|
|
269
269
|
**The rules:**
|
|
270
270
|
- **Frontend** fetch calls: use `/app/api/...`
|
|
271
|
-
- **Backend** Express routes: register as `/tasks`, `/health` —
|
|
272
|
-
-
|
|
271
|
+
- **Backend** Express routes: register as `/api/tasks`, `/api/health` — standard Express convention with `/api/` prefix
|
|
272
|
+
- The `/app` prefix is what distinguishes workspace backend routes from system routes
|
|
273
273
|
|
|
274
274
|
**Tunnel reliability:** All `/app/api/*` fetch calls from the dashboard are automatically proxied through WebSocket when available. This is transparent — just use `fetch('/app/api/...')` normally. The WebSocket proxy activates automatically and falls back to regular HTTP if unavailable. You don't need to handle this in your code.
|
|
275
275
|
|
|
@@ -367,6 +367,7 @@ There are TWO completely separate password systems in Fluxy. Understanding the d
|
|
|
367
367
|
POST /app/api/workspace/set-password
|
|
368
368
|
Body: { "password": "the_password" }
|
|
369
369
|
```
|
|
370
|
+
(The backend route is `/api/workspace/set-password` — the supervisor strips `/app` from the frontend URL.)
|
|
370
371
|
|
|
371
372
|
**How to remove it:**
|
|
372
373
|
```
|