Skip to content

fix(gateway): warn on stale control UI bundle - #242

Merged
keyKQ merged 2 commits into
use-agent-os:mainfrom
keyKQ:fix/control-ui-staleness
Aug 7, 2026
Merged

fix(gateway): warn on stale control UI bundle#242
keyKQ merged 2 commits into
use-agent-os:mainfrom
keyKQ:fix/control-ui-staleness

Conversation

@keyKQ

@keyKQ keyKQ commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #238, which conflicts with main after #239/#240 landed and could
not be updated in place (pushes to that fork are rejected despite
maintainer_can_modify). The original commit is carried here unchanged, so
@Callmedas69 keeps authorship — please merge with rebase, not squash.

What

#238's detection is right: nothing signalled that git pull had left the
bundled Control UI older than the frontend sources. This keeps that and fixes
how it was wired.

The verdict could not change its mind

frontend_input_mtime / control_ui_is_stale were @functools.cached.
Gateway boot calls them once so caching bought nothing there, but
doctor.status is a long-lived RPC. An operator ran the rebuild the FixStep
told them to run, re-ran agentos doctor, and got the same warning — until
they restarted the gateway. The evidence contradicted itself too, because
bundleMtime was re-stat'd fresh while stale and sourceMtime came from
the cache:

doctor #1: stale=True  sourceMtime=…300  bundleMtime=…200
doctor #2: stale=True  sourceMtime=…300  bundleMtime=…400
                                         ^ rebuilt, still reported stale

No test could catch it: every test used a fresh tmp_path, so no cache key was
ever hit twice.

Nothing is memoized now, and inspect_control_ui_bundle() returns one report
computed in a single pass, so those three fields can no longer disagree.

It broke two existing doctor tests

$ uv run pytest tests/test_gateway/test_rpc_doctor.py -q
FAILED test_doctor_status_degrades_when_noncritical_collection_fails  # assert 2 == 1
FAILED test_doctor_status_treats_no_channels_as_optional_setup        # assert 'degraded' == 'ready'

The collector read the real repo and wasn't stubbed by
_patch_ready_support_surfaces, so a plain git checkout — which rewrites
frontend source mtimes without touching the gitignored dist/ — leaked a
degrades finding into tests asserting exact impact counts. CI only stayed
green because ci.yml runs build_control_ui.py build immediately before
pytest.

Two fixes, both worth having: the helper is stubbed in that shared helper, and
the finding is readiness_impact="optional" instead of "degrades". Source
mtimes are a hint, and a heuristic git checkout can trip must not turn the
whole report yellow. The finding still shows up with its rebuild step.

Also

  • Moved to health/control_ui.py. The top-level agentos/control_ui_check.py
    existed to dodge the import contract — health has no approved outgoing edge
    to gateway, so the evaluator couldn't reach _DIST_DIR. Under health/ the
    evaluator imports it intra-package and the gateway reaches it over the
    already-approved ("gateway", "health") edge. No dodge, and the mirrored
    DIST_REL constant is gone: both gateway callers already own _DIST_DIR and
    now pass the bundle path in, which also deletes the contract test that only
    existed to police the duplication.
  • The doctor collector takes ctx and honours config.control_ui.enabled,
    which boot already did. No more warning about a bundle nobody serves.
  • The walk starts at frontend/ instead of a named allowlist. That makes
    the node_modules/dist prune load-bearing — it pruned nothing before, since
    the walk roots were frontend/src and frontend/public while the real
    node_modules sits at frontend/node_modules — and picks up inputs the list
    had missed: eslint.config.js, components.json, and
    scripts/check-bundle-budget.mjs, which npm run build invokes via
    bundle:budget. An allowlist's failure mode is a false negative nobody sees.
  • checkout_root() looks for the checkout markers instead of counting
    parent levels, so relocating the module can't silently retarget it at whatever
    sits above site-packages.
  • Both consumers are now tested. The boot ordering (missing outranks stale)
    moved into control_ui_boot_warning so it's testable without standing up a
    gateway, plus a guard test that keeps boot.py going through the helper.
  • CHANGELOG blank line, key != "stale", and the redundant lambda.

Validation

  • python scripts/build_control_ui.py build
  • npm --prefix frontend run check — 1738 passed
  • uv run ruff check src tests
  • uv run mypy src/agentos --show-error-codes — 592 files
  • uv run pytest tests -q — 7349 passed, 27 skipped
  • uv build --wheel

Fixes #200. Closes #238.

Callmedas69 and others added 2 commits August 7, 2026 20:30
Gateway boot and agentos doctor warn when the bundled React control UI predates the frontend sources in a checkout (gateway.control_ui.dist_stale). Wheel installs ship no frontend sources and stay silent.
Review follow-up on the stale-bundle warning. The detection was right; how it
was wired made it unable to change its mind, and it leaked into unrelated tests.

The verdict was memoized for the process lifetime. Gateway boot calls it once
so caching bought nothing there, but doctor.status is a long-lived RPC: an
operator ran the rebuild the FixStep told them to run, re-ran `agentos doctor`,
and got the same warning — until they restarted the gateway. The evidence
contradicted itself too, because bundleMtime was re-stat'd while stale and
sourceMtime came from the cache:

  doctor use-agent-os#1: stale=True  sourceMtime=...300  bundleMtime=...200
  doctor use-agent-os#2: stale=True  sourceMtime=...300  bundleMtime=...400

Nothing is memoized now, and inspect_control_ui_bundle() returns one report
computed in a single pass, so the three fields can no longer disagree.

The collector also broke two existing doctor tests. It read the real repo and
was not stubbed by _patch_ready_support_surfaces, so a plain `git checkout` —
which rewrites frontend source mtimes without touching the gitignored dist/ —
leaked a degrades finding into tests asserting exact impact counts. CI only
stayed green because ci.yml builds the Control UI immediately before pytest.
The helper is stubbed now, and the finding is readiness_impact="optional"
rather than "degrades": mtimes are a hint, and a heuristic that git checkout
can trip must not turn the whole report yellow.

Also:

- Moved to health/control_ui.py. The top-level module existed to dodge the
  import contract, since health has no approved outgoing edge to gateway.
  Under health/ the evaluator imports it intra-package and the gateway reaches
  it over the already-approved ("gateway", "health") edge — no dodge, and the
  mirrored DIST_REL constant is gone because both gateway callers already own
  _DIST_DIR and now pass the bundle path in.
- The doctor collector takes ctx and honours config.control_ui.enabled, which
  boot already did. No more warning about a bundle nobody serves.
- The walk starts at frontend/ instead of a named allowlist, which makes the
  node_modules/dist prune load-bearing (it pruned nothing before, since the
  walk roots were frontend/src and frontend/public) and picks up inputs the
  list had missed: eslint.config.js, components.json, and
  scripts/check-bundle-budget.mjs, which npm run build invokes.
- checkout_root() looks for the checkout markers instead of counting parent
  levels, so relocating the module cannot silently retarget it above
  site-packages.
- The boot ordering (missing outranks stale) lives in control_ui_boot_warning
  so it is testable without standing up a gateway; both consumers now have
  coverage, and a guard test keeps boot.py going through the helper.
- CHANGELOG blank line, `key != "stale"`, and the redundant lambda.

Gate: build_control_ui, npm check (1738), ruff, mypy (592), pytest (7349),
uv build --wheel.
@keyKQ
keyKQ merged commit 96c7b07 into use-agent-os:main Aug 7, 2026
7 checks passed
@keyKQ
keyKQ deleted the fix/control-ui-staleness branch August 7, 2026 13:51
@Callmedas69

Copy link
Copy Markdown
Contributor

Thanks for taking this over and carrying the original commit. Appreciate you keeping the authorship on it.

Read through #242. The detection idea survived, but the wiring is yours. #238 had the right instinct and the wrong mechanics. The memoized verdict was a real bug: doctor.status could never see the rebuild, and the test suite silently missed it because every test used a fresh tmp_path. The degrades impact breaking existing test_rpc_doctor tests was my miss too. I had not stubbed the collector in _patch_ready_support_surfaces, and the CI gate hid it because build_control_ui.py build ran first.

inspect_control_ui_bundle() computing one report in a single pass, the stub paired with readiness_impact="optional", walking from frontend/ so the prune is real and catches eslint.config.js, components.json, check-bundle-budget.mjs, and checkout_root() by markers instead of parent counting. All yours. Cleaner than the split reads and allowlist #238 shipped.

Synced local main to the merge. Lesson taken on stubbing host-dependent collectors and testing consumers, not just the helper.

Thanks again.

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.

No signal when a source checkout's control UI bundle is stale

2 participants