fix(pipeline-conductor): put the process age on the banned probe line - #9346
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS A stateless, fail-safe field that resolves a documented cross-cycle ambiguity; the incarnation token is the right precondition, not scope creep. The one behavior change beyond the field itself — token mismatch demoting [DESIGN-REVIEWED] 45a34e7 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsNo findings. The sole candidate — the negative-age clamp at [OPUS-REVIEWED] 45a34e7 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All checks are done. The change holds up: the age field has checkable provenance (linked issue #8192, a measured incident, tests that fail on base), the SKILL.md update is the mandated same-commit doc sync, the standalone stdlib-only script can't reuse the package's canonical age helper, and the bracket enumeration in the code matches the description. The one rider — the incarnation-token check that downgrades First-Principles-Verdict: PASS Confirm issue #8192 item 2 names this banned-line ambiguity — the linked provenance is the only fix premise not checkable in-repo. Not justified as shipped
What this change shipsIntent: let the conductor tell an unkilled banned process from a fresh offender on a recycled pid — a FIX.
The helpers are the sixth independent [FIRST-PRINCIPLES-REVIEWED] 45a34e7 |
771b3f2 to
d09cadc
Compare
d8d3825 to
86229aa
Compare
86229aa to
2a41467
Compare
…#9393) The comment-history gate matches "no longer" in fleet_probe.py's _probe docstring, putting the file at 3 spans against a baseline entry of 2, so Backend Lint & Type Check fails for any PR that touches the file (live on #9346). The sentence describes a property of the NUL-to-space transformation, not the code's history, so rewording it to present tense removes the false-positive match at zero cost to meaning. The two genuine narration spans (L244 incident date, L255 "used to") stay, level with the recorded entry of 2. Root cause: #9328 snapshotted comment-history-baseline.json against a tree older than the one it merged into, and the deliberately diff-scoped gate could not see drift in files #9328 did not touch. Per the Main Ratchet Audit policy (#9350), the drift is fixed on main rather than by raising the ceiling. Fixes #9372 Co-authored-by: Di Wu <dwu96@users.noreply.github.com>
2a41467 to
f05910e
Compare
f05910e to
62fb4bd
Compare
62fb4bd to
99a1d75
Compare
dcc6379 to
c30dc88
Compare
Verification recordVerified independently at head The red, attributed. Traced to a single commit by counting at each of that file's last five: everything up The other eight conjuncts. No lane pending. All five review lanes What the change earned along the wayThe review chain on this pull request produced two findings in the emission path, and GPT's first finding was that The response was not the one-line reorder either finding asked for. The token is The unknown rendering also collapses three distinct causes -- no Not merged by this pipeline. Reporting a verified state; the merge decision is a |
The fleet probe emits `BANNED pid=<pid> rule=<regex> cwd=<class>` and re-emits the same line every cycle a matching process is alive. A bare pid cannot say whether the process the conductor already stopped is still running or a new offender took its recycled number, so a re-emitted line is unreadable: it means either "handled, ignore" or "still burning the host". Issue #8192 item 2 names this as the banned signal collapsing no-answer and answer. Append `age=<secs>s` (or `age=?` when unreadable), derived at scan time from `/proc/<pid>/stat` field 22 plus `/proc/uptime`. An age that grows across cycles is one unkilled process; a small age under a re-appearing pid is a fresh violation. Both reads are world-readable like the cmdline this scan already trusts, so no new state and no second writer -- the probe stays read-only outside `--mark-handled`. Refs #8192
c30dc88 to
45a34e7
Compare
Verification record -- re-certified after rebaseVerified independently at head The lane that blocked this pull request is green. Board. 56 checks green, zero failing, zero cancelled, zero pending. The diff is unchanged from what was certified before the rebase, which I checked Comment-history count back at baseline. Tests. Linkage. For whoever mergesThis pipeline does not merge. The pull request needs an approving review; nothing is |
NicholasRBowers
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (4 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: fix with a clear root cause — a bare BANNED pid= probe line cannot distinguish a still-running offender from a fresh violation on a recycled pid; adds an incarnation-token-bracketed age= field, read-only, fail-closed to age=?s.
Problem / Motivation
The fleet probe prints
BANNED pid=<pid> rule=<regex> cwd=<class>when it findsa banned command, and prints the same line again every cycle that process is
still alive. A pid number alone cannot tell two very different things apart: the
process the conductor already stopped is still running, or that pid was freed and
a brand-new process took the recycled number. Measured tonight: a pid the probe
reported banned on three cycles in a row was still alive holding 404 worker
processes, load at 71, memory down to 15G -- and the line read the same as a
first sighting.
Why it matters
The conductor reads this line to decide whether to act. When "already handled,
ignore" and "still burning the host" produce the same line, the reader picks one,
and the wrong pick either re-stops a healthy owner or leaves a runaway running.
Issue #8192 calls this out as item 2: the banned signal collapses no-answer and
answer into one output.
A conductor patrolling with a five-day-stale copy of this probe read a banned line for a process that was a shell wrapper and had already exited. One line misled four ways at once: the pid was meaningless because the process was gone;
cwd=unknownsaid nothing; the match was wrong at the root because that stale copy predates the shell-wrapper fix (#8736); and a sibling reading printedcwd=fleet, which names a worktree a reader uses to decide WHICH worker to stop -- but a shell and a real test run in that worktree share the same cwd, so it accused an innocent session. Theage=field this PR adds resolves the gone-or-alive axis on the current probe:age=?ssays the process is gone, a growingage=<secs>ssays it is still running. It does not touch the cwd attribution -- that is the matcher's job and #8736 fixed it upstream; the age is the orthogonal fact the reader still lacked. (Measured on the conductor's own five-day-stale install.)What changed (motivation -> approach -> change)
Symptom: a re-emitted
BANNEDline is unreadable across cycles. Root cause: theline carries the detection but nothing about the process's own history, so a
recycled pid and a persisting pid look identical.
Approach: add the one fact that separates them -- the process's age. An age that
grows between cycles is one unkilled process; a small age under a re-appearing
pid is a fresh violation. Age is a fact about the running process, so it needs no
new state file and no second writer, which keeps the probe read-only outside
--mark-handled(a documented invariant).Change: a
_proc_age_secshelper reads/proc/<pid>/statfield 22 (starttime)and
/proc/uptime-- both world-readable, like the cmdline this scan alreadytrusts, so it survives the same access asymmetry that makes
cwd/exefail foranother user's process. The emit becomes
BANNED pid=<pid> rule=<regex> cwd=<class> age=<secs>s, orage=?swhen the ageis unreadable (the process already exited -- the expected reading for a
short-lived runner). It is one appended field, not a new report format.
SKILL.mdgains the field in the output block and a sentence in the
BANNEDaction row onhow to read
age=across cycles.Deliberately out of scope: issue items 3-7. Item 1 (shell-wrapper mismatch)
already landed in #8736. The other item this dispatch flagged -- an unharvested
terminal report ageing into
IDLEafter it scrolls pasttail_bytes-- is adifferent mechanism (terminal-report stickiness) and is left for a separate change.
One more read makes the whole record trustworthy. The scan reads several
/procfiles for one pid at different instants, so a pid recycled partway through would splice one process's identity onto another's fields -- the fidelity fix's own fidelity defect. The probe captures the processstarttime(a boot-relative incarnation token) BEFORE any other per-pid read and re-reads it before emitting. On a mismatch, unreadable token, or None token the WHOLE record is withheld:cwddrops tounknown(the non-stopping class, so a spliced record can never stop an innocent worker -- the field a stop is reserved for) andageto?s. pid and rule still print, so the violation is not dropped. No new output shape:age=?sandcwd=unknownalready existed.Every read in the emission path is bracketed by that one token -- enumerated so a reviewer can see there is no third unguarded read:
/proc/<pid>/stat/proc/<pid>/cmdline/proc/<pid>/exe/proc/<pid>/cwdcwd=unknown/proc/<pid>/statcwd=unknown,age=?s/proc/<pid>/stat+/proc/uptime_proc_age_secsage=?sstarttimeis monotonic per boot, so a recycle anywhere in the window changes it and is caught; a recycle back to the samestarttimeis impossible. The residual is therefore only the microseconds between capturing start_tok and the first read under it, and even that is bounded: the/procwalk runs every probe cycle andageis designed to be read as a SEQUENCE across cycles, so the worst case is one misleading line, once, in a field whose whole purpose is cross-cycle comparison -- it self-corrects on the next pass. A reader deciding whether to act on a single line should readageas a sequence, not a snapshot.On the #8343 question -- why not bind a start token and refuse, as #8467 (Refs #8343) does for the signed pid mapping. The answer is that this PR does both, on different axes. #8467's consumer makes a kill decision on one identity, so a stale mapping must be refused. This probe's consumer is a reader who must still act and needs to know WHICH case they are in, so the age is reported, not refused. But the token is what makes that age trustworthy:
starttimeis read to VALIDATE that the age and the cmdline describe one process (mismatch ->age=?s), exactly #8467's technique, used to make the age meaningful rather than to replace it. The token is not an alternative to the age; it is its precondition.Tests
test/test_pipeline_conductor_probe_banned_age.py, six cases over a fake/proc(theKIROCREW_PROBE_PROC_ROOTseam the script already exposes):age=600son its line;statis gone (exited mid-scan) printsage=?sand still emitsthe line -- age never blocks the signal or crashes the scan;
_proc_age_secsresumes the field parse after the LAST), so a comm holdingspaces and parentheses does not shift the starttime field;
os.sysconfunavailable (Windows), the helper returns None and the calleremits
age=?s-- the field is an explicit unknown, never a guessed number andnever absent.
reads), the helper returns None so the caller emits
age=?srather than aspliced age, and the matching token still yields the age;
the BANNED line with
age=?s.Commands run in the worktree, base
origin/maine7db5f5b8:Results: 6 passed (new file), 93 passed (existing probe + skill-contract), black
/ isort / flake8 / mypy clean. Mutation-verified: reverting the emit to the old
bare line reds
test_banned_line_reports_the_process_agewhile the existingtests stay green; and reverting the no-
sysconfguard to a guessed tick rate redstest_age_is_none_without_a_clock_tick_rate; and disabling the incarnation-token check redstest_age_refused_when_the_incarnation_token_movedand the recycled-mid-scan end-to-end test.Manual verification
N/A -- unit coverage over the fake-
/procseam exercises the emit and the agehelper directly, including the unreadable-age and paren-heavy-comm edge cases.
Related Issues
Refs #8192
Pattern harvest
Pattern: a signal built to resolve a pid-identity ambiguity was itself assembled from two unsynchronised
/procreads of that same ambiguous pid, so the fidelity fix carried a fidelity defect -- a recycled pid between thecmdlineread and the age read splices one process's identity onto another's age. The general shape: when a fix reads a mutable identifier more than once to describe one entity, bind the reads to an immutable incarnation token (here processstarttime) and refuse to combine them across a mismatch.Rule candidate: review-prompt -- for any change that reads
/proc/<pid>(or any reused OS handle) more than once to build one record, ask whether the reads are pinned to a single incarnation, since the pid can be recycled between them.