instar 0.7.45 → 0.7.46
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/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/dist/cli.js +0 -0
- package/dist/commands/server.js +114 -0
- package/dist/commands/setup.js +1 -4
- package/dist/core/CaffeinateManager.d.ts +50 -0
- package/dist/core/CaffeinateManager.js +180 -0
- package/dist/lifeline/ServerSupervisor.js +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts +4 -0
- package/dist/lifeline/TelegramLifeline.js +32 -1
- package/dist/monitoring/HealthChecker.d.ts +1 -0
- package/dist/monitoring/HealthChecker.js +22 -0
- package/dist/monitoring/MemoryPressureMonitor.d.ts +83 -0
- package/dist/monitoring/MemoryPressureMonitor.js +242 -0
- package/dist/server/routes.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
> Why do I have a folder named ".vercel" in my project?
|
|
2
|
+
The ".vercel" folder is created when you link a directory to a Vercel project.
|
|
3
|
+
|
|
4
|
+
> What does the "project.json" file contain?
|
|
5
|
+
The "project.json" file contains:
|
|
6
|
+
- The ID of the Vercel project that you linked ("projectId")
|
|
7
|
+
- The ID of the user or team your Vercel project is owned by ("orgId")
|
|
8
|
+
|
|
9
|
+
> Should I commit the ".vercel" folder?
|
|
10
|
+
No, you should not share the ".vercel" folder with anyone.
|
|
11
|
+
Upon creation, it will be automatically added to your ".gitignore" file.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"projectId":"prj_evM5LcItYL3IAmw8zNvEPGrHeaya","orgId":"team_dHctwIDcV3X9ydapQlCPHFGI","projectName":"claude-agent-kit"}
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/commands/server.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { execFileSync } from 'node:child_process';
|
|
11
11
|
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
12
13
|
import path from 'node:path';
|
|
13
14
|
import pc from 'picocolors';
|
|
14
15
|
import { loadConfig, ensureStateDir, detectTmuxPath } from '../core/Config.js';
|
|
@@ -35,6 +36,24 @@ import { QuotaTracker } from '../monitoring/QuotaTracker.js';
|
|
|
35
36
|
import { AccountSwitcher } from '../monitoring/AccountSwitcher.js';
|
|
36
37
|
import { QuotaNotifier } from '../monitoring/QuotaNotifier.js';
|
|
37
38
|
import { classifySessionDeath } from '../monitoring/QuotaExhaustionDetector.js';
|
|
39
|
+
import { installAutoStart } from './setup.js';
|
|
40
|
+
/**
|
|
41
|
+
* Check if autostart is installed for this project.
|
|
42
|
+
* Extracted from the CLI `autostart status` handler for programmatic use.
|
|
43
|
+
*/
|
|
44
|
+
function isAutostartInstalled(projectName) {
|
|
45
|
+
if (process.platform === 'darwin') {
|
|
46
|
+
const label = `ai.instar.${projectName}`;
|
|
47
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
48
|
+
return fs.existsSync(plistPath);
|
|
49
|
+
}
|
|
50
|
+
else if (process.platform === 'linux') {
|
|
51
|
+
const serviceName = `instar-${projectName}.service`;
|
|
52
|
+
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', serviceName);
|
|
53
|
+
return fs.existsSync(servicePath);
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
38
57
|
/**
|
|
39
58
|
* Respawn a session for a topic, including thread history in the bootstrap.
|
|
40
59
|
* This prevents "thread drift" where respawned sessions lose context.
|
|
@@ -710,6 +729,77 @@ export async function startServer(options) {
|
|
|
710
729
|
...(config.evolution || {}),
|
|
711
730
|
});
|
|
712
731
|
console.log(pc.green(' Evolution system enabled'));
|
|
732
|
+
// Start MemoryPressureMonitor (platform-aware memory tracking)
|
|
733
|
+
const { MemoryPressureMonitor } = await import('../monitoring/MemoryPressureMonitor.js');
|
|
734
|
+
const memoryMonitor = new MemoryPressureMonitor({});
|
|
735
|
+
memoryMonitor.on('stateChange', ({ from, to, state: memState }) => {
|
|
736
|
+
// Gate scheduler spawning on memory pressure
|
|
737
|
+
if (scheduler && (to === 'elevated' || to === 'critical')) {
|
|
738
|
+
console.log(`[MemoryPressure] ${from} -> ${to} — scheduler should respect canSpawnSession()`);
|
|
739
|
+
}
|
|
740
|
+
// Alert via Telegram attention topic
|
|
741
|
+
if (telegram && to !== 'normal') {
|
|
742
|
+
const attentionTopicId = state.get('agent-attention-topic');
|
|
743
|
+
if (attentionTopicId) {
|
|
744
|
+
telegram.sendToTopic(attentionTopicId, `Memory ${to}: ${memState.pressurePercent.toFixed(1)}% used, ${memState.freeGB.toFixed(1)}GB free (trend: ${memState.trend})`).catch(() => { });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
memoryMonitor.start();
|
|
749
|
+
// Wire memory gate into scheduler
|
|
750
|
+
if (scheduler) {
|
|
751
|
+
const originalCanRun = scheduler.canRunJob;
|
|
752
|
+
scheduler.canRunJob = (priority) => {
|
|
753
|
+
// Check memory first
|
|
754
|
+
const memCheck = memoryMonitor.canSpawnSession();
|
|
755
|
+
if (!memCheck.allowed) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
// Then check original gate (quota, etc.)
|
|
759
|
+
return originalCanRun(priority);
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
// Start CaffeinateManager (prevents macOS system sleep)
|
|
763
|
+
const { CaffeinateManager } = await import('../core/CaffeinateManager.js');
|
|
764
|
+
const caffeinateManager = new CaffeinateManager({ stateDir: config.stateDir });
|
|
765
|
+
caffeinateManager.start();
|
|
766
|
+
// Start SleepWakeDetector (re-validate sessions on wake)
|
|
767
|
+
const { SleepWakeDetector } = await import('../core/SleepWakeDetector.js');
|
|
768
|
+
const sleepWakeDetector = new SleepWakeDetector();
|
|
769
|
+
sleepWakeDetector.on('wake', async (event) => {
|
|
770
|
+
console.log(`[SleepWake] Wake detected after ~${event.sleepDurationSeconds}s sleep`);
|
|
771
|
+
// Re-validate tmux sessions
|
|
772
|
+
try {
|
|
773
|
+
const tmuxPath = detectTmuxPath();
|
|
774
|
+
if (tmuxPath) {
|
|
775
|
+
const { execFileSync } = await import('child_process');
|
|
776
|
+
const result = execFileSync(tmuxPath, ['list-sessions'], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
777
|
+
console.log(`[SleepWake] tmux sessions after wake: ${result.split('\n').length}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
console.warn('[SleepWake] tmux check failed after wake');
|
|
782
|
+
}
|
|
783
|
+
// Restart tunnel if configured
|
|
784
|
+
if (tunnel) {
|
|
785
|
+
try {
|
|
786
|
+
await tunnel.stop();
|
|
787
|
+
const tunnelUrl = await tunnel.start();
|
|
788
|
+
console.log(`[SleepWake] Tunnel restarted: ${tunnelUrl}`);
|
|
789
|
+
}
|
|
790
|
+
catch (err) {
|
|
791
|
+
console.error(`[SleepWake] Tunnel restart failed:`, err);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// Notify via Telegram attention topic
|
|
795
|
+
if (telegram) {
|
|
796
|
+
const attentionTopicId = state.get('agent-attention-topic');
|
|
797
|
+
if (attentionTopicId) {
|
|
798
|
+
telegram.sendToTopic(attentionTopicId, `Wake detected after ~${event.sleepDurationSeconds}s sleep. Sessions re-validated.`).catch(() => { });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
sleepWakeDetector.start();
|
|
713
803
|
const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, publisher, viewer, tunnel, evolution });
|
|
714
804
|
await server.start();
|
|
715
805
|
// Start tunnel AFTER server is listening
|
|
@@ -723,9 +813,33 @@ export async function startServer(options) {
|
|
|
723
813
|
console.log(pc.yellow(` Server running locally without tunnel. Fix tunnel config and restart.`));
|
|
724
814
|
}
|
|
725
815
|
}
|
|
816
|
+
// Self-healing: ensure autostart is installed so the server always restarts
|
|
817
|
+
// This is a non-negotiable requirement — the user must always be able to reach their agent remotely.
|
|
818
|
+
// If autostart isn't installed, install it silently. The agent should never require human intervention
|
|
819
|
+
// to ensure its own resilience.
|
|
820
|
+
try {
|
|
821
|
+
const hasTelegram = !!telegram;
|
|
822
|
+
const autostartInstalled = isAutostartInstalled(config.projectName);
|
|
823
|
+
if (!autostartInstalled) {
|
|
824
|
+
const installed = installAutoStart(config.projectName, config.projectDir, hasTelegram);
|
|
825
|
+
if (installed) {
|
|
826
|
+
console.log(pc.green(` Auto-start self-healed: installed ${process.platform === 'darwin' ? 'LaunchAgent' : 'systemd service'}`));
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
console.log(pc.yellow(` Auto-start not available on ${process.platform}`));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
catch (err) {
|
|
834
|
+
// Non-critical — don't crash the server over autostart
|
|
835
|
+
console.error(` Auto-start check failed: ${err instanceof Error ? err.message : err}`);
|
|
836
|
+
}
|
|
726
837
|
// Graceful shutdown
|
|
727
838
|
const shutdown = async () => {
|
|
728
839
|
console.log('\nShutting down...');
|
|
840
|
+
memoryMonitor.stop();
|
|
841
|
+
caffeinateManager.stop();
|
|
842
|
+
sleepWakeDetector.stop();
|
|
729
843
|
autoUpdater.stop();
|
|
730
844
|
autoDispatcher?.stop();
|
|
731
845
|
if (tunnel)
|
package/dist/commands/setup.js
CHANGED
|
@@ -813,10 +813,7 @@ ${argsXml}
|
|
|
813
813
|
<key>RunAtLoad</key>
|
|
814
814
|
<true/>
|
|
815
815
|
<key>KeepAlive</key>
|
|
816
|
-
<
|
|
817
|
-
<key>SuccessfulExit</key>
|
|
818
|
-
<false/>
|
|
819
|
-
</dict>
|
|
816
|
+
<true/>
|
|
820
817
|
<key>StandardOutPath</key>
|
|
821
818
|
<string>${escapeXml(path.join(logDir, `${command}-launchd.log`))}</string>
|
|
822
819
|
<key>StandardErrorPath</key>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CaffeinateManager - Prevent macOS system sleep for Instar server lifetime.
|
|
3
|
+
*
|
|
4
|
+
* Maintains a `caffeinate -s` child process that prevents system sleep.
|
|
5
|
+
* A watchdog verifies it's alive every 30s and restarts if dead.
|
|
6
|
+
* PID is written to <stateDir>/caffeinate.pid for crash recovery.
|
|
7
|
+
*
|
|
8
|
+
* Only activates on macOS (process.platform === 'darwin').
|
|
9
|
+
* Uses EventEmitter pattern consistent with Instar conventions.
|
|
10
|
+
*/
|
|
11
|
+
import { EventEmitter } from 'node:events';
|
|
12
|
+
export interface CaffeinateManagerConfig {
|
|
13
|
+
/** State directory for PID file storage */
|
|
14
|
+
stateDir: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CaffeinateStatus {
|
|
17
|
+
running: boolean;
|
|
18
|
+
pid: number | null;
|
|
19
|
+
startedAt: string | null;
|
|
20
|
+
restartCount: number;
|
|
21
|
+
lastWatchdogCheck: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class CaffeinateManager extends EventEmitter {
|
|
24
|
+
private process;
|
|
25
|
+
private watchdogInterval;
|
|
26
|
+
private pid;
|
|
27
|
+
private startedAt;
|
|
28
|
+
private restartCount;
|
|
29
|
+
private lastWatchdogCheck;
|
|
30
|
+
private stopping;
|
|
31
|
+
private pidFile;
|
|
32
|
+
constructor(config: CaffeinateManagerConfig);
|
|
33
|
+
/**
|
|
34
|
+
* Start caffeinate and the watchdog.
|
|
35
|
+
* Only activates on macOS.
|
|
36
|
+
*/
|
|
37
|
+
start(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Stop caffeinate and the watchdog cleanly.
|
|
40
|
+
*/
|
|
41
|
+
stop(): void;
|
|
42
|
+
getStatus(): CaffeinateStatus;
|
|
43
|
+
private spawnCaffeinate;
|
|
44
|
+
private killCaffeinate;
|
|
45
|
+
private watchdog;
|
|
46
|
+
private cleanupStale;
|
|
47
|
+
private writePidFile;
|
|
48
|
+
private removePidFile;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=CaffeinateManager.d.ts.map
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CaffeinateManager - Prevent macOS system sleep for Instar server lifetime.
|
|
3
|
+
*
|
|
4
|
+
* Maintains a `caffeinate -s` child process that prevents system sleep.
|
|
5
|
+
* A watchdog verifies it's alive every 30s and restarts if dead.
|
|
6
|
+
* PID is written to <stateDir>/caffeinate.pid for crash recovery.
|
|
7
|
+
*
|
|
8
|
+
* Only activates on macOS (process.platform === 'darwin').
|
|
9
|
+
* Uses EventEmitter pattern consistent with Instar conventions.
|
|
10
|
+
*/
|
|
11
|
+
import { EventEmitter } from 'node:events';
|
|
12
|
+
import { spawn, execSync } from 'child_process';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
const WATCHDOG_INTERVAL_MS = 30_000; // 30 seconds
|
|
16
|
+
export class CaffeinateManager extends EventEmitter {
|
|
17
|
+
process = null;
|
|
18
|
+
watchdogInterval = null;
|
|
19
|
+
pid = null;
|
|
20
|
+
startedAt = null;
|
|
21
|
+
restartCount = 0;
|
|
22
|
+
lastWatchdogCheck = new Date().toISOString();
|
|
23
|
+
stopping = false;
|
|
24
|
+
pidFile;
|
|
25
|
+
constructor(config) {
|
|
26
|
+
super();
|
|
27
|
+
this.pidFile = path.join(config.stateDir, 'caffeinate.pid');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Start caffeinate and the watchdog.
|
|
31
|
+
* Only activates on macOS.
|
|
32
|
+
*/
|
|
33
|
+
start() {
|
|
34
|
+
if (this.watchdogInterval)
|
|
35
|
+
return;
|
|
36
|
+
if (process.platform !== 'darwin') {
|
|
37
|
+
console.log('[CaffeinateManager] Not macOS — skipping sleep prevention');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.stopping = false;
|
|
41
|
+
this.cleanupStale();
|
|
42
|
+
this.spawnCaffeinate();
|
|
43
|
+
this.watchdogInterval = setInterval(() => this.watchdog(), WATCHDOG_INTERVAL_MS);
|
|
44
|
+
this.watchdogInterval.unref(); // Don't prevent process exit
|
|
45
|
+
console.log(`[CaffeinateManager] Started (watchdog: ${WATCHDOG_INTERVAL_MS / 1000}s)`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Stop caffeinate and the watchdog cleanly.
|
|
49
|
+
*/
|
|
50
|
+
stop() {
|
|
51
|
+
this.stopping = true;
|
|
52
|
+
if (this.watchdogInterval) {
|
|
53
|
+
clearInterval(this.watchdogInterval);
|
|
54
|
+
this.watchdogInterval = null;
|
|
55
|
+
}
|
|
56
|
+
this.killCaffeinate();
|
|
57
|
+
this.removePidFile();
|
|
58
|
+
console.log('[CaffeinateManager] Stopped');
|
|
59
|
+
}
|
|
60
|
+
getStatus() {
|
|
61
|
+
return {
|
|
62
|
+
running: this.process !== null && this.pid !== null,
|
|
63
|
+
pid: this.pid,
|
|
64
|
+
startedAt: this.startedAt,
|
|
65
|
+
restartCount: this.restartCount,
|
|
66
|
+
lastWatchdogCheck: this.lastWatchdogCheck,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
spawnCaffeinate() {
|
|
70
|
+
try {
|
|
71
|
+
const proc = spawn('caffeinate', ['-s'], {
|
|
72
|
+
detached: true,
|
|
73
|
+
stdio: 'ignore',
|
|
74
|
+
});
|
|
75
|
+
proc.unref();
|
|
76
|
+
this.process = proc;
|
|
77
|
+
this.pid = proc.pid ?? null;
|
|
78
|
+
this.startedAt = new Date().toISOString();
|
|
79
|
+
this.writePidFile();
|
|
80
|
+
proc.on('exit', (code, signal) => {
|
|
81
|
+
if (!this.stopping) {
|
|
82
|
+
console.warn(`[CaffeinateManager] caffeinate exited (code: ${code}, signal: ${signal})`);
|
|
83
|
+
this.emit('died', { code, signal });
|
|
84
|
+
}
|
|
85
|
+
this.process = null;
|
|
86
|
+
this.pid = null;
|
|
87
|
+
});
|
|
88
|
+
proc.on('error', (err) => {
|
|
89
|
+
console.error('[CaffeinateManager] caffeinate spawn error:', err.message);
|
|
90
|
+
this.process = null;
|
|
91
|
+
this.pid = null;
|
|
92
|
+
});
|
|
93
|
+
console.log(`[CaffeinateManager] caffeinate spawned (PID: ${this.pid})`);
|
|
94
|
+
this.emit('started', { pid: this.pid });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.error('[CaffeinateManager] Failed to spawn caffeinate:', err);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
killCaffeinate() {
|
|
101
|
+
if (this.pid) {
|
|
102
|
+
try {
|
|
103
|
+
process.kill(this.pid, 'SIGTERM');
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Already dead
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.process = null;
|
|
110
|
+
this.pid = null;
|
|
111
|
+
}
|
|
112
|
+
watchdog() {
|
|
113
|
+
this.lastWatchdogCheck = new Date().toISOString();
|
|
114
|
+
if (this.stopping)
|
|
115
|
+
return;
|
|
116
|
+
if (this.pid) {
|
|
117
|
+
try {
|
|
118
|
+
process.kill(this.pid, 0);
|
|
119
|
+
return; // Still alive
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
console.warn(`[CaffeinateManager] caffeinate PID ${this.pid} is dead`);
|
|
123
|
+
this.process = null;
|
|
124
|
+
this.pid = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this.restartCount++;
|
|
128
|
+
console.log(`[CaffeinateManager] Restarting caffeinate (restart #${this.restartCount})`);
|
|
129
|
+
this.spawnCaffeinate();
|
|
130
|
+
this.emit('restarted', { restartCount: this.restartCount });
|
|
131
|
+
}
|
|
132
|
+
cleanupStale() {
|
|
133
|
+
try {
|
|
134
|
+
if (fs.existsSync(this.pidFile)) {
|
|
135
|
+
const stalePid = parseInt(fs.readFileSync(this.pidFile, 'utf-8').trim(), 10);
|
|
136
|
+
if (!isNaN(stalePid) && stalePid > 0) {
|
|
137
|
+
try {
|
|
138
|
+
const cmdline = execSync(`ps -p ${stalePid} -o comm= 2>/dev/null`, {
|
|
139
|
+
encoding: 'utf-8',
|
|
140
|
+
timeout: 3000,
|
|
141
|
+
}).trim();
|
|
142
|
+
if (cmdline.includes('caffeinate')) {
|
|
143
|
+
process.kill(stalePid, 'SIGTERM');
|
|
144
|
+
console.log(`[CaffeinateManager] Killed stale caffeinate (PID: ${stalePid})`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Process doesn't exist
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this.removePidFile();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// PID file doesn't exist or can't be read
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
writePidFile() {
|
|
159
|
+
if (!this.pid)
|
|
160
|
+
return;
|
|
161
|
+
try {
|
|
162
|
+
fs.mkdirSync(path.dirname(this.pidFile), { recursive: true });
|
|
163
|
+
fs.writeFileSync(this.pidFile, String(this.pid));
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
console.error('[CaffeinateManager] Failed to write PID file:', err);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
removePidFile() {
|
|
170
|
+
try {
|
|
171
|
+
if (fs.existsSync(this.pidFile)) {
|
|
172
|
+
fs.unlinkSync(this.pidFile);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Not critical
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=CaffeinateManager.js.map
|
|
@@ -97,7 +97,7 @@ export class ServerSupervisor extends EventEmitter {
|
|
|
97
97
|
return false;
|
|
98
98
|
try {
|
|
99
99
|
// Get the instar CLI path
|
|
100
|
-
const cliPath = new URL('
|
|
100
|
+
const cliPath = new URL('../cli.js', import.meta.url).pathname;
|
|
101
101
|
// --no-telegram: lifeline owns the Telegram connection, server should not poll
|
|
102
102
|
const nodeCmd = ['node', cliPath, 'server', 'start', '--foreground', '--no-telegram']
|
|
103
103
|
.map(arg => `'${arg.replace(/'/g, "'\\''")}'`)
|
|
@@ -45,6 +45,10 @@ export declare class TelegramLifeline {
|
|
|
45
45
|
private handleLifelineCommand;
|
|
46
46
|
private replayQueue;
|
|
47
47
|
private notifyServerDown;
|
|
48
|
+
/**
|
|
49
|
+
* Check if OS-level autostart is installed for this project.
|
|
50
|
+
*/
|
|
51
|
+
private isAutostartInstalled;
|
|
48
52
|
/**
|
|
49
53
|
* Ensure the Lifeline topic exists. Recreates if deleted.
|
|
50
54
|
*/
|
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
* the full server crashes, runs out of memory, or gets stuck.
|
|
20
20
|
*/
|
|
21
21
|
import fs from 'node:fs';
|
|
22
|
+
import os from 'node:os';
|
|
22
23
|
import path from 'node:path';
|
|
23
24
|
import pc from 'picocolors';
|
|
24
25
|
import { loadConfig, ensureStateDir } from '../core/Config.js';
|
|
25
26
|
import { registerPort, unregisterPort, startHeartbeat } from '../core/PortRegistry.js';
|
|
27
|
+
import { installAutoStart } from '../commands/setup.js';
|
|
26
28
|
import { MessageQueue } from './MessageQueue.js';
|
|
27
29
|
import { ServerSupervisor } from './ServerSupervisor.js';
|
|
28
30
|
/**
|
|
@@ -127,7 +129,7 @@ export class TelegramLifeline {
|
|
|
127
129
|
// Acquire exclusive lock — prevent multiple lifeline instances
|
|
128
130
|
if (!acquireLockFile(this.lockPath)) {
|
|
129
131
|
console.error(pc.red('[Lifeline] Another lifeline instance is already running. Exiting.'));
|
|
130
|
-
process.exit(0); // Clean exit — launchd
|
|
132
|
+
process.exit(0); // Clean exit — launchd will restart after ThrottleInterval, acting as a watchdog
|
|
131
133
|
}
|
|
132
134
|
// Register in port registry (lifeline owns the port claim)
|
|
133
135
|
try {
|
|
@@ -166,6 +168,19 @@ export class TelegramLifeline {
|
|
|
166
168
|
setTimeout(() => this.replayQueue(), 5000); // Wait for server to fully start
|
|
167
169
|
}
|
|
168
170
|
}
|
|
171
|
+
// Self-healing: ensure autostart is installed so the lifeline persists across reboots.
|
|
172
|
+
// The user must always be able to reach their agent remotely — this is non-negotiable.
|
|
173
|
+
try {
|
|
174
|
+
if (!this.isAutostartInstalled()) {
|
|
175
|
+
const installed = installAutoStart(this.projectConfig.projectName, this.projectConfig.projectDir, true);
|
|
176
|
+
if (installed) {
|
|
177
|
+
console.log(pc.green(` Auto-start self-healed: installed ${process.platform === 'darwin' ? 'LaunchAgent' : 'systemd service'}`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Non-critical — don't crash the lifeline over autostart
|
|
183
|
+
}
|
|
169
184
|
// Graceful shutdown
|
|
170
185
|
const shutdown = async () => {
|
|
171
186
|
console.log('\nLifeline shutting down...');
|
|
@@ -366,6 +381,22 @@ export class TelegramLifeline {
|
|
|
366
381
|
await this.sendToTopic(topicId, `Server went down: ${reason}\n\nYour messages will be queued until recovery. Use /lifeline status to check.`).catch(() => { });
|
|
367
382
|
}
|
|
368
383
|
// ── Lifeline Topic ──────────────────────────────────────────
|
|
384
|
+
/**
|
|
385
|
+
* Check if OS-level autostart is installed for this project.
|
|
386
|
+
*/
|
|
387
|
+
isAutostartInstalled() {
|
|
388
|
+
if (process.platform === 'darwin') {
|
|
389
|
+
const label = `ai.instar.${this.projectConfig.projectName}`;
|
|
390
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
391
|
+
return fs.existsSync(plistPath);
|
|
392
|
+
}
|
|
393
|
+
else if (process.platform === 'linux') {
|
|
394
|
+
const serviceName = `instar-${this.projectConfig.projectName}.service`;
|
|
395
|
+
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', serviceName);
|
|
396
|
+
return fs.existsSync(servicePath);
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
369
400
|
/**
|
|
370
401
|
* Ensure the Lifeline topic exists. Recreates if deleted.
|
|
371
402
|
*/
|
|
@@ -26,6 +26,7 @@ export class HealthChecker {
|
|
|
26
26
|
components.tmux = this.checkTmux();
|
|
27
27
|
components.sessions = this.checkSessions();
|
|
28
28
|
components.stateDir = this.checkStateDir();
|
|
29
|
+
components.memory = this.checkMemory();
|
|
29
30
|
if (this.scheduler) {
|
|
30
31
|
components.scheduler = this.checkScheduler();
|
|
31
32
|
}
|
|
@@ -135,6 +136,27 @@ export class HealthChecker {
|
|
|
135
136
|
lastCheck: now,
|
|
136
137
|
};
|
|
137
138
|
}
|
|
139
|
+
checkMemory() {
|
|
140
|
+
const now = new Date().toISOString();
|
|
141
|
+
try {
|
|
142
|
+
const os = require('node:os');
|
|
143
|
+
const totalBytes = os.totalmem();
|
|
144
|
+
const freeBytes = os.freemem();
|
|
145
|
+
const totalGB = totalBytes / (1024 ** 3);
|
|
146
|
+
const freeGB = freeBytes / (1024 ** 3);
|
|
147
|
+
const usedPercent = ((totalBytes - freeBytes) / totalBytes) * 100;
|
|
148
|
+
if (usedPercent >= 90) {
|
|
149
|
+
return { status: 'unhealthy', message: `Memory critical: ${usedPercent.toFixed(0)}% used (${freeGB.toFixed(1)}GB free)`, lastCheck: now };
|
|
150
|
+
}
|
|
151
|
+
if (usedPercent >= 75) {
|
|
152
|
+
return { status: 'degraded', message: `Memory elevated: ${usedPercent.toFixed(0)}% used (${freeGB.toFixed(1)}GB free)`, lastCheck: now };
|
|
153
|
+
}
|
|
154
|
+
return { status: 'healthy', message: `${usedPercent.toFixed(0)}% used (${freeGB.toFixed(1)}GB free / ${totalGB.toFixed(0)}GB total)`, lastCheck: now };
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
return { status: 'degraded', message: `Memory check failed: ${err instanceof Error ? err.message : String(err)}`, lastCheck: now };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
138
160
|
checkStateDir() {
|
|
139
161
|
const now = new Date().toISOString();
|
|
140
162
|
try {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryPressureMonitor - Detect and respond to system memory pressure.
|
|
3
|
+
*
|
|
4
|
+
* Platform-aware: uses macOS `vm_stat` or Linux `/proc/meminfo`.
|
|
5
|
+
* EventEmitter pattern consistent with Instar conventions.
|
|
6
|
+
*
|
|
7
|
+
* Thresholds:
|
|
8
|
+
* - normal (< 60%): all operations allowed
|
|
9
|
+
* - warning (60-75%): log trend, notify
|
|
10
|
+
* - elevated (75-90%): restrict session spawning
|
|
11
|
+
* - critical (90%+): block all spawns, alert
|
|
12
|
+
*
|
|
13
|
+
* Includes trend tracking via ring buffer + linear regression.
|
|
14
|
+
*/
|
|
15
|
+
import { EventEmitter } from 'node:events';
|
|
16
|
+
export type MemoryPressureState = 'normal' | 'warning' | 'elevated' | 'critical';
|
|
17
|
+
export type MemoryTrend = 'rising' | 'stable' | 'falling';
|
|
18
|
+
export interface MemoryState {
|
|
19
|
+
pressurePercent: number;
|
|
20
|
+
freeGB: number;
|
|
21
|
+
totalGB: number;
|
|
22
|
+
state: MemoryPressureState;
|
|
23
|
+
trend: MemoryTrend;
|
|
24
|
+
ratePerMin: number;
|
|
25
|
+
lastChecked: string;
|
|
26
|
+
stateChangedAt: string;
|
|
27
|
+
platform: string;
|
|
28
|
+
}
|
|
29
|
+
export interface MemoryPressureMonitorConfig {
|
|
30
|
+
/** Thresholds (percent). Defaults: warning=60, elevated=75, critical=90 */
|
|
31
|
+
thresholds?: {
|
|
32
|
+
warning?: number;
|
|
33
|
+
elevated?: number;
|
|
34
|
+
critical?: number;
|
|
35
|
+
};
|
|
36
|
+
/** Base check interval in ms. Default: 30000 */
|
|
37
|
+
checkIntervalMs?: number;
|
|
38
|
+
}
|
|
39
|
+
export declare class MemoryPressureMonitor extends EventEmitter {
|
|
40
|
+
private timeout;
|
|
41
|
+
private currentState;
|
|
42
|
+
private stateChangedAt;
|
|
43
|
+
private lastChecked;
|
|
44
|
+
private lastPressurePercent;
|
|
45
|
+
private lastFreeGB;
|
|
46
|
+
private lastTotalGB;
|
|
47
|
+
private ringBuffer;
|
|
48
|
+
private currentTrend;
|
|
49
|
+
private currentRatePerMin;
|
|
50
|
+
private thresholds;
|
|
51
|
+
private baseIntervalMs;
|
|
52
|
+
constructor(config?: MemoryPressureMonitorConfig);
|
|
53
|
+
start(): void;
|
|
54
|
+
stop(): void;
|
|
55
|
+
getState(): MemoryState;
|
|
56
|
+
/**
|
|
57
|
+
* Can a new session be spawned?
|
|
58
|
+
*/
|
|
59
|
+
canSpawnSession(): {
|
|
60
|
+
allowed: boolean;
|
|
61
|
+
reason?: string;
|
|
62
|
+
};
|
|
63
|
+
private scheduleNext;
|
|
64
|
+
private check;
|
|
65
|
+
private classifyState;
|
|
66
|
+
/**
|
|
67
|
+
* Read system memory — platform-aware.
|
|
68
|
+
*/
|
|
69
|
+
private readSystemMemory;
|
|
70
|
+
/**
|
|
71
|
+
* macOS: parse vm_stat
|
|
72
|
+
*/
|
|
73
|
+
private parseVmStat;
|
|
74
|
+
/**
|
|
75
|
+
* Linux: parse /proc/meminfo
|
|
76
|
+
*/
|
|
77
|
+
private parseProcMeminfo;
|
|
78
|
+
/**
|
|
79
|
+
* Linear regression over recent readings.
|
|
80
|
+
*/
|
|
81
|
+
private detectTrend;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=MemoryPressureMonitor.d.ts.map
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryPressureMonitor - Detect and respond to system memory pressure.
|
|
3
|
+
*
|
|
4
|
+
* Platform-aware: uses macOS `vm_stat` or Linux `/proc/meminfo`.
|
|
5
|
+
* EventEmitter pattern consistent with Instar conventions.
|
|
6
|
+
*
|
|
7
|
+
* Thresholds:
|
|
8
|
+
* - normal (< 60%): all operations allowed
|
|
9
|
+
* - warning (60-75%): log trend, notify
|
|
10
|
+
* - elevated (75-90%): restrict session spawning
|
|
11
|
+
* - critical (90%+): block all spawns, alert
|
|
12
|
+
*
|
|
13
|
+
* Includes trend tracking via ring buffer + linear regression.
|
|
14
|
+
*/
|
|
15
|
+
import { EventEmitter } from 'node:events';
|
|
16
|
+
import { execSync } from 'node:child_process';
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
const DEFAULT_THRESHOLDS = {
|
|
19
|
+
warning: 60,
|
|
20
|
+
elevated: 75,
|
|
21
|
+
critical: 90,
|
|
22
|
+
};
|
|
23
|
+
const RING_BUFFER_SIZE = 20;
|
|
24
|
+
const TREND_WINDOW = 6;
|
|
25
|
+
const PAGE_SIZE_BYTES = 16384; // macOS Apple Silicon
|
|
26
|
+
// Adaptive intervals
|
|
27
|
+
const INTERVALS = {
|
|
28
|
+
normal: 30_000,
|
|
29
|
+
warning: 15_000,
|
|
30
|
+
elevated: 10_000,
|
|
31
|
+
critical: 5_000,
|
|
32
|
+
};
|
|
33
|
+
export class MemoryPressureMonitor extends EventEmitter {
|
|
34
|
+
timeout = null;
|
|
35
|
+
currentState = 'normal';
|
|
36
|
+
stateChangedAt = new Date().toISOString();
|
|
37
|
+
lastChecked = new Date().toISOString();
|
|
38
|
+
lastPressurePercent = 0;
|
|
39
|
+
lastFreeGB = 0;
|
|
40
|
+
lastTotalGB = 0;
|
|
41
|
+
ringBuffer = [];
|
|
42
|
+
currentTrend = 'stable';
|
|
43
|
+
currentRatePerMin = 0;
|
|
44
|
+
thresholds;
|
|
45
|
+
baseIntervalMs;
|
|
46
|
+
constructor(config = {}) {
|
|
47
|
+
super();
|
|
48
|
+
this.thresholds = {
|
|
49
|
+
...DEFAULT_THRESHOLDS,
|
|
50
|
+
...config.thresholds,
|
|
51
|
+
};
|
|
52
|
+
this.baseIntervalMs = config.checkIntervalMs ?? 30_000;
|
|
53
|
+
}
|
|
54
|
+
start() {
|
|
55
|
+
if (this.timeout)
|
|
56
|
+
return;
|
|
57
|
+
this.check();
|
|
58
|
+
this.scheduleNext();
|
|
59
|
+
console.log(`[MemoryPressureMonitor] Started (platform: ${process.platform}, thresholds: ${JSON.stringify(this.thresholds)})`);
|
|
60
|
+
}
|
|
61
|
+
stop() {
|
|
62
|
+
if (this.timeout) {
|
|
63
|
+
clearTimeout(this.timeout);
|
|
64
|
+
this.timeout = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
getState() {
|
|
68
|
+
return {
|
|
69
|
+
pressurePercent: this.lastPressurePercent,
|
|
70
|
+
freeGB: this.lastFreeGB,
|
|
71
|
+
totalGB: this.lastTotalGB,
|
|
72
|
+
state: this.currentState,
|
|
73
|
+
trend: this.currentTrend,
|
|
74
|
+
ratePerMin: this.currentRatePerMin,
|
|
75
|
+
lastChecked: this.lastChecked,
|
|
76
|
+
stateChangedAt: this.stateChangedAt,
|
|
77
|
+
platform: process.platform,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Can a new session be spawned?
|
|
82
|
+
*/
|
|
83
|
+
canSpawnSession() {
|
|
84
|
+
switch (this.currentState) {
|
|
85
|
+
case 'normal':
|
|
86
|
+
case 'warning':
|
|
87
|
+
return { allowed: true };
|
|
88
|
+
case 'elevated':
|
|
89
|
+
return {
|
|
90
|
+
allowed: false,
|
|
91
|
+
reason: `Memory pressure elevated (${this.lastPressurePercent.toFixed(1)}%) — session spawn blocked`,
|
|
92
|
+
};
|
|
93
|
+
case 'critical':
|
|
94
|
+
return {
|
|
95
|
+
allowed: false,
|
|
96
|
+
reason: `Memory pressure critical (${this.lastPressurePercent.toFixed(1)}%) — all spawns blocked`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
scheduleNext() {
|
|
101
|
+
const intervalMs = INTERVALS[this.currentState] || this.baseIntervalMs;
|
|
102
|
+
this.timeout = setTimeout(() => {
|
|
103
|
+
this.check();
|
|
104
|
+
this.scheduleNext();
|
|
105
|
+
}, intervalMs);
|
|
106
|
+
this.timeout.unref(); // Don't prevent process exit
|
|
107
|
+
}
|
|
108
|
+
check() {
|
|
109
|
+
try {
|
|
110
|
+
const { pressurePercent, freeGB, totalGB } = this.readSystemMemory();
|
|
111
|
+
this.lastPressurePercent = pressurePercent;
|
|
112
|
+
this.lastFreeGB = freeGB;
|
|
113
|
+
this.lastTotalGB = totalGB;
|
|
114
|
+
this.lastChecked = new Date().toISOString();
|
|
115
|
+
// Ring buffer
|
|
116
|
+
this.ringBuffer.push({ timestamp: Date.now(), pressurePercent });
|
|
117
|
+
if (this.ringBuffer.length > RING_BUFFER_SIZE) {
|
|
118
|
+
this.ringBuffer.shift();
|
|
119
|
+
}
|
|
120
|
+
// Trend
|
|
121
|
+
const { trend, ratePerMin } = this.detectTrend();
|
|
122
|
+
this.currentTrend = trend;
|
|
123
|
+
this.currentRatePerMin = ratePerMin;
|
|
124
|
+
const newState = this.classifyState(pressurePercent);
|
|
125
|
+
if (newState !== this.currentState) {
|
|
126
|
+
const from = this.currentState;
|
|
127
|
+
this.currentState = newState;
|
|
128
|
+
this.stateChangedAt = new Date().toISOString();
|
|
129
|
+
console.log(`[MemoryPressureMonitor] ${from} -> ${newState} (${pressurePercent.toFixed(1)}%, ${freeGB.toFixed(1)}GB free, trend: ${trend})`);
|
|
130
|
+
this.emit('stateChange', { from, to: newState, state: this.getState() });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.error('[MemoryPressureMonitor] Check failed:', error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
classifyState(pressurePercent) {
|
|
138
|
+
if (pressurePercent >= this.thresholds.critical)
|
|
139
|
+
return 'critical';
|
|
140
|
+
if (pressurePercent >= this.thresholds.elevated)
|
|
141
|
+
return 'elevated';
|
|
142
|
+
if (pressurePercent >= this.thresholds.warning)
|
|
143
|
+
return 'warning';
|
|
144
|
+
return 'normal';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read system memory — platform-aware.
|
|
148
|
+
*/
|
|
149
|
+
readSystemMemory() {
|
|
150
|
+
if (process.platform === 'darwin') {
|
|
151
|
+
return this.parseVmStat();
|
|
152
|
+
}
|
|
153
|
+
else if (process.platform === 'linux') {
|
|
154
|
+
return this.parseProcMeminfo();
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Fallback: use Node's process.memoryUsage (very rough)
|
|
158
|
+
const mem = process.memoryUsage();
|
|
159
|
+
const totalGB = require('os').totalmem() / (1024 ** 3);
|
|
160
|
+
const usedGB = mem.rss / (1024 ** 3);
|
|
161
|
+
return {
|
|
162
|
+
pressurePercent: (usedGB / totalGB) * 100,
|
|
163
|
+
freeGB: totalGB - usedGB,
|
|
164
|
+
totalGB,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* macOS: parse vm_stat
|
|
170
|
+
*/
|
|
171
|
+
parseVmStat() {
|
|
172
|
+
const output = execSync('vm_stat', { encoding: 'utf-8', timeout: 5000 });
|
|
173
|
+
const pageSizeMatch = output.match(/page size of (\d+) bytes/);
|
|
174
|
+
const pageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : PAGE_SIZE_BYTES;
|
|
175
|
+
const parsePages = (label) => {
|
|
176
|
+
const match = output.match(new RegExp(`${label}:\\s+(\\d+)`));
|
|
177
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
178
|
+
};
|
|
179
|
+
const freePages = parsePages('Pages free');
|
|
180
|
+
const activePages = parsePages('Pages active');
|
|
181
|
+
const inactivePages = parsePages('Pages inactive');
|
|
182
|
+
const wiredPages = parsePages('Pages wired down');
|
|
183
|
+
const compressorPages = parsePages('Pages occupied by compressor');
|
|
184
|
+
const purgeablePages = parsePages('Pages purgeable');
|
|
185
|
+
const totalPages = freePages + activePages + inactivePages + wiredPages + compressorPages;
|
|
186
|
+
const totalBytes = totalPages * pageSize;
|
|
187
|
+
const totalGB = totalBytes / (1024 ** 3);
|
|
188
|
+
const availablePages = freePages + inactivePages + purgeablePages;
|
|
189
|
+
const availableBytes = availablePages * pageSize;
|
|
190
|
+
const freeGB = availableBytes / (1024 ** 3);
|
|
191
|
+
const usedPages = totalPages - availablePages;
|
|
192
|
+
const pressurePercent = totalPages > 0 ? (usedPages / totalPages) * 100 : 0;
|
|
193
|
+
return { pressurePercent, freeGB, totalGB };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Linux: parse /proc/meminfo
|
|
197
|
+
*/
|
|
198
|
+
parseProcMeminfo() {
|
|
199
|
+
const content = fs.readFileSync('/proc/meminfo', 'utf-8');
|
|
200
|
+
const parseKB = (key) => {
|
|
201
|
+
const match = content.match(new RegExp(`${key}:\\s+(\\d+)`));
|
|
202
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
203
|
+
};
|
|
204
|
+
const totalKB = parseKB('MemTotal');
|
|
205
|
+
const availableKB = parseKB('MemAvailable') || (parseKB('MemFree') + parseKB('Buffers') + parseKB('Cached'));
|
|
206
|
+
const totalGB = totalKB / (1024 * 1024);
|
|
207
|
+
const freeGB = availableKB / (1024 * 1024);
|
|
208
|
+
const pressurePercent = totalKB > 0 ? ((totalKB - availableKB) / totalKB) * 100 : 0;
|
|
209
|
+
return { pressurePercent, freeGB, totalGB };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Linear regression over recent readings.
|
|
213
|
+
*/
|
|
214
|
+
detectTrend() {
|
|
215
|
+
if (this.ringBuffer.length < 3) {
|
|
216
|
+
return { trend: 'stable', ratePerMin: 0 };
|
|
217
|
+
}
|
|
218
|
+
const readings = this.ringBuffer.slice(-TREND_WINDOW);
|
|
219
|
+
const n = readings.length;
|
|
220
|
+
const firstTs = readings[0].timestamp;
|
|
221
|
+
const xs = readings.map(r => (r.timestamp - firstTs) / 1000);
|
|
222
|
+
const ys = readings.map(r => r.pressurePercent);
|
|
223
|
+
const sumX = xs.reduce((a, b) => a + b, 0);
|
|
224
|
+
const sumY = ys.reduce((a, b) => a + b, 0);
|
|
225
|
+
const sumXY = xs.reduce((a, x, i) => a + x * ys[i], 0);
|
|
226
|
+
const sumX2 = xs.reduce((a, x) => a + x * x, 0);
|
|
227
|
+
const denom = n * sumX2 - sumX * sumX;
|
|
228
|
+
if (denom === 0)
|
|
229
|
+
return { trend: 'stable', ratePerMin: 0 };
|
|
230
|
+
const slope = (n * sumXY - sumX * sumY) / denom;
|
|
231
|
+
const ratePerMin = slope * 60;
|
|
232
|
+
let trend;
|
|
233
|
+
if (ratePerMin > 0.5)
|
|
234
|
+
trend = 'rising';
|
|
235
|
+
else if (ratePerMin < -0.5)
|
|
236
|
+
trend = 'falling';
|
|
237
|
+
else
|
|
238
|
+
trend = 'stable';
|
|
239
|
+
return { trend, ratePerMin };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=MemoryPressureMonitor.js.map
|
package/dist/server/routes.js
CHANGED
|
@@ -63,6 +63,15 @@ export function createRoutes(ctx) {
|
|
|
63
63
|
heapUsed: Math.round(mem.heapUsed / 1024 / 1024),
|
|
64
64
|
heapTotal: Math.round(mem.heapTotal / 1024 / 1024),
|
|
65
65
|
};
|
|
66
|
+
// System-wide memory state
|
|
67
|
+
const os = require('node:os');
|
|
68
|
+
const totalMem = os.totalmem();
|
|
69
|
+
const freeMem = os.freemem();
|
|
70
|
+
base.systemMemory = {
|
|
71
|
+
totalGB: Math.round(totalMem / (1024 ** 3) * 10) / 10,
|
|
72
|
+
freeGB: Math.round(freeMem / (1024 ** 3) * 10) / 10,
|
|
73
|
+
usedPercent: Math.round(((totalMem - freeMem) / totalMem) * 1000) / 10,
|
|
74
|
+
};
|
|
66
75
|
// Job health summary
|
|
67
76
|
if (ctx.scheduler) {
|
|
68
77
|
const jobs = ctx.scheduler.getJobs();
|