maxpool 1.5.5 → 1.5.6
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 +1 -1
- package/src/config.js +4 -0
- package/src/index.js +11 -0
- package/src/sleep-guard.js +77 -0
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -46,6 +46,10 @@ export function createDefaultConfig() {
|
|
|
46
46
|
apiKey: 'mp-' + randomBytes(24).toString('base64url'),
|
|
47
47
|
},
|
|
48
48
|
upstream: 'https://api.anthropic.com',
|
|
49
|
+
// (macOS) Keep the system awake while requests are in flight so long overnight
|
|
50
|
+
// streams survive Maintenance Sleep; the Mac sleeps normally when idle. Set false
|
|
51
|
+
// to disable. AC power only; the display still sleeps (screen stays dark).
|
|
52
|
+
preventSleep: true,
|
|
49
53
|
// On startup, check npm for a newer maxpool and notify. Set false to disable.
|
|
50
54
|
updateCheck: true,
|
|
51
55
|
// When true, a newer version is installed automatically (npm i -g maxpool@latest)
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
5
|
import { loadOrCreateConfig, loadConfig, saveConfig, atomicConfigUpdate, getConfigPath, loadState, saveState, getStatePath, getLogPath, readGeneration, flushConfigWrites, flushStateWrites } from './config.js';
|
|
6
6
|
import { setEventLogPath, installConsoleMirror } from './event-log.js';
|
|
7
|
+
import { SleepGuard } from './sleep-guard.js';
|
|
7
8
|
import { AccountManager } from './account-manager.js';
|
|
8
9
|
import { createProxyServer } from './server.js';
|
|
9
10
|
import { Prober } from './prober.js';
|
|
@@ -469,6 +470,14 @@ async function serverWorkerCommand() {
|
|
|
469
470
|
|
|
470
471
|
const threshold = config.switchThreshold || 0.90;
|
|
471
472
|
const accountManager = new AccountManager(accounts, threshold, config.scheduler || {});
|
|
473
|
+
// (macOS) Keep the system awake ONLY while there is work in flight or queued, so a
|
|
474
|
+
// long overnight streaming request survives Maintenance Sleep; the Mac sleeps
|
|
475
|
+
// normally when idle. Disable via `preventSleep: false` in config.
|
|
476
|
+
const sleepGuard = new SleepGuard({
|
|
477
|
+
enabled: config.preventSleep !== false && process.platform === 'darwin' && process.env.MAXPOOL_DISABLE_SLEEP_GUARD !== '1',
|
|
478
|
+
getWorkPending: () => accountManager.getGlobalInFlight() > 0 || accountManager.queueState.waiting.length > 0,
|
|
479
|
+
log: msg => console.log(`[Maxpool] ${msg}`),
|
|
480
|
+
});
|
|
472
481
|
accountManager.setRoutingMode(
|
|
473
482
|
config.routing?.mode,
|
|
474
483
|
config.routing?.preferredAccount,
|
|
@@ -656,6 +665,7 @@ async function serverWorkerCommand() {
|
|
|
656
665
|
quotaSaveInterval.unref?.();
|
|
657
666
|
}
|
|
658
667
|
prober.start();
|
|
668
|
+
sleepGuard.start(); // (macOS) keep the system awake while serving, so long streams survive Maintenance Sleep
|
|
659
669
|
};
|
|
660
670
|
// Stop scheduling writes and flip the lease. Returns a promise that settles
|
|
661
671
|
// once any in-flight prober cycle has finished (so no probe-driven token
|
|
@@ -666,6 +676,7 @@ async function serverWorkerCommand() {
|
|
|
666
676
|
hasLease = false;
|
|
667
677
|
if (quotaSaveInterval) { clearInterval(quotaSaveInterval); quotaSaveInterval = null; }
|
|
668
678
|
accountManager.setWriterLease(false);
|
|
679
|
+
sleepGuard.stop(); // release the wake — only the active lease-holder keeps the Mac awake
|
|
669
680
|
await prober.stop(); // awaits any in-flight probe cycle (B1)
|
|
670
681
|
};
|
|
671
682
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Conditional sleep guard (macOS): while maxpool has work in flight, prevent the
|
|
2
|
+
// SYSTEM from sleeping so long-lived streaming requests survive overnight
|
|
3
|
+
// Maintenance Sleep (which powers down the network stack and kills the
|
|
4
|
+
// connection → "terminated"). When idle, it releases so the Mac sleeps normally.
|
|
5
|
+
//
|
|
6
|
+
// Uses the built-in `caffeinate -s` (prevent SYSTEM sleep only — the DISPLAY still
|
|
7
|
+
// sleeps, so the screen stays dark) with `-w <pid>` so caffeinate auto-exits if the
|
|
8
|
+
// worker dies (never a zombie keeping the Mac awake forever). `-s` is AC-power only
|
|
9
|
+
// by design. Complete no-op off macOS or when disabled (e.g. the test suite).
|
|
10
|
+
|
|
11
|
+
import { spawn as realSpawn } from 'node:child_process';
|
|
12
|
+
|
|
13
|
+
const POLL_MS = 5_000; // re-check the in-flight signal (idle-sleep timers are minutes)
|
|
14
|
+
const GRACE_MS = 60_000; // stay awake this long after the last request, to bridge gaps
|
|
15
|
+
|
|
16
|
+
export class SleepGuard {
|
|
17
|
+
constructor({ getWorkPending, log = () => {}, spawn = realSpawn, enabled, pollMs = POLL_MS, graceMs = GRACE_MS } = {}) {
|
|
18
|
+
this.getWorkPending = getWorkPending; // () => boolean: true while there's active/queued work
|
|
19
|
+
this.log = log;
|
|
20
|
+
this._spawn = spawn;
|
|
21
|
+
this.enabled = enabled ?? (process.platform === 'darwin' && process.env.MAXPOOL_DISABLE_SLEEP_GUARD !== '1');
|
|
22
|
+
this.pollMs = pollMs;
|
|
23
|
+
this.graceMs = graceMs;
|
|
24
|
+
this.child = null;
|
|
25
|
+
this.pollTimer = null;
|
|
26
|
+
this.graceTimer = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
start() {
|
|
30
|
+
if (!this.enabled || this.pollTimer) return;
|
|
31
|
+
this.pollTimer = setInterval(() => this._tick(), this.pollMs);
|
|
32
|
+
this.pollTimer.unref?.();
|
|
33
|
+
this._tick(); // engage immediately if there's already work
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_tick() {
|
|
37
|
+
let pending = false;
|
|
38
|
+
try { pending = Boolean(this.getWorkPending?.()); } catch { pending = false; }
|
|
39
|
+
if (pending) {
|
|
40
|
+
if (this.graceTimer) { clearTimeout(this.graceTimer); this.graceTimer = null; }
|
|
41
|
+
this._ensureAwake();
|
|
42
|
+
} else if (this.child && !this.graceTimer) {
|
|
43
|
+
// Idle: hold the wake for a grace period, then release (bridges brief gaps
|
|
44
|
+
// between back-to-back requests so we never drop it mid-conversation).
|
|
45
|
+
this.graceTimer = setTimeout(() => { this.graceTimer = null; this._allowSleep(); }, this.graceMs);
|
|
46
|
+
this.graceTimer.unref?.();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_ensureAwake() {
|
|
51
|
+
if (!this.enabled || this.child) return;
|
|
52
|
+
try {
|
|
53
|
+
const c = this._spawn('/usr/bin/caffeinate', ['-s', '-w', String(process.pid)], { stdio: 'ignore' });
|
|
54
|
+
this.child = c;
|
|
55
|
+
// Only clear the ref if it's still THIS child (a stale exit can't null a newer one).
|
|
56
|
+
// caffeinate missing/unspawnable → disable so we don't re-spawn every poll forever.
|
|
57
|
+
c.once('error', () => { if (this.child === c) this.child = null; this.enabled = false; });
|
|
58
|
+
c.once('exit', () => { if (this.child === c) this.child = null; });
|
|
59
|
+
// Honest: -s keeps the SYSTEM awake ON AC ONLY (a no-op on battery); the display
|
|
60
|
+
// still sleeps so the screen stays dark.
|
|
61
|
+
this.log('Keeping the system awake while busy (caffeinate -s; AC power only, display still sleeps)');
|
|
62
|
+
} catch { this.child = null; this.enabled = false; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_allowSleep() {
|
|
66
|
+
if (!this.child) return;
|
|
67
|
+
try { this.child.kill('SIGTERM'); } catch { /* already gone */ }
|
|
68
|
+
this.child = null;
|
|
69
|
+
this.log('Idle — allowing normal system sleep again');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
stop() {
|
|
73
|
+
if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
|
|
74
|
+
if (this.graceTimer) { clearTimeout(this.graceTimer); this.graceTimer = null; }
|
|
75
|
+
this._allowSleep();
|
|
76
|
+
}
|
|
77
|
+
}
|