tickmarkr 2.6.9 → 2.7.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.
@@ -253,8 +253,8 @@ export declare function verifyIntegrationTipCached(intWt: string, commands: Reco
253
253
  type SuitePidProbe = (pid: number) => number | undefined;
254
254
  /** Count full-suite roots in one process-table snapshot. The probes are arguments so the ownership
255
255
  * rules remain testable on hosts that forbid process inspection; production supplies cwd and the
256
- * inherited TICKMARKR_SUITE_PARENT marker from the process itself. */
257
- export declare function countLiveSuites(snapshot: string, repoRoot: string, daemonPid?: number, cwdForPid?: (pid: number) => string | undefined, suiteParentForPid?: SuitePidProbe): number;
256
+ * inherited TICKMARKR_SUITE_PARENT marker from one batched async probe (suite-census.ts, queue row 103). */
257
+ export declare function countLiveSuites(snapshot: string, repoRoot: string, daemonPid: number, cwdForPid: (pid: number) => string | undefined, suiteParentForPid: SuitePidProbe): number;
258
258
  export declare const setLiveSuiteCountForTests: (probe: (repoRoot: string) => Promise<number>) => void;
259
259
  export declare const resetLiveSuiteCountForTests: () => void;
260
260
  /** Live full-suite roots attributable to this repository or this daemon. Ancestors are excluded so
@@ -9,6 +9,7 @@ import { createRequire } from "node:module";
9
9
  import { tmpdir } from "node:os";
10
10
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
+ import { batchedProcessProbes, runnerPids } from "./suite-census.js";
12
13
  import { stringify } from "yaml";
13
14
  import { BOOTSTRAP_FAILURE_RE, classifyDeadChannel, classifyTransientCapacity, trailerPattern, writePrompt } from "../adapters/prompt.js";
14
15
  import { allAdapters, getAdapter, probeAll, readDoctor, rolePools } from "../adapters/registry.js";
@@ -1243,48 +1244,14 @@ async function observeWorkerProcessTree(marker, cwd) {
1243
1244
  }
1244
1245
  return tree.size === 0 ? "empty" : "running";
1245
1246
  }
1246
- function processCwd(pid) {
1247
- try {
1248
- return realpathSync(readlinkSync(`/proc/${pid}/cwd`));
1249
- }
1250
- catch { /* Darwin has no /proc */ }
1251
- try {
1252
- const out = execFileSync("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
1253
- encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
1254
- });
1255
- const path = out.split("\n").find((line) => line.startsWith("n"))?.slice(1);
1256
- return path ? realpathSync(path) : undefined;
1257
- }
1258
- catch {
1259
- return undefined;
1260
- }
1261
- }
1262
- function processSuiteParent(pid) {
1263
- try {
1264
- const env = readFileSync(`/proc/${pid}/environ`, "utf8").split("\0");
1265
- const value = env.find((entry) => entry.startsWith(`${SUITE_PARENT_ENV}=`))?.slice(SUITE_PARENT_ENV.length + 1);
1266
- return value && /^\d+$/.test(value) ? Number(value) : undefined;
1267
- }
1268
- catch { /* Darwin has no /proc process environments */ }
1269
- try {
1270
- const out = execFileSync("ps", ["eww", "-p", String(pid), "-o", "command="], {
1271
- encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000,
1272
- });
1273
- const value = new RegExp(`(?:^|\\s)${SUITE_PARENT_ENV}=(\\d+)(?:\\s|$)`).exec(out)?.[1];
1274
- return value ? Number(value) : undefined;
1275
- }
1276
- catch {
1277
- return undefined;
1278
- }
1279
- }
1280
1247
  const pathAtOrBelow = (root, candidate) => {
1281
1248
  const rel = relative(root, candidate);
1282
1249
  return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
1283
1250
  };
1284
1251
  /** Count full-suite roots in one process-table snapshot. The probes are arguments so the ownership
1285
1252
  * rules remain testable on hosts that forbid process inspection; production supplies cwd and the
1286
- * inherited TICKMARKR_SUITE_PARENT marker from the process itself. */
1287
- export function countLiveSuites(snapshot, repoRoot, daemonPid = process.pid, cwdForPid = processCwd, suiteParentForPid = processSuiteParent) {
1253
+ * inherited TICKMARKR_SUITE_PARENT marker from one batched async probe (suite-census.ts, queue row 103). */
1254
+ export function countLiveSuites(snapshot, repoRoot, daemonPid, cwdForPid, suiteParentForPid) {
1288
1255
  const rows = [];
1289
1256
  for (const line of snapshot.split("\n")) {
1290
1257
  const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
@@ -1344,7 +1311,11 @@ export async function liveSuiteCount(repoRoot) {
1344
1311
  if (liveSuiteCountForTests)
1345
1312
  return liveSuiteCountForTests(repoRoot);
1346
1313
  const snapshot = await shGit("ps -Aww -o pid=,ppid=,state=,command=", repoRoot, 15_000);
1347
- return snapshot.code === 0 ? countLiveSuites(snapshot.stdout, repoRoot) : 0;
1314
+ if (snapshot.code !== 0)
1315
+ return 0;
1316
+ // row 103: one batched async probe per snapshot — a synchronous lsof/ps per pid held this loop 44–46 s
1317
+ const probes = await batchedProcessProbes(runnerPids(snapshot.stdout));
1318
+ return countLiveSuites(snapshot.stdout, repoRoot, process.pid, probes.cwd, probes.suiteParent);
1348
1319
  }
1349
1320
  const OBSERVE_CHUNK_BYTES = 64 * 1024;
1350
1321
  const OBSERVE_BUDGET_BYTES = 256 * 1024 * 1024;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Queue row 103 (D-1553): the live-suite census asks each runner-looking process for its cwd and its inherited
3
+ * TICKMARKR_SUITE_PARENT marker. It used to ask one pid at a time with a synchronous `lsof` and `ps`, which held the
4
+ * daemon's event loop 44–46 s under a full suite (the stall traces recorded for queue row 8). Here every pid is answered by /proc where it
5
+ * exists (Linux, child-free), else by ONE async `lsof` and ONE async `ps eww`, plus one more `lsof` for suite parents
6
+ * not already probed — the census asks for a parent's cwd too. The answers feed countLiveSuites' existing lookups.
7
+ */
8
+ export type ProbeExec = (file: string, args: readonly string[]) => Promise<string>;
9
+ /** The pids of every live (non-zombie) row of a `ps -o pid=,ppid=,state=,command=` snapshot whose command is a runner. */
10
+ export declare function runnerPids(snapshot: string): number[];
11
+ /** The /proc reads, injectable so the Linux branch is testable anywhere; each throws where /proc has no entry. */
12
+ export interface ProcReader {
13
+ cwd(pid: number): string;
14
+ environ(pid: number): string;
15
+ }
16
+ export interface CensusProbes {
17
+ cwd: (pid: number) => string | undefined;
18
+ suiteParent: (pid: number) => number | undefined;
19
+ }
20
+ export declare function batchedProcessProbes(pids: readonly number[], exec?: ProbeExec, proc?: ProcReader): Promise<CensusProbes>;
@@ -0,0 +1,100 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFileSync, readlinkSync, realpathSync } from "node:fs";
3
+ import { isRunnerCommand } from "./lease.js";
4
+ import { SUITE_PARENT_ENV } from "./git.js";
5
+ // A pid that exits between the snapshot and the probe makes lsof/ps exit non-zero while still printing every other
6
+ // row, so an ordinary non-zero exit (a numeric code, no signal) keeps whatever was printed. D-1564: a timeout, a kill, a
7
+ // spawn failure or an output-cap overflow is no answer — the base's execFileSync threw there and discarded any partial
8
+ // output, so these answer nothing even when they buffered rows first.
9
+ // D-1561: execFile can also throw SYNCHRONOUSLY (EPERM, EAGAIN under fork pressure); the base's per-pid try/catch answered
10
+ // that as "unknown", so this probe never throws or rejects either — it answers nothing.
11
+ const probeExec = (file, args) => new Promise((resolve) => {
12
+ try {
13
+ execFile(file, [...args], { encoding: "utf8", timeout: 15_000, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => resolve(error && (error.killed || error.signal || typeof error.code !== "number") ? "" : String(stdout ?? "")));
14
+ }
15
+ catch {
16
+ resolve("");
17
+ }
18
+ });
19
+ /** The pids of every live (non-zombie) row of a `ps -o pid=,ppid=,state=,command=` snapshot whose command is a runner. */
20
+ export function runnerPids(snapshot) {
21
+ const pids = [];
22
+ for (const line of snapshot.split("\n")) {
23
+ const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/.exec(line);
24
+ if (match && !match[3].startsWith("Z") && isRunnerCommand(match[4]))
25
+ pids.push(Number(match[1]));
26
+ }
27
+ return pids;
28
+ }
29
+ const procReader = {
30
+ cwd: (pid) => realpathSync(readlinkSync(`/proc/${pid}/cwd`)),
31
+ environ: (pid) => readFileSync(`/proc/${pid}/environ`, "utf8"),
32
+ };
33
+ export async function batchedProcessProbes(pids, exec = probeExec, proc = procReader) {
34
+ // the census never rejects, whatever exec does (queue row 103's closed case table, D-1561)
35
+ const ask = async (file, args) => { try {
36
+ return await exec(file, args);
37
+ }
38
+ catch {
39
+ return "";
40
+ } };
41
+ const cwds = new Map();
42
+ const parents = new Map();
43
+ const parentIn = (text) => new RegExp(`(?:^|\\s)${SUITE_PARENT_ENV}=(\\d+)(?:\\s|$)`).exec(text)?.[1];
44
+ // D-1567 batch poisoning: lsof and ps fail the WHOLE command for one -p member they cannot take (a marker of 22 digits
45
+ // prints as 1e+21, 310 nines as Infinity; ps refuses one above int32), erasing every other pid's answer. The base probed
46
+ // per pid, so such a pid lost only its own answer: it stays unanswered here and never joins a batch.
47
+ const probeable = (pid) => Number.isSafeInteger(pid) && pid > 0 && pid <= 2_147_483_647;
48
+ const probeCwds = async (wanted) => {
49
+ const rest = [];
50
+ for (const pid of wanted.filter(probeable)) {
51
+ try {
52
+ cwds.set(pid, proc.cwd(pid));
53
+ }
54
+ catch {
55
+ rest.push(pid);
56
+ } // Darwin has no /proc
57
+ }
58
+ if (rest.length === 0)
59
+ return;
60
+ let pid;
61
+ for (const line of (await ask("lsof", ["-a", "-p", rest.join(","), "-d", "cwd", "-Fn"])).split("\n")) {
62
+ if (line.startsWith("p"))
63
+ pid = Number(line.slice(1));
64
+ else if (line.startsWith("n") && pid !== undefined) {
65
+ try {
66
+ cwds.set(pid, realpathSync(line.slice(1)));
67
+ }
68
+ catch { /* the directory is gone */ }
69
+ }
70
+ }
71
+ };
72
+ const probeParents = async (wanted) => {
73
+ const rest = [];
74
+ for (const pid of wanted.filter(probeable)) {
75
+ try {
76
+ // the base's exact rule (7c8009772 daemon.ts:1418-1423, D-1558): ONE NUL-delimited entry, its WHOLE value digits —
77
+ // a joined string would let another variable's text, or a value with trailing junk, pass for the marker
78
+ const value = proc.environ(pid).split("\0").find((entry) => entry.startsWith(`${SUITE_PARENT_ENV}=`))?.slice(SUITE_PARENT_ENV.length + 1);
79
+ if (value && /^\d+$/.test(value))
80
+ parents.set(pid, Number(value));
81
+ }
82
+ catch {
83
+ rest.push(pid);
84
+ } // Darwin has no /proc process environments
85
+ }
86
+ if (rest.length === 0)
87
+ return;
88
+ for (const line of (await ask("ps", ["eww", "-p", rest.join(","), "-o", "pid=,command="])).split("\n")) {
89
+ const row = /^\s*(\d+)\s+(.*)$/.exec(line);
90
+ const value = row ? parentIn(row[2]) : undefined;
91
+ if (row && value)
92
+ parents.set(Number(row[1]), Number(value));
93
+ }
94
+ };
95
+ const unique = [...new Set(pids)];
96
+ await probeCwds(unique);
97
+ await probeParents(unique);
98
+ await probeCwds([...new Set(parents.values())].filter((pid) => !cwds.has(pid)));
99
+ return { cwd: (pid) => cwds.get(pid), suiteParent: (pid) => parents.get(pid) };
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.6.9",
3
+ "version": "2.7.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",