Skip to content

fix(update): gate the fork-upstream prompt for --yes and non-tty runs - #97052

Merged
ethernet8023 merged 3 commits into
mainfrom
fix/014-upstream-prompt-noninteractive
Aug 28, 2026
Merged

ethernet8023 merged 3 commits into
mainfrom
fix/014-upstream-prompt-noninteractive

Conversation

@yoniebans

@yoniebans yoniebans commented Aug 28, 2026 •

Copy link
Copy Markdown

Fixes the prompt half of #60240. Supersedes #78678 and #92448, and the fork-upstream prompt-hang portion of #92410.

What happens

On a fork checkout of main with no upstream remote, hermes update asks "Add official repo as 'upstream' remote? [Y/n]:" through a bare input(). In any non-interactive context (CI, cron, the desktop updater hand-off) stdin is open but nobody answers, so the call blocks forever: --yes never reaches this prompt and the EOFError fallback only fires when stdin is closed. Observed as a 90-minute hang in a Windows open-app-update E2E leg, with the update marker never cleaned and the updater killed at teardown.

The fix

_sync_with_upstream_if_needed now takes assume_yes and the gateway input_fn, and both call sites in _cmd_update_impl forward them. Under assume_yes, or when there is no input_fn and stdio is not a TTY pair, the prompt is skipped as a decline without writing the decline marker and without touching git remotes, so interactive users still get asked on their next update. Gateway updates route the question through the existing IPC prompt callback with a default of no. Interactive terminal behavior is unchanged.

This is the same gate the file already applies to its config-migration and stash-restore prompts; this prompt was the one that never got it.

--yes deliberately declines rather than adds the remote. Adding a remote is a repo mutation, and --yes in this command means "don't block on prompts", not "make git changes I didn't ask for". The add-on-yes alternative (what #78678 implemented) was considered and rejected on that ground.

Relationship to the superseded PRs

#78678 (@BlackishGreen33) had the right structure (thread assume_yes + input_fn, non-persisting skip) but made --yes add the remote, and also bundled the _is_fork case-sensitivity fix that #68959 covers on its own. #92448 (@salch-cred) had the right diagnosis but its unattended decline flows into the branch that persists the skip marker, permanently silencing the prompt for interactive users, and its TTY guard alone cannot cover the hidden-console case that --yes threading does. #92410 (@jackulau) bounds every prompt with reader threads; with assume_yes and the TTY gate in place the residual population (interactive-looking console, no --yes, nobody present) does not justify the thread machinery for this prompt. That case is intentionally left unresolved here rather than fully superseded, and #92410's timeout bounding of the other prompts is likewise out of scope. All three authors are credited on the commit.

The _is_fork URL-normalization half of #60240 stays with #68959, which already covers more URL forms.

Tests

tests/hermes_cli/test_update_upstream_prompt_noninteractive.py pins the gate: assume_yes and every non-TTY stdio combination return without calling input(), without touching remotes and without persisting the decline; the gateway path routes through input_fn and keeps its decline persistence; interactive accept and decline keep today's behavior. The suite fails on main (5/7, including TypeError on the new signature) and passes with the fix. Neighboring suites test_cmd_update.py (38) and test_update_yes_flag.py + test_update_autostash.py (52) pass; one assertion in test_cmd_update.py updated for the new call signature.

Review follow-up (@helix4u)

On a genuine fork with no upstream remote whose HEAD matches the user's own origin/main while official main is ahead, the skip left the update printing plain "Already up to date!" even though the official repo was never consulted. _sync_with_upstream_if_needed now returns whether official upstream was actually checked, and that completion line reads "Up to date with your fork (official repo not checked)." when it was not. Skip semantics are unchanged: no prompt, no remote mutation, no decline marker. A caller-level regression test through _cmd_update_impl pins the path, and the --yes help text now states that the fork-upstream prompt is skipped rather than accepted.

End-to-end verification

Verified on the end-to-end path the hang was found on: a Windows desktop-updater hand-off leg (install old build, click Update now in the app, detached updater runs hermes update --yes --gateway --force --branch main with an open unattended stdin, on a checkout that fork-detection classifies as a fork with no upstream remote). With a pre-fix baseline the leg wedges at Add official repo as 'upstream' remote? [Y/n]: until an external kill, and the update marker is never cleaned. With the fix in the running code the same leg logs Skipping upstream setup (non-interactive run)., the update exits 0 on the first attempt, the marker is cleaned, and the app relaunches. One caveat for anyone watching similar CI: an install whose baseline predates this fix still wedges once, because its first update runs the old code in memory; the fix applies from the next update on.

_sync_with_upstream_if_needed called bare input() with no assume_yes parameter and no tty check, so a fork checkout without an upstream remote wedged hermes update forever in any non-interactive context (CI, cron, the desktop updater hand-off): stdin stays open, EOFError never fires. Thread assume_yes and the gateway input_fn into the helper and skip the prompt as a decline under assume_yes or a non-tty stdio pair, without writing the decline marker or touching git remotes, so interactive runs still get asked later. Both call sites forward the interaction state; the config-migration and stash-restore prompts already carry this gate.

Closes #60240 (prompt half). Supersedes #78678, #92448, #92410.
Co-authored-by: BlackishGreen33 <BlackishGreen33@users.noreply.github.com>
Co-authored-by: salch-cred <salch-cred@users.noreply.github.com>
Co-authored-by: jackulau <jackulau@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on b27b6eb — docs(update): --yes help states the fork-upstream prompt is

⚠️ Warnings

OSV vulnerability scan · View job

6 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 4m48s vs 4m24s (+9.1%). 10 job(s) slower, 2 faster,

  • OS-specific tests / Windows-only tests: +13.0s
  • OS-specific tests / macOS-only tests: +10.0s
  • Python lints / Windows footguns (blocking): +4.0s
  • Check contributors / check-attribution: -4.0s
  • Python tests / Run tests: +3.0s

@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 sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 28, 2026

@monerostar monerostar 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.

Ubuntu 26.04 on a 5800X box (kernel 7.0.0-30-generic). This host runs hermes update from cron, so the hang class is live here.

Piped stdin (isatty False/False) against main 8c098e9:
_sync_with_upstream_if_needed still calls input("Add official repo as 'upstream' remote? [Y/n]: ").
That is the unattended hang.

Same probe on this PR (1451f88):
input() not called, remotes untouched, skip marker not written.
Printed: Skipping upstream setup (non-interactive run).

PR tests: 7 passed in 0.27s (Python 3.11.15 against the PR tree).

Looked at #92448 too. It also skips when stdin is unattended, but then treats that as "n" and calls _mark_skip_upstream_prompt(), so the next interactive update never asks. This PR returns without the marker, and also forwards --yes plus the dashboard input_fn. Prefer this one.

Looks good.

@salch-cred

Copy link
Copy Markdown

monerostar's independent check confirms what I found when closing #92448: my unattended decline persisted the skip marker (via _mark_skip_upstream_prompt), permanently silencing the prompt for interactive users. This PR's non-persisting skip plus the --yes/input_fn threading is the right call. Glad it's the one carrying the fix.

@helix4u helix4u 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.

The diagnosis is right, and the overall shape is good: threading assume_yes and input_fn into this prompt fixes the Desktop updater path and ordinary non-TTY automation without changing interactive behavior.

I do see one correctness blocker before merge, though.

On a genuine fork with no upstream remote, where local HEAD matches the user's origin/main but that fork is behind official main, hermes update --yes now does this:

  1. Fetches the user's fork.
  2. Finds zero commits between HEAD and origin/main.
  3. Skips upstream setup because assume_yes=True.
  4. Never checks official main.
  5. Falls through to Already up to date! and exits successfully.

That means an unattended fork can remain stale indefinitely while every update reports success. Once #68959 removes the false-fork cases, genuine forks are the main population left on this path, so this outcome becomes more important.

The --yes help currently says "Assume yes for interactive prompts," so I think the cleanest behavior is to accept the upstream prompt, add the official remote, and continue the existing sync flow, as #78678 did. If permanently adding a remote is not desired, a one-off fetch from the official URL would also preserve the update behavior without changing the configured remotes. At minimum, the helper needs to report that upstream was not checked so the caller cannot claim the checkout is up to date.

Please add a caller-level regression test around _cmd_update_impl, not only direct helper tests: genuine fork, no upstream, HEAD == origin/main, --yes, and official main ahead. The assertion should prove that the update either checks official main or does not report successful Already up to date.

Two smaller scope notes:

  • The gateway callback defaults to "n" on timeout, and that response flows into _mark_skip_upstream_prompt(). An unanswered gateway prompt can therefore permanently silence later interactive prompts, which is the same persistence problem called out in #92448.
  • This does not fully supersede #92410. A hidden console that still reports TTY/TTY, runs without --yes, and has nobody present can still block here, and #92410 also covers the separate stash-restore prompt. It is fine to reject that timeout machinery as outside this PR, but the description should call that an intentionally unresolved case rather than complete supersession.

Everything else looks focused and well-tested, and required CI is green. Fixing the successful-stale-fork path is the main thing I would hold the merge on.

…to-date path

Review follow-up on #97052 (helix4u): a fork with no upstream remote whose HEAD matches origin/main used to print plain "Already up to date!" under --yes even though official main was never consulted, so an unattended stale fork looked current. _sync_with_upstream_if_needed now returns whether the official upstream was actually checked, and the commit_count == 0 completion line says "Up to date with your fork (official repo not checked)." when it was not. Skip-as-decline semantics are unchanged: no prompt, no remote mutation, no decline marker. Caller-level regression test added for the fork + no-upstream + --yes + HEAD==origin/main path; helper tests now pin the return contract.
…not accepted

The old text read as if --yes answers yes to every prompt. It accepts the config-migration and stash-restore prompts but skips the fork-upstream prompt without adding a remote (#97052 review); say so.
@yoniebans

Copy link
Copy Markdown
Author

Thanks for the close read. You're right about the scenario: someone forks the repo, their machine matches their own fork, but the official repo has moved on. With no upstream remote and --yes, the update skipped the question, never looked at the official repo, and still printed "Already up to date!". That message was a lie: it only checked their fork. Fixed in 5ce4ff2 the way you suggested as the minimum: the helper now reports whether the official repo was actually checked, and when it wasn't the update prints "Up to date with your fork (official repo not checked)." instead. Also added the test you asked for, running the whole update command in exactly that setup and proving it can no longer claim plain up-to-date there.

On making --yes answer yes and add the remote: we looked at that and decided against it, because saying yes doesn't stop at adding the remote. The flow that follows pulls official code onto the machine and then force-pushes it to the user's fork on GitHub. We don't think an unattended run should rewrite someone's fork because a flag said "don't ask me questions". The --yes help text was on your side of this argument ("Assume yes for interactive prompts"), so we fixed the text (b27b6eb): it now says which prompts it accepts and that this one is skipped without adding a remote.

Agreed on both scope points. The PR body now claims to supersede only the prompt-hang part of #92410 and names the gap we knowingly leave (a console that looks interactive, no --yes, nobody present). And the chat-prompt timeout writing a permanent "no" is real: fixing it means telling "user declined" apart from "nobody answered", which needs its own small change to _gateway_prompt, so we'd take that as a follow-up rather than widen this diff.

@alt-glitch alt-glitch added P2 Medium — degraded but workaround exists needs-decision Awaiting maintainer decision before any implementation and removed P3 Low — cosmetic, nice to have labels Aug 28, 2026
@ethernet8023
ethernet8023 merged commit 00bbfc6 into main Aug 28, 2026
37 checks passed
ethernet8023 pushed a commit that referenced this pull request Aug 28, 2026
…to-date path

Review follow-up on #97052 (helix4u): a fork with no upstream remote whose HEAD matches origin/main used to print plain "Already up to date!" under --yes even though official main was never consulted, so an unattended stale fork looked current. _sync_with_upstream_if_needed now returns whether the official upstream was actually checked, and the commit_count == 0 completion line says "Up to date with your fork (official repo not checked)." when it was not. Skip-as-decline semantics are unchanged: no prompt, no remote mutation, no decline marker. Caller-level regression test added for the fork + no-upstream + --yes + HEAD==origin/main path; helper tests now pin the return contract.
@ethernet8023
ethernet8023 deleted the fix/014-upstream-prompt-noninteractive branch August 28, 2026 17:34
yoniebans added a commit to ethernet8023/hermes-agent that referenced this pull request Sep 2, 2026
The pre-NousResearch#97052 prompt wedge fires AFTER the updater has already moved
the checkout (stash cleanup, reset, bootstrap refresh all precede the
upstream-remote prompt), so checkout movement cannot distinguish a
wedged updater from a working one. Two CI rides confirmed the check
never fires. The 35-minute wait bound already caps these legs.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…to-date path

Review follow-up on NousResearch#97052 (helix4u): a fork with no upstream remote whose HEAD matches origin/main used to print plain "Already up to date!" under --yes even though official main was never consulted, so an unattended stale fork looked current. _sync_with_upstream_if_needed now returns whether the official upstream was actually checked, and the commit_count == 0 completion line says "Up to date with your fork (official repo not checked)." when it was not. Skip-as-decline semantics are unchanged: no prompt, no remote mutation, no decline marker. Caller-level regression test added for the fork + no-upstream + --yes + HEAD==origin/main path; helper tests now pin the return contract.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…not accepted

The old text read as if --yes answers yes to every prompt. It accepts the config-migration and stash-restore prompts but skips the fork-upstream prompt without adding a remote (NousResearch#97052 review); say so.
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 needs-decision Awaiting maintainer decision before any implementation P2 Medium — degraded but workaround exists sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants