@volter/supercode-health 0.1.9 → 0.1.11
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 +47 -0
- package/lib/features.mjs +1 -0
- package/lib/sample.mjs +13 -1
- package/lib/thresholds.mjs +2 -0
- package/package.json +3 -2
package/lib/drift.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Release drift (t_8e109edc): a supercode process still running a release an upgrade replaced. An install replaces a
|
|
2
|
+
// release's files in place, and a long-lived process (the machine daemon, harness serve, message watch, an MCP server, a
|
|
3
|
+
// hosted session's pane, a board owner) keeps the code it started with: it then speaks to processes of the new release
|
|
4
|
+
// as the old, which was behind much of 2026-10-06's trouble ("half the problems are from version mismatches").
|
|
5
|
+
// A process runs a file of a supercode install (the native, `…/@volter/supercode-cli-<os>-<cpu>/bin/supercode`, or
|
|
6
|
+
// node running a file of an `@volter/supercode*` package); when that file was written after the process started (its
|
|
7
|
+
// ctime: an extractor may set a file's mtime, never its ctime), the release on disk is not the one it runs.
|
|
8
|
+
import { statSync } from 'node:fs';
|
|
9
|
+
import { run } from './run.mjs';
|
|
10
|
+
|
|
11
|
+
/** A supercode install's file: the native, or a script of an @volter/supercode* package. */
|
|
12
|
+
const INSTALL_FILE = /^\/\S*\/node_modules\/@volter\/supercode(?:-[a-z0-9-]+)?\/\S+$/u;
|
|
13
|
+
|
|
14
|
+
/** The file a command runs: its program, or the script node runs (the first word after node's own flags); only that
|
|
15
|
+
* one, never a path in its arguments (a Claude session's `--settings` names supercode's hooks). */
|
|
16
|
+
function ran(command) {
|
|
17
|
+
const words = command.split(/\s+/u);
|
|
18
|
+
const program = /(^|\/)node$/u.test(words[0] ?? '') ? words.slice(1).find((word) => !word.startsWith('-')) : words[0];
|
|
19
|
+
return program && INSTALL_FILE.test(program) ? program : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `ps`'s start time (`lstart`, as `Tue Oct 6 12:09:42 2026`) in ms, or NaN. */
|
|
23
|
+
const startedMs = (lstart) => Date.parse(String(lstart).trim().replace(/\s+/g, ' ').replace(/^\w{3} /, ''));
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The running supercode processes (on macOS and Linux) whose install file was written after they started:
|
|
27
|
+
* `[{ pid, command, file, startedAt, installedAt }]`, newest install first. `rows` (ps lines, for a reading without
|
|
28
|
+
* ps) is `pid lstart(5 words) command`.
|
|
29
|
+
*/
|
|
30
|
+
export async function staleSupercode({ rows = null } = {}) {
|
|
31
|
+
const lines = rows ?? String((await run('ps', ['-A', '-ww', '-o', 'pid=,lstart=,command='], { timeoutMs: 10_000 })).stdout ?? '').split('\n');
|
|
32
|
+
const stale = [];
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
const match = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(.*)$/u.exec(line);
|
|
35
|
+
if (!match) continue;
|
|
36
|
+
const [, pid, lstart, command] = match;
|
|
37
|
+
const file = ran(command);
|
|
38
|
+
if (!file) continue;
|
|
39
|
+
const started = startedMs(lstart);
|
|
40
|
+
let installed;
|
|
41
|
+
try { installed = statSync(file).ctimeMs; } catch { continue; } // a file gone is a removed install, said elsewhere
|
|
42
|
+
// ps says a start to the second: a file written within that second is the same start
|
|
43
|
+
if (Number.isNaN(started) || installed <= started + 1000) continue;
|
|
44
|
+
stale.push({ pid: Number(pid), command: command.slice(0, 200), file, startedAt: new Date(started).toISOString(), installedAt: new Date(installed).toISOString() });
|
|
45
|
+
}
|
|
46
|
+
return stale.sort((a, b) => b.installedAt.localeCompare(a.installedAt));
|
|
47
|
+
}
|
package/lib/features.mjs
CHANGED
|
@@ -13,6 +13,7 @@ export const FEATURES = [
|
|
|
13
13
|
{ id: 'disk-io', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'device throughput; top writers per process' },
|
|
14
14
|
{ id: 'tcp', barrier: 'none', platforms: ['darwin', 'linux', 'win32'], reads: 'sockets per state, ephemeral ports in use, TIME_WAIT by listener and its pid' },
|
|
15
15
|
{ id: 'files', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'open files against the system limit' },
|
|
16
|
+
{ id: 'drift', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'running supercode processes whose install was replaced after they started (one ps)' },
|
|
16
17
|
{ id: 'services', barrier: 'none', platforms: ['darwin', 'linux'], reads: "configured launchd/systemd user jobs: running, last exit, log silence" },
|
|
17
18
|
{ id: 'supercode', barrier: 'integration:machine-daemon', platforms: ['darwin', 'linux', 'win32'], reads: "this machine's supercode connector: answering, server link, stuck launches" },
|
|
18
19
|
{ id: 'browser-tabs', barrier: 'integration:browser-controller', platforms: ['darwin', 'linux', 'win32'], reads: "every tab with its owner (controller holder, or the process and session serving its local port) and the renderer serving it" },
|
package/lib/sample.mjs
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import { cpus, homedir, loadavg } from 'node:os';
|
|
4
4
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
5
5
|
import { run } from './run.mjs';
|
|
6
|
+
import { daemonCall } from './mail.mjs';
|
|
6
7
|
import { rendererOrigins, originOf, localPort } from './chrome.mjs';
|
|
7
8
|
import { expiredFailures, failureSummary, keyFor, Ledger, runningRelease, sayOnChanges, slowFailures } from './failures.mjs';
|
|
8
9
|
import { healthDir } from './paths.mjs';
|
|
10
|
+
import { staleSupercode } from './drift.mjs';
|
|
9
11
|
import { join } from 'node:path';
|
|
10
12
|
|
|
11
13
|
const MB = 1024 * 1024;
|
|
@@ -383,6 +385,14 @@ export class Sampler {
|
|
|
383
385
|
});
|
|
384
386
|
});
|
|
385
387
|
|
|
388
|
+
if (on.has('drift')) await attempt('drift', async () => {
|
|
389
|
+
const stale = await staleSupercode();
|
|
390
|
+
slow.families.add('release');
|
|
391
|
+
// every stale process this pass found; one no longer found is cleared
|
|
392
|
+
slow.observations.push({ enumerated: 'release', family: 'release', by: 'suffix', entities: new Set(stale.map((r) => String(r.pid))) });
|
|
393
|
+
slow.readings.drift = stale;
|
|
394
|
+
for (const r of stale) slow.observations.push({ key: `release.drift:${r.pid}`, family: 'release', def: alarm('release.drift'), value: 1, met: { warn: true, crit: false, clears: false }, summary: `supercode process ${r.pid} runs a release replaced at ${r.installedAt} (started ${r.startedAt}): ${r.command.slice(0, 120)}`, detail: r });
|
|
395
|
+
});
|
|
386
396
|
if (on.has('services')) await attempt('services', async () => {
|
|
387
397
|
const list = cfg.services ?? [];
|
|
388
398
|
const jobs = await this.reader.services(list.map((s) => s.label));
|
|
@@ -423,7 +433,9 @@ export class Sampler {
|
|
|
423
433
|
run(bin, ['teams', 'describe'], { timeoutMs: 15_000, env: this.env }).then((r) => ({ ...r, ms: Date.now() - t0 })),
|
|
424
434
|
run(bin, ['teams', 'launches', 'list'], { timeoutMs: 45_000, env: this.env }),
|
|
425
435
|
run(bin, ['teams', 'machines', 'list', '--json'], { timeoutMs: 20_000, env: this.env }),
|
|
426
|
-
|
|
436
|
+
// the mailbox's expiries are the daemon's own records (mail step 1): asked of it, never of the native binary
|
|
437
|
+
daemonCall('harness.v1.mail.expired', {}, { env: this.env, timeoutMs: 20_000 })
|
|
438
|
+
.then((answer) => ({ stdout: JSON.stringify(answer?.rows ?? []), error: null }), (error) => ({ stdout: '', error: error.message })),
|
|
427
439
|
]);
|
|
428
440
|
let described = null; try { described = JSON.parse(describe.stdout); } catch { /* the daemon did not answer */ }
|
|
429
441
|
let data = null; try { data = JSON.parse(status.stdout).data; } catch { /* unreadable */ }
|
package/lib/thresholds.mjs
CHANGED
|
@@ -32,6 +32,8 @@ export const DEFAULTS = {
|
|
|
32
32
|
'tcp.timeWaitTarget': { warn: 25, crit: 50, forSec: 30 },
|
|
33
33
|
'files.open': { warn: 70, crit: 90, forSec: 900 },
|
|
34
34
|
'service.down': { forSec: 60 },
|
|
35
|
+
// a process on a replaced release keeps speaking to the new one as the old: said once it has run so 5 min
|
|
36
|
+
'release.drift': { forSec: 300 },
|
|
35
37
|
'supercode.connector': { forSec: 60 },
|
|
36
38
|
'supercode.launch': { stuckMin: 5, failedWithinMin: 30 },
|
|
37
39
|
// one failure that keeps happening (the same command or door with the same outcome, the same mail expiry) in the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-health",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
".": "./index.mjs",
|
|
9
9
|
"./bin": "./bin/health.mjs",
|
|
10
10
|
"./format": "./lib/format.mjs",
|
|
11
|
-
"./paths": "./lib/paths.mjs"
|
|
11
|
+
"./paths": "./lib/paths.mjs",
|
|
12
|
+
"./drift": "./lib/drift.mjs"
|
|
12
13
|
},
|
|
13
14
|
"bin": {
|
|
14
15
|
"supercode-health": "./bin/health.mjs"
|