instar 0.7.49 → 0.7.51
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/.claude/skills/setup-wizard/skill.md +9 -40
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/dist/commands/server.js +141 -1
- package/dist/core/AutoUpdater.js +12 -5
- package/dist/core/CaffeinateManager.d.ts +50 -0
- package/dist/core/CaffeinateManager.js +180 -0
- package/dist/core/SessionManager.d.ts +6 -0
- package/dist/core/SessionManager.js +31 -27
- package/dist/core/UpdateChecker.d.ts +2 -1
- package/dist/core/UpdateChecker.js +37 -10
- package/dist/core/types.d.ts +8 -0
- package/dist/lifeline/ServerSupervisor.d.ts +2 -0
- package/dist/lifeline/ServerSupervisor.js +8 -1
- package/dist/lifeline/TelegramLifeline.d.ts +5 -0
- package/dist/lifeline/TelegramLifeline.js +86 -0
- package/dist/monitoring/HealthChecker.d.ts +4 -1
- package/dist/monitoring/HealthChecker.js +36 -1
- package/dist/monitoring/MemoryPressureMonitor.d.ts +83 -0
- package/dist/monitoring/MemoryPressureMonitor.js +242 -0
- package/dist/monitoring/SessionWatchdog.d.ts +83 -0
- package/dist/monitoring/SessionWatchdog.js +326 -0
- package/dist/scaffold/templates.js +31 -8
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/middleware.js +5 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.js +82 -3
- package/package.json +1 -1
|
@@ -226,9 +226,11 @@ export class UpdateChecker {
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
229
|
-
* Fetch human-readable changelog from GitHub releases
|
|
229
|
+
* Fetch human-readable changelog from GitHub releases, falling back to
|
|
230
|
+
* recent commit messages if no release exists for this version.
|
|
230
231
|
*/
|
|
231
232
|
async fetchChangelog(version) {
|
|
233
|
+
// Try GitHub release first
|
|
232
234
|
try {
|
|
233
235
|
const tag = version.startsWith('v') ? version : `v${version}`;
|
|
234
236
|
const response = await fetch(`${GITHUB_RELEASES_URL}/tags/${tag}`, {
|
|
@@ -238,16 +240,41 @@ export class UpdateChecker {
|
|
|
238
240
|
},
|
|
239
241
|
signal: AbortSignal.timeout(10000),
|
|
240
242
|
});
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
if (response.ok) {
|
|
244
|
+
const release = await response.json();
|
|
245
|
+
if (release.body) {
|
|
246
|
+
const summary = release.body.slice(0, 500);
|
|
247
|
+
return summary.length < release.body.length ? summary + '...' : summary;
|
|
248
|
+
}
|
|
249
|
+
if (release.name)
|
|
250
|
+
return release.name;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Non-critical — try commit fallback
|
|
255
|
+
}
|
|
256
|
+
// Fallback: fetch recent commits from GitHub
|
|
257
|
+
try {
|
|
258
|
+
const response = await fetch('https://api.github.com/repos/SageMindAI/instar/commits?per_page=5', {
|
|
259
|
+
headers: {
|
|
260
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
261
|
+
'User-Agent': 'instar-update-checker',
|
|
262
|
+
},
|
|
263
|
+
signal: AbortSignal.timeout(10000),
|
|
264
|
+
});
|
|
265
|
+
if (response.ok) {
|
|
266
|
+
const commits = await response.json();
|
|
267
|
+
if (commits.length > 0) {
|
|
268
|
+
const lines = commits
|
|
269
|
+
.map(c => {
|
|
270
|
+
// Take first line of commit message only
|
|
271
|
+
const firstLine = c.commit.message.split('\n')[0];
|
|
272
|
+
return `• ${firstLine}`;
|
|
273
|
+
})
|
|
274
|
+
.join('\n');
|
|
275
|
+
return `Recent changes:\n${lines}`;
|
|
276
|
+
}
|
|
248
277
|
}
|
|
249
|
-
if (release.name)
|
|
250
|
-
return release.name;
|
|
251
278
|
}
|
|
252
279
|
catch {
|
|
253
280
|
// Non-critical
|
package/dist/core/types.d.ts
CHANGED
|
@@ -712,6 +712,14 @@ export interface MonitoringConfig {
|
|
|
712
712
|
memoryMonitoring: boolean;
|
|
713
713
|
/** Health check interval in ms */
|
|
714
714
|
healthCheckIntervalMs: number;
|
|
715
|
+
/** Session watchdog — auto-remediation for stuck commands */
|
|
716
|
+
watchdog?: {
|
|
717
|
+
enabled: boolean;
|
|
718
|
+
/** Seconds before a command is considered stuck (default: 180) */
|
|
719
|
+
stuckCommandSec?: number;
|
|
720
|
+
/** Poll interval in ms (default: 30000) */
|
|
721
|
+
pollIntervalMs?: number;
|
|
722
|
+
};
|
|
715
723
|
}
|
|
716
724
|
/** @deprecated Use InstarConfig instead */
|
|
717
725
|
export type AgentKitConfig = InstarConfig;
|
|
@@ -22,6 +22,8 @@ export class ServerSupervisor extends EventEmitter {
|
|
|
22
22
|
restartBackoffMs = 5000;
|
|
23
23
|
isRunning = false;
|
|
24
24
|
lastHealthy = 0;
|
|
25
|
+
startupGraceMs = 20_000; // 20 seconds grace period after spawn before health checks
|
|
26
|
+
spawnedAt = 0;
|
|
25
27
|
constructor(options) {
|
|
26
28
|
super();
|
|
27
29
|
this.projectDir = options.projectDir;
|
|
@@ -95,7 +97,7 @@ export class ServerSupervisor extends EventEmitter {
|
|
|
95
97
|
return false;
|
|
96
98
|
try {
|
|
97
99
|
// Get the instar CLI path
|
|
98
|
-
const cliPath = new URL('
|
|
100
|
+
const cliPath = new URL('../cli.js', import.meta.url).pathname;
|
|
99
101
|
// --no-telegram: lifeline owns the Telegram connection, server should not poll
|
|
100
102
|
const nodeCmd = ['node', cliPath, 'server', 'start', '--foreground', '--no-telegram']
|
|
101
103
|
.map(arg => `'${arg.replace(/'/g, "'\\''")}'`)
|
|
@@ -108,6 +110,7 @@ export class ServerSupervisor extends EventEmitter {
|
|
|
108
110
|
], { stdio: 'ignore' });
|
|
109
111
|
console.log(`[Supervisor] Server started in tmux session: ${this.serverSessionName}`);
|
|
110
112
|
this.isRunning = true;
|
|
113
|
+
this.spawnedAt = Date.now();
|
|
111
114
|
this.startHealthChecks();
|
|
112
115
|
return true;
|
|
113
116
|
}
|
|
@@ -133,6 +136,10 @@ export class ServerSupervisor extends EventEmitter {
|
|
|
133
136
|
if (this.healthCheckInterval)
|
|
134
137
|
return;
|
|
135
138
|
this.healthCheckInterval = setInterval(async () => {
|
|
139
|
+
// Skip health checks during startup grace period — server needs time to boot
|
|
140
|
+
if (this.spawnedAt > 0 && (Date.now() - this.spawnedAt) < this.startupGraceMs) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
136
143
|
try {
|
|
137
144
|
const healthy = await this.checkHealth();
|
|
138
145
|
if (healthy) {
|
|
@@ -30,6 +30,7 @@ export declare class TelegramLifeline {
|
|
|
30
30
|
private stopHeartbeat;
|
|
31
31
|
private replayInterval;
|
|
32
32
|
private lifelineTopicId;
|
|
33
|
+
private lockPath;
|
|
33
34
|
constructor(projectDir?: string);
|
|
34
35
|
/**
|
|
35
36
|
* Start the lifeline — begins Telegram polling and server supervision.
|
|
@@ -44,6 +45,10 @@ export declare class TelegramLifeline {
|
|
|
44
45
|
private handleLifelineCommand;
|
|
45
46
|
private replayQueue;
|
|
46
47
|
private notifyServerDown;
|
|
48
|
+
/**
|
|
49
|
+
* Check if OS-level autostart is installed for this project.
|
|
50
|
+
*/
|
|
51
|
+
private isAutostartInstalled;
|
|
47
52
|
/**
|
|
48
53
|
* Ensure the Lifeline topic exists. Recreates if deleted.
|
|
49
54
|
*/
|
|
@@ -19,12 +19,61 @@
|
|
|
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';
|
|
30
|
+
/**
|
|
31
|
+
* Acquire an exclusive lock file to prevent multiple lifeline instances.
|
|
32
|
+
* Returns true if lock acquired, false if another instance holds it.
|
|
33
|
+
*/
|
|
34
|
+
function acquireLockFile(lockPath) {
|
|
35
|
+
try {
|
|
36
|
+
// Check if lock file exists and if the PID is still alive
|
|
37
|
+
if (fs.existsSync(lockPath)) {
|
|
38
|
+
const raw = fs.readFileSync(lockPath, 'utf-8');
|
|
39
|
+
const data = JSON.parse(raw);
|
|
40
|
+
if (data.pid && typeof data.pid === 'number') {
|
|
41
|
+
try {
|
|
42
|
+
// Signal 0 checks if process exists without killing it
|
|
43
|
+
process.kill(data.pid, 0);
|
|
44
|
+
// Process still alive — another lifeline is running
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Process is dead — stale lock, we can take over
|
|
49
|
+
console.log(`[Lifeline] Removing stale lock (PID ${data.pid} is dead)`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Write our PID
|
|
54
|
+
const tmpPath = `${lockPath}.${process.pid}.tmp`;
|
|
55
|
+
fs.writeFileSync(tmpPath, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
|
|
56
|
+
fs.renameSync(tmpPath, lockPath);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error(`[Lifeline] Lock acquisition failed: ${err}`);
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function releaseLockFile(lockPath) {
|
|
65
|
+
try {
|
|
66
|
+
if (fs.existsSync(lockPath)) {
|
|
67
|
+
const raw = fs.readFileSync(lockPath, 'utf-8');
|
|
68
|
+
const data = JSON.parse(raw);
|
|
69
|
+
// Only remove if we own it
|
|
70
|
+
if (data.pid === process.pid) {
|
|
71
|
+
fs.unlinkSync(lockPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch { /* best effort */ }
|
|
76
|
+
}
|
|
28
77
|
export class TelegramLifeline {
|
|
29
78
|
config;
|
|
30
79
|
projectConfig;
|
|
@@ -37,6 +86,7 @@ export class TelegramLifeline {
|
|
|
37
86
|
stopHeartbeat = null;
|
|
38
87
|
replayInterval = null;
|
|
39
88
|
lifelineTopicId = null;
|
|
89
|
+
lockPath;
|
|
40
90
|
constructor(projectDir) {
|
|
41
91
|
this.projectConfig = loadConfig(projectDir);
|
|
42
92
|
ensureStateDir(this.projectConfig.stateDir);
|
|
@@ -48,6 +98,7 @@ export class TelegramLifeline {
|
|
|
48
98
|
this.config = telegramConfig.config;
|
|
49
99
|
this.queue = new MessageQueue(this.projectConfig.stateDir);
|
|
50
100
|
this.offsetPath = path.join(this.projectConfig.stateDir, 'lifeline-poll-offset.json');
|
|
101
|
+
this.lockPath = path.join(this.projectConfig.stateDir, 'lifeline.lock');
|
|
51
102
|
this.supervisor = new ServerSupervisor({
|
|
52
103
|
projectDir: this.projectConfig.projectDir,
|
|
53
104
|
projectName: this.projectConfig.projectName,
|
|
@@ -75,6 +126,11 @@ export class TelegramLifeline {
|
|
|
75
126
|
console.log(` Port: ${this.projectConfig.port}`);
|
|
76
127
|
console.log(` State: ${this.projectConfig.stateDir}`);
|
|
77
128
|
console.log();
|
|
129
|
+
// Acquire exclusive lock — prevent multiple lifeline instances
|
|
130
|
+
if (!acquireLockFile(this.lockPath)) {
|
|
131
|
+
console.error(pc.red('[Lifeline] Another lifeline instance is already running. Exiting.'));
|
|
132
|
+
process.exit(0); // Clean exit — launchd will restart after ThrottleInterval, acting as a watchdog
|
|
133
|
+
}
|
|
78
134
|
// Register in port registry (lifeline owns the port claim)
|
|
79
135
|
try {
|
|
80
136
|
registerPort(`${this.projectConfig.projectName}-lifeline`, this.projectConfig.port + 1000, // Lifeline uses port + 1000 to avoid conflict
|
|
@@ -112,6 +168,19 @@ export class TelegramLifeline {
|
|
|
112
168
|
setTimeout(() => this.replayQueue(), 5000); // Wait for server to fully start
|
|
113
169
|
}
|
|
114
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
|
+
}
|
|
115
184
|
// Graceful shutdown
|
|
116
185
|
const shutdown = async () => {
|
|
117
186
|
console.log('\nLifeline shutting down...');
|
|
@@ -123,6 +192,7 @@ export class TelegramLifeline {
|
|
|
123
192
|
if (this.stopHeartbeat)
|
|
124
193
|
this.stopHeartbeat();
|
|
125
194
|
unregisterPort(`${this.projectConfig.projectName}-lifeline`);
|
|
195
|
+
releaseLockFile(this.lockPath);
|
|
126
196
|
await this.supervisor.stop();
|
|
127
197
|
process.exit(0);
|
|
128
198
|
};
|
|
@@ -326,6 +396,22 @@ export class TelegramLifeline {
|
|
|
326
396
|
await this.sendToTopic(topicId, `Server went down: ${reason}\n\nYour messages will be queued until recovery. Use /lifeline status to check.`).catch(() => { });
|
|
327
397
|
}
|
|
328
398
|
// ── Lifeline Topic ──────────────────────────────────────────
|
|
399
|
+
/**
|
|
400
|
+
* Check if OS-level autostart is installed for this project.
|
|
401
|
+
*/
|
|
402
|
+
isAutostartInstalled() {
|
|
403
|
+
if (process.platform === 'darwin') {
|
|
404
|
+
const label = `ai.instar.${this.projectConfig.projectName}`;
|
|
405
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
406
|
+
return fs.existsSync(plistPath);
|
|
407
|
+
}
|
|
408
|
+
else if (process.platform === 'linux') {
|
|
409
|
+
const serviceName = `instar-${this.projectConfig.projectName}.service`;
|
|
410
|
+
const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', serviceName);
|
|
411
|
+
return fs.existsSync(servicePath);
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
329
415
|
/**
|
|
330
416
|
* Ensure the Lifeline topic exists. Recreates if deleted.
|
|
331
417
|
*/
|
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
import type { SessionManager } from '../core/SessionManager.js';
|
|
8
8
|
import type { JobScheduler } from '../scheduler/JobScheduler.js';
|
|
9
9
|
import type { HealthStatus, InstarConfig } from '../core/types.js';
|
|
10
|
+
import type { SessionWatchdog } from './SessionWatchdog.js';
|
|
10
11
|
export declare class HealthChecker {
|
|
11
12
|
private config;
|
|
12
13
|
private sessionManager;
|
|
13
14
|
private scheduler;
|
|
15
|
+
private watchdog;
|
|
14
16
|
private checkInterval;
|
|
15
17
|
private lastStatus;
|
|
16
|
-
constructor(config: InstarConfig, sessionManager: SessionManager, scheduler?: JobScheduler | null);
|
|
18
|
+
constructor(config: InstarConfig, sessionManager: SessionManager, scheduler?: JobScheduler | null, watchdog?: SessionWatchdog | null);
|
|
17
19
|
/**
|
|
18
20
|
* Run all health checks and return aggregated status.
|
|
19
21
|
*/
|
|
@@ -33,6 +35,7 @@ export declare class HealthChecker {
|
|
|
33
35
|
private checkTmux;
|
|
34
36
|
private checkSessions;
|
|
35
37
|
private checkScheduler;
|
|
38
|
+
private checkMemory;
|
|
36
39
|
private checkStateDir;
|
|
37
40
|
}
|
|
38
41
|
//# sourceMappingURL=HealthChecker.d.ts.map
|
|
@@ -11,12 +11,14 @@ export class HealthChecker {
|
|
|
11
11
|
config;
|
|
12
12
|
sessionManager;
|
|
13
13
|
scheduler;
|
|
14
|
+
watchdog;
|
|
14
15
|
checkInterval = null;
|
|
15
16
|
lastStatus = null;
|
|
16
|
-
constructor(config, sessionManager, scheduler = null) {
|
|
17
|
+
constructor(config, sessionManager, scheduler = null, watchdog = null) {
|
|
17
18
|
this.config = config;
|
|
18
19
|
this.sessionManager = sessionManager;
|
|
19
20
|
this.scheduler = scheduler;
|
|
21
|
+
this.watchdog = watchdog;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Run all health checks and return aggregated status.
|
|
@@ -26,9 +28,21 @@ export class HealthChecker {
|
|
|
26
28
|
components.tmux = this.checkTmux();
|
|
27
29
|
components.sessions = this.checkSessions();
|
|
28
30
|
components.stateDir = this.checkStateDir();
|
|
31
|
+
components.memory = this.checkMemory();
|
|
29
32
|
if (this.scheduler) {
|
|
30
33
|
components.scheduler = this.checkScheduler();
|
|
31
34
|
}
|
|
35
|
+
if (this.watchdog) {
|
|
36
|
+
const wdStatus = this.watchdog.getStatus();
|
|
37
|
+
const intervening = wdStatus.sessions.filter(s => s.escalation && s.escalation.level > 0);
|
|
38
|
+
components.watchdog = {
|
|
39
|
+
status: intervening.length > 0 ? 'degraded' : 'healthy',
|
|
40
|
+
message: intervening.length > 0
|
|
41
|
+
? `Intervening on ${intervening.length} session(s)`
|
|
42
|
+
: `Monitoring${wdStatus.enabled ? '' : ' (disabled)'}`,
|
|
43
|
+
lastCheck: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
32
46
|
// Aggregate: worst component status becomes overall status
|
|
33
47
|
const statuses = Object.values(components).map(c => c.status);
|
|
34
48
|
let overall = 'healthy';
|
|
@@ -135,6 +149,27 @@ export class HealthChecker {
|
|
|
135
149
|
lastCheck: now,
|
|
136
150
|
};
|
|
137
151
|
}
|
|
152
|
+
checkMemory() {
|
|
153
|
+
const now = new Date().toISOString();
|
|
154
|
+
try {
|
|
155
|
+
const os = require('node:os');
|
|
156
|
+
const totalBytes = os.totalmem();
|
|
157
|
+
const freeBytes = os.freemem();
|
|
158
|
+
const totalGB = totalBytes / (1024 ** 3);
|
|
159
|
+
const freeGB = freeBytes / (1024 ** 3);
|
|
160
|
+
const usedPercent = ((totalBytes - freeBytes) / totalBytes) * 100;
|
|
161
|
+
if (usedPercent >= 90) {
|
|
162
|
+
return { status: 'unhealthy', message: `Memory critical: ${usedPercent.toFixed(0)}% used (${freeGB.toFixed(1)}GB free)`, lastCheck: now };
|
|
163
|
+
}
|
|
164
|
+
if (usedPercent >= 75) {
|
|
165
|
+
return { status: 'degraded', message: `Memory elevated: ${usedPercent.toFixed(0)}% used (${freeGB.toFixed(1)}GB free)`, lastCheck: now };
|
|
166
|
+
}
|
|
167
|
+
return { status: 'healthy', message: `${usedPercent.toFixed(0)}% used (${freeGB.toFixed(1)}GB free / ${totalGB.toFixed(0)}GB total)`, lastCheck: now };
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
return { status: 'degraded', message: `Memory check failed: ${err instanceof Error ? err.message : String(err)}`, lastCheck: now };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
138
173
|
checkStateDir() {
|
|
139
174
|
const now = new Date().toISOString();
|
|
140
175
|
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
|