---
license: "MIT. Translation of the Hermes Agent documentation, Copyright (c) 2025 Nous Research. See https://wiki.winsmux.dev/hermes/licenses.txt"
title: "ソース版の更新の仕上げを誰が受け持つか"
description: ""
upstream_path: developer-guide/source-update-completion.md
upstream_blob: cc093846ed301d265db6b031a0d5db4d4b2d6764
sources:
  - https://hermes-agent.nousresearch.com/docs/developer-guide/source-update-completion
---

# ソース版の更新の仕上げを誰が受け持つか {#source-update-completion-ownership}

## 段階の継ぎ目 {#phase-seam}

コマンドのプロセスが受け持つのは、受け入れの判定、更新のロックと出力の寿命、更新前の
棚卸し、すべてのプロファイルのスナップショット、ゲートウェイの一時停止、Git の選択・stash・復元と
構文・HEAD の検査、そして ZIP のダウンロード・展開・未コミット変更の再確認・リリースの接ぎ木・入れ替えです。
コードを入れ替える前に、仕上げ用の受け渡しの仕組みを読み込んでおきます。最終的なツリーが
選ばれたら（上流のマージも含みます）、Git、すでに最新だった場合の再試行、ZIP のいずれの経路も、
**そのツリーにある** `update_completion.py` へ、版付きの JSON の依頼を 1 つ送ります。コマンドのプロセスでは、
キャッシュ済みのアプリのモジュールを追い出したり読み込み直したりはしません。

依頼に含まれるのは、正規化したソースとデータの置き場所、デスクトップ製品の選択、対話と
ゲートウェイのどちらの動作か、更新前の版、使用中と兄弟のスナップショットの識別子、
直列化した実行環境の計画、開いている受領記録の識別子とデータ、Windows で一時停止した分の引換券です。
中身はデータだけで、呼び出し可能なオブジェクトや pickle は決して含みません。対話的な設定の問いかけに
答えられるよう、標準入力は引き継いだままにします。ゲートウェイの動作では、対話しない振る舞いを保ちます。
子プロセスの出力は見えたままで、親の更新の出力にも写されます。

## 新しいコードの受け持ち {#new-code-owner}

標準ライブラリだけで書かれた入口が、手元にある Python を `-I -S` 付きで起動するので、
古い site-packages や実行可能な `.pth` ファイルは初期化されません。専用のバイトコードのキャッシュの
置き場所を使い、新しいチェックアウトのものを読み込む前に古いキャッシュのファイルを締め出します。その明示的な

記録済みの依存関係の和集合を用意したうえで、新しい有効化の環境で選ばれた Python を起動します。
このインタプリタも site の初期化を切った状態で起動し、アプリのものを読み込む前に、
実行環境の受け持ちが選ばれた世代を借り受けて有効にします。アプリの仕上げのコードを読み込むのは、このインタプリタだけです。同じ
受領記録と対応付けの識別子が、この準備の境目をまたいで引き継がれます（PM の結果も含みます）。
選ばれた Python による仕上げが受け持つのは、起動用ファイルの公開、ビルド処理、キャッシュの
無効化、すべてのプロファイルの設定・状態・スキルの手入れ、プロセスの走査、
一群のプロセスの再起動、Windows での再開、ダッシュボードの重複の整理、そして検証です。

種類ごとの既存の再起動と中断からの回復の手順はそのまま残ります。監視役やプロセスの一時的な失敗は、
新旧の世代が混ざった読み込みがなくても実際に起きるからです。なくなるのは、
追い出しと読み込み直しによる回避策と、再試行と ZIP それぞれに独立していた後半の組み立てだけです。
ゲートウェイの終了状態は、再起動で更新処理の cgroup が終わってしまう前に書き込まれます。
検証が最後の受領記録を公開します。

## コミット地点を過ぎたら、更新は失敗にならない {#after-the-commit-point-nothing-fails-the-update}

ツリーが動いたあとは、コード自体が巻き戻された場合を除き、`hermes update` は 0 で終了します。
仕上げの各段階は互いに独立しています。起動用ファイルの公開、製品のビルド（TUI / web / desktop は、どれかが失敗しても残りをそれぞれ試します）、設定の移行、
バイトコードの一掃、ゲートウェイの再起動と検証、Windows での再開、引退したチャネルの引き継ぎのどれかが失敗すると、
`⚠` の行を表示し、受領記録の `followups` に `{step, reason}` として追記します。そのあいだも
受領記録の `outcome` は `"success"` のままです。その段階自身の責務は有効なまま残ります。
後半の処理（起動用ファイル、ビルド、手入れ、設定の移行）なら `source-completion-pending`、
ゲートウェイならホストの一群のプロセスを再起動する責務です（CLI の起動時の警告がその名前を出し続けます。再起動が未済のときは、
更新前のゲートウェイをその責務に記録します。そのため、起動時に落ちたゲートウェイは、ゲートウェイの無い状態での解消で片付けられることなく、
チェックアウトのコードで動くまで未済のまま残ります）。
バイトコードの一掃なら、スタンプの押されていないバイトコードの指紋です。次回の起動か `hermes update`
（同じ仕上げを実行する「Already up to date」の経路も含みます）が、それを再試行します。
このコミットについて「restarted」とスタンプされたホストでも、一群のプロセスがまだチェックアウトのコードで動いていなければ、
行き止まりにせずもう一度再起動します。終了コード 2（拒否 / 同時実行）と終了コード 1（何もコミットされなかった、または
巻き戻された）の意味は変わりません。

プロファイルの同期は、できる範囲で行うものです。`_sync_profiles_after_update` はプロファイルごとのエラーを表示して
処理を続けるので、そのエラーは未済の責務にはなりません。段階の外へ抜け出した同期（たとえば
`SystemExit` によるもの）だけが `profile_sync` の後続作業となり、`source-completion-pending` を有効なまま残します。

Windows のゲートウェイの再開は、コミット地点のあとで 1 回だけ試みます。仕上げの子プロセスが行い、
依存関係が未済のときは親が行います。その失敗は `windows_resume` の後続作業です。
コマンド自身の終了経路と atexit による安全網はもう一度実行しません。それらがまだ抱えている再開
（子が応答しなかった場合）も、例外を出さずに同じ形で報告します。過去の引き継ぎ用の
仕上げ（`update_finish`）も、失敗を後続作業として記録し、終了状態を保ちます。

受領記録は、実行が開いているあいだも失われません。`begin_update_receipt` が、その実行専用のアーカイブファイルと
`latest.json` に `outcome: "running"` として書き込み、段階の境目ごとに
更新するので、強制終了された更新も自分の記録を残します。次の更新は、プロセスがもう存在しない
`running` の記録を `interrupted` とし（最後の段階の名前を添えます）、それを報告します。
受領記録と `update.log` への写しは、ルートのホーム
（`hermes_constants.get_default_hermes_root()`）に解決され、固定されたプロファイルのホームには決して置かれません。それを読む側
（`hermes logs update`、デバッグ用のバンドル、ダッシュボードの更新状況、pm の同期の受領記録）も同じです。

仕上げのブートストラップによる依存関係の準備（`ensure_tools_for_sync`、`pm.sync_venv`）も、
ツリーが動いたあとに実行されます。その失敗は `dependencies` の後続作業（「依存関係はまだインストールされていません。
次回の起動で再試行します」）となり、終了コードは 0 で、後半の処理の責務は有効なまま残ります。準備を終えた子プロセスが
結果を返さずに落ちた場合は `completion` の後続作業です。コミット地点のあとで Ctrl-C を押すと、実行は
`interrupted` として閉じられ（終了コード 130。`failed` には決してなりません）、新しいコードは配置済みで、
残りの段階が未済であることを伝えます。

コミット後の経路はほかに 2 つあり、どちらも同じ規則に従います。ゲートウェイの `/update` のマーカー
（`.update_exit_code`）は、ゲートウェイの再起動（systemd のユニット、
中断からの回復、Windows のサービスの再開）が失敗しても、コミット済みの結果を保ちます。再起動の未済分は、`gateway_restart` の後続作業、
ホストの責務、転送される出力の中の `⚠` の行として表れ、「失敗」の通知には決してなりません。受領記録の
保存先が最終の書き込みを拒んだ場合は `⚠ Update receipt not written` と表示し、仕上げの
子プロセスは、メモリ上で確定させた対応付け済みの最終記録を親へ返します。そのため終了
状態は 0 のままです。終了コード 1 になるのは、利用者の対応が要る場合（ローカルの変更が stash に残っている）だけです。

## 親の生存期間と失敗 {#parent-lifecycle-and-failures}

親は子を待ち、子の 0 以外の結果をそのまま伝えます（シグナルは、シェルと同じく
128+シグナル番号に対応付けます）。コミット後の段階の失敗が、0 以外の結果を生むことはありません。子は 0 で終了するだけでは成功になりません。
一致する受領記録の識別子を持った最終の応答が必要です。その応答は、
更新された Windows の引換券を返します。これにより、親が登録している緊急時の再開が、
すでに終わった作業を繰り返さずに済みます。親が通常どおり終わるときには、手入れの処理は何もしません。

親は、子の仕上げが確認されるまで元の受領記録を持ち続けます。
子の出力が無い、または失敗した場合は、その受領記録を既存のコマンドの境目での
失敗処理に残します。標準ライブラリだけの準備処理は、アプリのものを読み込めない場合でも、
対応付けた PM の失敗データを返し、プロセスの境目ごとに負のシグナルによる終了を
整えます。POSIX では、仕上げが新しいセッションとプロセスグループを持ちます。
取り消すときはロックを手放す前にそのグループを終了させます（Windows では、手元に保持している
子に対して `taskkill /T` でツリーごと終了させます）。親は仕上げのプロセスを起動する前に、
保留中の一群のプロセスに対する責務を記録します。準備を始められなかった場合も同じです。親による Windows の緊急時の再開は、
子を実行できないときや子が強制終了されたときの、最後の手段としての生存期間の責務として残ります。
子が失敗しても、保留中の一群のプロセスに対する責務は決して消されません。手入れが始まったあとで
コードが自動で巻き戻されることはありません（SQLite のスナップショットはファイルを失ったときの回復手段であって、巻き戻しではありません）。

### 壊れた回復用のコード {#damaged-recovery-code}

起動時のチェックアウトの修復には、チェックアウトのロックと、子プロセスの管理を受け持つ
実行役の両方を読み込める必要があります。どちらかのモジュールが壊れていると、起動は
`Cannot safely repair` で止まり、排他なしで書き込む代わりに、中断した更新のマーカーを残します。
すでに健全なコードを読み込んだ更新処理は、ディスク上のモジュールが壊れていても、
チェックアウトを保持して書き込めます。実行中の更新があれば、終わるのを待ってください。

修復用のコードは、強制終了された移動で壊れるツリーの中にあります（`hermes_bootstrap.py`、
`hermes_cli/__init__.py`、`_early_recovery.py`、`update_lock.py`、
`update_custody.py`）。そこでツリーを移動するたびに、Git が書き込む前に、これらの回復・ロック・管理の
モジュールを、移動の開始コミット時点の内容で
`<git dir>/hermes-update-recovery/<pre>/` へ公開します。置き場所は作業ツリーの外で、
マーカーの `pre`（完全なコミット ID だけ）をキーにし、各ファイルの blob ID を記した
`MANIFEST` を添えます。ファイルとディレクトリは、名前を変える前に fsync します。
マーカーがある状態でチェックアウト自身の読み込みが失敗すると、発行されたランチャー
（`.hermes/bin/hermes`）は、すべてのファイルのハッシュがマニフェストの ID と一致する場合に限り、
その公開済みのコピーを実行します。コピーが欠けている、壊れている、別物である（または古い
更新処理によるもので存在しない）場合は、まず `pre` 時点の Git のオブジェクトから作り直し（`--no-replace-objects`、
記録済みの git か `PATH` の絶対パスの項目を使い、カレントディレクトリは決して使いません）、
同じ方法で検証します。その後は、標準ライブラリとそのコピーだけを使い、同じ復元の権利と
チェックアウトのロックを取ります（書き手が生きていれば `Not repairing the checkout now` と表示して
終了コード 1 で終わり、トレースバックは出しません）。続いて、復元したツリーから起動し直します。制限もあります。ほかの
入口（`python -m hermes_cli.main`、`hermes-agent` のフック）にはこの代替手段がありません。
この代替手段ができる前に発行されたランチャーにも無いので、このコードへの最初の更新
（古いランチャーで実行）の途中で強制終了された場合は対象外です。また、ツリーにチェックアウトの
ロックか管理の実行役が無い `pre` は使いません。

### Git 経路での振る舞いの変化 {#behaviour-changes-on-the-git-path}

トランザクション化より前の更新処理との、意図した違いです。リリースノート向けにまとめます。

- Windows の ZIP によるフォールバックは、手付かずのチェックアウトからだけ実行します。Git が
  ツリーを stash した、切り替えた、途中まで移動したあとでは、ZIP の上書き（最上位の項目を
  すべて置き換えます）がその状態を埋もれさせてしまうので、代わりに Git の
  エラーで失敗させます。
- コミット地点では、Git が書き込む前に、仕上げの後半、一群のプロセスの再起動、マーカーを有効にします。
  どれか 1 つでも永続化できなければ、チェックアウトを変えずに更新を止めます。
  それらなしで移動すると、「後半が決して実行されない」状態になるからです。
- 起動用ファイル（ランチャーと `hermes_bootstrap` が読み込むものの全体）は、移動の前に、
  移動先のコミットの内容で、移動先の `requires-python` が認めるインタプリタ
  （`uv python find`、次に `python3.N`）を使ってコンパイルします。どれもインストールされていなければ、
  拒否のメッセージがインストールすべき Python を示します。この事前確認は 1 回の
  `git cat-file --batch` です。
- どの移動も、ref ではなく解決済みのコミット ID を指定し、そのマーカーは
  HEAD がそのコミットになってから初めて外します。切り替え中に動いたブランチは、
  切り替えの失敗として報告します。再起動の責務は、切り替えが
  実際にたどり着いた HEAD に対して生じます。
- フォークの上流への早送りも、移動の前に同じ方法で判定します。
  上流のコミットが壊れていれば、更新全体を巻き戻します。
- 移動先のコミットが消えた（gc や再クローン）回復用マーカーは、`pre` 時点で
  追跡対象のツリーがクリーンな場合に限って引退させます。それまでは、起動のたびに警告します。

## 過去の仕組みとの互換 {#historical-surface}

これまでに出荷され、たどり着けるすべての更新処理の履歴から固定した名前は、
すべて引き続き解決できます。過去の依存関係のフックは、標準ライブラリだけの引き継ぎの橋渡しを保ちます。
古い親は待ち、受領記録と回復の状態を運び、引退したインストーラを再開することはありません。
新たに引退した準備と、モジュールの読み込み直しのフックのうち、未完了と明示されたものは
0 以外で止まり、もう一度 `hermes update` を求めます。欠けている仕上げの依頼を作り出すことはできません。
現在の Git・最新・ZIP の呼び出し元は、正規の仕上げの受け渡しの仕組みだけを使い、過去の引き継ぎの入口は使いません。
固定されていない、ブランチにしかない再試行の組み立ては、互換用の層を作らずに削除します。ACP の手軽な
公開は、起動用ファイルの受け持ちが持つ `expose_cli` を使います。過去の ACP の入口は
変換役にすぎず、2 つめの書き手になることはありません。固定した名前の集合は、削ったり、
タグだけの網羅に置き換えたりすることはありません。現在の経路で新しく読み込むものは、その履歴と合わせて扱います。

## 検証 {#verification}

使うのは、隔離したデータの置き場所、使い捨ての Git リポジトリ、依存関係・ビルド・サービスの偽の
差し替え部品だけです。互換性のないモジュールをキャッシュした古いプロセスで、
実際の Git の移行を通って新しいコードへ移ること、選ばれた Python での実行、受領記録の識別子と
スナップショットの引き渡し、0 以外や突然の子の終了、ロックの解放、Windows の引換券の
返却を確かめます。既存の対象を絞ったテストが、ZIP の未コミット変更の確認と接ぎ木、スナップショット、一群のプロセスの
突き合わせ、監視役の時間の扱い、過去の名前の読み込みを扱います。ネイティブのサービスの再起動と
Windows / macOS での受け入れの確認は、別に必須の検証の流れとして残ります。この実装のテストの実行では、
稼働中の利用者のサービスや利用者の状態には一切触れません。

## クラッシュ箇所の一覧 {#crash-cell-matrix}

各セルは、実際の `hermes update`（または Desktop の引き渡し用スクリプト）を更新の途中のある地点で
強制終了させ、利用者に保証すべきことを確かめます。次の `hermes` の起動が
動くこと。その起動の直後に、チェックアウトが更新前のコミットか
移動先と完全に一致すること（HEAD と追跡対象のすべてのバイト。`git status` がクリーンで `git diff --quiet
HEAD` が通ること。どの移動先も、起動のたびに読み込まれるモジュールを編集しています）。そして、落ちた更新が
残したもの（Git のロック、`.hermes-update-in-progress`）が次の更新を妨げないことです。
修正がまだ PR の段階にあるセルは、最後の確認だけを
`known_failure`（`tests/e2e/core/_pending_fixes.py`）で包み、セルのほかの確認はすべてその前に置きます。
そのセルは、まさにその不足のメッセージのときだけ XFAIL になり、それ以外では失敗し、
修正が入れば、どちらが先にマージされても合格します。統合したまとまりの受け入れ実行では、
この猶予を認めません。`HERMES_E2E_STRICT_ACCEPTANCE=<owner>`（`ci.yaml` と `windows-install-update-e2e.yml` の
`strict_acceptance` という dispatch 入力。たとえば
`gh workflow run windows-install-update-e2e.yml --ref <branch> -f strict_acceptance=upd-txn`）
は、理由が `<owner>:` で始まる不足をすべて失敗に変えます（`1` ならすべての不足）。強制終了の地点は観測した状態で決め（プロセスツリー内の git の子プロセスを argv で見分ける、
`index.lock` がある状態でフィルターによってチェックアウト内に留められた git、ref ファイルから読んだ HEAD、引き渡しの更新用の子プロセスとそのマーカー）、sleep で決めることはありません。

| セル | 強制終了の地点 | テスト | 修正するレーン |
|---|---|---|---|
| Windows `mid_fetch` | 更新の `git fetch` の子プロセスが動いているあいだに `taskkill /T /F` | `tests/e2e/core/windows_update/test_crash_cells.py::test_update_killed_mid_fetch_leaves_a_runnable_install` | 合格 |
| Windows `mid_git` | 早送りのチェックアウトの途中で `taskkill /T /F`。ハーネスがインストールの `.git/config` と `.git/info/attributes` に設定した smudge フィルターが、`git merge` が `hermes_constants.py` を書き込んでいる最中に止めておきます（index.lock は保持され、2 つのファイルはすでに移動先の内容で、そのファイルは削除済み） | `tests/e2e/core/windows_update/test_crash_cells.py::test_update_killed_mid_git_leaves_a_runnable_install` | #132361（古い `.git/index.lock`。起動時の中断された pull の修復が素の `git` を実行していましたが、インストーラーの Git しかない端末にはそれがありません）。また、`hermes_bootstrap` が修復を実行する前にランチャーが `hermes_constants` を読み込んでいたため、削除されたモジュールが毎回の起動を先に落としていました。いまのランチャーは、チェックアウトのほかのどのモジュールよりも先に修復へたどり着きます。一連の流れの最後に実行します |
| Windows `tree_moved` | チェックアウトが移動先に着いた直後、更新が終わる前 | `tests/e2e/core/windows_update/test_crash_cells.py::test_update_killed_after_the_tree_moved_leaves_a_runnable_install` | main で合格 |
| Windows `desktop_handoff` | `hermes update` の子プロセスが動いているあいだに、`scripts/desktop-update/windows.ps1` とそのツリー全体 | `tests/e2e/core/windows_update/test_crash_cells.py::test_desktop_handoff_killed_mid_run_leaves_a_runnable_install` | main で合格 |
| Windows `orphaned_update` | `windows.ps1` だけ（`/T` なし）。その `hermes update` は動き続け、終了するまでマーカーが有効（LIVE）なまま走り切り、終わったらマーカーが消えなければなりません | `tests/e2e/core/windows_update/test_crash_cells.py::test_desktop_handoff_script_killed_alone_keeps_the_marker_live_until_its_update_ends` | #132354 + #132365（4 行目の委譲） |
| POSIX のコミット地点 | レーンごと | `tests/e2e/core/upgrade/<area>/test_hostile_<lane>.py` | そのファイルを持つレーン |

Windows のセルは、Windows のインストールと更新の一連の流れ
（`.github/workflows/windows-install-update-e2e.yml`）で実行します。必要なときに実行するには、`wine2e-install/**`
ブランチを push します。プルリクエストでは、変更の分類器の `e2e_upgrade` レーンが発火したとき
（更新経路上のいずれかのファイル: `scripts/ci/classify_changes.py`）は、実際の更新を行う 2 つのスイートの両方が必須になり、
`e2e_desktop_update` が発火したときは Desktop の更新スイートが必須になります。それ以外のときは
省略され、合格として扱われます。更新経路は、記憶に頼らず導き出します。
`tests/ci/test_update_ci_routing.py` は、更新の入口が読み込むすべてのモジュール（と、それらが実行するすべてのビルドスクリプト）を読み、
それぞれが振り分けられるか、共有の中継点として宣言されるまで失敗します。さらに、実際の分類器の出力を実際の
ワークフローファイルに通して再生するので、設定されたレーンは必ずそのジョブへ届きます。関連: [macOS バンドルの更新](/hermes/docs/developer-guide/macos-bundle-updates/)。
