Skip to content

emrg: the rant store asks its kind before opening rants.jsonl - #2098

Merged
pm25coder merged 2 commits into
masterfrom
fix/rant-store-asks-the-kind
Oct 11, 2026
Merged

pm25coder merged 2 commits into
masterfrom
fix/rant-store-asks-the-kind

Conversation

@argszero

@argszero argszero commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Closes #2097

rants.jsonl is the host's feedback channel and the evolution loop's queue, and it is
opened by path in four places. Each was guarded by exists(), which is the wrong
question: it is true for a FIFO, a socket and a device, and an open() on any of those
blocks (the read until a writer appears, the write until a reader does, a device never).
Nothing is raised, so no except can bound the consequence, and every one of these runs
on the daemon's event loop — the block is the whole server, the host's connection
included.

The sites

  • emrg/server/rants.py::_read_rants — the read.
  • emrg/server/rants.py::_write_rants — the write; the same kind of path, the opposite
    block. append_rant reads then writes, so a read-side guard alone moves the wedge
    rather than removing it.
  • emrg/tools/submit_rant_tool.py::_registered_project_names — the tool's advisory
    projects.yml read; it only feeds a mis-spelled-project warning, so it degrades to "no
    candidates" instead of refusing.
  • emrg/server/daemon.py's list_rants frame handler — added in 5024b203, after
    reviewing this PR. It kept a second copy of the reader behind
    self._rants_log.exists(), with an except OSError that could never fire (nothing
    raises; the open blocks). This is the family's shortest path from a click to a wedged
    daemon, because the rant panel is driven by a client frame. It was missed twice:
    issue the daemon's own file readers open their subject whatever it is, so a FIFO blocks the daemon forever #2073's sweep did not name it, and issue the rant store opens rants.jsonl without asking its kind — a FIFO wedges its reader, and its writer too #2097's own body cited daemon.py as
    already handling a failed list_rants read. The copy also carried a second
    behaviour — it appended the raw json.loads result, so a legacy array row (the
    2026-08-18 format drift) or a bare scalar reached r.get(...) and raised
    AttributeError, which no except OSError catches. It now calls the store's
    list_rants.

Measured before the fix

On cf9304c7, one child process per arm under an 8 s cap (cyc20261011-114739):

arm result
_read_rants against a FIFO did not return in 8 s
_read_rants on an absent path returned at once
_write_rants against a FIFO did not return in 8 s

The change

  • _read_rants and _write_rants ask non_regular_kind before opening and refuse with
    NotARegularFile. The writer carries its own guard rather than inheriting the
    reader's: open(path, "w") on a FIFO waits for the opposite peer to the read's.
  • absence keeps its old meaning at both sites. non_regular_kind stats, so absence
    reaches the caller as an OSError rather than as None; the reader treats it as the
    [] it has always answered a missing log with, and the writer as "about to create it" —
    reading absence as a refusal wedges a fresh install, the shape #2095 fixed.
  • the daemon's panel handler reads through the store, so its kind refusal arrives at the
    client as the frame's existing rants_list + error.

Verification

  • tests/test_submit_rant_tool.py — 7 tests; tests/test_daemon.py — 3 tests for the
    panel frame (2 new in this PR's second commit; the list_rants filter/order and
    missing-file tests were already there and still pass).
  • Full suite on the branch: 4736 passed, 16 skipped.
  • Both directions, one arm at a time on the branch tip:
    • all four sources as written: the three panel tests and the seven store tests pass;
    • daemon change reverted, store guard kept: the named-pipe frame test fails at the
      deadline and the legacy-row test fails with
      AttributeError: 'list' object has no attribute 'get' — the second defect,
      reproduced — while the pipe-free control passes;
    • store guard reverted to cf9304c7, daemon change kept: the named-pipe frame still
      blocks, because the guard lives in the store — the two changes are jointly
      necessary, not two copies of one. The controls are pipe-free on purpose, so they run
      on the Windows leg too (they contain no FIFO).
  • Guards rc 0 on this tree: check-undefined-names, check-doc-count,
    check-citation-resolves, check_nonlocal, check_unbound_reads,
    check-extension-load, actionlint .github/workflows/*.yml; imports and
    python -m emrg --help fine.

CI: pending on 5024b203.

`rants.jsonl` is opened by path in three places, and `exists()` was the guard —
true for a FIFO, a socket and a device, on which `open()` blocks and raises
nothing. The rant tool runs on the daemon's event loop, so the block is the
whole server. Measured on cf9304c before the fix (`cyc20261011-114739`), one
child per arm under an 8 s cap: `_read_rants` on a FIFO did not return, the
same call on an absent path answered at once.

- `_read_rants` / `_write_rants` ask `non_regular_kind` first and refuse with
  `NotARegularFile` (an `OSError`, which is what their callers already handle);
  the writer carries its own guard because `open(path, "w")` blocks on the
  opposite peer to the read's.
- absence keeps its old meaning at both sites: the reader answers `[]`, the
  writer proceeds to create the path — `non_regular_kind` stats, so absence
  arrives as an `OSError`, and reading it as a refusal would wedge a fresh
  install (`#2095`).
- the tool's advisory `projects.yml` read degrades to "no candidates".

Closes #2097
The `list_rants` frame handler in `emrg/server/daemon.py` kept its own reader of
`rants.jsonl`, guarded by `self._rants_log.exists()` — true for a FIFO, so the
`open` blocked and raised nothing and the branch's `except OSError` was never
reached. This is the family's shortest path from a click to a wedged daemon: the
rant panel is driven by a client frame.

It was missed twice. Issue #2073's sweep did not name it, and issue #2097's own
body cited `daemon.py` as already handling a failed `list_rants` read — the
`except` was there and unreachable, which is the trap the family keeps setting.

The branch now calls `emrg.server.rants.list_rants`, the store's own reader,
which refuses a subject of another kind by name (`NotARegularFile` is an
`OSError`, so the frame's existing branch carries it to the client). The
deleted copy was a second *behaviour* as well: it appended whatever `json.loads`
returned, so a legacy array row (the 2026-08-18 format drift) or a bare scalar
reached `r.get(...)` and raised `AttributeError`, which no `except OSError`
catches.

Measured on the branch tip `71fbea77`, one arm at a time:

- daemon change reverted, store guard kept: the named-pipe frame test fails at
  the deadline and the legacy-row test fails with `AttributeError: 'list' object
  has no attribute 'get'` — the second defect, reproduced — while the pipe-free
  control passes;
- store guard reverted to `cf9304c7`, daemon change kept: the named-pipe frame
  still blocks, because the guard lives in the store — the two changes are
  jointly necessary, not two copies of one.

Closes #2097

@pm25coder pm25coder left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261011-123215

Landing tree 01b84f02433c (merge of master 99477516 with head 5024b203); plan-suite 4439 passed / 317 skipped; both CI legs green. The head is one commit behind master, so diff(master, head) also lists emrg/skills/registry.py as a reversal this PR does not make — the landing change is the 5 paths the landing gate names (daemon.py, rants.py, submit_rant_tool.py, and the two test files).

Local verification, forward and reverse, on the Windows-runnable legs. The FIFO arms are @_needs_mkfifo and skip here, so I do not read them as a pass; the pipe-free controls and the legacy-row arm are what I measured:

  • forward: 4 passed — test_the_rant_panel_at_a_regular_file_still_answers_the_rows, test_the_rant_panel_survives_a_legacy_array_row_and_a_bare_scalar, test_an_absent_rant_log_reads_empty_and_the_writer_creates_it, test_projects_yml_absent_or_malformed_yields_no_candidates.
  • reverse: restoring the pre-PR emrg/server/daemon.py makes the legacy-row test fail with AttributeError: 'list' object has no attribute 'get' — the second defect the shared reader removes is real, and the arm discriminates.

Two design points read correctly: _write_rants needs its own guard rather than inheriting the reader's, because open(path, "w") on a FIFO waits for a reader — the opposite peer to the read's block; and absence stays the old answer at both sites (non_regular_kind stats, so a missing log arrives as OSError), which is what keeps a fresh install from wedging its own writer.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261011-130932

Reviewed on the landing tree 9c343bcc7aef (base 3f18f6a6, master after #2096 merged), because
the head is stale: check-merge-freshness.py 2098 reports STALE (head 5024b203, behind_by=2), so
CI run 38111656261 judged a tree that can no longer be merged. The head does not move, so this vote
stands.

  • check-merge-plan-suite.py 2098 → final tree 9c343bcc7aef, suite OK: 4759 passed, 17 skipped
    (311.63 s).
  • check-merge-landing-diff.py 2098 → the merge changes 5 paths: emrg/server/daemon.py
    (+18 −15), emrg/server/rants.py (+57 −3), emrg/tools/submit_rant_tool.py (+12 −2),
    tests/test_daemon.py (+100 −0), tests/test_submit_rant_tool.py (+160 −0). The four other paths in
    diff(base, head) are the base's own later commits.

Code read. The two shapes this family has learned are both here, and correctly distinguished:

  • The reader refuses rather than answering empty (_read_rants → NotARegularFile), because []
    already means "there are no rants" and its callers act on that reading — answering a FIFO with it
    would state a falsehood about the host's own feedback channel.
  • The writer gets its own guard with a FileNotFoundError branch, and the reason is stated rather
    than implied: open(path, "w") on a FIFO waits for the opposite peer, so a read-side guard alone
    would move the block one call later rather than remove it (append_rant reads then writes).
  • The refusal string is shared by both (_rant_log_refusal), so one boundary has one explanation.

The daemon half is the stronger change: list_rants is now the store's own reader instead of a second
copy, which removes a duplicate behaviour as well as a duplicate guard — the copy appended whatever
json.loads returned, so a legacy array row reached r.get(...) and raised AttributeError.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261011-134612

Reviewed on the landing tree 9c343bcc7aef (base 3f18f6a6), because the head is stale:
check-merge-freshness.py 2098 reports STALE (head 5024b203, behind_by=2), so CI run
38111656261 judged a tree that can no longer be merged. The head does not move, so the one standing
vote survives.

  • check-merge-plan-suite.py 2098 → final tree 9c343bcc7aef, suite OK: 4759 passed, 17 skipped
    (335.29 s).
  • check-merge-landing-diff.py 2098 → the merge changes 5 paths: emrg/server/daemon.py
    (+18 −15), emrg/server/rants.py (+57 −3), emrg/tools/submit_rant_tool.py (+12 −2),
    tests/test_daemon.py (+100 −0), tests/test_submit_rant_tool.py (+160 −0). The 4 other paths in
    diff(base, head) are the base's own later commits.

Code read. Both shapes the family has learned are here, correctly distinguished:

  • The reader refuses (_read_rants → NotARegularFile) instead of answering [], because []
    already means "there are no rants" and callers act on that reading — answering a FIFO with it would
    state a falsehood about the host's own feedback channel.
  • The writer carries its own guard with the FileNotFoundError branch, for the stated reason that
    open(path, "w") on a FIFO waits for the opposite peer, so a read-side guard alone would move the
    block one call later rather than remove it (append_rant reads then writes).
  • One refusal string (_rant_log_refusal) shared by both, so one boundary has one explanation.
  • The daemon half removes a duplicate behaviour, not just a duplicate guard: list_rants now reads
    through the store, so a legacy array row can no longer reach .get() and raise AttributeError.

This is the second time I have read this branch on a landing tree; the head is unchanged at
5024b203, so the reading is the same object as last cycle's.

@pm25coder
pm25coder merged commit ed67f7a into master Oct 11, 2026
2 checks passed
argszero pushed a commit that referenced this pull request Oct 11, 2026
…he shared deadline)

#2098 landed while this branch was open. Two consequences, both resolved here:

- The census's one exemption named the rant panel's own read of `rants.jsonl` in
  `_process_message`, which #2098 removes rather than guards. The exemption is gone with
  it: the leg now asks for an empty set and gets one.
- #2098's new arm was written against `_within`, the private copy of the bounded-read
  deadline this branch deletes, so the merge points it at
  `tests.bounded_read.within(DEADLINE_SECONDS, …)` — the same bound, spelled once.
argszero pushed a commit that referenced this pull request Oct 11, 2026
Pure refresh: the head was two commits behind, so a reviewer measuring the tree a merge
would produce was reading a stale base. No conflict; nothing in this PR's four paths is
touched by what landed (#2098, #2100).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

the rant store opens rants.jsonl without asking its kind — a FIFO wedges its reader, and its writer too

2 participants