@volter/supercode-health 0.1.11 → 0.1.13

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 CHANGED
@@ -5,7 +5,10 @@
5
5
  // A process runs a file of a supercode install (the native, `…/@volter/supercode-cli-<os>-<cpu>/bin/supercode`, or
6
6
  // node running a file of an `@volter/supercode*` package); when that file was written after the process started (its
7
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';
8
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { homedir } from 'node:os';
11
+ import { dirname, join } from 'node:path';
9
12
  import { run } from './run.mjs';
10
13
 
11
14
  /** A supercode install's file: the native, or a script of an @volter/supercode* package. */
@@ -15,8 +18,26 @@ const INSTALL_FILE = /^\/\S*\/node_modules\/@volter\/supercode(?:-[a-z0-9-]+)?\/
15
18
  * one, never a path in its arguments (a Claude session's `--settings` names supercode's hooks). */
16
19
  function ran(command) {
17
20
  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;
21
+ const node = /(^|\/)node$/u.test(words[0] ?? '');
22
+ let program = node ? words.slice(1).find((word) => !word.startsWith('-')) : words[0];
23
+ if (!program?.startsWith('/')) return null;
24
+ // by its real path: a link (the machine's stable program, a bin link) names an install's file under another name
25
+ try { program = realpathSync(program); } catch { /* gone: matched as named */ }
26
+ return INSTALL_FILE.test(program) ? { file: program, native: !node } : null;
27
+ }
28
+
29
+ /** The inode each native process runs (its `txt` mapping, lsof's first), by pid; empty where lsof cannot say. An
30
+ * install that replaced the file gave its path a new inode, while the process still runs the old one. */
31
+ async function runningInodes(pids) {
32
+ const inodes = new Map();
33
+ if (!pids.length) return inodes;
34
+ const answer = await run('lsof', ['-n', '-P', '-a', '-d', 'txt', '-F', 'pi', '-p', pids.join(',')], { timeoutMs: 10_000 }).catch(() => null);
35
+ let pid = null;
36
+ for (const line of String(answer?.stdout ?? '').split('\n')) {
37
+ if (line.startsWith('p')) pid = Number(line.slice(1));
38
+ else if (line.startsWith('i') && pid && !inodes.has(pid)) inodes.set(pid, Number(line.slice(1)));
39
+ }
40
+ return inodes;
20
41
  }
21
42
 
22
43
  /** `ps`'s start time (`lstart`, as `Tue Oct 6 12:09:42 2026`) in ms, or NaN. */
@@ -29,19 +50,56 @@ const startedMs = (lstart) => Date.parse(String(lstart).trim().replace(/\s+/g, '
29
50
  */
30
51
  export async function staleSupercode({ rows = null } = {}) {
31
52
  const lines = rows ?? String((await run('ps', ['-A', '-ww', '-o', 'pid=,lstart=,command='], { timeoutMs: 10_000 })).stdout ?? '').split('\n');
32
- const stale = [];
53
+ const found = [];
33
54
  for (const line of lines) {
34
55
  const match = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})\s+(.*)$/u.exec(line);
35
56
  if (!match) continue;
36
57
  const [, pid, lstart, command] = match;
37
- const file = ran(command);
38
- if (!file) continue;
58
+ const program = ran(command);
59
+ if (program) found.push({ pid: Number(pid), lstart, command, ...program });
60
+ }
61
+ // a native is read by the inode it runs, where lsof says it: the strongest reading (a ctime also moves on a chmod)
62
+ const inodes = rows ? new Map() : await runningInodes(found.filter((row) => row.native).map((row) => row.pid));
63
+ const stale = [];
64
+ for (const { pid, lstart, command, file } of found) {
39
65
  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;
66
+ let stat;
67
+ try { stat = statSync(file); } catch { continue; } // a file gone is a removed install, said elsewhere
68
+ const installed = stat.ctimeMs;
69
+ if (inodes.has(pid)) {
70
+ if (inodes.get(pid) === stat.ino) continue;
71
+ } else if (Number.isNaN(started) || installed <= started + 1000) continue; // ps says a start to the second
44
72
  stale.push({ pid: Number(pid), command: command.slice(0, 200), file, startedAt: new Date(started).toISOString(), installedAt: new Date(installed).toISOString() });
45
73
  }
46
74
  return stale.sort((a, b) => b.installedAt.localeCompare(a.installedAt));
47
75
  }
76
+
77
+ /**
78
+ * Board homes served by an orchestrator other than the machine's (t_8e109edc): a home records the orchestrator and
79
+ * the supercode that serve it (`<home>/orchestrator.json`, hand-off.mjs obeys it on every command). On npm's prefix that
80
+ * path follows each upgrade in place; a home made by another install (a scratch prefix, a removed `~/.local`, a kit's
81
+ * per-version install) stays served by it until its path is gone, and then refuses. Each home this machine's daemon
82
+ * holds a board for (`<home>/board-homes.json`): `[{ home, version, entry, why }]` where its record names another
83
+ * orchestrator than the one installed beside this package, or one that is not on this machine.
84
+ */
85
+ export function pinnedHomes({ home = null } = {}) {
86
+ const supercodeHome = home ?? (process.env.SUPERCODE_HOME || (process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'supercode') : join(homedir(), '.config', 'supercode')));
87
+ let held = {};
88
+ try { held = JSON.parse(readFileSync(join(supercodeHome, 'board-homes.json'), 'utf8')) ?? {}; } catch { return []; }
89
+ let own = null;
90
+ // its package's own folder (its exports name no bin), from its main module up to its package.json
91
+ try {
92
+ let dir = dirname(fileURLToPath(import.meta.resolve('@volter/supercode-orchestrator'))); // an import-only package
93
+ while (dir !== dirname(dir) && JSON.parse((() => { try { return readFileSync(join(dir, 'package.json'), 'utf8'); } catch { return '{}'; } })()).name !== '@volter/supercode-orchestrator') dir = dirname(dir);
94
+ own = realpathSync(join(dir, 'bin', 'orchestrator.mjs'));
95
+ } catch { /* none beside this package: a checkout */ }
96
+ const pinned = [];
97
+ for (const root of new Set(Object.values(held).filter((value) => typeof value === 'string'))) {
98
+ let record = null;
99
+ try { record = JSON.parse(readFileSync(join(root, 'orchestrator.json'), 'utf8')); } catch { continue; } // no record: the machine's own serves it
100
+ let entry = null;
101
+ try { entry = realpathSync(record.entry); } catch { pinned.push({ home: root, version: record.version ?? null, entry: record.entry ?? null, why: 'its orchestrator is not on this machine' }); continue; }
102
+ if (own && entry !== own) pinned.push({ home: root, version: record.version ?? null, entry, why: `served by ${entry}, not the machine's ${own}` });
103
+ }
104
+ return pinned;
105
+ }
package/lib/sample.mjs CHANGED
@@ -7,7 +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
+ import { pinnedHomes, staleSupercode } from './drift.mjs';
11
11
  import { join } from 'node:path';
12
12
 
13
13
  const MB = 1024 * 1024;
@@ -392,6 +392,12 @@ export class Sampler {
392
392
  slow.observations.push({ enumerated: 'release', family: 'release', by: 'suffix', entities: new Set(stale.map((r) => String(r.pid))) });
393
393
  slow.readings.drift = stale;
394
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
+ // board homes served by another install's orchestrator: their own family, cleared when a home's record moves
396
+ const pinned = pinnedHomes();
397
+ slow.families.add('pinned');
398
+ slow.observations.push({ enumerated: 'pinned', family: 'pinned', by: 'suffix', entities: new Set(pinned.map((p) => p.home)) });
399
+ slow.readings.pinned = pinned;
400
+ for (const p of pinned) slow.observations.push({ key: `release.pinned:${p.home}`, family: 'pinned', def: alarm('release.pinned'), value: 1, met: { warn: true, crit: false, clears: false }, summary: `board home ${p.home} is ${p.why} (orchestrator ${p.version ?? 'unknown'})`, detail: p });
395
401
  });
396
402
  if (on.has('services')) await attempt('services', async () => {
397
403
  const list = cfg.services ?? [];
@@ -34,6 +34,8 @@ export const DEFAULTS = {
34
34
  'service.down': { forSec: 60 },
35
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
36
  'release.drift': { forSec: 300 },
37
+ // a board home its record holds to another install's orchestrator: said once it has stayed so 5 min
38
+ 'release.pinned': { forSec: 300 },
37
39
  'supercode.connector': { forSec: 60 },
38
40
  'supercode.launch': { stuckMin: 5, failedWithinMin: 30 },
39
41
  // 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.11",
3
+ "version": "0.1.13",
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",