instar 0.7.52 → 0.7.53
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/dashboard/index.html +843 -0
- package/dist/commands/server.js +27 -1
- package/dist/core/AutoUpdater.js +48 -16
- 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/monitoring/HealthChecker.d.ts +3 -1
- package/dist/monitoring/HealthChecker.js +15 -2
- package/dist/monitoring/SessionWatchdog.d.ts +83 -0
- package/dist/monitoring/SessionWatchdog.js +326 -0
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/WebSocketManager.d.ts +62 -0
- package/dist/server/WebSocketManager.js +288 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.js +21 -0
- package/package.json +4 -2
package/dist/commands/server.js
CHANGED
|
@@ -36,6 +36,7 @@ import { QuotaTracker } from '../monitoring/QuotaTracker.js';
|
|
|
36
36
|
import { AccountSwitcher } from '../monitoring/AccountSwitcher.js';
|
|
37
37
|
import { QuotaNotifier } from '../monitoring/QuotaNotifier.js';
|
|
38
38
|
import { classifySessionDeath } from '../monitoring/QuotaExhaustionDetector.js';
|
|
39
|
+
import { SessionWatchdog } from '../monitoring/SessionWatchdog.js';
|
|
39
40
|
import { installAutoStart } from './setup.js';
|
|
40
41
|
/**
|
|
41
42
|
* Check if autostart is installed for this project.
|
|
@@ -646,6 +647,31 @@ export async function startServer(options) {
|
|
|
646
647
|
scheduler.notifyJobComplete(session.id, session.tmuxSession);
|
|
647
648
|
});
|
|
648
649
|
}
|
|
650
|
+
// Session Watchdog — auto-remediation for stuck commands
|
|
651
|
+
let watchdog;
|
|
652
|
+
if (config.monitoring.watchdog?.enabled) {
|
|
653
|
+
watchdog = new SessionWatchdog(config, sessionManager, state);
|
|
654
|
+
watchdog.on('intervention', (event) => {
|
|
655
|
+
if (telegram) {
|
|
656
|
+
const topicId = telegram.getTopicForSession(event.sessionName);
|
|
657
|
+
if (topicId) {
|
|
658
|
+
const levelNames = ['Monitoring', 'Ctrl+C', 'SIGTERM', 'SIGKILL', 'Kill Session'];
|
|
659
|
+
const levelName = levelNames[event.level] || `Level ${event.level}`;
|
|
660
|
+
telegram.sendToTopic(topicId, `🔧 Watchdog [${levelName}]: ${event.action}\nStuck: \`${event.stuckCommand.slice(0, 60)}\``).catch(() => { });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
watchdog.on('recovery', (sessionName, fromLevel) => {
|
|
665
|
+
if (telegram) {
|
|
666
|
+
const topicId = telegram.getTopicForSession(sessionName);
|
|
667
|
+
if (topicId) {
|
|
668
|
+
telegram.sendToTopic(topicId, `✅ Watchdog: session recovered (was at escalation level ${fromLevel})`).catch(() => { });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
watchdog.start();
|
|
673
|
+
console.log(pc.green(' Session Watchdog enabled'));
|
|
674
|
+
}
|
|
649
675
|
// Set up feedback and update checking
|
|
650
676
|
let feedback;
|
|
651
677
|
if (config.feedback) {
|
|
@@ -800,7 +826,7 @@ export async function startServer(options) {
|
|
|
800
826
|
}
|
|
801
827
|
});
|
|
802
828
|
sleepWakeDetector.start();
|
|
803
|
-
const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, publisher, viewer, tunnel, evolution });
|
|
829
|
+
const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, publisher, viewer, tunnel, evolution, watchdog });
|
|
804
830
|
await server.start();
|
|
805
831
|
// Start tunnel AFTER server is listening
|
|
806
832
|
if (tunnel) {
|
package/dist/core/AutoUpdater.js
CHANGED
|
@@ -54,17 +54,11 @@ export class AutoUpdater {
|
|
|
54
54
|
if (this.interval)
|
|
55
55
|
return;
|
|
56
56
|
const intervalMs = this.config.checkIntervalMinutes * 60 * 1000;
|
|
57
|
-
//
|
|
58
|
-
// running from npx because the cache still resolves to the old version
|
|
59
|
-
// after npm installs the update. The restart finds the update again,
|
|
60
|
-
// applies it again, restarts again — forever, killing all sessions each time.
|
|
57
|
+
// Warn if running from npx cache (auto-updates won't work properly)
|
|
61
58
|
const scriptPath = process.argv[1] || '';
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.config.autoRestart = false;
|
|
66
|
-
console.warn('[AutoUpdater] Running from npx cache. Auto-apply and auto-restart disabled to prevent restart loops.\n' +
|
|
67
|
-
'[AutoUpdater] Run: npm install -g instar (then restart with: instar server start)');
|
|
59
|
+
if (scriptPath.includes('.npm/_npx') || scriptPath.includes('/_npx/')) {
|
|
60
|
+
console.warn('[AutoUpdater] WARNING: Running from npx cache. Auto-updates require a global install.\n' +
|
|
61
|
+
'[AutoUpdater] Run: npm install -g instar');
|
|
68
62
|
}
|
|
69
63
|
console.log(`[AutoUpdater] Started (every ${this.config.checkIntervalMinutes}m, ` +
|
|
70
64
|
`autoApply: ${this.config.autoApply}, autoRestart: ${this.config.autoRestart})`);
|
|
@@ -128,7 +122,8 @@ export class AutoUpdater {
|
|
|
128
122
|
if (!this.config.autoApply) {
|
|
129
123
|
// Just notify — don't apply
|
|
130
124
|
await this.notify(`Update available: v${info.currentVersion} → v${info.latestVersion}\n\n` +
|
|
131
|
-
(info.changeSummary ? `
|
|
125
|
+
(info.changeSummary ? `What changed:\n${info.changeSummary}\n\n` : '') +
|
|
126
|
+
`Details: ${info.changelogUrl || 'https://github.com/SageMindAI/instar/releases'}\n\n` +
|
|
132
127
|
`Auto-apply is disabled. Apply manually:\n` +
|
|
133
128
|
`curl -X POST http://localhost:${this.getPort()}/updates/apply`);
|
|
134
129
|
return;
|
|
@@ -153,13 +148,19 @@ export class AutoUpdater {
|
|
|
153
148
|
console.log(`[AutoUpdater] Updated: v${result.previousVersion} → v${result.newVersion}`);
|
|
154
149
|
// Step 5: Notify via Telegram
|
|
155
150
|
const restartNote = result.restartNeeded && this.config.autoRestart
|
|
156
|
-
? '
|
|
151
|
+
? '\nServer is restarting now...'
|
|
157
152
|
: result.restartNeeded
|
|
158
|
-
? '
|
|
153
|
+
? '\nA server restart is needed to use the new version.'
|
|
159
154
|
: '';
|
|
155
|
+
const changeSummary = info.changeSummary
|
|
156
|
+
? `What changed:\n${info.changeSummary}\n`
|
|
157
|
+
: '';
|
|
158
|
+
const detailsUrl = info.changelogUrl || 'https://github.com/SageMindAI/instar/releases';
|
|
160
159
|
await this.notify(`Updated: v${result.previousVersion} → v${result.newVersion}\n\n` +
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
changeSummary +
|
|
161
|
+
`Details: ${detailsUrl}\n` +
|
|
162
|
+
restartNote +
|
|
163
|
+
`\n\nTo disable auto-updates, set "autoApply": false in .instar/config.json under "updates".`);
|
|
163
164
|
// Step 6: Self-restart if needed and configured
|
|
164
165
|
if (result.restartNeeded && this.config.autoRestart) {
|
|
165
166
|
// Brief delay to let the Telegram notification send
|
|
@@ -206,6 +207,23 @@ export class AutoUpdater {
|
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
209
|
catch { /* not found globally */ }
|
|
210
|
+
// If `which instar` didn't find a global binary, try npm's prefix path directly.
|
|
211
|
+
// This handles the common case where npm's global bin directory is not in PATH
|
|
212
|
+
// (automation contexts, fresh shell sessions, custom npm prefixes).
|
|
213
|
+
if (!instarBin) {
|
|
214
|
+
try {
|
|
215
|
+
const npmPrefix = execFileSync('npm', ['prefix', '-g'], {
|
|
216
|
+
encoding: 'utf-8',
|
|
217
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
218
|
+
}).trim();
|
|
219
|
+
const candidate = `${npmPrefix}/bin/instar`;
|
|
220
|
+
if (fs.existsSync(candidate)) {
|
|
221
|
+
instarBin = candidate;
|
|
222
|
+
console.log(`[AutoUpdater] Found global binary via npm prefix: ${instarBin}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch { /* npm not available or prefix lookup failed */ }
|
|
226
|
+
}
|
|
209
227
|
let cmd;
|
|
210
228
|
if (instarBin) {
|
|
211
229
|
// Use the global binary — guaranteed to be the updated version
|
|
@@ -214,7 +232,21 @@ export class AutoUpdater {
|
|
|
214
232
|
console.log(`[AutoUpdater] Will restart from global binary: ${instarBin}`);
|
|
215
233
|
}
|
|
216
234
|
else {
|
|
217
|
-
//
|
|
235
|
+
// No global binary found. If we were running from npx cache, restarting
|
|
236
|
+
// from process.argv would loop (npx cache is the old version, which would
|
|
237
|
+
// detect the update again and restart again indefinitely).
|
|
238
|
+
const scriptPath = process.argv[1] || '';
|
|
239
|
+
const isNpxCache = scriptPath.includes('.npm/_npx') || scriptPath.includes('/_npx/');
|
|
240
|
+
if (isNpxCache) {
|
|
241
|
+
console.error('[AutoUpdater] Update applied but cannot restart — global binary not found in PATH or npm prefix.');
|
|
242
|
+
console.error('[AutoUpdater] Restarting from npx cache would cause a restart loop.');
|
|
243
|
+
console.error('[AutoUpdater] Manual restart required: npm install -g instar && instar server start');
|
|
244
|
+
void this.notify('Update applied but auto-restart skipped — global binary not in PATH.\n\n' +
|
|
245
|
+
'Run manually to activate the update:\n' +
|
|
246
|
+
'```\nnpm install -g instar\ninstar server start --foreground\n```');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Not from npx cache — safe to restart from current path
|
|
218
250
|
const args = process.argv.slice(1)
|
|
219
251
|
.map(a => `'${a.replace(/'/g, "'\\''")}'`)
|
|
220
252
|
.join(' ');
|
|
@@ -68,6 +68,12 @@ export declare class SessionManager extends EventEmitter {
|
|
|
68
68
|
* Send input to a running tmux session.
|
|
69
69
|
*/
|
|
70
70
|
sendInput(tmuxSession: string, input: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Send a tmux key sequence (without -l literal flag).
|
|
73
|
+
* Use for special keys like 'C-c' (Ctrl+C), 'Enter', 'Escape'.
|
|
74
|
+
* Unlike sendInput() which uses -l (literal), this sends key names directly.
|
|
75
|
+
*/
|
|
76
|
+
sendKey(tmuxSession: string, key: string): boolean;
|
|
71
77
|
/**
|
|
72
78
|
* List all sessions that are currently running.
|
|
73
79
|
* Pure filter — does not mutate state. The monitor tick handles lifecycle transitions.
|
|
@@ -297,6 +297,20 @@ export class SessionManager extends EventEmitter {
|
|
|
297
297
|
return false;
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Send a tmux key sequence (without -l literal flag).
|
|
302
|
+
* Use for special keys like 'C-c' (Ctrl+C), 'Enter', 'Escape'.
|
|
303
|
+
* Unlike sendInput() which uses -l (literal), this sends key names directly.
|
|
304
|
+
*/
|
|
305
|
+
sendKey(tmuxSession, key) {
|
|
306
|
+
try {
|
|
307
|
+
execFileSync(this.config.tmuxPath, ['send-keys', '-t', `=${tmuxSession}:`, key], { encoding: 'utf-8', timeout: 5000 });
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
300
314
|
/**
|
|
301
315
|
* List all sessions that are currently running.
|
|
302
316
|
* Pure filter — does not mutate state. The monitor tick handles lifecycle transitions.
|
|
@@ -464,35 +478,25 @@ export class SessionManager extends EventEmitter {
|
|
|
464
478
|
const exactTarget = `=${tmuxSession}:`;
|
|
465
479
|
try {
|
|
466
480
|
if (text.includes('\n')) {
|
|
467
|
-
// Multi-line:
|
|
481
|
+
// Multi-line: pipe into tmux load-buffer via stdin, then paste into pane.
|
|
468
482
|
// This avoids newlines being treated as Enter keypresses which would
|
|
469
483
|
// fragment the message into multiple Claude prompts.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
execFileSync(this.config.tmuxPath, ['send-keys', '-t', exactTarget, 'Enter'], {
|
|
487
|
-
encoding: 'utf-8', timeout: 5000,
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
finally {
|
|
491
|
-
try {
|
|
492
|
-
fs.unlinkSync(tmpPath);
|
|
493
|
-
}
|
|
494
|
-
catch { /* ignore */ }
|
|
495
|
-
}
|
|
484
|
+
// Uses stdin pipe (load-buffer -) instead of temp files to avoid
|
|
485
|
+
// macOS TCC "access data from other apps" permission prompts.
|
|
486
|
+
execFileSync(this.config.tmuxPath, ['load-buffer', '-'], {
|
|
487
|
+
encoding: 'utf-8', timeout: 5000, input: text,
|
|
488
|
+
});
|
|
489
|
+
execFileSync(this.config.tmuxPath, ['paste-buffer', '-t', exactTarget, '-p'], {
|
|
490
|
+
encoding: 'utf-8', timeout: 5000,
|
|
491
|
+
});
|
|
492
|
+
// Brief delay to let the terminal process the paste before sending Enter.
|
|
493
|
+
// Without this, the Enter arrives before paste processing completes and
|
|
494
|
+
// the message sits in the input buffer without being submitted.
|
|
495
|
+
execFileSync('/bin/sleep', ['0.3'], { timeout: 2000 });
|
|
496
|
+
// Send Enter to submit
|
|
497
|
+
execFileSync(this.config.tmuxPath, ['send-keys', '-t', exactTarget, 'Enter'], {
|
|
498
|
+
encoding: 'utf-8', timeout: 5000,
|
|
499
|
+
});
|
|
496
500
|
}
|
|
497
501
|
else {
|
|
498
502
|
// Single-line: simple send-keys
|
|
@@ -60,7 +60,8 @@ export declare class UpdateChecker {
|
|
|
60
60
|
updatedAt: string;
|
|
61
61
|
} | null;
|
|
62
62
|
/**
|
|
63
|
-
* Fetch human-readable changelog from GitHub releases
|
|
63
|
+
* Fetch human-readable changelog from GitHub releases, falling back to
|
|
64
|
+
* recent commit messages if no release exists for this version.
|
|
64
65
|
*/
|
|
65
66
|
fetchChangelog(version: string): Promise<string | undefined>;
|
|
66
67
|
/**
|
|
@@ -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;
|
|
@@ -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
|
*/
|
|
@@ -6,17 +6,20 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { execFileSync } from 'node:child_process';
|
|
8
8
|
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
9
10
|
import path from 'node:path';
|
|
10
11
|
export class HealthChecker {
|
|
11
12
|
config;
|
|
12
13
|
sessionManager;
|
|
13
14
|
scheduler;
|
|
15
|
+
watchdog;
|
|
14
16
|
checkInterval = null;
|
|
15
17
|
lastStatus = null;
|
|
16
|
-
constructor(config, sessionManager, scheduler = null) {
|
|
18
|
+
constructor(config, sessionManager, scheduler = null, watchdog = null) {
|
|
17
19
|
this.config = config;
|
|
18
20
|
this.sessionManager = sessionManager;
|
|
19
21
|
this.scheduler = scheduler;
|
|
22
|
+
this.watchdog = watchdog;
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
25
|
* Run all health checks and return aggregated status.
|
|
@@ -30,6 +33,17 @@ export class HealthChecker {
|
|
|
30
33
|
if (this.scheduler) {
|
|
31
34
|
components.scheduler = this.checkScheduler();
|
|
32
35
|
}
|
|
36
|
+
if (this.watchdog) {
|
|
37
|
+
const wdStatus = this.watchdog.getStatus();
|
|
38
|
+
const intervening = wdStatus.sessions.filter(s => s.escalation && s.escalation.level > 0);
|
|
39
|
+
components.watchdog = {
|
|
40
|
+
status: intervening.length > 0 ? 'degraded' : 'healthy',
|
|
41
|
+
message: intervening.length > 0
|
|
42
|
+
? `Intervening on ${intervening.length} session(s)`
|
|
43
|
+
: `Monitoring${wdStatus.enabled ? '' : ' (disabled)'}`,
|
|
44
|
+
lastCheck: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
33
47
|
// Aggregate: worst component status becomes overall status
|
|
34
48
|
const statuses = Object.values(components).map(c => c.status);
|
|
35
49
|
let overall = 'healthy';
|
|
@@ -139,7 +153,6 @@ export class HealthChecker {
|
|
|
139
153
|
checkMemory() {
|
|
140
154
|
const now = new Date().toISOString();
|
|
141
155
|
try {
|
|
142
|
-
const os = require('node:os');
|
|
143
156
|
const totalBytes = os.totalmem();
|
|
144
157
|
const freeBytes = os.freemem();
|
|
145
158
|
const totalGB = totalBytes / (1024 ** 3);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionWatchdog — Auto-remediation for stuck Claude sessions (Instar port).
|
|
3
|
+
*
|
|
4
|
+
* Detects when a Claude session has a long-running bash command and escalates
|
|
5
|
+
* from gentle (Ctrl+C) to forceful (SIGKILL + session kill). Adapted from
|
|
6
|
+
* Dawn Server's SessionWatchdog for Instar's self-contained architecture.
|
|
7
|
+
*
|
|
8
|
+
* Escalation pipeline:
|
|
9
|
+
* Level 0: Monitoring (default)
|
|
10
|
+
* Level 1: Ctrl+C via tmux send-keys
|
|
11
|
+
* Level 2: SIGTERM the stuck child PID
|
|
12
|
+
* Level 3: SIGKILL the stuck child PID
|
|
13
|
+
* Level 4: Kill tmux session
|
|
14
|
+
*/
|
|
15
|
+
import { EventEmitter } from 'node:events';
|
|
16
|
+
import type { SessionManager } from '../core/SessionManager.js';
|
|
17
|
+
import type { StateManager } from '../core/StateManager.js';
|
|
18
|
+
import type { InstarConfig } from '../core/types.js';
|
|
19
|
+
export declare enum EscalationLevel {
|
|
20
|
+
Monitoring = 0,
|
|
21
|
+
CtrlC = 1,
|
|
22
|
+
SigTerm = 2,
|
|
23
|
+
SigKill = 3,
|
|
24
|
+
KillSession = 4
|
|
25
|
+
}
|
|
26
|
+
interface EscalationState {
|
|
27
|
+
level: EscalationLevel;
|
|
28
|
+
levelEnteredAt: number;
|
|
29
|
+
stuckChildPid: number;
|
|
30
|
+
stuckCommand: string;
|
|
31
|
+
retryCount: number;
|
|
32
|
+
}
|
|
33
|
+
export interface InterventionEvent {
|
|
34
|
+
sessionName: string;
|
|
35
|
+
level: EscalationLevel;
|
|
36
|
+
action: string;
|
|
37
|
+
stuckCommand: string;
|
|
38
|
+
stuckPid: number;
|
|
39
|
+
timestamp: number;
|
|
40
|
+
}
|
|
41
|
+
export interface WatchdogEvents {
|
|
42
|
+
intervention: [event: InterventionEvent];
|
|
43
|
+
recovery: [sessionName: string, fromLevel: EscalationLevel];
|
|
44
|
+
}
|
|
45
|
+
export declare class SessionWatchdog extends EventEmitter {
|
|
46
|
+
private config;
|
|
47
|
+
private sessionManager;
|
|
48
|
+
private state;
|
|
49
|
+
private interval;
|
|
50
|
+
private escalationState;
|
|
51
|
+
private interventionHistory;
|
|
52
|
+
private enabled;
|
|
53
|
+
private running;
|
|
54
|
+
private stuckThresholdMs;
|
|
55
|
+
private pollIntervalMs;
|
|
56
|
+
constructor(config: InstarConfig, sessionManager: SessionManager, state: StateManager);
|
|
57
|
+
start(): void;
|
|
58
|
+
stop(): void;
|
|
59
|
+
setEnabled(enabled: boolean): void;
|
|
60
|
+
isEnabled(): boolean;
|
|
61
|
+
isManaging(sessionName: string): boolean;
|
|
62
|
+
getStatus(): {
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
sessions: Array<{
|
|
65
|
+
name: string;
|
|
66
|
+
escalation: EscalationState | null;
|
|
67
|
+
}>;
|
|
68
|
+
interventionHistory: InterventionEvent[];
|
|
69
|
+
};
|
|
70
|
+
private poll;
|
|
71
|
+
private checkSession;
|
|
72
|
+
private handleEscalation;
|
|
73
|
+
private getClaudePid;
|
|
74
|
+
private getChildProcesses;
|
|
75
|
+
private isExcluded;
|
|
76
|
+
private parseElapsed;
|
|
77
|
+
private sendSignal;
|
|
78
|
+
private isProcessAlive;
|
|
79
|
+
private killTmuxSession;
|
|
80
|
+
private recordIntervention;
|
|
81
|
+
}
|
|
82
|
+
export {};
|
|
83
|
+
//# sourceMappingURL=SessionWatchdog.d.ts.map
|