@volter/supercode-health 0.1.18 → 0.1.19
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/mail.mjs +27 -0
- package/lib/promises.mjs +23 -1
- package/package.json +1 -1
- package/promises.json +2 -0
package/lib/mail.mjs
CHANGED
|
@@ -59,6 +59,33 @@ export function daemonCall(method, params, { env = process.env, timeoutMs = 15_0
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Whether this machine's daemon answers its local socket, asked from outside it: a connection and the daemon's hello.
|
|
64
|
+
* `{ state: 'answered', ms }`, `{ state: 'silent', connected, ms }` (nothing came back within `timeoutMs`; a socket
|
|
65
|
+
* whose process is alive and serving nothing still takes the connection, since the kernel holds the queue), or
|
|
66
|
+
* `{ state: 'absent', error }` (nothing listens there). No request is made, so it reads the daemon's event loop alone.
|
|
67
|
+
*/
|
|
68
|
+
export function daemonHello({ env = process.env, timeoutMs = 6_000 } = {}) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const started = performance.now(), socket = connect(daemonSocket(env));
|
|
71
|
+
let connected = false, buffer = Buffer.alloc(0);
|
|
72
|
+
const done = (value) => { clearTimeout(timer); socket.destroy(); resolve({ ...value, ms: Math.round(performance.now() - started) }); };
|
|
73
|
+
const timer = setTimeout(() => done({ state: 'silent', connected }), timeoutMs);
|
|
74
|
+
socket.on('error', (error) => done(connected ? { state: 'silent', connected, error: error.message } : { state: 'absent', error: error.code ?? error.message }));
|
|
75
|
+
socket.on('close', () => done({ state: 'silent', connected, error: 'the socket closed before the daemon answered' }));
|
|
76
|
+
socket.on('connect', () => {
|
|
77
|
+
connected = true;
|
|
78
|
+
const bytes = Buffer.from(JSON.stringify({ p: 'supercode-machine/1', nv: 3 })), head = Buffer.alloc(4);
|
|
79
|
+
head.writeUInt32BE(bytes.length, 0);
|
|
80
|
+
socket.write(Buffer.concat([head, bytes]));
|
|
81
|
+
});
|
|
82
|
+
socket.on('data', (chunk) => {
|
|
83
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
84
|
+
if (buffer.length >= 4 && buffer.length >= 4 + buffer.readUInt32BE(0)) done({ state: 'answered' });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
/** File one alarm mail for each address. Answers what was filed and the first failure, if any. */
|
|
63
90
|
export async function fileAlarm({ machine, env = process.env, to, key, subject, body }) {
|
|
64
91
|
const filed = [];
|
package/lib/promises.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { homedir, tmpdir, userInfo } from 'node:os';
|
|
|
15
15
|
import { delimiter, isAbsolute, join } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { run } from './run.mjs';
|
|
18
|
-
import { daemonCall } from './mail.mjs';
|
|
18
|
+
import { daemonCall, daemonHello } from './mail.mjs';
|
|
19
19
|
import { healthDir, machineName } from './paths.mjs';
|
|
20
20
|
import { staleSupercode } from './drift.mjs';
|
|
21
21
|
import { harnessMarks } from './markers.mjs';
|
|
@@ -41,6 +41,7 @@ export const PROMISE_DEFAULTS = Object.freeze({
|
|
|
41
41
|
diskFreeGB: 5, // extrapolated: admission's copy of an 800 MB mailbox and an install both need room
|
|
42
42
|
fleetSeenHours: 24, // a machine silent longer than this is taken to be off, not broken
|
|
43
43
|
harnesses: ['codex', 'claude'],
|
|
44
|
+
daemonAnswerMs: 6000, // extrapolated: above the longest step the daemon is known to wait on in one call (a 5 s child)
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
/** The server's machine list, asked once a pass and shared by the two promises that read it. The server alone can say
|
|
@@ -117,6 +118,27 @@ const PROBES = {
|
|
|
117
118
|
: yes(`the daemon (pid ${record.pid}) serves mail; none expired undelivered in the last ${context.lines.mailExpiryMin} min`, detail);
|
|
118
119
|
},
|
|
119
120
|
|
|
121
|
+
// The daemon is one event loop: one call the operating system does not return from stops its heartbeat, its link,
|
|
122
|
+
// its socket and its mail at once, with the process alive, so nothing that watches for an exit sees it. This probe
|
|
123
|
+
// is a process of its own and asks the socket from outside. A daemon that is not running at all is not this
|
|
124
|
+
// promise's to say (`server-sees-machine` reads false for it, and a restart passes through that state): unknown.
|
|
125
|
+
async socket(context) {
|
|
126
|
+
const record = json(join(teamsHome(context.env), 'machine.json'));
|
|
127
|
+
if (!record?.pid) return unknown('no machine daemon has a record on this machine');
|
|
128
|
+
const bound = context.lines.daemonAnswerMs;
|
|
129
|
+
const answer = await daemonHello({ env: context.env, timeoutMs: bound });
|
|
130
|
+
let alive = true; try { process.kill(record.pid, 0); } catch (error) { alive = error.code === 'EPERM'; }
|
|
131
|
+
const detail = { pid: record.pid, alive, state: answer.state, connected: answer.connected ?? null, ms: answer.ms, error: answer.error ?? null };
|
|
132
|
+
if (answer.state === 'answered') return yes(`the daemon (pid ${record.pid}) answered its socket in ${answer.ms} ms`, detail);
|
|
133
|
+
if (!alive) return unknown(`the daemon's recorded process (pid ${record.pid}) is not running`);
|
|
134
|
+
// said where it is read: the daemon carries this machine's mail, so this alarm waits in the health snapshot and
|
|
135
|
+
// log, and is filed when the daemon answers again
|
|
136
|
+
const route = 'while it does not answer it sends no heartbeat and files no mail, this alarm included: it stands in this machine\'s health snapshot and log until the daemon answers or its service is restarted';
|
|
137
|
+
return answer.state === 'silent'
|
|
138
|
+
? no(`the daemon's process (pid ${record.pid}) is alive and ${answer.connected ? 'took a connection on its socket but did not answer it' : 'did not take a connection on its socket'} within ${bound / 1000} s; ${route}`, detail)
|
|
139
|
+
: no(`the daemon's process (pid ${record.pid}) is alive and nothing listens on its socket (${answer.error}); ${route}`, detail);
|
|
140
|
+
},
|
|
141
|
+
|
|
120
142
|
// The login shell is started because only it can say its PATH (its profile builds it), and each install is asked its
|
|
121
143
|
// own version because only it knows: the two things a launch does. Codex's background server names its version in
|
|
122
144
|
// the folder it runs from, which is read from the process list.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-health",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
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",
|
package/promises.json
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"promise": "A restart of this machine's daemon would not be refused for state already standing here (the runtime, the pane wrappers, the Maildir against its retirement receipt)." },
|
|
9
9
|
{ "name": "mail-serves", "probe": "mail", "bound_ms": 1500, "activation": "report",
|
|
10
10
|
"promise": "The daemon answers on its socket, serves mail, and no mail expired undelivered in the last 15 minutes." },
|
|
11
|
+
{ "name": "daemon-answers", "probe": "socket", "bound_ms": 6500, "activation": "report",
|
|
12
|
+
"promise": "The daemon's running process answers its local socket within 6 seconds, asked by the health probe's own process: it serves, not only lives." },
|
|
11
13
|
{ "name": "harness-one-install", "probe": "harness", "bound_ms": 4000, "activation": "report",
|
|
12
14
|
"promise": "Each harness CLI on this machine (codex, claude) is one version wherever a launch could find it (the login shell's PATH, then the daemon's), and Codex's background server runs that version." },
|
|
13
15
|
{ "name": "no-harness-marks", "probe": "marks", "bound_ms": 2000, "activation": "report",
|