mslxdff 0.1.11 → 0.1.13
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/mslxdff.js +49 -82
- package/package.json +1 -1
- package/src/daemon.js +5 -1
- package/src/events.js +39 -0
- package/src/routes.js +9 -4
- package/src/server.js +8 -3
package/bin/mslxdff.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { readFileSync, existsSync, statSync
|
|
3
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { dirname, join, basename } from "node:path";
|
|
6
6
|
import { startServer, resolvePort } from "../src/server.js";
|
|
7
|
+
import { DEFAULT_PORT } from "../src/state.js";
|
|
7
8
|
import { createRouter } from "../src/routes.js";
|
|
8
9
|
import { createUpstreamClient } from "../src/upstream.js";
|
|
9
10
|
import { createModelsService } from "../src/models.js";
|
|
@@ -11,8 +12,9 @@ import { loadToken, refreshToken, setPort, getPort, loadGroupsJoined, saveGroups
|
|
|
11
12
|
import { startDaemon, stopDaemon, writePid, pidFile, logFile, readPid, readPidVersion, isPidAlive } from "../src/daemon.js";
|
|
12
13
|
import { createAutoSelector } from "../src/auto.js";
|
|
13
14
|
import { createPeersService } from "../src/peers.js";
|
|
15
|
+
import { createEventBus } from "../src/events.js";
|
|
14
16
|
import { createGroupsService, createBansService, refreshGroupMembers, syncPeersFromMembers } from "../src/groups.js";
|
|
15
|
-
import { logDir, recentCalls, lastError, appendCall, appendError, appendEvent,
|
|
17
|
+
import { logDir, recentCalls, lastError, appendCall, appendError, appendEvent, recentEvents } from "../src/logs.js";
|
|
16
18
|
|
|
17
19
|
const logs = { appendCall, appendError, appendEvent };
|
|
18
20
|
|
|
@@ -130,11 +132,22 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
130
132
|
process.exit(0);
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
// -debug:
|
|
134
|
-
//
|
|
135
|
+
// -debug: stop the background daemon and run the server in THIS terminal
|
|
136
|
+
// (foreground), printing every event to stdout in real time via the in-memory
|
|
137
|
+
// event bus — no filesystem polling. Ctrl+C / SIGTERM restarts the daemon in
|
|
138
|
+
// the background, then exits. See the daemon body below for the stream wiring.
|
|
135
139
|
if (args.includes("-debug") || args.includes("--debug")) {
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
const recent = recentEvents(100);
|
|
141
|
+
if (recent.length) {
|
|
142
|
+
console.log(`--- last ${recent.length} event(s) ---`);
|
|
143
|
+
for (const e of recent) console.log(fmtEvent(e));
|
|
144
|
+
}
|
|
145
|
+
console.log("--- live (Ctrl+C: stop debugging and restore background daemon) ---");
|
|
146
|
+
const { stopped, pid } = stopDaemon();
|
|
147
|
+
if (stopped) console.log(`[debug] stopped background daemon (pid ${pid})`);
|
|
148
|
+
process.env.MSLXDFF_DEBUG = "1";
|
|
149
|
+
process.env.MSLXDFF_DAEMON = "1";
|
|
150
|
+
// fall through to the daemon body below — no process.exit() here
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
// -creategroup <name> | -group create <name> | -group sync | -group leave <name> | -group list |
|
|
@@ -405,8 +418,34 @@ const peers = createPeersService({ cooldownMs: peerCooldownMs(), heatMs: peerHea
|
|
|
405
418
|
const groups = createGroupsService({});
|
|
406
419
|
const bans = createBansService({ windowMs: banWindowMs(), threshold: banThreshold() });
|
|
407
420
|
|
|
408
|
-
const
|
|
409
|
-
const
|
|
421
|
+
const isDebug = process.env.MSLXDFF_DEBUG === "1";
|
|
422
|
+
const bus = createEventBus();
|
|
423
|
+
const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans, bus });
|
|
424
|
+
const srv = startServer({ router, signals: !isDebug });
|
|
425
|
+
|
|
426
|
+
// -debug: push every event straight to this terminal.
|
|
427
|
+
if (isDebug) {
|
|
428
|
+
bus.subscribe((e) => {
|
|
429
|
+
try {
|
|
430
|
+
console.log(fmtEvent(e));
|
|
431
|
+
} catch {
|
|
432
|
+
// malformed event — skip
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
// Ctrl+C / SIGTERM: restore the background daemon, then exit.
|
|
436
|
+
const restore = () => {
|
|
437
|
+
console.log("\n[debug] restoring background daemon...");
|
|
438
|
+
try {
|
|
439
|
+
const restoredPid = startDaemon([]);
|
|
440
|
+
console.log(`[debug] daemon restored (pid ${restoredPid})`);
|
|
441
|
+
} catch (err) {
|
|
442
|
+
console.error(`[debug] could not restore daemon: ${err.message}`);
|
|
443
|
+
}
|
|
444
|
+
setTimeout(() => process.exit(0), 300);
|
|
445
|
+
};
|
|
446
|
+
process.on("SIGINT", restore);
|
|
447
|
+
process.on("SIGTERM", restore);
|
|
448
|
+
}
|
|
410
449
|
|
|
411
450
|
await srv.ready();
|
|
412
451
|
models.startAutoRefresh();
|
|
@@ -706,80 +745,8 @@ function fmtEvent(e) {
|
|
|
706
745
|
}
|
|
707
746
|
}
|
|
708
747
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
async function liveDebug() {
|
|
712
|
-
const file = eventsFile();
|
|
713
|
-
// ensure the event file exists so watch() doesn't fail on a fresh daemon dir
|
|
714
|
-
const dir = dirname(file);
|
|
715
|
-
if (!existsSync(file)) {
|
|
716
|
-
try {
|
|
717
|
-
mkdirSync(dir, { recursive: true });
|
|
718
|
-
closeSync(openSync(file, "a"));
|
|
719
|
-
} catch {
|
|
720
|
-
console.error(`cannot create event file: ${file}`);
|
|
721
|
-
process.exit(1);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
const recent = recentEvents(100);
|
|
725
|
-
if (recent.length) {
|
|
726
|
-
console.log(`--- last ${recent.length} event(s) ---`);
|
|
727
|
-
for (const e of recent) console.log(fmtEvent(e));
|
|
728
|
-
}
|
|
729
|
-
console.log("--- live (Ctrl+C to exit) ---");
|
|
730
|
-
console.log(`watching: ${file}`);
|
|
731
|
-
if (!existsSync(file)) console.log("(no events yet — trigger a chat request to see the flow)");
|
|
732
|
-
|
|
733
|
-
let pos = existsSync(file) ? statSync(file).size : 0;
|
|
734
|
-
let buf = "";
|
|
735
|
-
const pump = () => {
|
|
736
|
-
try {
|
|
737
|
-
if (!existsSync(file)) {
|
|
738
|
-
pos = 0;
|
|
739
|
-
buf = "";
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
const size = statSync(file).size;
|
|
743
|
-
if (size < pos) {
|
|
744
|
-
pos = 0; // file trimmed/rewritten: re-follow from the start of what remains
|
|
745
|
-
buf = "";
|
|
746
|
-
}
|
|
747
|
-
if (size === pos) return;
|
|
748
|
-
buf += readFileSync(file, "utf8").slice(pos);
|
|
749
|
-
pos = size;
|
|
750
|
-
const lines = buf.split("\n");
|
|
751
|
-
buf = lines.pop() || "";
|
|
752
|
-
for (const l of lines) {
|
|
753
|
-
if (!l.trim()) continue;
|
|
754
|
-
try {
|
|
755
|
-
console.log(fmtEvent(JSON.parse(l)));
|
|
756
|
-
} catch {
|
|
757
|
-
// partial/corrupt line while trimming — skip
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
} catch (err) {
|
|
761
|
-
console.error(`[debug] poll error: ${err.message}`);
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
// fs.watch is unreliable across platforms for freshly-created/rotated files,
|
|
765
|
-
// so polling is the source of truth (200ms); the directory watch just adds
|
|
766
|
-
// low-latency wake-ups and silently degrades when it misbehaves.
|
|
767
|
-
try {
|
|
768
|
-
mkdirSync(dir, { recursive: true });
|
|
769
|
-
watch(dir, (_evt, filename) => {
|
|
770
|
-
if (!filename || basename(String(filename)) !== basename(file)) return;
|
|
771
|
-
pump();
|
|
772
|
-
});
|
|
773
|
-
} catch {
|
|
774
|
-
console.error(`cannot watch event dir: ${dir} (falling back to polling only)`);
|
|
775
|
-
}
|
|
776
|
-
// Poll is the source of truth; 200ms keeps human-perceived latency negligible.
|
|
777
|
-
setInterval(pump, 200);
|
|
778
|
-
pump();
|
|
779
|
-
await new Promise(() => {}); // run until Ctrl+C
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function fmtTs(iso) { if (!iso) return "-";
|
|
748
|
+
function fmtTs(iso) {
|
|
749
|
+
if (!iso) return "-";
|
|
783
750
|
try {
|
|
784
751
|
return new Date(iso).toISOString().replace("T", " ").slice(5, 19);
|
|
785
752
|
} catch {
|
package/package.json
CHANGED
package/src/daemon.js
CHANGED
|
@@ -24,10 +24,14 @@ export function startDaemon(args = []) {
|
|
|
24
24
|
const dir = daemonDir();
|
|
25
25
|
mkdirSync(dir, { recursive: true });
|
|
26
26
|
const logFd = openSync(logFile(), "a", 0o600);
|
|
27
|
+
const env = { ...process.env, MSLXDFF_DAEMON: "1" };
|
|
28
|
+
// a -debug foreground session wouldn't pass MSLXDFF_DEBUG to the
|
|
29
|
+
// background daemon it restores (that flag means "print events to stdout")
|
|
30
|
+
delete env.MSLXDFF_DEBUG;
|
|
27
31
|
const child = spawn(process.execPath, [entry, ...args, "--daemon"], {
|
|
28
32
|
detached: true,
|
|
29
33
|
stdio: ["ignore", logFd, logFd],
|
|
30
|
-
env
|
|
34
|
+
env,
|
|
31
35
|
});
|
|
32
36
|
child.unref();
|
|
33
37
|
return child.pid;
|
package/src/events.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// In-memory event bus for live debugging: routes.js emits, -debug streams
|
|
2
|
+
// over HTTP. Keeps a small ring buffer so late subscribers can catch up.
|
|
3
|
+
|
|
4
|
+
const MAX_BUFFERED = 500;
|
|
5
|
+
|
|
6
|
+
export function createEventBus() {
|
|
7
|
+
let buffer = [];
|
|
8
|
+
const listeners = new Set();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
// emit an event to every current subscriber (sync, never throws)
|
|
12
|
+
emit(event) {
|
|
13
|
+
buffer.push(event);
|
|
14
|
+
if (buffer.length > MAX_BUFFERED) buffer.splice(0, buffer.length - MAX_BUFFERED);
|
|
15
|
+
for (const fn of listeners) {
|
|
16
|
+
try {
|
|
17
|
+
fn(event);
|
|
18
|
+
} catch {
|
|
19
|
+
// subscriber errors must never break the request path
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
// subscribe; returns an unsubscribe fn
|
|
24
|
+
subscribe(fn) {
|
|
25
|
+
listeners.add(fn);
|
|
26
|
+
return () => listeners.delete(fn);
|
|
27
|
+
},
|
|
28
|
+
// buffered events since start (or from offset), consumed left-to-right
|
|
29
|
+
replay({ since = 0 } = {}) {
|
|
30
|
+
return buffer.slice(since);
|
|
31
|
+
},
|
|
32
|
+
replayAll() {
|
|
33
|
+
return buffer.slice();
|
|
34
|
+
},
|
|
35
|
+
size() {
|
|
36
|
+
return buffer.length;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
package/src/routes.js
CHANGED
|
@@ -6,12 +6,13 @@ import { DEFAULT_MAX_HOPS } from "./peers.js";
|
|
|
6
6
|
|
|
7
7
|
export const errMsg = (err) => String(err?.message || err);
|
|
8
8
|
|
|
9
|
-
export function createRouter({ token, upstream, models, auto, logs, peers, maxHops = DEFAULT_MAX_HOPS, groups, bans }) {
|
|
9
|
+
export function createRouter({ token, upstream, models, auto, logs, peers, maxHops = DEFAULT_MAX_HOPS, groups, bans, bus }) {
|
|
10
10
|
return async function router(req, res) {
|
|
11
11
|
const method = req.method || "GET";
|
|
12
12
|
const path = (req.url || "").split("?")[0];
|
|
13
13
|
|
|
14
14
|
const route = ROUTES.find((r) => r.method === method && r.path === path);
|
|
15
|
+
|
|
15
16
|
if (!route) return notFound(res);
|
|
16
17
|
|
|
17
18
|
if (route.requiresAuth && !authorized(req, token)) {
|
|
@@ -20,7 +21,7 @@ export function createRouter({ token, upstream, models, auto, logs, peers, maxHo
|
|
|
20
21
|
return json(res, 401, { error: "Unauthorized" });
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
await route.handler({ req, res, upstream, models, auto, logs, peers, maxHops, groups, bans, token });
|
|
24
|
+
await route.handler({ req, res, upstream, models, auto, logs, peers, maxHops, groups, bans, token, bus });
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -175,7 +176,7 @@ const ROUTES = [
|
|
|
175
176
|
method: "POST",
|
|
176
177
|
path: "/v1/chat/completions",
|
|
177
178
|
requiresAuth: true,
|
|
178
|
-
handler: async ({ req, res, upstream, auto, logs, peers, maxHops }) => {
|
|
179
|
+
handler: async ({ req, res, upstream, auto, logs, peers, maxHops, bus }) => {
|
|
179
180
|
let body;
|
|
180
181
|
try {
|
|
181
182
|
body = await readBody(req);
|
|
@@ -205,7 +206,11 @@ const ROUTES = [
|
|
|
205
206
|
logs?.appendCall({ model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(body.stream) });
|
|
206
207
|
const logError = (model, status, message) =>
|
|
207
208
|
logs?.appendError({ model, auto: useAuto, status, message });
|
|
208
|
-
const evt = (type, data) =>
|
|
209
|
+
const evt = (type, data) => {
|
|
210
|
+
const entry = { ts: Date.now(), type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt };
|
|
211
|
+
if (bus) bus.emit(entry);
|
|
212
|
+
logs?.appendEvent?.(entry);
|
|
213
|
+
};
|
|
209
214
|
evt("request", { hops, ip: clientIp(req), stream: Boolean(body.stream), prompt: summarizePrompt(body) });
|
|
210
215
|
|
|
211
216
|
let lastErr = null;
|
package/src/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer as httpCreateServer } from "node:http";
|
|
2
2
|
import { DEFAULT_PORT, getPort } from "./state.js";
|
|
3
3
|
|
|
4
|
-
export function startServer({ router }, port = resolvePort()) {
|
|
4
|
+
export function startServer({ router, signals = true }, port = resolvePort()) {
|
|
5
5
|
const server = httpCreateServer((req, res) => {
|
|
6
6
|
router(req, res).catch((err) => {
|
|
7
7
|
res.statusCode = 500;
|
|
@@ -19,10 +19,15 @@ export function startServer({ router }, port = resolvePort()) {
|
|
|
19
19
|
const close = () =>
|
|
20
20
|
new Promise((resolve) => {
|
|
21
21
|
server.close(resolve);
|
|
22
|
+
// SSE debug streams keep connections open — force-close so shutdown
|
|
23
|
+
// never waits on them.
|
|
24
|
+
server.closeAllConnections?.();
|
|
22
25
|
});
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
if (signals) {
|
|
28
|
+
process.on("SIGINT", close);
|
|
29
|
+
process.on("SIGTERM", close);
|
|
30
|
+
}
|
|
26
31
|
|
|
27
32
|
return { server, ready, close };
|
|
28
33
|
}
|