Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ All notable changes to MessageFoundry are documented here. The format follows
ON). Residuals in [SERVICE.md](docs/SERVICE.md).

### Fixed
- **The harness message list could livelock and stop updating entirely.** `MessagesPanel._apply` cleared
its in-flight guard, re-fired any refresh latched during the read, then returned *before* rendering —
discarding the snapshot as superseded. Whenever refreshes arrive faster than a read completes there is
always a latch waiting when the read lands, so every snapshot was discarded and the table never
updated: permanently stuck, not merely slow (measured: 391 reads served, 0 rendered). It now renders
first and drains afterwards, costing at most one read of staleness while still converging on the
latest filter. This surfaced as an intermittent CI failure that no timeout or retry could fix, because
neither addresses a livelock.
- **Two config blocks in the off-loopback runbook aborted at load** — `[diagnostics].audit_all_authz` and
`[ai].data_class` had been relocated by ADR 0118, so an operator copy-pasting either block got an
immediate start failure. Corrected to `[security].audit_all_authorization_decisions` and
Expand Down
10 changes: 7 additions & 3 deletions scripts/hooks/push_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@

WHAT THIS IS NOT. A guardrail, not a security boundary: ``git push --no-verify`` skips it, and it is
local-only, so a different machine is unprotected. It removes the ACCIDENT, not the capability. The
durable server-side fix is ``enforce_admins=true``, which is deliberately NOT in place yet because a
known flaky test (BACKLOG #17) has blocked two consecutive PRs, and removing the override while a
flake can strand a merge trades an accidental-push risk for a cannot-ship risk.
durable server-side fix is ``enforce_admins=true``, which was deliberately NOT enabled while an
intermittent harness-monitor failure was blocking consecutive PRs -- removing the admin override while
a flake can strand a merge trades an accidental-push risk for a cannot-ship risk. That failure turned
out to be a livelock in ``MessagesPanel._apply`` rather than a flake, and is fixed
(``tests/test_console_messages_refresh.py``), so the argument against ``enforce_admins`` is weaker now
than when this was written. (An earlier revision of this note cited "BACKLOG #17" for it; that is the
py3.11 pytest/aiosqlite deadlock, OBSOLETE and unrelated.)

Stdlib only, like the other gates -- most worktrees have no project .venv.

Expand Down
10 changes: 8 additions & 2 deletions tests/test_console_messages_refresh.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 MessageFoundry Organization and contributors
"""MessagesPanel refresh convergence — the BACKLOG #17 livelock.
"""MessagesPanel refresh convergence — the discard-every-snapshot livelock.

NOT BACKLOG #17, despite what earlier comments here and in test_harness_monitor.py said. #17 is the
py3.11 pytest/aiosqlite cancellation deadlock (OBSOLETE since the 3.14-only migration); following that
citation lands on a closed item about an unrelated bug. The misattribution was inherited from a comment
on the flaky test and then repeated across new files while fixing something else entirely. This
livelock has no backlog entry — it was found, fixed and covered in one pass.

``refresh()`` reads the message list off the main thread and guards against pile-up with ``_loading``;
a refresh requested mid-flight is latched in ``_pending`` and re-fired when the read lands. ``_apply``
Expand Down Expand Up @@ -75,7 +81,7 @@ def test_a_latched_refresh_no_longer_discards_the_snapshot(qapp: Any) -> None:
panel._apply(_MessagesSnapshot([_msg("m1")], "1 shown of 1", False, None))

assert panel._table.rowCount() == 1, (
"snapshot discarded as 'superseded' — this is the BACKLOG #17 livelock"
"snapshot discarded as 'superseded' — this is the refresh livelock"
)
# Dropping the render must not come back as dropping the re-fire either.
assert panel._pending is None, "the latched refresh was never drained"
Expand Down
20 changes: 12 additions & 8 deletions tests/test_harness_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,17 @@ def test_monitor_panel_builds_disconnected(qapp: Any) -> None:
# Override the global 60s per-test watchdog: the two waits share a 60s budget, and fixture/engine
# startup sits outside it, so a slow-but-passing CI run could otherwise brush the 60s cap.
#
# NO reruns=2 here any more, deliberately. It was added to let BACKLOG #17 "self-heal" within a run,
# on the theory that this was a residual timing flake on loaded Windows runners. It was not: the
# message list was livelocking (MessagesPanel._apply discarded every snapshot as superseded while this
# test polled refresh() at 50ms — see tests/test_console_messages_refresh.py), which is why neither
# 10s→30s nor the reruns ever fixed it. That is now fixed at the source. Keeping the retry would only
# hide the next real defect the same way it hid this one, and would make the fix unmeasurable — a
# masked failure looks exactly like a working one.
# NO reruns=2 here any more, deliberately. It was added to let this "self-heal" within a run, on the
# theory that it was a residual timing flake on loaded Windows runners. It was not: the message list
# was livelocking (MessagesPanel._apply discarded every snapshot as superseded while this test polled
# refresh() at 50ms — see tests/test_console_messages_refresh.py), which is why neither 10s→30s nor the
# reruns ever fixed it. That is now fixed at the source. Keeping the retry would only hide the next real
# defect the same way it hid this one, and would make the fix unmeasurable — a masked failure looks
# exactly like a working one.
#
# The comment this replaced blamed "BACKLOG #17". That is the py3.11 pytest/aiosqlite cancellation
# deadlock, OBSOLETE since the 3.14-only migration and unrelated to anything here — a wrong citation
# that survived long enough to be copied into new files. This defect has no backlog entry.
@pytest.mark.timeout(120)
def test_monitor_observes_engine(qapp: Any, server: tuple[str, Path]) -> None:
url, inbox = server
Expand Down Expand Up @@ -201,7 +205,7 @@ def ctx() -> str:
def test_monitor_observes_a_message_that_arrives_after_connect(
qapp: Any, server: tuple[str, Path]
) -> None:
"""The deterministic form of what CI kept hitting, and the reason BACKLOG #17 looked like a flake.
"""The deterministic form of what CI kept hitting, and the reason this looked like a flake at all.

The sibling test writes the message BEFORE the panel connects, so the single initial ``refresh()``
in ``_build_inner`` normally renders it and the polling path below is never exercised at all. That
Expand Down
Loading