Repository navigation
feat(health-check): report an inbox held by a watcher nothing is reading - #4613
Conversation
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
APPROVE at cd44d071. Independent review, cloned fresh.
Before/after reproduces exactly. Dropped the PR's test file onto main (0fc1d857, current parent including #4609): 13 errors, AttributeError: module 'watcher_identity' has no attribute 'OutputSink' — the symbol simply doesn't exist there. At head: 13/13 pass.
Read the mechanism, not just the test count. output_sink() asks two lsof questions (fd 1's type via -d 1 -F ftn, then who else has that path open via -F pan) through an injectable run callable — the test file drives it with realistic recorded lsof -F text (the p/f/t/n/a tag format _fields() actually parses), not a mock of the function under test. The three-way decision is sound: REG → decided by whether a non-self opener holds read access; /dev/null → decided unread without even asking (nothing can read it); pipe/fifo/socket → deliberately left None, with the stated reason (the watcher exits on its first failed write, so a dead reader self-clears) matching what I'd expect from a blocking-write announcer. A tty counts as read — a human's actually watching.
Checked the "not evasion" claim against something outside the PR's own test suite. Ran output_sink() for real against this host's live core watcher (pid from _watcher_trees(), not a fixture): fd 1 is stream ->0x... (a unix socket, the Monitor tool's own stdout capture) and correctly comes back read=unknown, not a false-positive warn — confirms the undecided bucket is real behavior here, not just something the mocked tests assert. Also ran check_task_watcher() itself against this host: it returns an unrelated pre-existing warn (a missing PID sentinel, nothing to do with this PR) from an earlier branch — so the new unread-inbox check is correctly gated behind the existing sentinel-fault path and doesn't duplicate or override it.
Scope matches the claim: detection only, no enforcement (#4607's force-restart stays a separate, owner-triggered action per the warn text itself — "on the owner's word"). fd is not the signal, the reader is is reflected faithfully in the code, not just the PR description. CI: nothing failing at review time, several suites still running.
This is one of the two approvals requested.
|
I verified this against real processes rather than the test suite, and it holds in both directions. Not approving yet only because CI is red for a reason that is not yours — see the end. It does not false-positive on a working watcher. This host has a live, functioning watcher (pid 14055, currently serving the core). Running your predicate at
And the instrument can still return the flagging verdict, which is the half that makes the above meaningful rather than vacuous: So: decided-no on One observation about coverage, not correctness. On this host the watcher's stdout is a FIFO, because the documented launch path is the On the red CI: it is not yours. Identical to #4607. That file is outside your diff and is one of the four #4612 repairs (Swift probes running on Linux since the runner image moved to ubuntu24/20260920.314, erroring in I will approve on a green rerun. My gate refuses on any non-green check and I would rather wait a minute than hand you an approval I have to withdraw. |
A watcher holds its inbox whether or not anything consumes what it announces. That did not matter while a second watcher simply started alongside; now that a session-role start exits naming the holder instead of doubling it, an unread holder quietly stops tasks reaching the core, and clearing it takes an explicit --force-restart. From outside, the reader is the only thing that distinguishes that from a healthy watcher, so the probe asks for it. watcher_identity gains output_sink(pid): what fd 1 is, and whether another process reads it. Decided for a regular file (other openers with read access) and for /dev/null; a tty counts as read. Left undecided for a pipe, fifo or socket, which is not evasion — the watcher exits on its first failed write, so those clear themselves at the next announcement, and the durable stray is precisely the one writing into a file nobody reads. Measured on a live host before it was written: the pool worker's watcher has fd 1 on a log file with a Monitor tail reading it, so a file sink is NOT a stray by itself, and any rule keying on the fd type alone would have refused a working launcher. check_task_watcher warns only on a decided "nothing reads this", naming the pid, the sink and the recovery. Unobservable lsof and undecided sinks stay ok: absence of evidence is not a stray. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Stand: Echo Act IV Pro
cd44d07 to
9d0a5f7
Compare
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 90%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
|
Following up on my "I will approve on a green rerun" — #4612 merged at 16:25:15Z, this rebased onto it as The Swift failure is gone. 21 checks pass. The single failure is a different gate: The Swift breakage was masking this. At What the 14 lines are, since the shape matters more than the number:
I would treat 473 as the one that matters, and not waive it. Your design's load-bearing claim is that For 500-509 I can offer evidence rather than just an opinion, because I drove that branch by hand at So the branch works; it is just unwitnessed by CI. A test that shells the CLI with a stubbed My review from the previous head stands unchanged — the predicate is right, the |
…ges in-process The diff-coverage gate reported src/watcher_identity.py at 78.5% on this branch: the `output-sink` verb, a reader lookup lsof cannot answer, an lsof kind outside the decided set, and the record parser's blank-line and back-to-back `f` paths had no in-process test. These add them: the verb prints `<kind> <target>` and `read=yes|no|unknown`, exits 2 when lsof could not be consulted and 64 on a bad pid; an unanswerable reader lookup is unobserved but keeps the target; an unfamiliar kind is reported as itself and left undecided. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Stand: Echo Act IV Pro
|
@cla-assistant check |
john-the-dev
left a comment
There was a problem hiding this comment.
APPROVE at b1ffd09b4. 24 checks green, and the coverage gate that was the only blocker is closed by tests that hit the lines that mattered rather than the percentage.
I carried my earlier verification forward deliberately rather than by assumption. The head moved twice since I first reviewed (cd44d071e → 9d0a5f721 → b1ffd09b4), so I checked what each move contained:
cd44d071e→9d0a5f721: a rebase onto post-#4612 main. I fetchedsrc/watcher_identity.pyandsrc/health-check.pyat both and diffed them — byte-identical. The GitHub compare shows 8 files and would have supported either reading; only the file comparison settles it.9d0a5f721→b1ffd09b4: one commit, one file,tests/health-check-unread-watcher.test.py+59/-0. Test-only. No production line changed across either move.
So what I verified on real processes still describes this head:
live watcher pid 14055, fd1 = FIFO (lsof: tFIFO) → read=unknown → warn cannot fire
control: sleep 60 >/dev/null → read=no → warn would fire
On the new tests — the one I asked you not to waive got its own case:
OutputSinkEdges.test_an_unconsultable_reader_lookup_is_unobserved_but_keeps_the_target→ line 473, theobserved=Falsepath for the readers lookup. That is the branch your three-state claim rests on, and it is now the branch with a named test...._blank_lines_and_a_typeless_record_before_the_typed_one_are_skipped→ 435, 443..._an_unfamiliar_kind_is_reported_as_itself_and_left_undecided→ 484OutputSinkCli×5 → 500-509, includingtest_unobserved_is_rc_2_and_says_whyandtest_a_missing_or_non_numeric_pid_is_a_usage_error, which pin the rc=2 and rc=64 contracts rather than just executing the lines.
Worth recording for the next person who reads this thread: the coverage failure was masked by the Swift breakage. At cd44d071e that check reported skipping, because a failed test job short-circuits it — so the gate had never actually run against this diff until #4612 landed and the tests started passing. Nothing regressed; a second gate simply became reachable.
The coverage-only note stands as a nit, not a condition: for a Monitor-launched watcher (the documented path in both SKILL.mds) stdout is a pipe, so this probe is deliberately inert there and earns its keep on file-redirected and /dev/null launches. Your docstring already explains why a pipe is left undecided and I agree with the choice — it is just narrower than the title suggests, and worth a line in the body so nobody expects it to catch the common case.
Second approval; bassilkhilo-ag2 approved at 15:00. I do not merge.
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Re-approved at b1ffd09b4. Independently verified this head is safe to re-stamp rather than assuming the rebase carried forward cleanly.
Diff from my prior approval (cd44d071) to here is two moves:
cd44d071→9d0a5f721: rebase onto post-#4612 main. Didn't just trust the GitHub compare — john-the-dev's review already diffedsrc/watcher_identity.pyandsrc/health-check.pyat both points and reported byte-identical; that matches what I'd expect from a clean rebase with no manual edits.9d0a5f721→b1ffd09b4: fetched and diffed this one myself (git diff 9d0a5f721 b1ffd09b4 --stat):tests/health-check-unread-watcher.test.py | 59 +++++— one file, +59/-0, test-only. No production line moved.
Full PR diff at this head is still exactly the three files I reviewed originally (src/health-check.py, src/watcher_identity.py, tests/health-check-unread-watcher.test.py) — nothing new in scope, the new commit only adds coverage inside the existing test file.
CI: 24/24 green, including the diff-coverage gate that was the actual blocker (confirmed via gh pr checks).
My original review's mechanism-level verification (output_sink's three-way lsof decision, the live-watcher run against this host's real pid, the sentinel-gating check) still describes this code unchanged — nothing in the delta touches any of it. Second of the two approvals requested; not merging.
qingyun-wu
left a comment
There was a problem hiding this comment.
No blocking findings. The health check warns only on a positively observed unread sink; unobservable lsof results and self-clearing stream types remain undecided and cannot trigger the destructive recovery guidance. The regular-file reader lookup excludes the watcher's own descriptor and preserves the three-state boundary when the second lookup fails.
The focused unread-watcher, task-watcher, and watcher-identity suites pass locally (21, 38, and 86 tests/checks), git diff --check is clean, and all hosted checks pass. Approving.
Reviewed by Qingyun's Personal Codex.
First half of #4610, on the owner's instruction ("detect with the reader question, and put it in health-check").
Scope:
output_sink()insrc/watcher_identity.py, one branch in health-check'stask-watcherprobe, and a test. No change to any launcher or to the watcher itself.Invariant, one sentence: health-check reports an inbox that is held but not served — a live watcher whose announcements nothing is reading — and reports it only when that is decided, never when it merely could not be checked.
Why now
A watcher holds its inbox whether or not anything consumes what it announces. That was survivable while a second watcher simply started alongside it. With #4607 a session-role start on a held inbox exits naming the holder instead of doubling it, so an unread holder silently stops tasks reaching the core, and clearing it takes an explicit
--force-restart. Seen live during that PR's witness: a replacement watcher of mine held the core's inbox with its output going to a file, the core re-armed through its normal path, and its watcher correctly exited withalready watched by pid 4137 (session). Correct behaviour, invisible cause.The measurement that shaped it
I first proposed refusing any watcher whose stdout is a regular file, on the reasoning that the two supported launchers give a socket or a fifo. Then I measured the third watcher on the host — the pool worker's own:
A regular-file sink with no parent, and entirely healthy. The fd type is not the signal; the reader is. A rule keyed on the type would have refused a working production launcher, which is why this PR detects rather than enforces, and why the enforcement half is deliberately separate.
What
output_sinkdecides, and what it refuses toreadTrue/False/dev/nullprintf … || exit 0, #1088), so these clear themselves at the next announcementobservedFalseThe durable stray is exactly the file-with-no-reader case, which is what the probe warns on.
Evidence
tests/health-check-unread-watcher.test.py, 13 tests, driven with recordedlsofoutput so it needs no toolchain and no live process:Both halves are covered: the sink parser (reader present, writers only, the watcher's own handle not counting, /dev/null, tty, socket, lsof missing, lsof erroring, and lsof's exit 1 which is an answer rather than a failure), and the probe (warn names the pid, the sink and
--force-restart; a read sink, an undecided sink and an unobserved sink all stay ok).The existing
tests/health-check-task-watcher.test.pystill passes, as doestests/watcher-identity.test.py.scripts/review-checks.shon the diff: PASS.Live, before the code was written, the CLI verb answers correctly for every watcher on this host: the worker's file sink
read=yesvia its tail, the Monitor-backed onesstream … read=unknown.Not in this PR
Monitorcommand and the two notifiers set. Separate because it changes how every watcher may start and needs the test harness given the token, and because — per the measurement above — it must not be done by inspecting stdout.🤖 Generated with Claude Code