画像を一括で整えるコマンドをエージェントに任せて、席を離れたときのことです。戻ってくると、ターミナル欄は Running のまま動いておらず、出力も増えていませんでした。
遅いだけなのだろうと思い、そのまま 10 分ほど待ちました。それでも終わらないので、次はエージェントに伝える待ち時間の上限を倍にしました。結果は芳しくありませんでした。待つ時間が長くなっただけで、コマンドは最後まで終わらなかったのです。
あとから振り返ると、私は「遅い」と「終わりの合図が届いていない」を同じものとして扱っておりました。ここでは、後者が起きる形を 3 つに分け、手元のシェルで測った数字とあわせて書き残します。
最初にお伝えしたいのは、疑う対象です
Running のまま動かないとき、疑うのは処理の重さではなく、コマンドが「もう終わりました」と言える道が塞がれていないかのほうです。塞がれる形は、私の場合ほぼ次の 3 つに収まりました。
| 形 | 何が起きているか | 見分けるきっかけ |
|---|---|---|
| 標準入力の待ち | 入力の終わり(EOF)が来ず、読み続けている | 引数なしの cat や、ファイル指定のない読み込み処理 |
| 子プロセスの居残り | 親は終わったが、裏に残った子が出力の口を持っている | 末尾の &、開発サーバーや監視の起動 |
| 確認・編集画面の待ち | 人が答える前提の画面で止まっている | -m なしの git commit、-y のない導入コマンド |
なお、これから書く数字は、Antigravity のターミナルそのものではなく、手元の Linux シェル(Python 3.13、git 2.43)で測ったものです。エージェントのターミナルが同じ条件で同じ挙動になるとは限りませんので、その点はご自身の環境で確かめていただければ幸いです。
形1: 標準入力の待ち
入力を受け取るコマンドは、入力が閉じられるまで終わりません。開いたままの入力を与えて 3 秒で打ち切ってみました。
# 入力が開いたまま(EOF が来ない)状態を作る
timeout 3 sh -c 'cat' < <(sleep 100)
echo "exit=$?" # → 124(3.0 秒で打ち切られた=自力では終わらなかった)
# 入力を最初から閉じておく
timeout 3 sh -c 'cat' < /dev/null
echo "exit=$?" # → 0(0.007 秒で終了)cat だけでなく、python3 -c 'import sys; print(len(sys.stdin.read()))' でも同じで、開いた入力では 3.0 秒で打ち切られ、/dev/null を渡すと即座に 0 を返して終わりました。
なぜこう書くのかと言えば、< /dev/null は「この先に入力は来ません」とコマンドに先に伝える一行だからです。読む側は EOF を受け取って、自分から終わってくれます。
形2: 子プロセスの居残り
私が最初にはまったのは、こちらでした。親のシェルは終わっているのに、出力を受け取る側がいつまでも待たされる形です。
# 裏に sleep を残したまま、出力を cat で受ける
timeout 40 sh -c 'sleep 30 & echo started' | cat
# → "started" は一瞬で出るのに、コマンド全体は 30.0 秒後にようやく戻る
# 子の出力と入力を切り離しておく
timeout 40 sh -c 'sleep 30 >/dev/null 2>&1 </dev/null & echo started' | cat
# → 0.004 秒で戻る同じ & を付けたコマンドでも、前者は 30.0 秒、後者は 0.004 秒でした。差を生んでいるのは、裏に残った子が親と同じ出力の口を持ち続けているかどうかだけです。開発サーバーを立ち上げて「起動したら次へ」とエージェントに頼むと、まさにこの形になります。
起動するだけのコマンドは、出力を必ずファイルへ逃がしておきます。
# サーバーを裏で起動して、ターミナルは先へ進める
nohup npm run dev > dev.log 2>&1 < /dev/null &
echo "pid=$!"形3: 確認や編集画面の待ち
三つ目は、人が答える前提の画面で止まる形です。-m を付けずに git commit を呼ぶと、エディタが開いて入力を待ちます。エディタを開かせないよう GIT_EDITOR=true を指定して試すと、待たずに Aborting commit due to empty commit message. を出して終了コード 1 で戻りました。固まる代わりに、失敗として見えるようになったわけです。
つまり、ここでの対策は止まった画面を待つことではなく、そもそも画面が出ない書き方に変えることです。
- コミットには必ず
-mを付けます。 - 導入系のコマンドには
-yや--yesを付けます。 - 対話を避けられない場合は、その作業をエージェントに頼まず、自分の手で実行します。
どの形かを見分ける
3つのどれなのかは、止まっているあいだに別のターミナルから覗くと分かります。Linux では次のとおりです(macOS では lsof -p <PID> で近いことができます)。
# 止まっているコマンドの PID を調べて、状態と入力の先を見る
ps -o pid,stat,wchan:14,cmd -p <PID>
ls -l /proc/<PID>/fd/0先ほどの cat で試すと、wchan が anon_pipe_read、標準入力が pipe:[2107] と表示されました。入力のパイプを読み続けている、という意味です。/dev/null を渡した場合は、確認する前に終わってしまいます。
子プロセスの居残りは、ps -o pid,ppid,stat,cmd -C sleep のように残った子を探すと、親が 1 に付け替わった sleep 30 が見つかりました。親はもういないのに子だけが生きている、という形を目で確かめられます。
効かない場面と、線引き
ここまでの対策には、効かない場面があります。
< /dev/null は、入力を渡すのが目的のコマンドを壊します。psql < query.sql や docker run -i のように、入力を受け取ってこそ意味のあるものには付けられません。また、timeout で打ち切った場合の終了コード 124 は「失敗」ではなく「時間切れ」で、処理が途中まで進んでいたのかどうかは分かりません。打ち切りは原因の切り分けであって、解決ではないのです。
いま思えば、最初に待ち時間を倍にしたのは、原因を見る手間を避けたかったからなのかもしれません。いまは次の順番にしています。待ち時間を延ばす前に、終わりの合図が届く道を作ります。 延ばしてよいのは、道を作ったあとに本当に遅いと分かったときだけです。
エージェントへの指示文には、次の 3 行を入れておきます。
- 入力を読むコマンドには < /dev/null を付け、60 秒で timeout をかけること
- 裏で動かすものは nohup と出力のファイル化で切り離すこと
- git commit には -m、導入系には -y を付け、対話の画面を出さないこと次の一歩
いま Running のまま止まっているコマンドがあれば、まず timeout 20 <同じコマンド> < /dev/null で動かしてみてください。20 秒で戻れば入力待ち、戻らなければ子プロセスか確認待ちを疑う順番になります。許可ダイアログのほうで作業が止まる場合は Antigravity の許可ダイアログが何度も表示される・「常に許可」が効かないときの診断と解決ガイド を、時間切れと成功を取り違えないための確認は Antigravity CLI 1.1.28 で print-timeout 超過が成功終了になりました — 無人実行の判定を見直す をご覧ください。
この確認だけは、急いでいる日でも省かないようにしています。