tickmarkr 2.7.1 → 2.7.3
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/dist/gates/baseline.js +6 -2
- package/dist/gates/run-gates.js +3 -7
- package/dist/gates/test-manifest.d.ts +3 -0
- package/dist/gates/test-manifest.js +14 -9
- package/dist/run/daemon.d.ts +16 -1
- package/dist/run/daemon.js +25 -14
- package/dist/run/stall.js +20 -18
- package/package.json +1 -1
package/dist/gates/baseline.js
CHANGED
|
@@ -7,7 +7,7 @@ import { basename, join } from "node:path";
|
|
|
7
7
|
import { DEFAULT_EVIDENCE_QUOTA_BYTES } from "../config/config.js";
|
|
8
8
|
import { dependencyLinkRefusal, DEFAULT_SHELL_TIMEOUT_MS, describeCapacity, sameCapacity, sh } from "../run/git.js";
|
|
9
9
|
import { executionSignal } from "../run/execution-budget.js";
|
|
10
|
-
import { isVitestTestCommand, manifestFileCount } from "./test-manifest.js";
|
|
10
|
+
import { isVitestTestCommand, manifestChildEnv, manifestFileCount } from "./test-manifest.js";
|
|
11
11
|
export const EVIDENCE_TAIL_BYTES = 16 * 1024;
|
|
12
12
|
/** The default per-run quota; `gates.evidenceQuotaBytes` in config overrides it (OBS-1140). */
|
|
13
13
|
export const EVIDENCE_RUN_QUOTA_BYTES = DEFAULT_EVIDENCE_QUOTA_BYTES;
|
|
@@ -726,7 +726,11 @@ export async function captureBaseline(cwd, commands, opts = {}) {
|
|
|
726
726
|
continue;
|
|
727
727
|
}
|
|
728
728
|
const evidence = beginGateEvidence(cwd, name, cmd, opts.evidence);
|
|
729
|
-
|
|
729
|
+
// Queue row 104 (D-1644): a manifested test command is captured under the test gate's own runner environment
|
|
730
|
+
// (node_modules/.bin first), so a bare repo-local runner resolves here exactly as it does in the gate — built on the
|
|
731
|
+
// caller's own evidence environment when it supplies one (D-1659), never in place of it.
|
|
732
|
+
const env = (name === "test" || name === "tipTest") && isVitestTestCommand(cmd, cwd) ? manifestChildEnv(cwd, opts.evidence?.env) : opts.evidence?.env;
|
|
733
|
+
const r = await sh(cmd, cwd, CAPTURE_CEILING_MS, { env, onReceipt: receipt => { evidence.observe(receipt); opts.onReceipt?.(receipt); } });
|
|
730
734
|
(base.evidenceReceipts ??= {})[name] = evidence.finish(r.stdout, r.stderr);
|
|
731
735
|
// ponytail: strip the executing cwd so repo-root capture and worktree compare fingerprint identically; /private-vs-/tmp symlink variance is out of scope
|
|
732
736
|
// ponytail: a capture that was itself killed records the ceiling as its "measurement", which
|
package/dist/gates/run-gates.js
CHANGED
|
@@ -15,11 +15,11 @@ import { disallowedBy, observedSeat } from "../route/preference.js";
|
|
|
15
15
|
import { marginalCostRank } from "../route/router.js";
|
|
16
16
|
import { carriedAuthorVendors, gateReviewerFloor, pickReviewer, reviewGate } from "./review.js";
|
|
17
17
|
import { scopeGate } from "./scope.js";
|
|
18
|
-
import { discoverTestManifest, evaluateManifestedTest, isVitestTestCommand,
|
|
18
|
+
import { discoverTestManifest, evaluateManifestedTest, isVitestTestCommand, manifestChildEnv } from "./test-manifest.js";
|
|
19
19
|
import { RUNNER_INFRA_DIAGNOSTIC_RE, timeoutShaped } from "./timeout-shaped.js";
|
|
20
20
|
import { executionSignal } from "../run/execution-budget.js";
|
|
21
21
|
import { failureDisposition } from "../run/recovery.js";
|
|
22
|
-
import { dependencyLinkRefusal,
|
|
22
|
+
import { dependencyLinkRefusal, preserveWorktree, producerFields, readCapacity, shGit, resolvedCapacity, sameCapacity, verificationProtocol } from "../run/git.js";
|
|
23
23
|
import { withJudgeInvocationEvidence } from "../run/journal.js";
|
|
24
24
|
import { computeVerificationIdentity, verificationIdentityKey, formatReusedRow, getVerdictStore, isInfraResult, resolveStateDir, reusedIdentity, } from "./cache.js";
|
|
25
25
|
const productionLoadProvider = () => loadavg()[0] ?? 0;
|
|
@@ -285,11 +285,7 @@ export function diagnosticAdmission(baseline, selected) {
|
|
|
285
285
|
* discovery receives (test-manifest.ts manifestEnvironment), so it compares with the one a verdict
|
|
286
286
|
* certified. Undefined when the runner cannot list — an unlisted manifest certifies nothing. */
|
|
287
287
|
async function listFullManifest(cmd, worktree) {
|
|
288
|
-
const env =
|
|
289
|
-
[VITEST_CACHE_ENV]: worktreeVitestCache(worktree),
|
|
290
|
-
[FORK_CAP_ENV]: String(resolvedCapacity().forkCap), [SUITE_PARENT_ENV]: String(process.pid) };
|
|
291
|
-
for (const key of [...ROUTING_ENV_SEAMS, "VITEST", "TEST", "VITEST_WORKER_ID", "VITEST_POOL_ID"])
|
|
292
|
-
delete env[key];
|
|
288
|
+
const env = manifestChildEnv(worktree);
|
|
293
289
|
const dir = mkdtempSync(join(tmpdir(), "tickmarkr-full-manifest-"));
|
|
294
290
|
try {
|
|
295
291
|
return (await discoverTestManifest(cmd, worktree, { dir, nonce: randomUUID(), env })).files;
|
|
@@ -206,6 +206,9 @@ export interface ManifestGateOutcome {
|
|
|
206
206
|
exitCode: number;
|
|
207
207
|
reportPath: string;
|
|
208
208
|
}
|
|
209
|
+
/** Queue row 104 (D-1644): THE child environment of a manifested test command — its listing and run here, the full
|
|
210
|
+
* manifest re-listing (run-gates.ts) and its baseline capture (baseline.ts) — so every one resolves the same runner. */
|
|
211
|
+
export declare function manifestChildEnv(cwd: string, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
209
212
|
export interface DiscoveredManifest {
|
|
210
213
|
files: string[];
|
|
211
214
|
listing: string;
|
|
@@ -504,17 +504,22 @@ export function runManifestedTest(cmd, cwd, opts) {
|
|
|
504
504
|
* `npm_config_ignore_scripts` from the environment over every npmrc); the verdict records it so a
|
|
505
505
|
* verdict measured with `pretest` hooks is never compared to one without. */
|
|
506
506
|
function manifestEnvironment(cwd) {
|
|
507
|
-
const env =
|
|
507
|
+
const env = manifestBaseEnv(cwd);
|
|
508
|
+
return { env: scrubbedManifestEnv(env), verification: verificationProtocol(env, cwd) };
|
|
509
|
+
}
|
|
510
|
+
function manifestBaseEnv(cwd, base = process.env) {
|
|
511
|
+
return { ...base, PATH: `${join(cwd, "node_modules/.bin")}:${base.PATH ?? ""}`,
|
|
508
512
|
[VITEST_CACHE_ENV]: worktreeVitestCache(cwd),
|
|
509
513
|
[FORK_CAP_ENV]: String(resolvedCapacity().forkCap), [SUITE_PARENT_ENV]: String(process.pid) };
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
514
|
+
}
|
|
515
|
+
// A gate can itself be tested under Vitest. Do not give the child the outer worker identity.
|
|
516
|
+
const OUTER_RUNNER_KEYS = new Set([...ROUTING_ENV_SEAMS, "VITEST", "TEST", "VITEST_WORKER_ID", "VITEST_POOL_ID"]);
|
|
517
|
+
const scrubbedManifestEnv = (env) => Object.fromEntries(Object.entries(env).filter(([key]) => !OUTER_RUNNER_KEYS.has(key)));
|
|
518
|
+
/** Queue row 104 (D-1644): THE child environment of a manifested test command — its listing and run here, the full
|
|
519
|
+
* manifest re-listing (run-gates.ts) and its baseline capture (baseline.ts) — so every one resolves the same runner. */
|
|
520
|
+
export function manifestChildEnv(cwd, base) {
|
|
521
|
+
// built on `base` when a caller supplies its own environment (captureBaseline's evidence env), else on process.env
|
|
522
|
+
return scrubbedManifestEnv(manifestBaseEnv(cwd, base));
|
|
518
523
|
}
|
|
519
524
|
/** OBS-1044: THE discovery seam — the files the runner would collect under this exact invocation,
|
|
520
525
|
* from its own listing and never from a stdout summary. The gate and the baseline capture share it,
|
package/dist/run/daemon.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type DriverChoice } from "../drivers/index.js";
|
|
|
4
4
|
import { type ExecutorDriver, type Slot } from "../drivers/types.js";
|
|
5
5
|
import { type Baseline, type BaselineProvenance, type GateEvidenceOptions } from "../gates/baseline.js";
|
|
6
6
|
import type { GateResult } from "../gates/types.js";
|
|
7
|
-
import { type RunGraph } from "../graph/schema.js";
|
|
7
|
+
import { type RunGraph, type Task } from "../graph/schema.js";
|
|
8
8
|
import { type PreserveProducer } from "./git.js";
|
|
9
9
|
import { Journal, type JournalEvent } from "./journal.js";
|
|
10
10
|
export declare function closeLiveSlot(liveSlots: Set<Slot>, driver: Pick<ExecutorDriver, "close">, slot: Slot): Promise<void>;
|
|
@@ -144,6 +144,19 @@ export declare const gateSatisfied: (g: GateResult) => boolean;
|
|
|
144
144
|
* preference (ranked last, never excluded) — every review-pool-demotion except the launch-caused ones, which
|
|
145
145
|
* never demote a seat (live or replayed). plan.ts previews a resume with this same fold. */
|
|
146
146
|
export declare function replayedReviewerDemotions(events: readonly JournalEvent[]): Set<string>;
|
|
147
|
+
/** C: a journaled test red whose failing-file attribution is nonempty, trustworthy and complete (repair
|
|
148
|
+
* selection's own rule: a behavioral red on a real subject from a full suite or a screen that names
|
|
149
|
+
* every failure), POSITIVELY certified complete by its runner, uncontradicted by the files its own text
|
|
150
|
+
* names, and wholly outside a declared files[]. Empty files[] is unrestricted and never qualifies;
|
|
151
|
+
* every other shape keeps its replay. */
|
|
152
|
+
export declare function outsideScopeRed(t: Task, taskId: string, data: Record<string, unknown>, commit: string): boolean;
|
|
153
|
+
/** OBS-1106 residual, D-607: what ONE isolated rerun of a timeout-shaped red's attributed `files` says about that
|
|
154
|
+
* red. Lifted unchanged out of adjudicateInfraShapedRed (queue row 102) so its closed outcome table runs on this
|
|
155
|
+
* function in-process. Pure: it reads only its arguments; the caller classifies the rerun first. */
|
|
156
|
+
export declare function reobservationOutcome(files: readonly string[], rerun: GateResult): {
|
|
157
|
+
outcome: "reproduced" | "passed" | "ambiguous";
|
|
158
|
+
selection: string[] | undefined;
|
|
159
|
+
};
|
|
147
160
|
/**
|
|
148
161
|
* T4 (OBS-265): the journal with the review objections a round did NOT hinge on removed. Judge and
|
|
149
162
|
* review are now launched together, so a round can journal a failed review that the serial walk would
|
|
@@ -250,6 +263,8 @@ export declare function verifyIntegrationTipCached(intWt: string, commands: Reco
|
|
|
250
263
|
signal?: AbortSignal;
|
|
251
264
|
evidence?: GateEvidenceOptions;
|
|
252
265
|
}): Promise<boolean>;
|
|
266
|
+
type WorkerProcessTree = "empty" | "running" | "unmeasurable";
|
|
267
|
+
export declare function observeWorkerProcessTree(marker: string, cwd: string): Promise<WorkerProcessTree>;
|
|
253
268
|
type SuitePidProbe = (pid: number) => number | undefined;
|
|
254
269
|
/** Count full-suite roots in one process-table snapshot. The probes are arguments so the ownership
|
|
255
270
|
* rules remain testable on hosts that forbid process inspection; production supplies cwd and the
|
package/dist/run/daemon.js
CHANGED
|
@@ -34,7 +34,7 @@ import { executionSignal, remainingExecutionMs, withExecutionBudget, withoutExec
|
|
|
34
34
|
import { repairSelectionDecision, repairSelectionEnabled } from "./repair-selection.js";
|
|
35
35
|
import { failureDisposition, reserveInfrastructureRetry } from "./recovery.js";
|
|
36
36
|
import { runEnvironment } from "./environment.js";
|
|
37
|
-
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
37
|
+
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
38
38
|
import { MASK } from "./redact.js";
|
|
39
39
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
40
40
|
import { classifyRepairDisposition, resolveScopeHints } from "./repair-disposition.js";
|
|
@@ -409,7 +409,7 @@ const RUNNER_CERTIFICATE_RE = /runner-level diagnostic: never-started ([^;\s]+);
|
|
|
409
409
|
* every failure), POSITIVELY certified complete by its runner, uncontradicted by the files its own text
|
|
410
410
|
* names, and wholly outside a declared files[]. Empty files[] is unrestricted and never qualifies;
|
|
411
411
|
* every other shape keeps its replay. */
|
|
412
|
-
function outsideScopeRed(t, taskId, data, commit) {
|
|
412
|
+
export function outsideScopeRed(t, taskId, data, commit) {
|
|
413
413
|
if (!t.files.length || data.gate !== "test" || data.pass !== false || data.skipped === true || data.commit !== commit
|
|
414
414
|
// a failed producer — an infra row, a dirty-tree refusal, a recovery the runner could not finish —
|
|
415
415
|
// never attributes anything, whatever files it lists
|
|
@@ -456,6 +456,21 @@ const observedSelection = (g) => {
|
|
|
456
456
|
/** D-607: on the manifest path only the reporter's own attribution counts — a `FAIL path` line inside
|
|
457
457
|
* captured stdout is text, not attribution. Headline lexing is for runners without a reporter. */
|
|
458
458
|
const rerunFailingFiles = (g) => metaFiles(g.meta?.manifest) ? [...new Set(metaFiles(g.meta?.failingFiles) ?? [])].sort() : attributedFailingFiles(g);
|
|
459
|
+
/** OBS-1106 residual, D-607: what ONE isolated rerun of a timeout-shaped red's attributed `files` says about that
|
|
460
|
+
* red. Lifted unchanged out of adjudicateInfraShapedRed (queue row 102) so its closed outcome table runs on this
|
|
461
|
+
* function in-process. Pure: it reads only its arguments; the caller classifies the rerun first. */
|
|
462
|
+
export function reobservationOutcome(files, rerun) {
|
|
463
|
+
// Reproduced only by failure evidence the rerun ATTRIBUTES to one of exactly these files, from an
|
|
464
|
+
// OBSERVED selection that was exactly these files (D-607): a path merely printed in output, a
|
|
465
|
+
// positional filter that also collected another file, or a runner that ignored the file
|
|
466
|
+
// arguments and reported no manifest of its selection, says nothing about the original red.
|
|
467
|
+
const failing = rerunFailingFiles(rerun);
|
|
468
|
+
const selection = observedSelection(rerun);
|
|
469
|
+
const isolated = selection !== undefined && selection.length === files.length
|
|
470
|
+
&& files.every((file) => selection.includes(file)) && failing.every((file) => files.includes(file));
|
|
471
|
+
const outcome = rerun.pass ? "passed" : isolated && !isInfraResult(rerun) && failing.length ? "reproduced" : "ambiguous";
|
|
472
|
+
return { outcome, selection };
|
|
473
|
+
}
|
|
459
474
|
// v1.85 T3: the gates whose failure IS a deterministic measurement — a machine re-ran a command over a
|
|
460
475
|
// tree and printed the same bytes. Those are the failures the fingerprint cap governs (the ruling names
|
|
461
476
|
// it a "deterministic-gate" cap): a third identical answer to a question already answered twice is the
|
|
@@ -1222,8 +1237,9 @@ async function gateCommitSubject(base, head, wt) {
|
|
|
1222
1237
|
// OBS-737 needs. A readable process table with no marker root is measured empty; a failed or empty
|
|
1223
1238
|
// table is unmeasurable. Descendants are closed over PPID because not every child retains the
|
|
1224
1239
|
// dispatch-script marker in its own argv.
|
|
1225
|
-
async function observeWorkerProcessTree(marker, cwd) {
|
|
1226
|
-
|
|
1240
|
+
export async function observeWorkerProcessTree(marker, cwd) {
|
|
1241
|
+
// queue row 113: `ps` is not git, so the payload entry — the own-git entry's pin probes are two SYNC git spawns per call
|
|
1242
|
+
const snapshot = await shell("ps -Awwo pid=,ppid=,command=", cwd, 15_000);
|
|
1227
1243
|
if (snapshot.code !== 0)
|
|
1228
1244
|
return "unmeasurable";
|
|
1229
1245
|
const rows = [];
|
|
@@ -1312,7 +1328,8 @@ export const resetLiveSuiteCountForTests = () => { liveSuiteCountForTests = unde
|
|
|
1312
1328
|
export async function liveSuiteCount(repoRoot) {
|
|
1313
1329
|
if (liveSuiteCountForTests)
|
|
1314
1330
|
return liveSuiteCountForTests(repoRoot);
|
|
1315
|
-
|
|
1331
|
+
// queue row 113: `ps` is not git, so the payload entry — the own-git entry's pin probes are two SYNC git spawns per call
|
|
1332
|
+
const snapshot = await shell("ps -Aww -o pid=,ppid=,state=,command=", repoRoot, 15_000);
|
|
1316
1333
|
if (snapshot.code !== 0)
|
|
1317
1334
|
return 0;
|
|
1318
1335
|
// row 103: one batched async probe per snapshot — a synchronous lsof/ps per pid held this loop 44–46 s
|
|
@@ -3080,15 +3097,9 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3080
3097
|
rerun = await withCommandContext(t.id, () => reobserveTestFiles(wt, commands.test, baseline, files, journal.dir))
|
|
3081
3098
|
.catch((error) => ({ gate: "test", pass: false, details: `infra: rerun failed: ${error instanceof Error ? error.message : String(error)}`, meta: { infra: true } }));
|
|
3082
3099
|
classifyInfraResult(rerun);
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
// arguments and reported no manifest of its selection, says nothing about the original red.
|
|
3087
|
-
const failing = rerunFailingFiles(rerun);
|
|
3088
|
-
selection = observedSelection(rerun);
|
|
3089
|
-
const isolated = selection !== undefined && selection.length === files.length
|
|
3090
|
-
&& files.every((file) => selection.includes(file)) && failing.every((file) => files.includes(file));
|
|
3091
|
-
outcome = rerun.pass ? "passed" : isolated && !isInfraResult(rerun) && failing.length ? "reproduced" : "ambiguous";
|
|
3100
|
+
const judged = reobservationOutcome(files, rerun);
|
|
3101
|
+
outcome = judged.outcome;
|
|
3102
|
+
selection = judged.selection;
|
|
3092
3103
|
}
|
|
3093
3104
|
journal.append("gate-reobserved", t.id, {
|
|
3094
3105
|
gate: "test", attempt, commit: subject, fingerprint, ...(observation ? { observation } : {}), files, outcome,
|
package/dist/run/stall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { isAbsolute, relative, sep } from "node:path";
|
|
4
|
-
import { activeShellIdentities, processIdentity, resolveActiveShellIdentities,
|
|
4
|
+
import { activeShellIdentities, processIdentity, resolveActiveShellIdentities, shell, SUITE_PARENT_ENV } from "./git.js";
|
|
5
5
|
// OBS-82: normalize known presentation tokens before measuring transcript extent or filtering an
|
|
6
6
|
// LLM-bound transcript. This remains a closed allowlist — ANSI/VT escapes, braille-range spinner
|
|
7
7
|
// glyphs, and elapsed-time tokens bound to time-unit suffixes. Every other byte passes through
|
|
@@ -71,7 +71,8 @@ export async function reapOwnedProcessGroup(group, cwd, ownership) {
|
|
|
71
71
|
return reapWorkerProcesses(group, cwd, ownership);
|
|
72
72
|
if (group === undefined)
|
|
73
73
|
return null;
|
|
74
|
-
|
|
74
|
+
// queue row 113: `ps` is not git, so the payload entry — the own-git entry's pin probes are two SYNC git spawns per call
|
|
75
|
+
const own = await shell(`ps -o pgid= -p ${process.pid}`, cwd, 5_000);
|
|
75
76
|
// An in-process driver fixture (or a non-isolating host) can share the daemon's
|
|
76
77
|
// group. It is not an owned worker group: let the driver's close retire that slot.
|
|
77
78
|
if (own.code !== 0 || !/^\d+$/.test(own.stdout.trim()) || Number(own.stdout.trim()) === group)
|
|
@@ -87,7 +88,7 @@ export async function reapOwnedProcessGroup(group, cwd, ownership) {
|
|
|
87
88
|
// SIGKILL delivery and process retirement are asynchronous. Bound the confirmation
|
|
88
89
|
// grace, retaining names if a process is still present after it.
|
|
89
90
|
for (let probe = 0; probe < 5; probe++) {
|
|
90
|
-
const snapshot = await
|
|
91
|
+
const snapshot = await shell("ps -Awwo pid=,pgid=,stat=", cwd, 5_000);
|
|
91
92
|
if (snapshot.code !== 0)
|
|
92
93
|
return null;
|
|
93
94
|
survivors = snapshot.stdout.split("\n").flatMap((line) => {
|
|
@@ -377,9 +378,9 @@ let linuxClockTickMs;
|
|
|
377
378
|
function linuxProcessCpuMs(pid, cwd) {
|
|
378
379
|
if (!existsSync("/proc/self/stat"))
|
|
379
380
|
return Promise.resolve(undefined);
|
|
380
|
-
//
|
|
381
|
-
// profile (nvm/pyenv/direnv side effects included)
|
|
382
|
-
linuxClockTickMs ??=
|
|
381
|
+
// shell (login false), not sh: the accountant samples at 100ms cadence and must not run the operator's login
|
|
382
|
+
// profile (nvm/pyenv/direnv side effects included). Not shGit either: getconf is not git (queue row 113).
|
|
383
|
+
linuxClockTickMs ??= shell("getconf CLK_TCK", cwd, 15_000).then((r) => {
|
|
383
384
|
const ticks = r.code === 0 ? Number(r.stdout.trim()) : Number.NaN;
|
|
384
385
|
return Number.isFinite(ticks) && ticks > 0 ? 1_000 / ticks : undefined;
|
|
385
386
|
});
|
|
@@ -403,12 +404,21 @@ function linuxProcessCpuMs(pid, cwd) {
|
|
|
403
404
|
// finds that root and its descendants. An empty tree is measurable zero; a failed or unparseable
|
|
404
405
|
// snapshot is undefined because missing evidence can never prove inactivity.
|
|
405
406
|
async function workerTreeCpuSnapshot(marker, cwd, group, identities) {
|
|
406
|
-
|
|
407
|
+
// queue row 113: `ps` is not git, so the payload entry — the own-git entry's pin probes are two SYNC git spawns per call
|
|
408
|
+
// ONE snapshot carries the group column too (it was a second `ps -Awwo pid=,pgid=` per sample).
|
|
409
|
+
const snapshot = await shell("ps -Awwo pid=,ppid=,pgid=,time=,command=", cwd, 15_000);
|
|
407
410
|
if (snapshot.code !== 0)
|
|
408
411
|
return undefined;
|
|
409
412
|
const rows = [];
|
|
413
|
+
const grouped = [];
|
|
410
414
|
for (const line of snapshot.stdout.split("\n")) {
|
|
411
|
-
|
|
415
|
+
// Group membership reads EVERY row, as the separate group snapshot did; a CPU row also needs a parseable time.
|
|
416
|
+
const ids = /^\s*(\d+)\s+(\d+)\s+(\d+)(?:\s|$)/.exec(line);
|
|
417
|
+
if (!ids)
|
|
418
|
+
continue;
|
|
419
|
+
if (group !== undefined && Number(ids[3]) === group)
|
|
420
|
+
grouped.push(ids[1]);
|
|
421
|
+
const m = /^\s*(\d+)\s+(\d+)\s+\d+\s+(\S+)\s+(.*)$/.exec(line);
|
|
412
422
|
if (!m)
|
|
413
423
|
continue;
|
|
414
424
|
const cpu = parsePsCpu(m[3]);
|
|
@@ -420,16 +430,8 @@ async function workerTreeCpuSnapshot(marker, cwd, group, identities) {
|
|
|
420
430
|
const tree = new Set(rows.filter((p) => p.cmd.includes(marker)).map((p) => p.pid));
|
|
421
431
|
// CPU presentation matching is intentionally broader than authority to signal a process.
|
|
422
432
|
const owned = new Set(rows.filter((p) => p.cmd === `bash ${marker}` || p.cmd === `/bin/bash ${marker}`).map((p) => p.pid));
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (groups.code !== 0)
|
|
426
|
-
return undefined;
|
|
427
|
-
for (const line of groups.stdout.split("\n")) {
|
|
428
|
-
const row = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
429
|
-
if (row && Number(row[2]) === group)
|
|
430
|
-
tree.add(row[1]);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
+
for (const pid of grouped)
|
|
434
|
+
tree.add(pid);
|
|
433
435
|
// ps output is not topologically ordered; relax the parent -> child closure until stable.
|
|
434
436
|
for (let grew = true; grew;) {
|
|
435
437
|
grew = false;
|