agent-dag 3.2.0 → 3.3.0

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.
@@ -1069,12 +1069,19 @@ function sweepOrphanedNotes(keep) {
1069
1069
  }
1070
1070
  }
1071
1071
 
1072
- // Signal 0 delivers nothing; it only asks whether the pid could be signalled.
1073
- // EPERM is someone else's process answering, which is still a process — the
1074
- // same test sweepStaleDiscovery makes of the discovery files.
1072
+ // Signal 0 delivers nothing; it asks whether the pid could be signalled.
1073
+ //
1074
+ // BOTH ERRNOS, and the second one is the Windows spelling. POSIX `kill(2)`
1075
+ // answers EPERM for a process this account may not signal. On Windows
1076
+ // `uv_kill` calls `OpenProcess`, a denial is ERROR_ACCESS_DENIED, and libuv
1077
+ // maps that to EACCES — so a deck started from an elevated terminal, or under
1078
+ // another account, read as DEAD to every probe in this repo. What followed was
1079
+ // silent: the live deck's discovery file was unlinked on the next hook fire,
1080
+ // rewritten five seconds later by keepDiscovery, and its banner went on
1081
+ // claiming it was receiving events it had stopped receiving.
1075
1082
  function processAlive(pid) {
1076
1083
  try { process.kill(pid, 0); return true; }
1077
- catch (e) { return e?.code === "EPERM"; }
1084
+ catch (e) { return e?.code === "EPERM" || e?.code === "EACCES"; }
1078
1085
  }
1079
1086
 
1080
1087
  /**
@@ -1198,7 +1205,12 @@ export function startUpgrade({ pkgRoot, name = "agents-deck" }) {
1198
1205
  // installedVersion() disagree with the running one, and the ordinary
1199
1206
  // drift path takes it from there — including its wait for an idle moment.
1200
1207
  _upgrade = { state: "done", command, error: null, at: Date.now() };
1201
- } else if (!timedOut) {
1208
+ } else if (!timedOut && _upgrade?.state !== "failed") {
1209
+ // Not over a failure the 'error' handler already explained. A missing npm
1210
+ // emits 'error' with ENOENT and THEN 'close' with a null code, and this
1211
+ // branch used to replace "spawn npm ENOENT" with "npm exited -2" — the
1212
+ // one message that says what is wrong, overwritten by the one that does
1213
+ // not.
1202
1214
  _upgrade = { state: "failed", command, error: lastMeaningfulLine(err) || `npm exited ${code}`, at: Date.now() };
1203
1215
  }
1204
1216
  });
@@ -182,10 +182,29 @@ const C_LOCALE = { LC_ALL: "", LC_NUMERIC: "C" };
182
182
  function run(file, args, timeoutMs = 2_000) {
183
183
  return new Promise(resolve => {
184
184
  let child;
185
- try { child = spawn(file, args, { windowsHide: true, env: { ...process.env, ...C_LOCALE } }); }
185
+ try {
186
+ child = spawn(file, args, {
187
+ windowsHide: true,
188
+ env: { ...process.env, ...C_LOCALE },
189
+ // stderr is PIPED AND NEVER READ, which is a deadlock waiting for a
190
+ // chatty child: a pipe nobody drains fills at 64 KB and the writer
191
+ // blocks there until this function's own deadline kills it. Nothing
192
+ // here has ever looked at it — `run` resolves on stdout or null — so
193
+ // the honest arrangement is not to open it.
194
+ stdio: ["ignore", "pipe", "ignore"],
195
+ });
196
+ }
186
197
  catch { return resolve(null); }
187
198
  let out = "";
188
199
  const timer = setTimeout(() => { try { child.kill(); } catch {} resolve(null); }, timeoutMs);
200
+ // DECODE THE STREAM, NOT EACH CHUNK. `out += d` on a Buffer calls toString()
201
+ // per chunk, and a chunk boundary falls wherever the pipe happened to break
202
+ // — measured on `ps` output as three chunks of 8192/8192/5718 — so a
203
+ // multi-byte character split across two of them became two replacement
204
+ // characters. An application called `Яндекс Музыка` in the process list
205
+ // rendered as `Ян��екс Музыка`. setEncoding carries the partial sequence
206
+ // across the boundary, which is the whole reason it exists.
207
+ child.stdout?.setEncoding?.("utf8");
189
208
  child.stdout?.on("data", d => { out += d; });
190
209
  child.on("error", () => { clearTimeout(timer); resolve(null); });
191
210
  child.on("close", code => { clearTimeout(timer); resolve(code === 0 ? out : null); });
@@ -599,7 +618,14 @@ async function readProcessesNow(platform) {
599
618
  "-NoProfile", "-NonInteractive", "-Command",
600
619
  "Get-Process | Select-Object Id,ProcessName,CPU,@{n='WorkingSetPrivate';e={$_.PrivateMemorySize64}} | ConvertTo-Json -Compress",
601
620
  ], 6_000);
602
- if (!out) return [];
621
+ // The same shape the POSIX branch below returns, and not a bare array: the
622
+ // caller reads `read.procs.length` to decide whether this was a real
623
+ // reading, so an array meant a TypeError rather than an empty list. Every
624
+ // Windows failure — a non-zero exit, a spawn that never started, the 6s
625
+ // deadline this module's own comment says the runtime sits right on —
626
+ // therefore answered `/api/system/processes` with a 500 and a logged stack,
627
+ // every four seconds for as long as the machine panel was open.
628
+ if (!out) return { procs: [], total: 0 };
603
629
  const rows = parseGetProcessJson(out.trim(), os.totalmem());
604
630
  const now = Date.now();
605
631
  const ranked = cpuFromDeltas(rows, prevProcCpu, now - prevProcAt);
@@ -988,40 +1014,6 @@ export function zoneLabel(raw) {
988
1014
  return name || "Thermal zone";
989
1015
  }
990
1016
 
991
- /**
992
- * Windows thermal zones out of MSAcpi_ThermalZoneTemperature.
993
- *
994
- * The SECOND source, and only reachable by a deck that happens to be elevated:
995
- * root\\wmi requires administrator and refuses an ordinary terminal outright.
996
- * Kept because some boards publish a zone to ACPI and no counter, and because a
997
- * deck launched from an elevated shell can read it.
998
- *
999
- * CurrentTemperature is in tenths of a Kelvin, the same unit as the counter
1000
- * above, and reading it as anything else gives a number that is
1001
- * plausible-looking and wrong.
1002
- *
1003
- * The zone is labelled "Thermal zone" when there is one and by its own name
1004
- * when there are several, because ACPI does not say which zone is the CPU and
1005
- * this module does not guess. `TZ00` is not a friendly label; it is an honest
1006
- * one, and it only appears on a machine that has more than one.
1007
- */
1008
- export function tempFromMsAcpiJson(json) {
1009
- let rows;
1010
- try { rows = typeof json === "string" ? JSON.parse(json) : json; }
1011
- catch { return []; }
1012
- if (!rows) return [];
1013
- if (!Array.isArray(rows)) rows = [rows];
1014
- const found = [];
1015
- for (const r of rows) {
1016
- const k = Number(r?.CurrentTemperature);
1017
- if (!Number.isFinite(k)) continue;
1018
- const celsius = Math.round(k / 10 - 273.15);
1019
- if (!plausible(celsius)) continue;
1020
- found.push({ label: zoneLabel(r?.InstanceName), celsius, warnAt: WARN_C, critAt: CRIT_C });
1021
- }
1022
- if (found.length === 1) found[0].label = "Thermal zone";
1023
- return found.slice(0, 2);
1024
- }
1025
1017
 
1026
1018
  /**
1027
1019
  * The macOS rows, from whatever the three sources answered.
@@ -46,16 +46,52 @@ const DOWNLOAD_TIMEOUT_MS = 120_000;
46
46
  * Rosetta is deliberately not special-cased: an x64 Node on Apple Silicon
47
47
  * reports x64 and gets the x64 build, which runs.
48
48
  */
49
- export function assetName(platform = process.platform, arch = process.arch) {
50
- const a = { x64: "x86_64", arm64: "aarch64", ia32: "i686" }[arch];
49
+ export function assetName(platform = process.platform, arch = process.arch, libc = detectLibc(platform)) {
50
+ // `arm` is 32-bit ARM — a Raspberry Pi on the armhf image, which uv publishes
51
+ // as armv7. Left out, that machine got `unsupported_platform` for a target
52
+ // that exists.
53
+ const a = { x64: "x86_64", arm64: "aarch64", ia32: "i686", arm: "armv7" }[arch];
51
54
  if (!a) return null;
52
55
  // Apple publishes no i686 build, and never will.
53
56
  if (platform === "darwin") return a === "i686" ? null : `uv-${a}-apple-darwin.tar.gz`;
54
57
  if (platform === "win32") return `uv-${a}-pc-windows-msvc.zip`;
55
- if (platform === "linux") return `uv-${a}-unknown-linux-gnu.tar.gz`;
58
+ if (platform === "linux") {
59
+ // MUSL IS ITS OWN TARGET, and asking for the gnu build on Alpine is not a
60
+ // degraded install, it is an endless one: the glibc ELF interpreter is
61
+ // absent, so the downloaded binary cannot start, `bootstrapUv` returns
62
+ // `does_not_run`, and because ensureCswap runs whenever cswapVersion() is
63
+ // null the 35 MB archive is fetched, hashed and extracted again on EVERY
64
+ // launch — with the accounts panel permanently unavailable. `node:alpine`
65
+ // is a common enough base image for this to be somebody's whole experience
66
+ // of the deck.
67
+ if (libc === "musl") return `uv-${a}-unknown-linux-musl.tar.gz`;
68
+ return `uv-${a}-unknown-linux-gnu.tar.gz`;
69
+ }
56
70
  return null;
57
71
  }
58
72
 
73
+ /**
74
+ * "glibc" or "musl" for this process.
75
+ *
76
+ * Node's own report names the glibc it is linked against, and a musl build has
77
+ * no such field — which is the check every native-addon loader uses, and needs
78
+ * no child process. Anything unexpected reads as glibc, because that is the
79
+ * overwhelmingly common case and the wrong guess there is the state this code
80
+ * was already in.
81
+ */
82
+ export function detectLibc(platform = process.platform, report = () => process.report?.getReport?.()) {
83
+ // The question only exists on Linux. macOS and Windows have no glibc field
84
+ // either, so asking there and reading the absence as musl would answer "musl"
85
+ // for every Mac — which is why the platform comes first.
86
+ if (platform !== "linux") return "glibc";
87
+ try {
88
+ const header = report()?.header;
89
+ if (!header) return "glibc";
90
+ if (typeof header.glibcVersionRuntime === "string") return "glibc";
91
+ return "musl";
92
+ } catch { return "glibc"; }
93
+ }
94
+
59
95
  /** Path to a uv this function installed earlier, or null. */
60
96
  export function existingBootstrappedUv() {
61
97
  const bin = join(UV_DIR, process.platform === "win32" ? "uv.exe" : "uv");