Skip to content

fix(update): preserve clean refusal when gateway resume fails - #108288

Closed
bgrablin wants to merge 2 commits into
NousResearch:mainfrom
bgrablin:fix/update-resume-clean-refusal
Closed

bgrablin wants to merge 2 commits into
NousResearch:mainfrom
bgrablin:fix/update-resume-clean-refusal

Conversation

@bgrablin

@bgrablin bgrablin commented Sep 11, 2026 •

Copy link
Copy Markdown

What does this PR do?

Keeps the Windows venv-holder refusal path on its intended exit-2 contract when best-effort gateway recovery raises. Recovery exceptions are contained on both the synchronous refusal path and the registered atexit retry, so neither can replace the refusal with a traceback.

The Windows hand-off child performs its pending retry before the existing os._exit boundary, which skips atexit. If the first launch reached liveness verification, the retry verifies that launch again instead of creating duplicate detached restart watchers.

This complements #106702, which covers the separate already-up-to-date path.

Related Issue

Related to #106692. Complements #106702.

Type of Change

  • ☑ 🐛 Bug fix (non-breaking change that fixes an issue)
  • ☐ ✨ New feature (non-breaking change that adds functionality)
  • ☐ 🔒 Security fix
  • ☐ 📝 Documentation update
  • ☐ ✅ Tests (adding or improving test coverage)
  • ☐ ♻️ Refactor (no behavior change)
  • ☐ 🎯 New skill (bundled or hub)

Changes Made

  • Treat gateway recovery as best effort before a venv-holder refusal exits with status 2.
  • Register the best-effort recovery wrapper for the atexit path so repeated liveness failures cannot emit an exit-time traceback.
  • Run pending gateway cleanup before the Windows hand-off child reaches its existing hard-exit boundary.
  • Preserve retry state while preventing a liveness retry from launching duplicate detached restart watchers.
  • Add regression tests for the refusal, atexit, hand-off retry, and duplicate-launch paths.

How to Test

  1. The clean-refusal regression fails without exception containment because the simulated gateway liveness error escapes before SystemExit(2).
  2. The duplicate-launch regression fails without verification-pending state because the hand-off retry creates the same detached restart watcher twice.
  3. Run the focused Windows updater suites: 151 tests pass with 0 failures; 4 Windows-only tests skip on the Linux harness across 11 files.
  4. Run Ruff, bytecode compilation, the Windows-footgun scanner, and the whitespace error check: all exit 0.
  5. The earlier canonical full-suite run completed with 9 failed tests across eight files, and three additional files hit the 300-second per-file timeout. All failures were outside the changed files; the updater-adjacent failure reproduced unchanged on the unmodified base (1 passed, 1 failed).

Checklist

Code

  • ☑ I've read the Contributing Guide
  • ☑ My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • ☑ I searched for existing PRs to make sure this isn't a duplicate
  • ☑ My PR contains only changes related to this fix/feature (no unrelated commits)
  • ☐ I've run pytest tests/ -q and all tests pass
  • ☑ I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • ☑ I've tested on my platform: Linux test harness exercising the Windows-specific path with controlled fakes; live Windows validation was not rerun

Documentation & Housekeeping

  • ☑ I've updated relevant documentation (README, docs/, docstrings) — N/A; no user-facing documentation changed
  • ☑ I've updated cli-config.yaml.example if I added/changed config keys — N/A; no config keys changed
  • ☑ I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A; no architecture or workflow changed
  • ☑ I've considered cross-platform impact (Windows, macOS) per the compatibility guide — Windows-only recovery behavior is isolated and covered by host-independent tests
  • ☑ I've updated tool descriptions/schemas if I changed tool behavior — N/A; no tool contract changed

Screenshots / Logs

Not applicable. Deterministic regression and focused-suite evidence are listed above.

Infographic

Initial diagnostic poster showing a recovery error causing a traceback before the fix, then best-effort containment preserving exit status 2 and a pending retry; the initial candidate passed 75 targeted tests.

The infographic reflects the initial candidate. The current candidate passed the expanded 151-test focused gate described above.

Copilot AI lite review requested due to automatic review settings September 11, 2026 14:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The registered retry path and hand-off cleanup behavior still need to be addressed.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR preserves the Windows updater’s intended exit-2 refusal behavior when gateway recovery fails.

Changes:

  • Handles gateway recovery as best effort.
  • Preserves pending recovery state.
  • Adds regression coverage for clean refusal behavior.
File summaries
File Summary
tests/hermes_cli/test_update_orphan_backend_reap.py Adds coverage for recovery failure preserving exit status 2.
hermes_cli/update_cmd_windows.py Contains two unresolved moderate findings: atexit retry exceptions remain unsuppressed (2 votes), and hand-off hard exit skips the registered cleanup (1 vote).
Review details

Suppressed comments (1)

hermes_cli/update_cmd_windows.py:1215

  • This converts a resume failure into SystemExit(2), but the Windows hand-off child (HERMES_UPDATE_REEXEC=1) treats that as an early exit and calls os._exit in cmd_update's finally; os._exit skips Python atexit handlers. Therefore the callback registered before this guard never gets the promised retry, and any gateway that failed to resume remains down. Please run the pending cleanup before the hand-off hard exit, or avoid hard-exiting this refusal path.
        with _best_effort("Windows gateway service restart incomplete: %s"):
            _m()._resume_windows_gateways_after_update(_windows_gateway_resume)
        # Keep a failed token pending for the registered atexit retry.
        sys.exit(2)
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread hermes_cli/update_cmd_windows.py Resolved
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard area/install-update Installer, updater, packaging, wheels, doctor platform/windows sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Sep 11, 2026
@bgrablin

bgrablin commented Oct 4, 2026

Copy link
Copy Markdown
Author

Closing this to free an open-PR slot. The branch stays.

@bgrablin bgrablin closed this Oct 4, 2026
@alt-glitch alt-glitch added os/windows Native Windows behaviour or breakage: PowerShell install, Windows paths, cmd.exe, WSL-vs-native diff and removed platform/windows labels Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/cli CLI entry point, hermes_cli/, setup wizard os/windows Native Windows behaviour or breakage: PowerShell install, Windows paths, cmd.exe, WSL-vs-native diff P3 Low — cosmetic, nice to have sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants