thinknagent 0.1.22 → 0.1.24
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/lib/agent.js +17 -1
- package/lib/alerts.js +12 -10
- package/lib/connect.js +18 -0
- package/lib/history.js +143 -0
- package/lib/metrics.js +88 -4
- package/package.json +1 -1
package/lib/agent.js
CHANGED
|
@@ -6,6 +6,7 @@ const MetricsPoller = require('./metrics');
|
|
|
6
6
|
const LogWatcher = require('./logwatcher');
|
|
7
7
|
const AlertEngine = require('./alerts');
|
|
8
8
|
const ShellBridge = require('./shell');
|
|
9
|
+
const HistoryManager = require('./history');
|
|
9
10
|
const store = require('./store');
|
|
10
11
|
const chokidar = require('chokidar');
|
|
11
12
|
const fs = require('fs');
|
|
@@ -79,12 +80,16 @@ class Agent {
|
|
|
79
80
|
});
|
|
80
81
|
|
|
81
82
|
this.alerts.conn = this.conn;
|
|
83
|
+
this.history = new HistoryManager();
|
|
82
84
|
|
|
83
85
|
this.metrics = new MetricsPoller({
|
|
84
86
|
connection: this.conn,
|
|
85
87
|
gpu: cfg.gpu || false,
|
|
86
88
|
interval: cfg.interval || 1000,
|
|
87
|
-
onMetricsEmit: (payload) =>
|
|
89
|
+
onMetricsEmit: (payload) => {
|
|
90
|
+
this.alerts.evaluate(payload);
|
|
91
|
+
this.history.recordSnapshot(payload);
|
|
92
|
+
},
|
|
88
93
|
});
|
|
89
94
|
|
|
90
95
|
this.logs = new LogWatcher({
|
|
@@ -117,6 +122,17 @@ class Agent {
|
|
|
117
122
|
this.metrics.pollNow();
|
|
118
123
|
});
|
|
119
124
|
|
|
125
|
+
this.conn.socket.on('agent:get_history', ({ hours, requestId }) => {
|
|
126
|
+
const targetHours = typeof hours === 'number' ? hours : 72;
|
|
127
|
+
console.log(`[agent] Historical metrics requested (${targetHours}h)`);
|
|
128
|
+
const historyData = this.history.getHistory(targetHours);
|
|
129
|
+
this.conn.socket.emit('agent:history_data', {
|
|
130
|
+
requestId,
|
|
131
|
+
hours: targetHours,
|
|
132
|
+
history: historyData
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
120
136
|
this.conn.socket.on('agent:send_logs', () => {
|
|
121
137
|
console.log('[agent] Logs refresh requested');
|
|
122
138
|
if (this.logs && this.logs.logPaths) {
|
package/lib/alerts.js
CHANGED
|
@@ -44,7 +44,7 @@ class AlertEngine {
|
|
|
44
44
|
const elapsed = (now - state.firstTriggeredAt) / 1000; // seconds
|
|
45
45
|
if (elapsed >= rule.for && !state.fired) {
|
|
46
46
|
state.fired = true;
|
|
47
|
-
this._fire(rule, val);
|
|
47
|
+
this._fire(rule, val, metrics);
|
|
48
48
|
}
|
|
49
49
|
} else {
|
|
50
50
|
// Condition cleared
|
|
@@ -57,17 +57,19 @@ class AlertEngine {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
_fire(rule, currentValue) {
|
|
60
|
+
_fire(rule, currentValue, metrics = null) {
|
|
61
61
|
console.warn(`[alerts] FIRING: ${rule.id} — ${rule.metric} ${rule.op} ${rule.value} (current: ${currentValue})`);
|
|
62
62
|
this.conn.emit('agent:alert', {
|
|
63
|
-
id:
|
|
64
|
-
metric:
|
|
65
|
-
op:
|
|
66
|
-
threshold:
|
|
67
|
-
current:
|
|
68
|
-
severity:
|
|
69
|
-
status:
|
|
70
|
-
firedAt:
|
|
63
|
+
id: rule.id,
|
|
64
|
+
metric: rule.metric,
|
|
65
|
+
op: rule.op,
|
|
66
|
+
threshold: rule.value,
|
|
67
|
+
current: currentValue,
|
|
68
|
+
severity: rule.severity || 'warning',
|
|
69
|
+
status: 'active',
|
|
70
|
+
firedAt: Date.now(),
|
|
71
|
+
diagnostics: metrics?.diagnostics || null,
|
|
72
|
+
topProcesses: metrics?.processes?.top || [],
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
75
|
|
package/lib/connect.js
CHANGED
|
@@ -143,6 +143,24 @@ s.on('agent:approved', ({ agentToken, role, roomId }) => {
|
|
|
143
143
|
process.exit(0);
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
// ── Periodic HMAC Challenge-Response Zero-Trust Protocol ───────────────────
|
|
147
|
+
s.on('agent:auth_challenge', ({ challenge, ts }) => {
|
|
148
|
+
const cfg = store.read();
|
|
149
|
+
const token = cfg.agentToken || this.agentToken;
|
|
150
|
+
const crypto = require('crypto');
|
|
151
|
+
if (!token) return;
|
|
152
|
+
|
|
153
|
+
const raw = `${challenge}:${ts}:${cfg.agentId}`;
|
|
154
|
+
const signature = crypto.createHmac('sha256', token).update(raw).digest('hex');
|
|
155
|
+
|
|
156
|
+
s.emit('agent:auth_challenge_response', {
|
|
157
|
+
challenge,
|
|
158
|
+
ts,
|
|
159
|
+
signature,
|
|
160
|
+
agentId: cfg.agentId
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
146
164
|
s.on('connect_error', (err) => {
|
|
147
165
|
console.error(`[thinknagent] Connection error: ${err.message}`);
|
|
148
166
|
});
|
package/lib/history.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const HOME = os.homedir();
|
|
8
|
+
const CONFIG_DIR = path.join(HOME, '.thinknagent');
|
|
9
|
+
const HISTORY_DIR = path.join(CONFIG_DIR, 'history');
|
|
10
|
+
const RETENTION_DAYS = 3;
|
|
11
|
+
const SNAPSHOT_INTERVAL_MS = 30000; // Snapshot to disk every 30 seconds
|
|
12
|
+
|
|
13
|
+
class HistoryManager {
|
|
14
|
+
constructor() {
|
|
15
|
+
this._ensureDir();
|
|
16
|
+
this._lastSnapshotTime = 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_ensureDir() {
|
|
20
|
+
try {
|
|
21
|
+
if (!fs.existsSync(HISTORY_DIR)) {
|
|
22
|
+
fs.mkdirSync(HISTORY_DIR, { recursive: true, mode: 0o700 });
|
|
23
|
+
}
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.warn('[history] Failed to create history directory:', err.message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_getDateKey(timestamp = Date.now()) {
|
|
30
|
+
const d = new Date(timestamp);
|
|
31
|
+
const y = d.getUTCFullYear();
|
|
32
|
+
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
33
|
+
const day = String(d.getUTCDate()).padStart(2, '0');
|
|
34
|
+
return `${y}-${m}-${day}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_getFilePathForDate(dateKey) {
|
|
38
|
+
return path.join(HISTORY_DIR, `metrics_${dateKey}.jsonl`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Called on every metrics tick (we sample every 30s to disk)
|
|
42
|
+
recordSnapshot(metrics) {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (now - this._lastSnapshotTime < SNAPSHOT_INTERVAL_MS) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this._lastSnapshotTime = now;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const dateKey = this._getDateKey(now);
|
|
51
|
+
const filePath = this._getFilePathForDate(dateKey);
|
|
52
|
+
|
|
53
|
+
const entry = {
|
|
54
|
+
ts: now,
|
|
55
|
+
cpu: metrics.cpu?.usage ?? 0,
|
|
56
|
+
load: metrics.cpu?.loadAvg ?? 0,
|
|
57
|
+
mem: metrics.memory?.usedPct ?? 0,
|
|
58
|
+
memUsedMB: Math.round((metrics.memory?.used || 0) / (1024 * 1024)),
|
|
59
|
+
rxSec: metrics.network?.[0]?.rxSec ?? 0,
|
|
60
|
+
txSec: metrics.network?.[0]?.txSec ?? 0,
|
|
61
|
+
procs: metrics.processes?.total ?? 0,
|
|
62
|
+
diag: metrics.diagnostics?.hasSpike ? metrics.diagnostics : undefined,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Append atomic line to daily disk file (survives crashes, restarts, reboots)
|
|
66
|
+
fs.appendFileSync(filePath, JSON.stringify(entry) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
67
|
+
|
|
68
|
+
// Run daily pruning check
|
|
69
|
+
this._pruneOldFiles();
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error('[history] Error writing snapshot to disk:', err.message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Deletes any history files older than RETENTION_DAYS (3 days)
|
|
76
|
+
_pruneOldFiles() {
|
|
77
|
+
try {
|
|
78
|
+
const files = fs.readdirSync(HISTORY_DIR);
|
|
79
|
+
const cutoffTime = Date.now() - (RETENTION_DAYS * 24 * 60 * 60 * 1000);
|
|
80
|
+
const cutoffDateKey = this._getDateKey(cutoffTime);
|
|
81
|
+
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
if (!file.startsWith('metrics_') || !file.endsWith('.jsonl')) continue;
|
|
84
|
+
const fileDateKey = file.replace('metrics_', '').replace('.jsonl', '');
|
|
85
|
+
if (fileDateKey < cutoffDateKey) {
|
|
86
|
+
try {
|
|
87
|
+
fs.unlinkSync(path.join(HISTORY_DIR, file));
|
|
88
|
+
console.log(`[history] Pruned old history file: ${file}`);
|
|
89
|
+
} catch (e) {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
// ignore
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Retrieve historical data across the last N hours (up to 72 hours / 3 days)
|
|
98
|
+
getHistory(hours = 72) {
|
|
99
|
+
const minTimestamp = Date.now() - (hours * 60 * 60 * 1000);
|
|
100
|
+
const records = [];
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
if (!fs.existsSync(HISTORY_DIR)) return [];
|
|
104
|
+
|
|
105
|
+
const files = fs.readdirSync(HISTORY_DIR)
|
|
106
|
+
.filter(f => f.startsWith('metrics_') && f.endsWith('.jsonl'))
|
|
107
|
+
.sort(); // ascending date order
|
|
108
|
+
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
const fullPath = path.join(HISTORY_DIR, file);
|
|
111
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
112
|
+
const lines = content.split('\n');
|
|
113
|
+
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
if (!line.trim()) continue;
|
|
116
|
+
try {
|
|
117
|
+
const data = JSON.parse(line);
|
|
118
|
+
if (data.ts >= minTimestamp) {
|
|
119
|
+
records.push(data);
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// If more than 300 points requested, downsample evenly to keep UI charts silky smooth
|
|
126
|
+
if (records.length > 300) {
|
|
127
|
+
const step = Math.ceil(records.length / 300);
|
|
128
|
+
const sampled = [];
|
|
129
|
+
for (let i = 0; i < records.length; i += step) {
|
|
130
|
+
sampled.push(records[i]);
|
|
131
|
+
}
|
|
132
|
+
return sampled;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return records;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error('[history] Error reading history from disk:', err.message);
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = HistoryManager;
|
package/lib/metrics.js
CHANGED
|
@@ -47,6 +47,12 @@ class MetricsPoller {
|
|
|
47
47
|
si.processes(),
|
|
48
48
|
]);
|
|
49
49
|
|
|
50
|
+
// True active process memory (excluding OS buffer / page cache)
|
|
51
|
+
const realUsedBytes = (typeof mem.available === 'number' && mem.available > 0)
|
|
52
|
+
? (mem.total - mem.available)
|
|
53
|
+
: (mem.active || (mem.used - (mem.buffcache || 0)));
|
|
54
|
+
const realUsedPct = parseFloat(Math.min(100, Math.max(0, (realUsedBytes / mem.total) * 100)).toFixed(1));
|
|
55
|
+
|
|
50
56
|
const payload = {
|
|
51
57
|
ts: Date.now(),
|
|
52
58
|
cpu: {
|
|
@@ -55,10 +61,15 @@ class MetricsPoller {
|
|
|
55
61
|
loadAvg: cpu.avgLoad ?? 0,
|
|
56
62
|
},
|
|
57
63
|
memory: {
|
|
58
|
-
total:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
total: mem.total,
|
|
65
|
+
active: realUsedBytes,
|
|
66
|
+
activePct: realUsedPct,
|
|
67
|
+
buffcache: mem.buffcache || Math.max(0, mem.used - realUsedBytes),
|
|
68
|
+
rawUsed: mem.used,
|
|
69
|
+
rawUsedPct: parseFloat(((mem.used / mem.total) * 100).toFixed(1)),
|
|
70
|
+
free: mem.available || mem.free,
|
|
71
|
+
used: realUsedBytes,
|
|
72
|
+
usedPct: realUsedPct,
|
|
62
73
|
},
|
|
63
74
|
disk: disk.map(d => ({
|
|
64
75
|
fs: d.fs,
|
|
@@ -128,6 +139,79 @@ class MetricsPoller {
|
|
|
128
139
|
}
|
|
129
140
|
}
|
|
130
141
|
|
|
142
|
+
// ─── Automated Spike Diagnostic & Root Cause Engine ────────────────────────
|
|
143
|
+
const curCpuUsage = parseFloat(cpu.currentLoad.toFixed(1));
|
|
144
|
+
const curMemPct = realUsedPct;
|
|
145
|
+
|
|
146
|
+
// Filter real user/application processes with actual resource footprint
|
|
147
|
+
const sortedMemProcs = (procs.list || [])
|
|
148
|
+
.filter(p => p && p.pid > 1 && (p.pmem > 0.5 || p.pcpu > 0.5))
|
|
149
|
+
.sort((a, b) => (b.pmem || 0) - (a.pmem || 0));
|
|
150
|
+
const topMemProc = sortedMemProcs[0] || (procs.list || []).sort((a, b) => (b.pmem || 0) - (a.pmem || 0))[0] || null;
|
|
151
|
+
|
|
152
|
+
const sortedCpuProcs = (procs.list || [])
|
|
153
|
+
.filter(p => p && p.pid > 1 && (p.pcpu > 0.5 || p.pmem > 0.5))
|
|
154
|
+
.sort((a, b) => (b.pcpu || 0) - (a.pcpu || 0));
|
|
155
|
+
const topCpuProc = sortedCpuProcs[0] || (procs.list || []).sort((a, b) => (b.pcpu || 0) - (a.pcpu || 0))[0] || null;
|
|
156
|
+
|
|
157
|
+
let diagnostics = {
|
|
158
|
+
hasSpike: false,
|
|
159
|
+
type: 'nominal',
|
|
160
|
+
culprit: null,
|
|
161
|
+
deltaPct: 0,
|
|
162
|
+
explanation: 'System resource levels nominal and stable.'
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const prevMem = this._lastMemPct !== undefined ? this._lastMemPct : curMemPct;
|
|
166
|
+
const prevCpu = this._lastCpuUsage !== undefined ? this._lastCpuUsage : curCpuUsage;
|
|
167
|
+
const memDelta = parseFloat((curMemPct - prevMem).toFixed(1));
|
|
168
|
+
const cpuDelta = parseFloat((curCpuUsage - prevCpu).toFixed(1));
|
|
169
|
+
this._lastMemPct = curMemPct;
|
|
170
|
+
this._lastCpuUsage = curCpuUsage;
|
|
171
|
+
|
|
172
|
+
const topMemMB = topMemProc ? Math.round(((topMemProc.pmem || 0) / 100) * (mem.total / (1024 * 1024))) : 0;
|
|
173
|
+
const topMemPct = topMemProc ? parseFloat((topMemProc.pmem || 0).toFixed(1)) : 0;
|
|
174
|
+
const hasRealMemCulprit = topMemProc && (topMemPct >= 3.0 || topMemMB >= 35);
|
|
175
|
+
|
|
176
|
+
// Trigger spike only on sudden surge OR critically high memory with an identified culprit
|
|
177
|
+
if (memDelta >= 5.0 || (curMemPct >= 90 && hasRealMemCulprit)) {
|
|
178
|
+
diagnostics.hasSpike = true;
|
|
179
|
+
diagnostics.type = 'memory';
|
|
180
|
+
diagnostics.deltaPct = memDelta;
|
|
181
|
+
if (hasRealMemCulprit) {
|
|
182
|
+
diagnostics.culprit = {
|
|
183
|
+
pid: topMemProc.pid,
|
|
184
|
+
name: topMemProc.name,
|
|
185
|
+
command: topMemProc.command ? topMemProc.command.slice(0, 80) : topMemProc.name,
|
|
186
|
+
memPct: topMemPct,
|
|
187
|
+
memMB: topMemMB,
|
|
188
|
+
cpu: parseFloat((topMemProc.pcpu || 0).toFixed(1))
|
|
189
|
+
};
|
|
190
|
+
diagnostics.explanation = `Memory surge detected (${curMemPct}% RAM, delta: ${memDelta >= 0 ? '+' : ''}${memDelta}%). Primary consumer is "${topMemProc.name}" (PID ${topMemProc.pid}) utilizing ${topMemPct}% (${topMemMB} MB). Probable cause: Rapid memory allocation, large dataset buffer in memory, or memory leak.`;
|
|
191
|
+
} else {
|
|
192
|
+
diagnostics.explanation = `Memory load is elevated (${curMemPct}% RAM, delta: ${memDelta >= 0 ? '+' : ''}${memDelta}%), distributed across system caches and background services. No single runaway process.`;
|
|
193
|
+
}
|
|
194
|
+
} else if (cpuDelta >= 30.0 || (curCpuUsage >= 85 && topCpuProc && (topCpuProc.pcpu || 0) >= 10.0)) {
|
|
195
|
+
const topCpuVal = topCpuProc ? parseFloat((topCpuProc.pcpu || 0).toFixed(1)) : 0;
|
|
196
|
+
diagnostics.hasSpike = true;
|
|
197
|
+
diagnostics.type = 'cpu';
|
|
198
|
+
diagnostics.deltaPct = cpuDelta;
|
|
199
|
+
if (topCpuProc && topCpuVal >= 5.0) {
|
|
200
|
+
diagnostics.culprit = {
|
|
201
|
+
pid: topCpuProc.pid,
|
|
202
|
+
name: topCpuProc.name,
|
|
203
|
+
command: topCpuProc.command ? topCpuProc.command.slice(0, 80) : topCpuProc.name,
|
|
204
|
+
cpu: topCpuVal,
|
|
205
|
+
memPct: parseFloat((topCpuProc.pmem || 0).toFixed(1))
|
|
206
|
+
};
|
|
207
|
+
diagnostics.explanation = `High CPU workload spike detected (${curCpuUsage}% CPU, delta: ${cpuDelta >= 0 ? '+' : ''}${cpuDelta}%). Primary consumer is "${topCpuProc.name}" (PID ${topCpuProc.pid}) utilizing ${topCpuVal}% CPU. Probable cause: Intensive computation, complex query/loop execution, or heavy background processing.`;
|
|
208
|
+
} else {
|
|
209
|
+
diagnostics.explanation = `High CPU utilization (${curCpuUsage}% CPU, delta: ${cpuDelta >= 0 ? '+' : ''}${cpuDelta}%) spread across multiple threads.`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
payload.diagnostics = diagnostics;
|
|
214
|
+
|
|
131
215
|
this._history.push({ ts: payload.ts, cpu: payload.cpu.usage, mem: payload.memory.usedPct });
|
|
132
216
|
if (this._history.length > 60) this._history.shift();
|
|
133
217
|
payload.history = this._history;
|
package/package.json
CHANGED