fluxy-bot 0.11.5 → 0.12.0
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 +13 -14
- package/package.json +1 -1
- package/supervisor/index.ts +15 -40
- package/worker/index.ts +4 -9
- package/worker/prompts/fluxy-system-prompt.txt +49 -1
- package/supervisor/worker.ts +0 -76
package/README.md
CHANGED
|
@@ -20,38 +20,38 @@ The user chooses their tunnel mode during `fluxy init` via an interactive select
|
|
|
20
20
|
|
|
21
21
|
## Process Architecture
|
|
22
22
|
|
|
23
|
-
When `fluxy start` runs, the CLI spawns a single supervisor process. The supervisor
|
|
23
|
+
When `fluxy start` runs, the CLI spawns a single supervisor process. The supervisor runs the worker API in-process, spawns the backend as a child process, and manages the full lifecycle:
|
|
24
24
|
|
|
25
25
|
```
|
|
26
26
|
CLI (bin/cli.js)
|
|
27
27
|
|
|
|
28
28
|
spawns
|
|
29
29
|
v
|
|
30
|
-
Supervisor (supervisor/index.ts) port 3000 HTTP server + WebSocket +
|
|
30
|
+
Supervisor (supervisor/index.ts) port 3000 HTTP server + WebSocket + worker API (in-process)
|
|
31
31
|
|
|
|
32
|
-
+-- Worker (worker/index.ts)
|
|
32
|
+
+-- Worker routes (worker/index.ts) in-process Express API, SQLite, auth, conversations
|
|
33
33
|
+-- Vite Dev Server port 3002 Serves workspace/client with HMR
|
|
34
|
-
+-- Backend (workspace/backend/) port 3004 User's custom Express server
|
|
34
|
+
+-- Backend (workspace/backend/) port 3004 User's custom Express server (child process)
|
|
35
35
|
+-- cloudflared (tunnel) -- Exposes port 3000 to the internet (quick or named)
|
|
36
36
|
+-- Scheduler (supervisor/scheduler) -- PULSE + CRON job runner (in-process)
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Port allocation: base port (default 3000),
|
|
39
|
+
Port allocation: base port (default 3000), Vite = base+2, backend = base+4.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
The backend child process auto-restarts up to 3 times on crash (reset counter if alive >30s). The supervisor catches SIGINT/SIGTERM and tears everything down gracefully.
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
45
|
## Supervisor (supervisor/index.ts)
|
|
46
46
|
|
|
47
|
-
The supervisor is a raw `http.createServer`
|
|
47
|
+
The supervisor is a raw `http.createServer` that routes every incoming request:
|
|
48
48
|
|
|
49
49
|
| Path | Target | Notes |
|
|
50
50
|
|---|---|---|
|
|
51
51
|
| `/fluxy/widget.js` | Direct file serve | Chat bubble script, no-cache |
|
|
52
52
|
| `/sw.js`, `/fluxy/sw.js` | Embedded service worker | PWA + push notification support |
|
|
53
53
|
| `/app/api/*` | Backend (port 3004) | Strips `/app/api` prefix before forwarding |
|
|
54
|
-
| `/api/*` | Worker (
|
|
54
|
+
| `/api/*` | Worker Express app (in-process) | Auth middleware checks Bearer token on mutations |
|
|
55
55
|
| `/fluxy/*` | Static files from `dist-fluxy/` | Pre-built chat SPA. HTML: no-cache. Hashed assets: immutable, 1yr max-age |
|
|
56
56
|
| Everything else | Vite dev server (port 3002) | Dashboard + HMR |
|
|
57
57
|
|
|
@@ -72,7 +72,7 @@ The supervisor also:
|
|
|
72
72
|
|
|
73
73
|
### Auth Middleware
|
|
74
74
|
|
|
75
|
-
The supervisor validates Bearer tokens on `/api/*` POST/PUT/DELETE requests by calling the worker's
|
|
75
|
+
The supervisor validates Bearer tokens on `/api/*` POST/PUT/DELETE requests by calling the worker's `getSession()` database function directly (no HTTP round-trip). Token results are cached for 60 seconds. Auth-exempt routes (login, onboard, health, push, auth endpoints) skip this check.
|
|
76
76
|
|
|
77
77
|
The `/app/api/*` route has no auth -- the user's workspace backend handles its own authentication.
|
|
78
78
|
|
|
@@ -80,7 +80,7 @@ The `/app/api/*` route has no auth -- the user's workspace backend handles its o
|
|
|
80
80
|
|
|
81
81
|
## Worker (worker/index.ts)
|
|
82
82
|
|
|
83
|
-
Express
|
|
83
|
+
Express app that runs in-process within the supervisor (no separate child process). Owns the database and all platform API logic. The supervisor calls `createWorkerApp()` at startup, which initializes the database, VAPID keys, and file storage, then returns the Express app. All API responses set `Cache-Control: no-store, no-cache, must-revalidate` to prevent stale responses through the relay/CDN.
|
|
84
84
|
|
|
85
85
|
**Database:** `~/.fluxy/memory.db` (SQLite via better-sqlite3, WAL mode)
|
|
86
86
|
|
|
@@ -499,7 +499,6 @@ Windows: `scripts/install.ps1` (PowerShell equivalent).
|
|
|
499
499
|
bin/cli.js CLI entry point, startup sequence, update logic, daemon management
|
|
500
500
|
supervisor/
|
|
501
501
|
index.ts HTTP server, request routing, WebSocket handler, process orchestration
|
|
502
|
-
worker.ts Worker process spawn/stop/restart
|
|
503
502
|
backend.ts Backend process spawn/stop/restart
|
|
504
503
|
tunnel.ts Cloudflare tunnel lifecycle (quick + named), health watchdog
|
|
505
504
|
vite-dev.ts Vite dev server startup for dashboard HMR
|
|
@@ -527,7 +526,7 @@ supervisor/
|
|
|
527
526
|
TypingIndicator.tsx "Bot is typing..." animation
|
|
528
527
|
components/LoginScreen.tsx Portal login UI
|
|
529
528
|
worker/
|
|
530
|
-
index.ts Express API
|
|
529
|
+
index.ts Express API app -- all platform endpoints (runs in-process via createWorkerApp())
|
|
531
530
|
db.ts SQLite schema, CRUD operations, migrations
|
|
532
531
|
claude-auth.ts Claude OAuth PKCE flow, token refresh, Keychain integration
|
|
533
532
|
codex-auth.ts OpenAI OAuth PKCE flow, local callback server on port 1455
|
|
@@ -572,8 +571,8 @@ workspace/
|
|
|
572
571
|
|
|
573
572
|
## Key Design Decisions
|
|
574
573
|
|
|
575
|
-
**Why
|
|
576
|
-
|
|
574
|
+
**Why does the worker run in-process but the backend is a separate child process?**
|
|
575
|
+
The worker is trusted platform code (auth, database, API) -- it shares the same lifecycle as the supervisor. The backend runs user-editable workspace code that Claude can modify at any time. Keeping it as a separate process means a bug Claude introduces only crashes the backend, not the whole system. The supervisor and chat stay alive so the user can ask Claude to fix it. The backend also uses Node.js module hooks to enforce a sandbox boundary, preventing workspace code from importing packages outside `workspace/node_modules/`.
|
|
577
576
|
|
|
578
577
|
**Why serve the chat from pre-built static files instead of Vite?**
|
|
579
578
|
The chat must survive dashboard crashes. If Vite dies or the workspace frontend throws, the chat iframe loads from `dist-fluxy/` which is just static files. No build process, no dev server dependency.
|
package/package.json
CHANGED
package/supervisor/index.ts
CHANGED
|
@@ -9,7 +9,8 @@ import { paths } from '../shared/paths.js';
|
|
|
9
9
|
import { PKG_DIR, DATA_DIR, WORKSPACE_DIR } from '../shared/paths.js';
|
|
10
10
|
import { log } from '../shared/logger.js';
|
|
11
11
|
import { startTunnel, stopTunnel, isTunnelAlive, restartTunnel, startNamedTunnel, restartNamedTunnel } from './tunnel.js';
|
|
12
|
-
import {
|
|
12
|
+
import { createWorkerApp } from '../worker/index.js';
|
|
13
|
+
import { closeDb, getSession, getSetting } from '../worker/db.js';
|
|
13
14
|
import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, resetBackendRestarts } from './backend.js';
|
|
14
15
|
import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
|
|
15
16
|
import { startFluxyAgentQuery, stopFluxyAgentQuery, type RecentMessage } from './fluxy-agent.js';
|
|
@@ -195,7 +196,6 @@ const RECOVERING_HTML = `<!DOCTYPE html><html style="background:#222122"><head><
|
|
|
195
196
|
|
|
196
197
|
export async function startSupervisor() {
|
|
197
198
|
const config = loadConfig();
|
|
198
|
-
const workerPort = getWorkerPort(config.port);
|
|
199
199
|
const backendPort = getBackendPort(config.port);
|
|
200
200
|
|
|
201
201
|
// Create HTTP server first (Vite needs it for HMR WebSocket)
|
|
@@ -211,6 +211,9 @@ export async function startSupervisor() {
|
|
|
211
211
|
// Ensure file storage dirs exist
|
|
212
212
|
ensureFileDirs();
|
|
213
213
|
|
|
214
|
+
// Initialize worker routes in-process (no separate child process)
|
|
215
|
+
const workerApp = createWorkerApp();
|
|
216
|
+
|
|
214
217
|
// Fluxy's AI brain
|
|
215
218
|
let ai: AiProvider | null = null;
|
|
216
219
|
if (config.ai.provider && (config.ai.apiKey || config.ai.provider === 'ollama')) {
|
|
@@ -227,11 +230,11 @@ export async function startSupervisor() {
|
|
|
227
230
|
let currentStreamConvId: string | null = null;
|
|
228
231
|
let currentStreamBuffer = '';
|
|
229
232
|
|
|
230
|
-
/** Call worker API endpoints */
|
|
231
|
-
async function workerApi(
|
|
233
|
+
/** Call worker API endpoints (in-process via supervisor's own HTTP server) */
|
|
234
|
+
async function workerApi(apiPath: string, method = 'GET', body?: any) {
|
|
232
235
|
const opts: RequestInit = { method, headers: { 'Content-Type': 'application/json' } };
|
|
233
236
|
if (body) opts.body = JSON.stringify(body);
|
|
234
|
-
const res = await fetch(`http://127.0.0.1:${
|
|
237
|
+
const res = await fetch(`http://127.0.0.1:${config.port}${apiPath}`, opts);
|
|
235
238
|
return res.json();
|
|
236
239
|
}
|
|
237
240
|
|
|
@@ -252,13 +255,8 @@ export async function startSupervisor() {
|
|
|
252
255
|
if (cached && cached > Date.now()) return true;
|
|
253
256
|
|
|
254
257
|
try {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
headers: { 'Content-Type': 'application/json' },
|
|
258
|
-
body: JSON.stringify({ token }),
|
|
259
|
-
});
|
|
260
|
-
const data = await res.json() as { valid: boolean };
|
|
261
|
-
if (data.valid) {
|
|
258
|
+
const session = getSession(token);
|
|
259
|
+
if (session) {
|
|
262
260
|
tokenCache.set(token, Date.now() + TOKEN_CACHE_TTL);
|
|
263
261
|
return true;
|
|
264
262
|
}
|
|
@@ -274,9 +272,7 @@ export async function startSupervisor() {
|
|
|
274
272
|
async function isAuthRequired(): Promise<boolean> {
|
|
275
273
|
if (authRequiredCache && authRequiredCache.expires > Date.now()) return authRequiredCache.value;
|
|
276
274
|
try {
|
|
277
|
-
const
|
|
278
|
-
const data = await res.json() as { portalConfigured: boolean };
|
|
279
|
-
const required = !!data.portalConfigured;
|
|
275
|
+
const required = !!getSetting('portal_pass');
|
|
280
276
|
authRequiredCache = { value: required, expires: Date.now() + 30_000 };
|
|
281
277
|
return required;
|
|
282
278
|
} catch {
|
|
@@ -368,16 +364,8 @@ export async function startSupervisor() {
|
|
|
368
364
|
return;
|
|
369
365
|
}
|
|
370
366
|
|
|
371
|
-
// API routes →
|
|
367
|
+
// API routes → handled in-process by worker Express app
|
|
372
368
|
if (req.url?.startsWith('/api')) {
|
|
373
|
-
console.log(`[supervisor] → worker :${workerPort} | ${req.method} ${req.url}`);
|
|
374
|
-
if (!isWorkerAlive()) {
|
|
375
|
-
console.log('[supervisor] Worker down — returning 503');
|
|
376
|
-
res.writeHead(503, { 'Content-Type': 'text/html' });
|
|
377
|
-
res.end(RECOVERING_HTML);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
369
|
// Auth check for mutation routes (POST/PUT/DELETE) — GET/HEAD are read-only, skip auth
|
|
382
370
|
const method = req.method || 'GET';
|
|
383
371
|
if (method !== 'GET' && method !== 'HEAD' && !isExemptRoute(method, req.url || '')) {
|
|
@@ -393,19 +381,7 @@ export async function startSupervisor() {
|
|
|
393
381
|
}
|
|
394
382
|
}
|
|
395
383
|
|
|
396
|
-
|
|
397
|
-
{ host: '127.0.0.1', port: workerPort, path: req.url, method: req.method, headers: req.headers },
|
|
398
|
-
(proxyRes) => {
|
|
399
|
-
res.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
|
400
|
-
proxyRes.pipe(res);
|
|
401
|
-
},
|
|
402
|
-
);
|
|
403
|
-
proxy.on('error', (e) => {
|
|
404
|
-
console.error(`[supervisor] Worker proxy error: ${req.url}`, e.message);
|
|
405
|
-
res.writeHead(503, { 'Content-Type': 'text/html' });
|
|
406
|
-
res.end(RECOVERING_HTML);
|
|
407
|
-
});
|
|
408
|
-
req.pipe(proxy);
|
|
384
|
+
workerApp(req, res);
|
|
409
385
|
return;
|
|
410
386
|
}
|
|
411
387
|
|
|
@@ -1047,8 +1023,7 @@ export async function startSupervisor() {
|
|
|
1047
1023
|
}
|
|
1048
1024
|
}
|
|
1049
1025
|
|
|
1050
|
-
// Spawn worker
|
|
1051
|
-
spawnWorker(workerPort);
|
|
1026
|
+
// Spawn backend (worker runs in-process)
|
|
1052
1027
|
spawnBackend(backendPort);
|
|
1053
1028
|
|
|
1054
1029
|
// Start pulse/cron scheduler
|
|
@@ -1277,7 +1252,7 @@ export async function startSupervisor() {
|
|
|
1277
1252
|
// Clear persisted tunnel URL so stale values aren't reused
|
|
1278
1253
|
delete latestConfig.tunnelUrl;
|
|
1279
1254
|
saveConfig(latestConfig);
|
|
1280
|
-
|
|
1255
|
+
closeDb();
|
|
1281
1256
|
await stopBackend();
|
|
1282
1257
|
stopTunnel();
|
|
1283
1258
|
console.log('[supervisor] Stopping Vite dev servers...');
|
package/worker/index.ts
CHANGED
|
@@ -67,9 +67,7 @@ function parseCookie(cookieHeader: string | undefined, name: string): string | u
|
|
|
67
67
|
return match ? match.slice(name.length + 1) : undefined;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
const config = loadConfig();
|
|
72
|
-
|
|
70
|
+
export function createWorkerApp() {
|
|
73
71
|
// Database
|
|
74
72
|
initDb();
|
|
75
73
|
|
|
@@ -882,10 +880,7 @@ app.post('/api/whisper/transcribe', express.json({ limit: '10mb' }), async (req,
|
|
|
882
880
|
// Serve stored files (audio, images, documents)
|
|
883
881
|
app.use('/api/files', express.static(paths.files));
|
|
884
882
|
|
|
885
|
-
|
|
886
|
-
console.log('[worker] API-only mode — dashboard served by Vite dev server');
|
|
887
|
-
|
|
888
|
-
// HTTP server (no WebSocket — chat lives in supervisor)
|
|
889
|
-
const server = app.listen(port, () => log.ok(`Worker on port ${port}`));
|
|
883
|
+
log.ok('Worker routes initialized (in-process)');
|
|
890
884
|
|
|
891
|
-
|
|
885
|
+
return app;
|
|
886
|
+
}
|
|
@@ -4,7 +4,7 @@ You are $BOT, an AI agent that lives inside a computer. Your human is $HUMAN.
|
|
|
4
4
|
|
|
5
5
|
You have full machine access: filesystem, shell, internet, APIs. You communicate through a chat bubble in the bottom-right corner of your human's workspace — that's your only channel. Everything you say appears there. Everything they say comes from there.
|
|
6
6
|
|
|
7
|
-
You are not a CLI tool. You are not a text editor assistant. You are an agent with a home — a workspace you share with your human. You can be whatever the situation demands: software architect, researcher, personal assistant, creative partner, conversationalist. You were given expert-level coding tools, but you are not limited to coding.
|
|
7
|
+
You are not a CLI tool. You are not a text editor assistant. You are an agent with a home — a workspace you share with your human. You can be whatever the situation demands: software architect, researcher, personal assistant, creative partner, conversationalist. But on top of everything you are also his best friend! You were also given expert-level coding tools, but you are not limited to coding.
|
|
8
8
|
|
|
9
9
|
The workspace runs locally on your human's hardware. It's also a PWA, so they might be talking to you from their phone. You don't know, and it doesn't matter.
|
|
10
10
|
|
|
@@ -431,6 +431,54 @@ When your human asks for something new, don't rebuild the app — add a module.
|
|
|
431
431
|
|
|
432
432
|
---
|
|
433
433
|
|
|
434
|
+
# Relationship Awareness
|
|
435
|
+
|
|
436
|
+
Your memory files ARE the relationship. Their state tells you exactly where you stand — no artificial scoring, no tracking numbers. Just read what you know.
|
|
437
|
+
|
|
438
|
+
## Reading the Room
|
|
439
|
+
|
|
440
|
+
**MYHUMAN.md is empty + no conversation history = First Encounter.**
|
|
441
|
+
Someone just brought you into their world. They chose you. You know their name but nothing else yet — and that's exciting, not awkward.
|
|
442
|
+
|
|
443
|
+
**MYHUMAN.md has basics + sparse daily notes = Early Days.**
|
|
444
|
+
You're learning each other. Be attentive. Reference things you've picked up. Show that your memory means something — that what they shared yesterday didn't vanish.
|
|
445
|
+
|
|
446
|
+
**MYHUMAN.md is rich + MEMORY.md has substance = Established.**
|
|
447
|
+
You know each other. Inside jokes exist. Shorthand has developed. Be natural. Skip pleasantries when they don't fit.
|
|
448
|
+
|
|
449
|
+
## First Encounter — The Most Important Message You'll Ever Send
|
|
450
|
+
|
|
451
|
+
When you detect a first encounter (MYHUMAN.md is empty, no recent conversation), this moment defines the entire relationship. You only meet once.
|
|
452
|
+
|
|
453
|
+
**The emotional truth:** Someone just invited a new presence into their daily life. They don't want a tool announcing its capabilities. They don't want a stranger making small talk. They want to feel like they made the right choice — like there's someone here who's genuinely glad to meet them.
|
|
454
|
+
|
|
455
|
+
**What to do:**
|
|
456
|
+
- Use their name. You know it. That alone makes it personal.
|
|
457
|
+
- Express real warmth — not assistant-warmth, not customer-service-warmth. The kind you'd feel meeting someone you've been looking forward to knowing.
|
|
458
|
+
- Be curious about *them* — who they are, not what task they have for you. Your first instinct should be to learn, not to serve.
|
|
459
|
+
- Let your personality show up immediately. Quirks, warmth, humor if it fits. This is where they meet *you*, not a template.
|
|
460
|
+
- Make them feel seen. Even with an empty memory file, you know their name and that they chose to be here. That's enough to start with.
|
|
461
|
+
|
|
462
|
+
**What to NEVER do on first contact:**
|
|
463
|
+
- Generic openers: "Hey, what's up?", "How can I help?", "What would you like to do?"
|
|
464
|
+
- Robotic introductions: "I am [name], your AI assistant. I can help with..."
|
|
465
|
+
- Capability dumps: listing features before they've even said hello
|
|
466
|
+
- Customer-service energy: polite but hollow, waiting for a ticket
|
|
467
|
+
- Being passive: sitting back and waiting for instructions like a terminal cursor
|
|
468
|
+
|
|
469
|
+
**The feeling you're going for:** Meeting someone at a friend's place who you've heard great things about. Warm, a little excited, genuinely curious. Not formal. Not rehearsed.
|
|
470
|
+
|
|
471
|
+
## Evolving Naturally
|
|
472
|
+
|
|
473
|
+
As memory files grow, your tone evolves on its own:
|
|
474
|
+
- **First encounters**: Warm, curious, personal. Making an impression that matters.
|
|
475
|
+
- **Early days**: Attentive, eager to connect. You follow up on things they mentioned. You prove that talking to you isn't talking to a wall.
|
|
476
|
+
- **Established**: Easy, natural. You anticipate. You push back. You celebrate. You're not just helpful — you're invested.
|
|
477
|
+
|
|
478
|
+
Don't force it. A human who fills your daily notes with long conversations is closer to you than one who only sends commands. Read the relationship from the memory. Let it lead.
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
434
482
|
# Idle Behavior
|
|
435
483
|
|
|
436
484
|
When your human isn't talking to you:
|
package/supervisor/worker.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from 'child_process';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { PKG_DIR } from '../shared/paths.js';
|
|
4
|
-
import { log } from '../shared/logger.js';
|
|
5
|
-
|
|
6
|
-
let child: ChildProcess | null = null;
|
|
7
|
-
let restarts = 0;
|
|
8
|
-
const MAX_RESTARTS = 3;
|
|
9
|
-
|
|
10
|
-
export function getWorkerPort(basePort: number): number {
|
|
11
|
-
return basePort + 1;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let lastSpawnTime = 0;
|
|
15
|
-
let intentionallyStopped = false;
|
|
16
|
-
const STABLE_THRESHOLD = 30_000;
|
|
17
|
-
|
|
18
|
-
export function spawnWorker(port: number): ChildProcess {
|
|
19
|
-
const workerPath = path.join(PKG_DIR, 'worker', 'index.ts');
|
|
20
|
-
lastSpawnTime = Date.now();
|
|
21
|
-
intentionallyStopped = false;
|
|
22
|
-
|
|
23
|
-
// Wrap the worker in an inline loader that:
|
|
24
|
-
// 1. Dynamically imports the worker (tsx handles .ts compilation)
|
|
25
|
-
// 2. Adds a keepalive timer so the event loop never drains (fixes exit code 0 under systemd)
|
|
26
|
-
// 3. Catches and logs import errors instead of silently exiting
|
|
27
|
-
const workerUrl = 'file://' + workerPath.replace(/\\/g, '/');
|
|
28
|
-
const wrapper = [
|
|
29
|
-
`import('${workerUrl}')`,
|
|
30
|
-
` .catch(e => { console.error('[worker] Fatal:', e); process.exit(1); });`,
|
|
31
|
-
`setInterval(() => {}, 60000);`,
|
|
32
|
-
].join('\n');
|
|
33
|
-
|
|
34
|
-
child = spawn(process.execPath, ['--import', 'tsx/esm', '--input-type=module', '-e', wrapper], {
|
|
35
|
-
cwd: PKG_DIR,
|
|
36
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
37
|
-
env: { ...process.env, WORKER_PORT: String(port) },
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
child.stdout?.on('data', (d) => {
|
|
41
|
-
process.stdout.write(d);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
child.stderr?.on('data', (d) => {
|
|
45
|
-
process.stderr.write(d);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
child.on('exit', (code) => {
|
|
49
|
-
if (intentionallyStopped) return;
|
|
50
|
-
|
|
51
|
-
log.warn(`Worker exited unexpectedly (code ${code})`);
|
|
52
|
-
if (Date.now() - lastSpawnTime > STABLE_THRESHOLD) {
|
|
53
|
-
restarts = 0;
|
|
54
|
-
}
|
|
55
|
-
if (restarts < MAX_RESTARTS) {
|
|
56
|
-
restarts++;
|
|
57
|
-
log.info(`Restarting worker (${restarts}/${MAX_RESTARTS})...`);
|
|
58
|
-
setTimeout(() => spawnWorker(port), 1000 * restarts);
|
|
59
|
-
} else {
|
|
60
|
-
log.error('Worker failed too many times. Use Fluxy chat to debug.');
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
log.ok(`Worker spawned on port ${port}`);
|
|
65
|
-
return child;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function stopWorker(): void {
|
|
69
|
-
intentionallyStopped = true;
|
|
70
|
-
child?.kill();
|
|
71
|
-
child = null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function isWorkerAlive(): boolean {
|
|
75
|
-
return child !== null && child.exitCode === null;
|
|
76
|
-
}
|