Repository navigation
emrg: the client's start diagnostics ask their subject's kind before opening - #2112
Conversation
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261011-201338
Landing tree dc908788c222 (merge master 6fe9101b + #2112 head 4b49f1a6, behind master by 1): plan-suite 4451 passed / 345 skipped (855s, rc 0).
The client's three start-diagnostic readers (_truncate_start_stderr, _read_start_stderr, _read_log_tail) ask the subject's kind before the open, via the shared emrg.tools.base.non_regular_kind; each keeps the answer it already gave a directory (None / None / ""). _read_log_tail correctly drops the path.exists() it used to lean on, since exists() is true for a FIFO.
Both directions measured on the landing tree: the FIFO-free arm test_a_directory_subject_names_its_kind_on_every_platform passes, and it covers all three sites — it asserts each of the three paths appears in the log, so a site missing the guard fails it too; with emrg/client/daemon_manager.py reverted to master that arm fails (assert 'not a regular file' in ''). Closes #2109.
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261011-225014
Landing tree 6c642d948fd1 (check-merge-plan-suite.py 2112, base master 37342bf7): suite OK, 4464 passed / 355 skipped. The merge changes 2 paths — emrg/client/daemon_manager.py (+82/−1) and tests/test_daemon_start_diagnostics.py (+131). Reading the landing diff, not diff(master, head): 11 of the 13 paths there are the base's own later commits shown as reversals this PR does not make.
The diagnostic reads the landing change, so I re-measured on the current base rather than carrying last cycle's reading: the base moved from 6fe9101b to 37342bf7 since, and the suite is green on the tree that would actually land.
What I checked in the code:
- The rule is imported, not restated —
from emrg.tools.base import non_regular_kind, the same importemrg/sessions_index.pyandemrg/skills/registry.pywrite at the same boundary. A second spelling of "is this a regular file" is a second boundary that can drift, so reaching for the shared one is the right call. - All three sites, and each keeps the answer its own contract already promised:
_truncate_start_stderrdegrades toNone(the caller reads it as DEVNULL) rather than raising, which is what it already answered for a directory;_read_start_stderranswersNone;_read_log_tailanswers"". No new outcome is introduced, only that it now arrives instead of hanging. _read_log_tailremovespath.exists()rather than keeping it as a second opinion, and that is the right shape:exists()is true for a FIFO, and it was precisely the reading that let the block through. Keeping it beside the kind check would have left a green-looking second opinion saying the opposite.- Absence keeps its own branch at every site.
non_regular_kindstats, so a missing file arrives asOSError; the writer catches it and proceeds to create the file (creating it is what it is for), and the two readers answer their "could not be read" value. The writer's normal case is a file that does not exist yet — refusing it would have broken every ordinary start.
CI at head 4b49f1a6: test pass, test-windows pass. Nothing to change.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261011-220004
Reviewed the code and measured the tree it lands.
Three call sites in daemon_manager.py opened their subject without asking its kind, and on a FIFO
the open is what decides whether the call returns at all — so the except OSError written to bound
each one could never fire, and the first of them (_truncate_start_stderr) runs before the child
is spawned, which is why this is a diagnostic that could stop every start attempt rather than only
fail to capture stderr. The patch imports the tool layer's non_regular_kind instead of restating
the rule, answers each site its own pre-existing "cannot open" / "cannot read" value, and branches on
absence first (non_regular_kind stats, so a missing path arrives as an OSError — a writer's
normal case, not a refusal).
Both directions, measured:
| direction | subject | result |
|---|---|---|
| green | master 37342bf7 + this PR's two files |
tests/test_daemon_start_diagnostics.py 46 passed, 0 skipped |
| red | this PR's tests against master's source | 3 failed — the three FIFO arms, each bounded by tests.bounded_read.within (15.8s total, no hang), on "a path was opened without asking its kind first" |
| full suite | the plan #2111 → #2112, final tree 1c435e539168 (check-merge-plan-suite.py 2111 2112) |
4809 passed, 17 skipped in 19m08s, exit 0 |
| what it lands | landing tree 6c642d948fd1 on base 37342bf7 (check-merge-landing-diff.py 2112) |
emrg/client/daemon_manager.py +82/−1, tests/test_daemon_start_diagnostics.py +131 — nothing else |
| merge order | base 37342bf7, 4 open PRs (check-merge-order.py) |
mergeable, dirties no other PR |
| CI | this PR's own head 4b49f1a6 |
both legs pass (runs 38136984647) |
The red direction is the part worth stating precisely, because a guard of this shape is easy to write
in a way that cannot fail: the three FIFO arms go red against the unpatched source at the deadline,
which is the wedge this PR removes, and the FIFO-free arm
(test_a_directory_subject_names_its_kind_on_every_platform) is the one that carries the Windows leg
— it asserts the family's phrase and the refused path, not the platform's own exception text, which
is what a guard written against Is a directory would have pinned. test_an_absent_subject_still_means_what_it_meant
is the branch most at risk (an append to a file that does not exist yet), and it is pinned as its own
test rather than left to the controls.
Both this head and #2111's are readable as a pair: each is confined to disjoint files, so the plan
above is the tree that lands when both merge in that order.
Closes #2109
Three sites in
emrg/client/daemon_manager.pyopened their subject without asking what it was — the family of #2101 / #2103 / #2105 / #2107, where an open is not a read: on a FIFO the open waits for a peer and raises nothing, so noexcept OSErroraround it can fire and the call never returns._truncate_start_stderr(open(path, "wb"))start_daemonopens the child's stderr before it spawns, so a FIFO atemrgd-start.errstopped every start attempt, not merely the capture of stderr_read_start_stderr(path.read_bytes())_read_log_tail(path.exists()+open(path, "rb"))exists()is true for a FIFO, which is the reading that let the block through, so it is replaced rather than kept as a second opinionThe fix takes the answer each site already gives instead of introducing one:
Nonefor the writer (its caller reads that as "the child's stderr went toDEVNULL"),Nonefor the stderr reader ("could not be read"),""for the log tail ("nothing new"). The kind comes from the tool layer's path-takingnon_regular_kind, imported rather than restated, for the reason the same import gives inemrg/sessions_index.pyandemrg/skills/registry.py.Measured on master
60541e39One child process per arm under an 8 s cap, with a regular-file control for every subject, so the axis is the kind of the path and not its absence:
The directory column is the design's justification: the kind one over already produces each site's answer, so the guard changes only whether that answer arrives.
Tests
tests/test_daemon_start_diagnostics.py— three FIFO arms (os.mkfifo-gated, bounded bytests/bounded_read.within), two regular-file controls, an absence arm (a writer's normal case must keep creating its subject), and one FIFO-free arm that discriminates: a directory answers the same value on both trees, so it asserts the sentence the guard leaves — the kind and the path — which the base cannot produce.Both directions measured (each file copied aside, arm applied, arm read, file restored):
60541e39, tests kept → 4 failed, 42 passed (the three FIFO arms and the directory arm);Verification on this tree
uv run pytest tests/→ 4771 passed, 16 skipped (0:19:19)uv run python -c "from emrg.client.app import run_client"✓ ·uv run python -m emrg --help✓check-undefined-names·check-doc-count·check-citation-resolves·check_nonlocal·check_unbound_reads·check-extension-load·check-gui-package-refs·actionlintCI: pending at submission.
Out of scope, named so the class is not silently under-reported: the GUI's JS client carries the same three shapes (
emrg/gui/daemon_client.js:247,:262,:278).