@volter/supercode-health 0.1.10 → 0.1.12

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 ADDED
@@ -0,0 +1,72 @@
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 { realpathSync, 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 node = /(^|\/)node$/u.test(words[0] ?? '');
19
+ let program = node ? words.slice(1).find((word) => !word.startsWith('-')) : words[0];
20
+ if (!program?.startsWith('/')) return null;
21
+ // by its real path: a link (the machine's stable program, a bin link) names an install's file under another name
22
+ try { program = realpathSync(program); } catch { /* gone: matched as named */ }
23
+ return INSTALL_FILE.test(program) ? { file: program, native: !node } : null;
24
+ }
25
+
26
+ /** The inode each native process runs (its `txt` mapping, lsof's first), by pid; empty where lsof cannot say. An
27
+ * install that replaced the file gave its path a new inode, while the process still runs the old one. */
28
+ async function runningInodes(pids) {
29
+ const inodes = new Map();
30
+ if (!pids.length) return inodes;
31
+ const answer = await run('lsof', ['-n', '-P', '-a', '-d', 'txt', '-F', 'pi', '-p', pids.join(',')], { timeoutMs: 10_000 }).catch(() => null);
32
+ let pid = null;
33
+ for (const line of String(answer?.stdout ?? '').split('\n')) {
34
+ if (line.startsWith('p')) pid = Number(line.slice(1));
35
+ else if (line.startsWith('i') && pid && !inodes.has(pid)) inodes.set(pid, Number(line.slice(1)));
36
+ }
37
+ return inodes;
38
+ }
39
+
40
+ /** `ps`'s start time (`lstart`, as `Tue Oct 6 12:09:42 2026`) in ms, or NaN. */
41
+ const startedMs = (lstart) => Date.parse(String(lstart).trim().replace(/\s+/g, ' ').replace(/^\w{3} /, ''));
42
+
43
+ /**
44
+ * The running supercode processes (on macOS and Linux) whose install file was written after they started:
45
+ * `[{ pid, command, file, startedAt, installedAt }]`, newest install first. `rows` (ps lines, for a reading without
46
+ * ps) is `pid lstart(5 words) command`.
47
+ */
48
+ export async function staleSupercode({ rows = null } = {}) {
49
+ const lines = rows ?? String((await run('ps', ['-A', '-ww', '-o', 'pid=,lstart=,command='], { timeoutMs: 10_000 })).stdout ?? '').split('\n');
50
+ const found = [];
51
+ for (const line of lines) {
52
+ const match = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(.*)$/u.exec(line);
53
+ if (!match) continue;
54
+ const [, pid, lstart, command] = match;
55
+ const program = ran(command);
56
+ if (program) found.push({ pid: Number(pid), lstart, command, ...program });
57
+ }
58
+ // a native is read by the inode it runs, where lsof says it: the strongest reading (a ctime also moves on a chmod)
59
+ const inodes = rows ? new Map() : await runningInodes(found.filter((row) => row.native).map((row) => row.pid));
60
+ const stale = [];
61
+ for (const { pid, lstart, command, file } of found) {
62
+ const started = startedMs(lstart);
63
+ let stat;
64
+ try { stat = statSync(file); } catch { continue; } // a file gone is a removed install, said elsewhere
65
+ const installed = stat.ctimeMs;
66
+ if (inodes.has(pid)) {
67
+ if (inodes.get(pid) === stat.ino) continue;
68
+ } else if (Number.isNaN(started) || installed <= started + 1000) continue; // ps says a start to the second
69
+ stale.push({ pid: Number(pid), command: command.slice(0, 200), file, startedAt: new Date(started).toISOString(), installedAt: new Date(installed).toISOString() });
70
+ }
71
+ return stale.sort((a, b) => b.installedAt.localeCompare(a.installedAt));
72
+ }
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
@@ -7,6 +7,7 @@ import { daemonCall } from './mail.mjs';
7
7
  import { rendererOrigins, originOf, localPort } from './chrome.mjs';
8
8
  import { expiredFailures, failureSummary, keyFor, Ledger, runningRelease, sayOnChanges, slowFailures } from './failures.mjs';
9
9
  import { healthDir } from './paths.mjs';
10
+ import { staleSupercode } from './drift.mjs';
10
11
  import { join } from 'node:path';
11
12
 
12
13
  const MB = 1024 * 1024;
@@ -384,6 +385,14 @@ export class Sampler {
384
385
  });
385
386
  });
386
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
+ });
387
396
  if (on.has('services')) await attempt('services', async () => {
388
397
  const list = cfg.services ?? [];
389
398
  const jobs = await this.reader.services(list.map((s) => s.label));
@@ -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.10",
3
+ "version": "0.1.12",
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"