著者: 著者: やまねこ タグ:
Mac miniのプロセスが眠ったように停止し、swap領域だけが積み上がっている概念図

Hermes Agentのconfig.yamlに1行足しただけのはずが、応答が完全に止まりました。直感的には「さっき変えた設定が悪さをしている」と疑いたくなる場面です。

この記事では、2026年7月2日に実際に遭遇した無応答インシデントを、pslsofvm_statという一次データで切り分けた記録として残します。結論から言うと、状況証拠は設定変更とは別の候補(既存のswap問題)を浮かび上がらせました。ただし再現実験はしておらず、原因を完全に確定できたわけではありません。「直近の変更を疑う」というデバッグの定石が、今回は状況証拠の上ではそのまま当てはまらなかった、という一つの事例として読んでいただければと思います。
こちらの記事の実例コラムで短く触れたインシデントの詳細版です。まだお読みでない方は、先に読んでおくと理解しやすくなります。

ざっくりまとめ

  • 「応答が途中で切れる」(finish_reason:length)と「応答が完全に止まる」(ハング)は、まったく別の症状です。
  • Hermes側のエラー確認・lsof・最小APIリクエスト・ps/vm_statという順序で状況証拠を積み上げていくと、原因の見立てが立てやすくなります。
  • 今回のインシデントでは、設定変更ではなく既存のswap問題が候補の一つとして浮上しました。確定診断ではありません。

「応答が切れる」と「応答が止まる」はまったく別の症状です

Hermes Agent + ローカルLLM構成で「応答がおかしい」と感じたとき、実はこれは一つの症状ではありません。

一つは、応答が途中で切れる症状です。OpenAIのAPI仕様では、レスポンスのfinish_reasonlengthになっている場合、リクエストで指定した最大トークン数に達したことを意味します(参考: OpenAI API Reference: Chat Completions)。この場合、サーバー自体は正常に動いており、出力の途中で処理を打ち切っただけです。

もう一つは、応答が完全に止まる症状です。こちらはfinish_reasonすら返ってきません。リクエストがサーバーに届いているのかどうかすら、見た目だけでは分かりません。

この二つを混同すると、診断の方向性を誤ります。出力が切れる症状に対してmax_tokensを増やしても、無応答(ハング)は直りません。逆も同様です。まずこの二つを切り分けるところから始めます。

finish_reasonの正体 — Hermes側のリトライロジック

finish_reason:lengthが返ってくると、Hermes Agentは「System: Your previous response was truncated by the output length limit…」という継続プロンプトを付けて自動的にリトライします。

筆者が2026年7月1日にローカル環境の~/.hermes/hermes-agentのソースを確認した時点では、このリトライは最大3回まで行われ、それでも完走しなければ最終的に失敗として扱われる実装でした。つまり、この文言が1〜2回表示されるのは想定内の動作であり、3回を超えて完走しない場合にはじめて実害があります。

また、finish_reason:lengthが実際には正常なstopであるにもかかわらず誤検知されるケースも調べましたが、これはOllama経由のGLMモデルに限定された救済ロジックによるもので、Tailscale越しにcustomプロバイダを使う今回の構成には当てはまりませんでした。

ここで一つ、正直に書いておきたいことがあります。この記事を書くにあたり2026年7月15日にHermes Agent公式リポジトリの最新版を確認したところ、該当のリトライ処理の行番号や上限回数がすでに変わっていました。つまり、上記の「最大3回」やコードの詳細は、あくまで筆者が調査した2026年7月1日時点のローカル環境の話です。OSSプロジェクトのコードは変わり続けるので、この記事の技術的な細部をそのまま今のバージョンに当てはめないでください。

なお、finish_reason:lengthはKV cacheの圧縮設定とは別の原因です。KV cache圧縮は長いcontextを扱うためのメモリ余裕を作る対策であり、出力できるトークン数の上限そのものを増やすものではありません。出力が切れる症状には、max_tokens側の調整で対応します。

まず何を確認するか — Hermes側エラー・lsof・最小APIリクエスト

ここからは、無応答(ハング)を疑ったときにどう切り分けるかの話です。先に断っておくと、以下の手順は2026年7月2日の当日にすべてこの順番で実施したものではなく、後から振り返って整理した推奨フローです。当日実際に確認したのは主にps・errログ・vm_statでした(詳しくは後述します)。次に同じ状況になったら、この順で確認しようと考えています。

  1. Hermes側のエラー・タイムアウト表示を確認する。 クライアント側で何がどう見えているかを、まず把握します。
  2. lsof -nP -iTCP:8080 -sTCP:LISTENでサーバープロセスがポートを待ち受けているか確認する。 リストが空なら、そもそもサーバープロセスが落ちています。
  3. 同一ホストから最小のchat completionリクエストを直接送る。 Hermes Agentを経由せず、curlでサーバーに直接リクエストを送ってみます。これで応答が返れば、原因はHermes側かネットワーク経路にある可能性が高く、サーバープロセスそのものがハングしているわけではありません。
  4. リクエスト送信の前後でerrログに/v1/chat/completionsへのPOSTが記録されるか確認する。 リクエストがサーバーまで届いているかどうかの判定材料になります。

psとvm_statで状況証拠を積み上げる — 複数時点観測とswap差分

上記だけでは切り分けられない場合、プロセスの状態そのものを見ます。

ps -o pid,rss,vsz,%cpu,stat,comm -p "$(lsof -tiTCP:8080 -sTCP:LISTEN | head -n1)"

ここで大事なのは、一度だけでなく数十秒間隔で2〜3回観測することです。%CPUが0.0%付近のまま、STATS(sleeping)のまま変化しなければ、プロセスが何かを待ったまま動いていない状況証拠になります。ただしSTAT S自体は、正常に待機しているプロセスでもよく見られる状態です。この一つだけで「ハングしている」と判定はできません。

もう一つの手がかりがvm_statです。

vm_stat 1

Swapins(swap in)・Swapouts(swap out)・Pages freeを、一時点の値としてではなく、時間を空けて差分で見ます。過去に長いcontextのingestを行っていると、モデルの重みの一部がswap outされていることがあり、そこへ再びアクセスするとページフォールトが発生してハングしうる、という一つの仮説が立てられます(参考: Apple Developer Documentation: About the Virtual Memory System)。あくまで仮説であり、確定ではありません。

RSS(実際に使用している物理メモリ量)が、普段のベースライン実測(このモデル構成では単体で約27GB)よりも明らかに小さい場合も、同様の傍証になります。

ここまでのポイント

「応答なし」を見たら、いきなりconfigを疑って変更を戻す前に、次の順で状況証拠を積み上げます。

  1. Hermes側のエラー表示を確認する
  2. lsofでポートの待ち受けを確認する
  3. 最小のAPIリクエストを直接送ってみる
  4. psvm_statを複数時点で観測する

実際に起きた話 — 2026年7月2日、config変更直後の無応答

~/.hermes/profiles/brain/config.yamlmodel:セクションにmax_tokens: 4000を追記した直後のことです。それまで動いていたHermes Agentから、何の応答も返らなくなりました。

その場で確認したpsの結果は次のようなものでした。

  • PID 59660のプロセスが、%CPU 0.0STAT Sのまま変化しない
  • errログを確認しても、/v1/chat/completionsへのPOSTが一件も記録されていない
  • vm_statのSwapouts累積が約1,280,316ページ、概算で約20GB相当

この数値は、あくまでMac mini M4 Pro 64GBというこの構成、2026年7月2日というこの時点での実測値です。また、Swapoutsの累積値はシステム全体のものであり、Hermes Agentが使っているプロセス単体のswap使用量ではありません。ここは誤解しやすいポイントなので、あらためて書いておきます。

launchctl kickstart -kだけでよかった(処理中リクエストは中断される注意点つき)

復旧に使ったのは、launchdの再起動コマンドです。

launchctl kickstart -k gui/$(id -u)/local.mlx-optiq-hermes

-kオプションは、実行中のサービスを一度終了させてから起動し直します(参考: Kickstarting and tearing down with launchctl)。このとき、今回のケースではデータやKV configのファイル自体は書き換わりませんでした。ただし、これは今回の観測結果であり、launchctl kickstart -kという仕組み自体が処理中のリクエストを中断させる(=実行中の何かがあれば、それは失われる)ことに変わりはありません。「安全な復旧手順」と無条件には言えない理由がここにあります。

再起動後、プロセスIDは85246に変わりました。軽いchat completionを送って確認したところ、finish_reason:"length"・応答時間0.626秒で、正常に応答が返ってきました。

「直近の変更」だけでは説明できない可能性が浮上したが、確定診断はしていない話

ここまでの状況証拠を並べると、次のようになります。

  • Hermes側の設定変更(クライアント側)は、M4 Proサーバー側のプロセス状態に直接影響する仕組みではありません。
  • errログにPOST記録が一件もなかったことは、リクエストがサーバーへ届く前に何かが起きていた可能性を示します。ただし、これはconfig側やネットワーク側の要因を完全に排除するものではありません。
  • 過去の長文ingestで積み上がっていたswapが、config変更とは無関係な既存の問題として候補に上がりました。

この三つを並べると、「直近のconfig変更だけでは、この無応答を説明しきれないのではないか」という見立てになります。ただし、これは状況証拠からの推定であり、再現実験による確定ではありません。単発のインシデントから、一般的な結論を導くべきでもありません。

「直近の変更を疑う」というデバッグの定石は、多くの場面で有効です。ですが、一次データが変更以外の候補を浮かび上がらせているなら、まずそちらも確認してみる価値があります。今回はそれで、少なくとも「設定を戻せば直る」という早計な判断を避けられました。

まとめ:応答が止まったら、まず何を見るか

  • 「応答が途中で切れる」(finish_reason:length)と「応答が完全に止まる」(ハング)は別の症状であり、診断の入り口が違います。
  • ハングを疑ったら、Hermes側エラー確認→lsof→最小APIリクエスト→ps/vm_statの複数時点観測、という順で状況証拠を積み上げます。
  • STAT Svm_statの累積値は、それぞれ単独では判定材料になりません。複数の一次データを組み合わせて見立てます。
  • 直近の変更を疑う前に、一次データが他の候補を示していないか確認する価値があります。
  • この記事は一つのインシデントの記録です。同じ数値・同じ結論が、他の環境でもそのまま当てはまるとは限りません。

シリーズ①でHermes Agent + ローカルLLMの基本構築を、シリーズ②でOptiQへの置換とベンチマークを扱いました。まだお読みでない方は、こちらの記事もあわせてご覧ください。

よくある質問

finish_reasonがlengthになったら、どうすればいい?

サーバー自体は正常です。max_tokensを増やすか、リクエストを分割してください。

launchctl kickstart -kで処理中のリクエストはどうなる?

中断されます。今回のケースではデータやconfigファイル自体は書き換わりませんでしたが、実行中の処理があれば失われる点は変わりません。

swapが増えたら必ずハングするの?

必ずではありません。今回の記事で示したのも、あくまで状況証拠の一つです。