Commit Graph

319 Commits

Author SHA1 Message Date
Teknium
ea665a911d fix(update): one custody policy for every updater git and Node child
R2: hermes_cli/update_custody.run_git is THE updater git runner;
update_cmd._git_run, update_cmd_git._git_run, update_cmd_stash,
update_cmd_check._git, gitlock, update_cmd_zip and the fleet ancestry probe
all go through it.

- every updater git call carries -c gc.autoDetach=false
  -c maintenance.auto=false: no detached gc/maintenance child ever forks
  under (POSIX) or outlives (Windows) the checkout lock;
- POSIX: only local mutators (reset, checkout, merge, stash, rebase,
  update-ref, clean, apply, commit, ...) inherit the lock fd, with
  core.fsmonitor=false; fetch/ls-remote/credential and readers run without
  it, so a credential-cache daemon never holds the checkout (reviewer: up to
  2 h of installer waits). On Linux those fd-less children die with the
  updater (PR_SET_PDEATHSIG) instead of rewriting refs under the next owner;
- Windows: inside an update every child is created suspended, assigned to
  the owner's kill-on-close job and only then resumed; the Node build
  (pm.progress.run_contained) joins it through a stdlib launcher.

The fork sync fast-forwards locally (merge --ff-only
refs/remotes/upstream/main after its fetch) instead of a networked pull
under the mutator fd.
2026-10-06 04:36:02 -07:00
Teknium
a66193a8c7 fix(update): A7 marker mutex, one incarnation rule, delegate-aware release, checkout custody
R3 / A7 rule 1: every marker decision-and-mutation (reclaim of a dead claim,
adopt + delegate append, release, ownership hand-over) now runs while holding
the kernel lock on the never-deleted sidecar `<marker>.lock` (POSIX flock;
Windows: the file opened share-none, the same lock the PowerShell hand-off
takes), bounded to 10 s. Only the exclusive first publish stays lock-free
(rule 2). Python (update_lock.marker_mutex) and Rust (marker.rs lock_marker)
interoperate on the same lock. A stale reader paused before its unlink can no
longer delete a claim published after its read.

A7 rule 4 / R12: one incarnation rule. Our own pid is ours only within 5 ms of
our creation time; a no-ct claim naming our pid is a previous incarnation
(dead) — in Python (`update_lock.incarnation_live`, exported for the pause
record) and in marker.rs (no more 2 s self tolerance; Electron's v1 own-pid
pre-write is reclaimed and republished as v2; the exit-2 heal requires our
exact incarnation).

A7 rule 5: release is identity-based (`_release_decision` / `release_decision`).
The owner removes its claim regardless of delegate lines unless a LIVE
delegate other than itself exists, which then inherits the claim (rewritten
with the delegate as owner, started_at and run lines kept). A delegate drops
its line while the owner lives, else deletes. This is the native Windows
`orphaned_update` root cause: the hand-off script named `hermes update` as
delegate before it claimed, the child adopted without writing, and its exit
released nothing, leaving '<dead owner> ... delegate:<exited child>'.

Parser (A7 rule 7): lines 4+ are tagged — first well-formed delegate line,
every `run:<id>` line; pid must fit u32. Both consumers run
tests/fixtures/update_marker_corpus.json (judge + release cases).

R2: checkout mutators keep custody. `_git_run` passes the held checkout lock
fd to git (and its hooks); `run_source_script` passes it to the Node build;
`complete_source_checkout` ACQUIRES `UpdateLock(install_root=root)` instead of
claiming only its home's marker; venv_sync's interrupted-update finish acquires
the checkout lock before mutating instead of sampling it and hands the fd to
its completion child; the historical takeover chain passes it down. On Windows
a child of the update joins the lock when its holder record names a live
ancestor (it runs in that owner's kill-on-close job).

R11: a live pre-root-marker claim under <root>/profiles/*/ (the old per-profile
marker) is observed: a new update refuses while one is live and not a partner.

Real-process tests: tests/hermes_cli/test_update_lock_protocol.py (each fails on
6ff4055cd74), corpus consumers in Python and cargo.
2026-10-06 04:36:02 -07:00
Teknium
0b5b01ba7a fix: a post-commit follow-up is owed once, through one owe_followup helper
"record the follow-up, then amend the closed receipt" was hand-written at six
sites (three Windows-resume sites, the historical-takeover resume,
gateway_restart, channel_adoption). Two of them -- _complete_source_update's
Windows resume and channel_adoption -- amended without checking that the
run's receipt was already closed. On an open run record_followup has just
persisted the follow-up into the running record, so the amend appended a
second copy to that durable record and to latest.json (what an interrupted
run leaves behind).

update_receipt.owe_followup(update_id, step, reason) records the step and
amends the archive only once the run is closed; every site now calls it.
2026-10-06 03:44:49 -07:00
Teknium
c7db1c0819 fix(update): the gateway /update status follows a committed run's receipt
After the run's receipt closed as `success`, three boundaries could still
write 1 to .update_exit_code (and the completion child could still answer
130), so the gateway watcher reported a failed update the receipt calls a
success:

- cmd_update's catch-alls (main.py, unchanged) call
  _write_gateway_update_exit_code(False) for any escaping exception. The
  writer now keeps 0 when this command's run is closed as a success
  (update_receipt.committed_success: the receipt scope records the terminal
  outcome it finalized, or adopted from the completion child).
- The parent adopts the child's terminal receipt and drops its stale
  pre-child context as soon as the result is accepted, before the parent-
  side Windows resume, so nothing after it can write that context.
- update_completion._finish: an interrupt that lands after verification
  finalized the run answers 0 (the receipt's verdict); the 1 for a parked
  stash (partial) or an interrupt before the run closed is unchanged, and
  the status is written once the code is settled.
- update_finish (historical takeover): the same rule for its finally.

An interrupted run (receipt `interrupted`, exit 130) still writes 1, as
test_interrupt_after_child_success_demotes_gateway_marker_at_boundary pins.

Review: kshitijk4poor 5411222454 regression 3 / open thread F4.
2026-10-06 03:44:49 -07:00
Teknium
8e9b1bcb58 fix(update): a lost completion is owed without adopting a retired channel
Follow-up to the lost-completion settle (review P2):

- A retired channel is adopted only after a VERIFIED completion: a lost
  one exits 0 with the owed `completion` follow-up but leaves the config
  on the retired channel; the next `hermes update` resolves the
  retirement again and adopts it (the same retry the adoption-failure
  follow-up already names).
- The settle triggers on a non-zero answer without a receipt (what
  run_completion returns for every answer that is not a correlated
  terminal receipt) and runs at most once per run (a spawn failure that
  could not finalize does not settle twice).

Test changes (test_source_channel_integration.py), each pinning the
failure invariant 3 forbids:
- test_retirement_adopts_destination_only_after_success[failed]: the
  completion answered exit 1 without a receipt and the test expected
  SystemExit; it is now an owed completion (exit 0). The channel
  assertion is unchanged: the destination is still not adopted.
- test_retirement_waits_for_correlated_completion_process[uncorrelated|
  missing]: same, an uncorrelated/missing answer was SystemExit and is
  now owed; the channel still stays on the retired one. [failure] (the
  child answered a correlated failed receipt) still raises.

Review P2.
2026-10-06 03:44:49 -07:00
Teknium
1faf3b5e76 fix(update): a lost completion after the commit point is owed, not failed
After the tree moved, an OSError from run_completion's TemporaryDirectory,
_write_json or Popen (or anything the spawn path raises) reached
main.cmd_update as "Update failed": receipt failed, .update_exit_code 1,
exit 1. A completion child killed by OOM/SIGKILL, or one that exited
without a correlated terminal result, returned `code or 1` and exited
non-zero the same way. Both contradict invariant 3: the code is
committed, only the tail is owed.

_complete_source_update now settles both through
update_completion.settle_lost_completion (pre-swap module, everything it
uses is already imported in the parent): it arms the source-update tail
for the next launch (the bootstrap child may never have run to arm it),
names the owed Desktop build (P4), records a `completion` follow-up and
finalizes the run success-with-follow-ups, publishes the gateway
watcher's 0, and returns exit 0 -- so the parent also resumes paused
Windows gateways the lost child never resumed. Meanings kept: Ctrl-C
stays 130 (a 130 without a result finalizes `interrupted`, not
`failed`), parked local changes stay `partial` / exit 1 (#122557), and a
child that finalized then died keeps its own terminal record.
run_completion no longer prints "✗ ... did not finish"; it hands the
reason to the caller.

Test change: test_missing_child_result_fails_boundary_receipt_and_releases_lock
pinned exit `code or 1`, receipt `failed`, .update_exit_code 1 and
resume_needed still True -- exactly the outcome invariant 3 forbids. It is
now test_missing_child_result_is_an_owed_completion_and_releases_lock
(adds a SIGKILL case): exit 0, receipt success with a `completion`
follow-up, watcher 0, gateways resumed, lock released. New: spawn
OSError is owed; a lost completion with parked changes stays partial/1.

Review P2.
2026-10-06 03:44:49 -07:00
Teknium
7b3dae822b chore(update): post-commit catch-all boundaries carry the code-health waiver
main's code-health ratchet (4156715960) ignores `# noqa: BLE001`, and ruff does not
select BLE here, so those markers waived nothing. Each catch-all is the C3 boundary:
after the commit point the failure is a receipt follow-up, never the update's status.
The stated reason moves into `# health: allow BLE001 -- <why>` unchanged.
2026-10-06 03:44:49 -07:00
Teknium
ccfbb2e861 fix(update): a refused Windows resume after the commit point never fails the update (M4)
- After the completion child answers (or the A6 branch resumes), the post-commit
  resume attempt is settled: its failure is already the windows_resume follow-up,
  so the parent's atexit net is unregistered and the command's finally does not
  replay it. A replay waited again and, on the ZIP route, its RuntimeError reached
  main.py's BaseException handler: exit 1 and "/update failed".
- The ZIP and CalledProcessError finally blocks resume only what is still owed and
  report a failure as the windows_resume follow-up instead of raising.
- update_finish (historical takeover) records a refused resume as a follow-up and
  keeps its exit status (was code = 1), and reports the attempt as handled so the
  historical parent does not replay it.

Tests (real processes, refusing SCM resume is the only service seam):
test_zip_update_never_replays_a_refused_resume_after_the_commit_point,
test_refused_gateway_resume_after_commit_is_a_followup_not_exit_1.
2026-10-06 03:44:49 -07:00
Teknium
8e59e0ec20 fix(update): a dependency sync that fails after the tree moved is a follow-up (A6); Ctrl-C says the code moved
The completion bootstrap's PM step (ensure_tools_for_sync / pm.sync_venv) runs
after the checkout moved. Its failure ended in exit 1 with no receipt, which the
Desktop reads as "still on the previous version" while the tree is new.

- update_completion._bootstrap: a failure there is now settled as a post-commit
  follow-up: the tail obligation stays armed, the run's receipt is a success
  naming a `dependencies` follow-up ("dependencies not installed yet — the next
  launch retries"), exit 0 (.update_exit_code 0 in gateway mode). A prepared
  child that dies without a result settles the same way (`completion`).
- The parent resumes paused Windows gateways itself when the bootstrap could
  not (it has no dependencies to do so).
- Ctrl-C after the commit point (parent in run_completion, or the child) closes
  the run as `interrupted` (never `failed`), exit 130, and prints that the new
  code is in place and its remaining steps are owed.
- TAIL_FOLLOWUPS gains `dependencies`.

Restores the dropped zip-completion "preparation" case with the new expectation.
2026-10-06 03:44:49 -07:00
Teknium
f82c091019 refactor(update): move post-restart fleet verification into update_cmd_fleet_verify
update_cmd_fleet.py had grown past the 2,000-line cap (2,230). The verify phase
(_verify_fleet_after_update, the settle-polled fleet snapshot, owed-inventory
recording, legacy-unit warning) is its own topic: it runs after the restart
phase and decides follow-up vs clear. It now lives in a sibling; restart-phase
helpers it uses are read through update_cmd_fleet so their test patches still
apply. update_cmd re-imports _verify_fleet_after_update from the new module.
Tests retarget their patches to where production reads. No behaviour change.
2026-10-06 03:44:49 -07:00
Teknium
045615ac5a fix(update): after the commit point nothing fails hermes update (C3) 2026-10-06 03:44:49 -07:00
Teknium
54365962e8 refactor(update): move the no-prompt git kwargs helper where the bootstrap can reach it
The convert-first step runs in the bootstrap interpreter, which cannot import
update_cmd (ruamel), so it carried its own raw os.environ copy (the spawn-env
guard flagged it) and lacked the Windows console-hide flags (#117781).
Move the body unchanged to _subprocess_compat.no_prompt_git_kwargs: prompts
off, credential helpers kept, consoles hidden. update_cmd keeps the
_no_prompt_git_kwargs name as an import because old updaters load it after
the checkout swap (tests/compat/old_updater_surface.json); existing callers
are untouched.
2026-10-05 17:32:54 -07:00
teknium1
a0f47657b9 feat(update): clean up a partial clone's packs a little on every update
Each `hermes update` now spends at most 60 s on the on-demand packs a
partial clone piles up (#129712), and every finished unit is kept, so a
large checkout converges over a few updates instead of blocking one
update for 20+ minutes:

1. Erase on-demand packs (empty .promisor marker, older than an hour)
   that hold no commits. Their objects are copies of what the promisor
   remote serves; git fetches what it needs again. This frees disk,
   which the old gc fold never did. A pack holding commits is kept
   because a ref may point into it; a pack git cannot classify in time
   counts as holding one.
2. While more than 50 promisor packs remain, merge the smallest few
   with `git pack-objects --stdin-packs` (cost proportional to those
   packs, no reachability walk), marker first and index last so the
   merged pack is a promisor pack from the moment git sees it.

Verdicts and a merge batch size that halves after a timed-out merge are
remembered in .git/hermes-pack-tidy.json, so no update repeats work an
earlier one could not finish. bounded_probe_run gains `input=` for
stdin-fed git commands.
2026-10-05 03:00:01 -07:00
teknium1
1fd75357e9 fix(update): stop folding lazy-fetch packs during hermes update
The fold added in #131679 runs `git gc --auto`, which on a large partial
clone is a full repack: a 9.6 GB checkout with 463 packs sat in it for
20 minutes per update, got killed at the cap, and started over on the
next update without ever finishing. It also never freed disk (on-demand
packs are kept, only merged). Turn it off until a bounded, progressive
cleanup replaces it.

The partial-clone keys that stop git writing a commit-graph (#127711)
stay: settle_partial_clone_maintenance re-applies them every update and
leaves full clones alone, and git's own automatic gc still folds packs.
2026-10-04 17:18:50 -07:00
teknium1
4e3fcd5cd7 fix(update): give the lazy-fetch pack fold room to finish and say when it runs out
The fold was killed after 120 s on the assumption it takes "well under a second". That holds
for ~100 small packs, not for the checkouts it exists for (#129712: 2,475 packs / 39 GiB): those
need a full repack, and a killed fold restarted from scratch on every update without a word,
so the worst-hit installs never converged.

- Bound raised to 20 minutes (measured: 750 packs / 530 MB of hermes-agent history fold in
  5.8 s at 7.4 cores; a 39 GiB pack dir on a slow disk is a different order of magnitude).
- consolidate_lazy_fetch_packs returns None on timeout; the shared update_cmd_check reporter
  (used by both `hermes update` and `hermes update --check`) prints the hand-fold command.
- on_fold_start announces a fold that will actually run (pack count past gc.autoPackLimit), so
  a multi-minute first fold is not a silent hang.
- Test fixture disables auto-maintenance: each lazy fetch's detached gc otherwise raced the
  test and folded the packs itself.
2026-10-02 11:41:19 -07:00
liuhao1024
add654131f fix(update): fold lazy-fetch packfiles so a partial clone's .git stops growing
Every on-demand fetch on the promisor remote of a tree:0 installer
checkout writes its own small packfile, and no update-path step ever
consolidates them, so .git grows without bound (2,475 packs / 39 GiB
observed for a ~1 GiB repo, #129712).

Ride git's own threshold: run `git gc --auto` from the debris-cleanup
step both `hermes update --check` and `hermes update` already run.
Below gc.autoPackLimit (default 50) small packs it exits in
milliseconds; past it, the lazy-fetch packs fold into one. The folded
pack is re-marked .promisor so git 2.53+ fetches keep working (#124272),
and the gc does not write a commit-graph, which git <=2.50 misreads on
the next lazy fetch.
2026-10-02 11:41:19 -07:00
Brooklyn Nicholson
b9b1328b7d fix(cli,desktop): hide every console-subsystem child spawn on Windows
A console-less parent (pythonw backend, detached daemons) allocates a visible
console per bare spawn, so Windows startup flashes ~7-8 console windows
(#117781) and the GPU statusbar flashes two more every 5s (#101895). Threads
creationflags=windows_hide_flags() through every hermes_cli spawn site the
desktop reaches: gitlock (tasklist probe + git plumbing), the scheduled-task
PowerShell probe, the updater's _git_run/_no_prompt_git_kwargs/_GIT_TEXT_KW
runners, the safe-directory config probe, and consolidates the statusbar's two
nvidia-smi reads per poll into one cached hidden query. The statusbar also
stops polling while the document is hidden and resumes on visibilitychange
(#120262).

Supersedes #117805, #117833, #102047, #120285 (thanks @JoaoMarcos44, @Finn763,
@monerostar, @szzhoujiarui).
2026-09-29 18:49:58 -05:00
kshitijk4poor
db17e07389 fix(update): mark current updater frames with a sentinel, not missing locals
The pre_update_version co_varnames check added in af26acab73 rejected the
current updater's entrypoints, but it also rejected historical updaters
from 2026-07-26..08-16, whose _cmd_update_impl/cmd_update never declared
that local — their in-process dependency installs would have continued on
the old interpreter instead of handing off to the takeover child.

Invert the gate: the CURRENT updater declares the
_hermes_current_updater_frame sentinel local in both entrypoints; no
historical on-disk version can contain it. in_historical_update() now
hands off on entrypoint match unless the sentinel is present, restoring
the handoff for every historical window while still refusing current
frames. Verified live for all four frame classes; mutation check: with
the sentinel check removed, the current-updater refusal test fails.
2026-09-29 16:47:48 +05:30
kshitijk4poor
282ad01cb5 fix(update): mark unmarked packs instead of the inert promisor retry (#124272)
git 2.53+ `index-pack --promisor` hands every object outside the promisor
packs to pack-objects, which BUG()s (should_include_obj) on the missing
objects those objects lead to. A partial clone with unmarked packs therefore
keeps crashing its fetches.

The retry added for this ran `git -c remote.origin.promisor= fetch`. That
override does nothing: git registers promisor remotes additively, so the
repo's own `true` still wins (GIT_TRACE on 2.50 and 2.55 shows
`index-pack --promisor` and `filter tree:0` under the override). The retry
only passed when the crashed fetch had already stored the pack; a retry that
has to transfer again crashed again.

- On the crash, give every pack without a `.promisor` marker one, then retry
  the same fetch once. The clone stays partial, no refetch, no config change.
  If git still crashes, the updater points at the docs section.
- The two fetches that turn a full clone partial now mark the clone's old
  packs afterwards: the depth-1 `tree:0` unshallow in
  `fetch_full_commit_graph`, and `worktree_ops._deepen_shallow_repo`
  (`--filter=blob:none`). git writes the partial-clone config before it
  fetches, so this also runs when the fetch fails. Without it, a converted
  depth-1 install crashed every later fetch on git 2.55 (3 of 3 rounds in a
  local repro).
2026-09-29 15:24:15 +05:30
teknium1
2005a9a817 fix(telemetry): update initiator reads the lock marker raw
The Desktop-initiator fact called read_live_update(), whose pid liveness probe
(os.kill) and stale-marker unlink are update-lock policy. Running them from a
metrics helper tripped the retired-holder-gate tripwire in
test_update_venv_holder_retirement and could delete a marker as a side effect.
We already hold the lock, so a marker naming another pid is the orchestrator.
2026-09-28 12:43:03 -07:00
teknium1
fd14f50b5d feat(metrics): hermes.update.run/stage derived from the final update receipt
The update pipeline already writes one machine-readable receipt per run, so the
run/stage counters are derived from it in the process that finalizes it instead
of instrumenting stages for metrics. The receipt gains minimal stage END marks
(plan, snapshot, apply[mode], deps, build, restart; verify is inferred from the
fleet matrix), an initiator fact (desktop when a live orchestrator claim
names another pid) and the pre-update commit date, which is all the derivation
needs for outcome, failed stage, duration and from-version-age buckets.

A pre-pull interpreter must never import pulled code: when it is the finalizer
(same pid, checkout sha moved) it parks the receipt (stdlib only) and the next
Hermes start records it. Collection off => nothing imported beyond a config read.
The completion-process test stub gains the new record_stage API the real
module now exposes (no assertion changed).
2026-09-28 12:43:03 -07:00
Teknium
a5bd246865 Old pre-decomposition import paths are gone: plugin compat layer removed on schedule (#126164)
* refactor(plugins): remove the Sep 2026 decomposition compat layer on schedule

The PLUGIN-COMPAT layer (2776813df3 + d63e380324 + 0a5164cebe) kept pre-#102117 import paths
alive for external plugins until 2026-09-14. That window closed two weeks ago; since then the loader
has already been skipping plugins that use the old paths. This removes the layer itself:

- 328 appended `PLUGIN-COMPAT` blocks (lazy `__getattr__` pointer tables, re-exported third-party
  names, restored dead definitions) and the three re-export stub modules
  (gateway/startup_watchdog, hermes_cli/observability/relay_runtime, tools/environments/modal_utils)
- COMPAT_MANIFEST.md, compat_manifest.json, scripts/check_compat_pointers.py and its lint step
- the reporting surfaces: CLI banner notice, `hermes plugins compat`, the `hermes doctor` section,
  the post-update notice, the Desktop one-time dialog, the loader's pre-import skip and the
  `plugins.allow_deprecated_imports` escape hatch

An external plugin that still imports an old path now fails to load with its ImportError as the
reason in `hermes plugins list`, the same path as any broken plugin.

hermes_cli/plugin_compat.py stays as three inert stubs (compat_report, removal_in_effect,
summary_lines): an already-running pre-removal `hermes update` lazy-imports them after the checkout
swap (tests/compat/old_updater_surface.json).

In-tree fallout, both already dead: hermes_cli/setup.py::_check_espeak_ng (no callers; its
`shutil` came from a compat block) and gateway/config.py::SessionResetPolicy ("retained solely for
the scheduled plugin-compat window"). Two test_run_agent patches targeted the removed
`run_agent.handle_function_call` pointer; they now patch `model_tools.handle_function_call`, the
seam production reads, like every sibling test in that file.

* chore: retrigger CI (zero-job startup_failure phantom)

* test: drop resolution allowlist rows for the two deleted which() sites

hermes_cli/setup.py::_check_espeak_ng (dead) and tools/skillevaluator_scan.py::scanner_available
(a restored definition inside a PLUGIN-COMPAT block) no longer exist; the stale-row gate requires
their allowlist entries go with them.
2026-09-28 10:21:41 -07:00
teknium1
52d814cfd0 fix(update): an autostash the update could not restore ends the update partial, never 'Update complete!'
A restore that conflicts (or whose untracked baseline is unknown, or whose
untracked files the update replaced) parks the user's patch and the update
still printed 'Update complete!' and exited 0 (#122557). The run's stash now
stays unsettled until it is restored, discarded, or parked because the user
asked (--keep-stash, declined prompt); an unsettled stash becomes the
completion line, which is not a success line, so the update ends partial,
exits 1 and the receipt says partial. Failure verdicts name it too.
2026-09-28 05:33:25 -07:00
finn763
f77f1a2a37 fix(update): name this run's unsettled autostash before any update outcome (#122557) 2026-09-28 05:33:25 -07:00
JoaoMarcos44
353ac62b31 fix(update): refuse active Git operations before mutation 2026-09-28 05:33:25 -07:00
JoaoMarcos44
f4b49df25f fix(update): prove divergence before reset fallback 2026-09-28 05:33:25 -07:00
teknium1
f46c7159e4 fix(update): trim SHA-less obligation salvage to the checkout fallback (#125952, salvage #125964)
Follow-up trim of the #125964 salvage:

- Armer falls back to _current_checkout_sha() (the identity the reader compares the fleet
  against) instead of a new receipt helper; at arm time the receipt has no post_update yet.
- Reader: the SHA-less, inventory-less record follows the existing inventory-less rule
  (live fleet all current on the checkout) with no extra pid-liveness gate, matching the
  invariant the SHA-bearing inventory-less record already has. The gatewayless branch stays
  fail-closed for an SHA-less record (checkout_contains("") is not evidence).
- Drop the obligation_fields pid plumbing and update_receipt helper; keep 2 tests:
  discharge on live-fleet evidence, and the armer recording the checkout SHA.
2026-09-28 04:44:29 -07:00
kokhlo
e57f6d0f2d fix(update): discharge an SHA-less host restart obligation on live-fleet evidence
A no-op `hermes update` (pre == post SHA) whose head capture failed armed the
host update-restart record with `expected_sha: ""` and no inventory. The reader
bailed out on the empty SHA before the live-fleet reconciliation, so the
"gateways may still be serving pre-update modules" warning could never clear
even when every live gateway was current on the checkout.

Reader (hermes_cli/update_cmd_fleet.py):
- An inventoried obligation without its SHA stays fail-closed.
- An inventory-less, SHA-less record now falls through to the existing
  live-fleet check once the process that armed it is gone (pid liveness via
  pid_exists_stdlib), so a status call cannot discharge a restart phase that
  is still running.

Armer (hermes_cli/update_cmd.py):
- When the completion request carries no SHA, fall back to the receipt's
  post_update.sha (then pre_update.sha) instead of arming with "".

obligation_fields() now reports the arming pid alongside expected_sha.

Tests: discharge on all-current fleet, kept when the armer pid is alive, kept
on a stale fleet, kept with an inventory but no SHA; the discharge test fails
on the pre-fix reader with the issue's exact warning.
2026-09-28 04:44:29 -07:00
kshitijk4poor
e59df70a12 refactor(update): name the parked rollback once and reuse the test helpers
Review follow-up: compute the parked-branch case once in the rollback,
fold the baseline comments into one, reuse pull() and a new complete()
helper in the tests, and say in the docs that a rollback restores the
commit the checkout ran before the pull on every update path.
2026-09-28 16:00:01 +05:30
kshitijk4poor
9e8202f802 fix(update): never leave a failed branch rollback on broken code
Review follow-ups for the branch-switch rollback:

- If the parked branch cannot be checked out again (for example, another
  worktree holds it), restore its commit detached. Before this, the install
  stayed on the broken update branch, and the recovery hint repeated the
  failing command.
- Any `merge-base --is-ancestor` result other than "contained" keeps the
  strict baseline. rc 128 used to fall back to the lenient one, which
  contradicted its own comment.
- `_pull_updates` returns the baseline it verified, and
  `_apply_pulled_update` checks against it. Before, that function
  recomputed the baseline with a second rev-parse and merge-base against a
  hardcoded `origin/<branch>`, which a fork push in between could move.
- Drop a `commit_count == -1` check that could never be false.
- Update the rollback description in `updating.md`.
- Tests share the fixture `git` helper, a repo-init helper and a `pull()`
  wrapper instead of repeating those calls.
2026-09-28 16:00:01 +05:30
kshitijk4poor
a4ed805290 fix(update): tidy the branch-switch rollback and name a switch-only update
Rollback picks its command in one if/elif and names the branch it
restores, and `_pull_updates` passes `rollback_branch` straight through.
`_CheckoutPlan` always has the field, so `_apply_pulled_update` reads it
directly instead of through getattr.

A switch that changes the running code with no new commits to count set
commit_count to -1, which printed "commit count unknown on this shallow
checkout" on non-shallow checkouts. The plan now records that case, and
the update says it switched the running checkout instead.

A regression test pins the stale-local-main case. A merge that returns 0
without moving HEAD must still be refused, which rules out both a
running-code baseline and a "the switch already moved" bypass.
2026-09-28 16:00:01 +05:30
funky-xamarin
593eaf6c07 fix(update): distinguish early fork sync from pending branch repair 2026-09-28 16:00:01 +05:30
funky-xamarin
a58fc4fa8e fix(update): retain branch repair baseline through fork completion 2026-09-28 16:00:01 +05:30
funky-xamarin
495ab057f1 fix(update): verify stale branch repair against its immediate tip 2026-09-28 16:00:01 +05:30
funky-xamarin
57b3868d52 fix(update): preserve running checkout across branch-switch updates 2026-09-28 16:00:01 +05:30
teknium1
010618c767 fix(update): one commit-graph fetch for the pre-fetch heal and the post-update pass
The pre-fetch heal (#123254) and the post-update tag fetch now share
fetch_full_commit_graph, so both follow one clone-mode rule: a partial clone
repeats its own filter, a full clone fetches unfiltered (#122353), and only a
depth-limited clone whose history is really missing unshallows as tree:0. A
full clone grafted by a user's `git fetch --depth 1` already has its history
locally, so it unshallows unfiltered and stays full.

Also: the network-timeout message no longer claims "no response from the
remote" (a large transfer on a slow link hits the same cap), without adding a
classifier rule that would assert the opposite.
2026-09-28 03:08:56 -07:00
Konstantin Khlopkov
e377309d86 fix(update): heal stale shallow history before the bounded fetch
A depth-1 installer checkout far behind main could never update: the plain
main fetch pulled nearly the full object graph (merge-heavy history drags in
side-branch ancestry past the shallow boundary), hit the 300s network cap
mid-transfer every time, and the unshallow step that makes later fetches
incremental only ran after a successful update. Unshallow the checkout first
(commit-graph-only fetch with the same 900s budget the post-update tag fetch
uses, pulling the update branch along), non-fatally, and stop reporting a
wall-clock kill as 'no response from the remote'.

Fixes #123254
2026-09-28 03:08:56 -07:00
Yuan Li
fc6144e3c1 fix(update): self-heal git pack-objects crash on partial-clone fetches
On a partial clone (clone --filter=tree:0), git 2.53/2.54 crashes every
fetch: index-pack --promisor runs repack_local_links(), which feeds
pack-objects --exclude-promisor-objects-best-effort, and pack-objects
BUG()s (SIGABRT, 'should only be called on existing objects') when the
link traversal hits a legitimately missing promisor object. The crash is
deterministic, so every fetch — and with it the CLI's update/check flows
— stays broken until the user applies the manual workaround themselves.

The recovery is the reporter's verified workaround, wired in: on this
exact crash (all three stderr markers), retry the fetch once with
-c remote.origin.promisor= (per-invocation only; the user's filter
choice stays in their config). Wired into both fetch sites — the update
fetch and the check fetch (which also serves forks via upstream).
Unrelated fetch failures are returned untouched; if the retry still
crashes, the update path prints the manual heal command.

Fixes #124272
2026-09-28 03:08:56 -07:00
kokhlo
c70374b561 fix(update): map SSL_CERT_FILE into GIT_SSL_CAINFO for git children 2026-09-28 02:00:41 -07:00
kshitijk4poor
57fba5f772 fix(update): narrow the source target's branch before building the refspec
SourceTarget carries either a commit or a branch, but types both optional;
the typed tracking_refspec() made ty see the branch arm as str | None.
Assert the constructor invariant where the branch is taken (ty diff vs
main: 9 vs 10 diagnostics, none new).
2026-09-28 01:53:35 +05:30
kshitijk4poor
80d6e395db fix(update): count new commits from where HEAD was when checkout -B moves it
A checkout with no local copy of the target branch (every tag-pinned narrow
clone, which is detached) reaches the branch with `checkout -B <b> origin/<b>`.
That lands HEAD on the target before `rev-list HEAD..origin/<b>` runs, so the
count was always 0: the update printed "Already up to date!" after moving the
code, and skipped dependency sync, config migration and restarts.

Capture HEAD before that checkout, count from it, and hand it to the pull
phase as pre_sync_sha — the same channel the fork upstream sync already uses —
so the did-HEAD-move guard and syntax rollback compare against the code that
was actually running.
2026-09-28 01:53:35 +05:30
kshitijk4poor
0ee82ff4cc refactor(update): build the tracking refspec in one place
The forced `+refs/heads/<b>:refs/remotes/<r>/<b>` refspec was spelled out at
all three fetch sites (update --check, the apply fetch, the fork upstream
sync). Its leading `+` is load-bearing on depth-1 clones, so one helper,
`update_cmd_check.tracking_refspec`, now owns the string and the reason.
2026-09-28 01:53:35 +05:30
Yuan Li
ab6cc2fd84 fix(updater): fetch branches by explicit refspec so narrow clones resolve the compare ref
Tag-pinned --single-branch clones configure remote.origin.fetch as a tag-only
refspec, so fetching a branch by name writes FETCH_HEAD and never creates
refs/remotes/origin/<branch>. Every downstream rev-parse of the compare branch
then failed with 'Branch not found' even though the remote plainly has it
(#125112).

Cover the whole class: the --check fetch, the apply-path fetch, and the fork
upstream-sync fetch all pass an explicit +refs/heads/<branch>: mapping.

Verified A/B on a real narrow clone: branch-name fetch leaves origin/main
unresolvable, refspec fetch resolves it.

(cherry picked from commit 6ad2ff3f3f6190ad1fdaed4356d00406019e8dcc)
2026-09-28 01:53:35 +05:30
teknium1
d1484ab44b fix(update): only an explicit hermes update retries channel reads
Passive checks (`hermes --version`, the banner, the Desktop/dashboard
update check) now make one channel-read attempt again. Offline usually
surfaces as DNS EAI_AGAIN or ENETUNREACH, which pm.network.is_transient
classes as transient, so wrapping every read in retry_network added 7 s
of backoff to the synchronous version line, stretched a hung CDN from
30 s to ~127 s, and logged a WARNING to stderr on every retry.

`release_channels.retrying_reads()` opts a block in; update_cmd wraps
the channel resolution of `hermes update` in it. Tests: keep the
Retry-After retry test (now scoped to the update path) and replace the
budget-exhaustion test with one pinning that passive reads and 404s make
exactly one attempt.
2026-09-27 03:41:42 -07:00
teknium1
2270d3511d fix(update): expose PM's git only for git checkouts, and record the installer's copy
A git-less Windows ZIP install has no .git and never runs git, yet
_prepare_git_command called expose_pm_git() before it checked
use_zip_update: offline or with no pin, pm.ensure raised and aborted an
update that never needed git. expose_pm_git now takes the project root
and does nothing unless it is a git checkout (both callers).

When install.ps1 runs as one process, the git it staged into PM's store
is on the inherited PATH, so expose_pm_git returned early and PM's facts
never recorded it; plain hermes processes (plugin git installs, doctor,
version info) had no git until the first `hermes update`. A git found
under PM's store is now treated as that unrecorded copy and ensured, so
the source completion writes the git fact at install time.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-09-27 02:54:03 -07:00
teknium1
305072f4a6 fix(update): give git-less Windows installs PM's git for update and source completion
On a Windows machine with no git, scripts/install.ps1 stages the pinned Git
for Windows into the pm store for its own process only, and pm's facts
never record it. Every later product process that ran a bare `git` failed:

- `hermes update` died before its first step:
  "✗ Update failed: [WinError 2] The system cannot find the file specified"
- the source completion that finishes an installer re-run or an update
  found no commit, deleted the install stamp, and the next boot wrote an
  adoption stamp that names no commit. It also printed
  "Could not refresh release history ([WinError 2] ...)".

expose_pm_git(): when Windows resolves no git, ensure PM's git explicitly
(both callers are user-initiated, like ensure_tools_for_sync) and put its
composed PATH (cmd and usr\bin) on the process, so children inherit it.
The update calls it before its first git, and the source completion calls it
before its builds and stamp. A machine with a working git is untouched.
main_desktop already ensures PM's git for its build for the same reason.
2026-09-27 02:54:03 -07:00
teknium1
ff46826872 fix(update): keep commits made on a detached HEAD behind a named rescue ref
Both update shapes move HEAD off a detached commit: the branch update checks
out main, the release update checks out the release commit. A commit made on
the detached HEAD is on no branch, so after the move only the expiring reflog
reached it, and the branch path printed nothing about it (gated on #124643).

One helper, _park_detached_head, now runs at both sites before HEAD moves.
When HEAD is detached at a commit no ref contains (refs/stash excluded: the
autostash is dropped after the update, which is why the branch path parks
before stashing), it writes refs/hermes-update-backups/detached-<branch>-<ts>-<sha>
(the divergence rescue refs' scheme, now pruned on the same terms) and prints
the ref and how to list the commits. If the write fails the update stops
rather than orphan the work. A detached HEAD already on a branch or tag (a
pinned release) gets no ref. It replaces the release path's silent, never
pruned refs/hermes/pre-release/<sha>.
2026-09-27 01:00:31 -07:00
Brooklyn Nicholson
53a785449f fix(update): rebuild Desktop for an installed app with no checkout build
`hermes update` only rebuilt Desktop when apps/desktop/release/ or dist/
existed, so an installed Hermes.app that only the checkout update refreshes
(a bootstrap build) never got newer once release/ was gone or never built.

Count such bundles as a Desktop to keep current. Ownership is read from the
bundle's install-stamp.json: `updateMechanism: self` (or a stamp older than
the field). Self-updating releases and commit builds are never rebuilt or
copied over, and only the checkout an installed app actually runs (the one
under the default Hermes home) claims it. The post-build install now uses
the same set.
2026-09-26 21:42:48 -05:00
ethernet
a67891b0ee refactor(update): split hermes update --check into update_cmd_check
_cmd_update_check (CC 30, 224 lines) did debris cleanup, channel
resolution, the scoped upstream/origin fetch, shallow-graft repair and
two verdict printers inline. Those steps move to the topical sibling
hermes_cli/update_cmd_check.py; the facade function (a frozen updater
surface) stays in update_cmd and orchestrates them at CC 10.

Output, exit codes and git argv are unchanged. The sibling reads
_capture_head_sha / _no_prompt_git_kwargs through the facade and
imports gitlock, source_releases, source_check and config per call, so
existing monkeypatch seams keep reaching production.
2026-09-24 14:08:22 -04:00
ethernet
cb7b18431a Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	apps/desktop/src/app/settings/connections-registry.tsx
#	scripts/install.ps1
#	scripts/install.sh
#	tests/hermes_cli/test_update_autostash.py
2026-09-24 03:48:31 -04:00