@volter/supercode-health 0.1.14 → 0.1.16

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
@@ -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/features.mjs CHANGED
@@ -14,6 +14,7 @@ export const FEATURES = [
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
16
  { id: 'recorded', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'every command supercode recorded outside itself (harness configs, hooks baked into live sessions), one of each program and verb run as its consumer runs it' },
17
+ { id: 'markers', barrier: 'none', platforms: ['darwin', 'linux'], reads: "terminal windows and tmux processes carrying a harness's child-session mark; Claude Code on a terminal with no file in its session registry (one ps)" },
17
18
  { id: 'drift', barrier: 'none', platforms: ['darwin', 'linux'], reads: 'running supercode processes whose install was replaced after they started (one ps)' },
18
19
  { id: 'services', barrier: 'none', platforms: ['darwin', 'linux'], reads: "configured launchd/systemd user jobs: running, last exit, log silence" },
19
20
  { id: 'supercode', barrier: 'integration:machine-daemon', platforms: ['darwin', 'linux', 'win32'], reads: "this machine's supercode connector: answering, server link, stuck launches" },
@@ -0,0 +1,59 @@
1
+ // Harness marks where no session is (R75): a coding harness marks what its tools run (Claude Code's CLAUDECODE and
2
+ // CLAUDE_CODE_CHILD_SESSION), and a terminal window or tmux process that carries the marks hands them to every shell a
3
+ // person opens there. Claude Code started in such a shell takes itself for a nested session: it saves no transcript,
4
+ // writes no file in its session registry, and supercode's mail cannot tell who it is. Twelve windows on
5
+ // yuerans-macbook-pro carried the marks from 2026-10-07 to 10-09 and nothing read them. Two readings, one ps:
6
+ // - every terminal app and tmux process whose own environment holds a child-session mark with a value;
7
+ // - every Claude Code process on a terminal with no file in its session registry (`<claude home>/sessions/<pid>.json`).
8
+ // Only names and pids are kept: an environment's values never leave this function.
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { createRequire } from 'node:module';
11
+ import { homedir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ import { run, clockSeconds } from './run.mjs';
14
+
15
+ /** The marks a harness takes itself for a nested session by: the terminal SDK's list, the one home of it. */
16
+ export function childSessionMarkers() {
17
+ const file = createRequire(import.meta.url).resolve('@volter/supercode-terminal/harness-markers.json');
18
+ return JSON.parse(readFileSync(file, 'utf8')).child_session;
19
+ }
20
+
21
+ const TERMINALS = new Set(['wezterm-gui', 'wezterm', 'kitty', 'ghostty', 'alacritty', 'iTerm2', 'Terminal', 'xterm', 'tmux']);
22
+ const CLAUDE = new Set(['claude', 'claude.exe']);
23
+ const program = (command) => (command.split(/\s+/u)[0] ?? '').split('/').pop();
24
+
25
+ /** One process's environment as [name, value] pairs: Linux's own file for it; on macOS `ps -E` printed it after the command. */
26
+ function environmentOf(pid, printed) {
27
+ if (process.platform === 'linux') {
28
+ try { return readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0').map((entry) => entry.split(/=(.*)/su)); } catch { return []; }
29
+ }
30
+ return [...printed.matchAll(/(?:^|\s)([A-Za-z_][A-Za-z0-9_]*)=(\S*)/gu)].map((match) => [match[1], match[2]]);
31
+ }
32
+
33
+ /**
34
+ * { marked: [{ pid, program, markers }], unregistered: [{ pid, tty, ageSec, registry }] }. `minAgeSec` leaves a Claude
35
+ * Code process time to write its registry file.
36
+ */
37
+ export async function harnessMarks({ markers = childSessionMarkers(), minAgeSec = 120, ps = null } = {}) {
38
+ const listed = ps ?? await run('/bin/ps', [process.platform === 'linux' ? '-axww' : '-axEww', '-o', 'pid=,tty=,etime=,command='], { timeoutMs: 10_000 });
39
+ if (!listed.ok) throw new Error(`ps: ${listed.error}`);
40
+ const marked = [], unregistered = [];
41
+ for (const line of listed.stdout.split('\n')) {
42
+ const row = /^\s*(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/u.exec(line);
43
+ if (!row) continue;
44
+ const [, pid, tty, etime, command] = row, name = program(command);
45
+ if (!TERMINALS.has(name) && !CLAUDE.has(name)) continue;
46
+ const environment = new Map(environmentOf(pid, command));
47
+ if (TERMINALS.has(name)) {
48
+ const carried = markers.filter((marker) => environment.get(marker));
49
+ if (carried.length) marked.push({ pid: Number(pid), program: name, markers: carried });
50
+ continue;
51
+ }
52
+ // a Claude Code process with no terminal is a tool's own child (a subagent, a print run): nobody resumes or mails it
53
+ if (/^\?+$|^-$/u.test(tty) || clockSeconds(etime) < minAgeSec) continue;
54
+ const home = environment.get('CLAUDE_CONFIG_DIR') || join(environment.get('HOME') || homedir(), '.claude');
55
+ const registry = join(home, 'sessions', `${pid}.json`);
56
+ if (!existsSync(registry)) unregistered.push({ pid: Number(pid), tty, ageSec: clockSeconds(etime), registry });
57
+ }
58
+ return { marked, unregistered };
59
+ }
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
- const raisedNow = new Set(transitions.filter((t) => t.to !== 'none').map((t) => t.key));
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 mail = composedState ? composeState({ machine: this.machine, raised: alarms }) : compose({ machine: this.machine, transitions, reminders, raised: alarms, unknowns: unknownsDue });
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/sample.mjs CHANGED
@@ -9,6 +9,7 @@ import { expiredFailures, failureSummary, keyFor, Ledger, runningRelease, sayOnC
9
9
  import { healthDir } from './paths.mjs';
10
10
  import { pinnedHomes, staleSupercode } from './drift.mjs';
11
11
  import { brokenRecorded } from './recorded.mjs';
12
+ import { harnessMarks } from './markers.mjs';
12
13
  import { join } from 'node:path';
13
14
 
14
15
  const MB = 1024 * 1024;
@@ -408,6 +409,17 @@ export class Sampler {
408
409
  slow.readings.recorded = broken;
409
410
  for (const b of broken) slow.observations.push({ key: `recorded.broken:${b.shape}`, family: 'recorded', def: alarm('recorded.broken'), value: b.count, met: { warn: true, crit: true, clears: false }, summary: `${b.count} recorded command(s) cannot run (${b.where.slice(0, 3).join(', ')}): ${b.error}; ${b.command.slice(0, 120)}`, detail: b });
410
411
  });
412
+ if (on.has('markers')) await attempt('markers', async () => {
413
+ const def = alarm('harness.unregistered');
414
+ const { marked, unregistered } = await harnessMarks({ minAgeSec: def.minAgeSec });
415
+ slow.families.add('marked'); slow.families.add('unregistered');
416
+ // every marked window and unregistered session this pass found; one no longer found is cleared
417
+ slow.observations.push({ enumerated: 'marked', family: 'marked', by: 'suffix', entities: new Set(marked.map((m) => String(m.pid))) });
418
+ slow.observations.push({ enumerated: 'unregistered', family: 'unregistered', by: 'suffix', entities: new Set(unregistered.map((u) => String(u.pid))) });
419
+ slow.readings.markers = { marked, unregistered };
420
+ for (const m of marked) slow.observations.push({ key: `harness.marked:${m.pid}`, family: 'marked', def: alarm('harness.marked'), value: 1, met: { warn: true, crit: true, clears: false }, summary: `${m.program} ${m.pid} carries a harness's child-session mark (${m.markers.join(', ')}): Claude Code started in any shell it opens saves no transcript and gets no mail; replace that window`, detail: m });
421
+ for (const u of unregistered) slow.observations.push({ key: `harness.unregistered:${u.pid}`, family: 'unregistered', def, value: 1, met: { warn: true, crit: true, clears: false }, summary: `Claude Code ${u.pid} on ${u.tty} has no ${u.registry}: it saves nothing and no mail reaches it`, detail: u });
422
+ });
411
423
  if (on.has('services')) await attempt('services', async () => {
412
424
  const list = cfg.services ?? [];
413
425
  const jobs = await this.reader.services(list.map((s) => s.label));
@@ -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: {
@@ -38,6 +39,10 @@ export const DEFAULTS = {
38
39
  'release.pinned': { forSec: 300 },
39
40
  // a recorded command that cannot run fails its harness on every use (a hook on every tool call): said at once
40
41
  'recorded.broken': { forSec: 0 },
42
+ // a window whose shells start Claude Code as a nested session, and a session that saves nothing: said at once (a
43
+ // Claude Code process is read only once it has run minAgeSec, the time it has to write its registry file)
44
+ 'harness.marked': { forSec: 0 },
45
+ 'harness.unregistered': { forSec: 0, minAgeSec: 120 },
41
46
  'supercode.connector': { forSec: 60 },
42
47
  'supercode.launch': { stuckMin: 5, failedWithinMin: 30 },
43
48
  // 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.14",
3
+ "version": "0.1.16",
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",
@@ -10,7 +10,8 @@
10
10
  "./format": "./lib/format.mjs",
11
11
  "./paths": "./lib/paths.mjs",
12
12
  "./drift": "./lib/drift.mjs",
13
- "./recorded": "./lib/recorded.mjs"
13
+ "./recorded": "./lib/recorded.mjs",
14
+ "./markers": "./lib/markers.mjs"
14
15
  },
15
16
  "bin": {
16
17
  "supercode-health": "./bin/health.mjs"