@volter/supercode-health 0.1.14 → 0.1.15
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/drift.mjs +12 -1
- package/lib/probe.mjs +37 -3
- package/lib/thresholds.mjs +1 -0
- package/package.json +1 -1
package/lib/drift.mjs
CHANGED
|
@@ -71,7 +71,18 @@ export async function staleSupercode({ rows = null } = {}) {
|
|
|
71
71
|
} else if (Number.isNaN(started) || installed <= started + 1000) continue; // ps says a start to the second
|
|
72
72
|
stale.push({ pid: Number(pid), command: command.slice(0, 200), file, startedAt: new Date(started).toISOString(), installedAt: new Date(installed).toISOString() });
|
|
73
73
|
}
|
|
74
|
-
return stale.sort((a, b) => b.installedAt.localeCompare(a.installedAt));
|
|
74
|
+
return moved(stale).sort((a, b) => b.installedAt.localeCompare(a.installedAt));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Less the processes that answer through the release on disk: an MCP server's pump says so in `<home>/run/mcp/<pid>`
|
|
78
|
+
* once a child of the release an install put there answers for it (lib/mcp.mjs), at or after that install. */
|
|
79
|
+
function moved(stale) {
|
|
80
|
+
const home = process.env.SUPERCODE_HOME || (process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'supercode') : join(homedir(), '.config', 'supercode'));
|
|
81
|
+
return stale.filter((row) => {
|
|
82
|
+
let said = null;
|
|
83
|
+
try { said = JSON.parse(readFileSync(join(home, 'run', 'mcp', String(row.pid)), 'utf8')); } catch { return true; }
|
|
84
|
+
return !(said?.pid === row.pid && Date.parse(said.at) >= Date.parse(row.installedAt));
|
|
85
|
+
});
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
/**
|
package/lib/probe.mjs
CHANGED
|
@@ -51,6 +51,9 @@ export class Probe {
|
|
|
51
51
|
this.f = healthFiles(dir);
|
|
52
52
|
this.machine = machineName(machine || env.SUPERCODE_HEALTH_MACHINE || hostname());
|
|
53
53
|
this.state = {};
|
|
54
|
+
// what the last mail said of each alarm (key → its level, summary, detail): a mail says what changed since, never each
|
|
55
|
+
// flip in between (t_be880329: disk and memory flapping mailed the maintainer ~1,200 times a day on yuerans)
|
|
56
|
+
this.said = new Map(); this.changesMailAt = 0; this.raisedLastAt = new Map(); // key → when it was last seen raised
|
|
54
57
|
this.unknowns = {}; this.unknownMailAt = {}; // unknownMailAt: a kind of unknown (unknownKind: its key's prefix) → last mailed
|
|
55
58
|
this.lastHistory = 0;
|
|
56
59
|
this.sampleMs = null;
|
|
@@ -99,6 +102,7 @@ export class Probe {
|
|
|
99
102
|
// A raised alarm whose reading was unknown keeps its unknown since across the restart, as its unknowns entry does.
|
|
100
103
|
for (const a of (previous?.alarms ?? []).filter((x) => !x.detail?.pid || alive(x.detail.pid))) this.state[a.key] = { key: a.key, family: a.family, level: a.level, warnSince: null, critSince: null, raisedAt: a.since ? Date.parse(a.since) : Date.now(), lastMailAt: this.droppedOutbox ? 0 : Date.now(), restoredUntil: Date.now() + 2 * 60_000, value: a.value, unit: a.unit, summary: a.summary, detail: a.detail,
|
|
101
104
|
...(a.unknown?.since ? { unread: a.unknown.why, unreadAt: Date.parse(a.unknown.since) } : {}) };
|
|
105
|
+
for (const a of (previous?.alarms ?? [])) this.said.set(a.key, { level: a.level, summary: a.summary, detail: a.detail ?? null });
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
/** Remove an earlier pack's outbox; answers how many mails it held, logged by recipient. */
|
|
@@ -138,7 +142,26 @@ export class Probe {
|
|
|
138
142
|
const repeatMs = this.config.repeatMin * 60_000;
|
|
139
143
|
// A raised alarm is reminded each repeatMin after it was last said: in a mail of its own transition or reminder, never
|
|
140
144
|
// pushed back by a mail about something else.
|
|
141
|
-
|
|
145
|
+
// What changed since the last mail said it (net of every raise and clear between): said at most once each quietMin,
|
|
146
|
+
// a new crit at once, and with any mail going anyway (a reminder, an unknown due). A key that raised and cleared
|
|
147
|
+
// between two mails is never said.
|
|
148
|
+
const quietMs = (this.config.quietMin ?? 15) * 60_000;
|
|
149
|
+
const levels = new Map(alarms.map((a) => [a.key, a]));
|
|
150
|
+
const lastTransition = new Map(transitions.map((t) => [t.key, t]));
|
|
151
|
+
const net = [];
|
|
152
|
+
for (const a of alarms) {
|
|
153
|
+
const was = this.said.get(a.key)?.level ?? 'none';
|
|
154
|
+
if (was !== a.level) net.push({ key: a.key, from: was, to: a.level, value: a.value, unit: a.unit ?? null, summary: a.summary, detail: a.detail ?? null });
|
|
155
|
+
}
|
|
156
|
+
for (const a of alarms) this.raisedLastAt.set(a.key, now);
|
|
157
|
+
for (const [key, was] of this.said) {
|
|
158
|
+
if (levels.has(key)) continue;
|
|
159
|
+
// a clear is said once it has held a whole quiet window: an alarm that flaps is said raised once, never each clear
|
|
160
|
+
if (now - (this.raisedLastAt.get(key) ?? 0) < quietMs) continue;
|
|
161
|
+
const t = lastTransition.get(key);
|
|
162
|
+
net.push({ key, from: was.level, to: 'none', value: null, summary: t?.to === 'none' ? t.summary : `${was.summary} — cleared`, detail: t?.detail ?? was.detail });
|
|
163
|
+
}
|
|
164
|
+
const raisedNow = new Set(net.filter((t) => t.to !== 'none').map((t) => t.key));
|
|
142
165
|
const reminders = alarms.filter((a) => !raisedNow.has(a.key) && now - (this.state[a.key].lastMailAt ?? 0) >= repeatMs);
|
|
143
166
|
// A reading unknown for unknownMin is the maintainers' to know (raised or not), and again each repeatMin until read.
|
|
144
167
|
// Timed per kind (a key's prefix; a family's whole key): a kind is due once its earliest key has been unknown
|
|
@@ -159,14 +182,25 @@ export class Probe {
|
|
|
159
182
|
const to = subscribers(this.f, this.config);
|
|
160
183
|
// after dropping an earlier pack's undelivered mail, the first mail states the whole current state, clears included
|
|
161
184
|
const composedState = Boolean(this.droppedOutbox && to.length);
|
|
162
|
-
const
|
|
185
|
+
const changesDue = net.length > 0 && (now - this.changesMailAt >= quietMs || net.some((t) => t.to === 'crit' && t.from !== 'crit') || reminders.length > 0 || unknownsDue.length > 0);
|
|
186
|
+
const changes = changesDue ? net : [];
|
|
187
|
+
const mail = composedState ? composeState({ machine: this.machine, raised: alarms }) : compose({ machine: this.machine, transitions: changes, reminders: reminders.filter((a) => !changes.some((t) => t.key === a.key)), raised: alarms, unknowns: unknownsDue });
|
|
163
188
|
if (composedState) this.droppedOutbox = 0;
|
|
164
189
|
if (mail) {
|
|
165
190
|
// the kinds of unknown this mail said (the whole-state mail says none: they stay due)
|
|
166
191
|
if (!composedState) for (const [kind] of dueKinds) this.unknownMailAt[kind] = now;
|
|
167
192
|
// only the alarms this mail said: raised by it, or reminded (the whole-state mail says every one)
|
|
168
|
-
const said = composedState ? alarms : alarms.filter((a) => raisedNow.has(a.key) || reminders.includes(a));
|
|
193
|
+
const said = composedState ? alarms : alarms.filter((a) => (changes.length && raisedNow.has(a.key)) || reminders.includes(a));
|
|
169
194
|
for (const a of said) this.state[a.key].lastMailAt = now;
|
|
195
|
+
// the state this mail said is what the next mail is measured from: the whole state, or the last one said with this
|
|
196
|
+
// mail's changes applied. A clear still in its hold was not said, so its alarm stays said raised until a mail
|
|
197
|
+
// says the clear (#1268 review P1: replacing it with the alarms raised now lost that clear for good)
|
|
198
|
+
if (composedState) this.said = new Map(alarms.map((a) => [a.key, { level: a.level, summary: a.summary, detail: a.detail ?? null }]));
|
|
199
|
+
else for (const t of changes) {
|
|
200
|
+
if (t.to === 'none') this.said.delete(t.key);
|
|
201
|
+
else this.said.set(t.key, { level: t.to, summary: t.summary, detail: t.detail ?? null });
|
|
202
|
+
}
|
|
203
|
+
if (composedState || changes.length) this.changesMailAt = now;
|
|
170
204
|
this.log(`${mail.subject}${to.length ? '' : ' (no maintainer subscribed)'}\n${mail.body}`);
|
|
171
205
|
}
|
|
172
206
|
const snapshot = {
|
package/lib/thresholds.mjs
CHANGED
|
@@ -5,6 +5,7 @@ export const DEFAULTS = {
|
|
|
5
5
|
slowIntervalSec: 60, // disk space, services, the connector, tabs
|
|
6
6
|
footprintTopEverySec: 300, // macOS: every user's largest footprints through top (about 2 s of top per run)
|
|
7
7
|
repeatMin: 60, // a raised alarm mails again this often (netdata's repeat cap)
|
|
8
|
+
quietMin: 15, // what changed is mailed at most this often (a new crit at once): a flapping alarm is no stream
|
|
8
9
|
unknownMin: 15, // a reading unknown this long (raised or not) is mailed to the maintainers, and again each repeatMin
|
|
9
10
|
historyHours: 24,
|
|
10
11
|
daemons: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-health",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Optional machine pack: a cheap, deterministic probe of a machine's vitals that mails the machine's maintainer when a threshold is crossed. Each feature sits behind its own permission barrier.",
|
|
6
6
|
"license": "MIT",
|