夜のうちに走らせていた処理のログを、朝いちばんに開いた日のことです。行末には「成功」とだけ書いてありました。ところが同じ時刻に書き出されているはずの成果物は、途中の段落で切れておりました。
終了コードは 0 です。スクリプトはその 0 だけを見て、成功として記録していました。
原因を探して設定を疑い、プロンプトを疑い、最後に版の更新履歴へ戻りました。答えはそこにありました。読み落としていたのだと気づいたのは、三度目に開いたときでした。Antigravity CLI 1.1.28(9月9日)で、--print-timeout の超過が「失敗」から「部分出力を返しての正常終了」へ変わっていたのです。
9月12日時点の最新は 1.2.2 で、この挙動は 1.2 系にもそのまま入っております。1.1.27 以前から一気に上げた場合は、以下の変更をまとめて受け取ることになります。
1.1.28 で変わったのは、失敗の伝え方でした
まず全体像をお伝えします。1.1.28 の変更のうち、無人実行に直接効くのは次の4点です。
| 変更点 | 1.1.27 以前 | 1.1.28 以降 |
|---|---|---|
| --print-timeout の超過 | タイムアウトエラーで失敗 | 部分出力を返し、stderr に警告を出して正常終了(Ctrl+C 等の割り込みは従来どおり非ゼロ) |
| 外部 URL の取得 | 既定で常に許可 | 既定で先に承認を求める(事前に許可していない場合) |
| 致命的エラーの出力 | 黙って終わることがあった | stderr に error: という安定したマーカー付きで出る。打ち切りの可能性がある場合は注記も出る |
| -p の終了タイミング | — | 最終回答を返したら速やかに終了。実行中のバックグラウンドタスクとタイマーは --print-timeout の範囲で待ち、dev server などのデーモンは止めずに残す |
一段落目と二段落目は、方向が逆に見えます。片方は失敗を成功として返すようにし、もう片方は黙っていた失敗を喋らせるようにしました。
けれども並べてみると、意図は一つに見えてきます。終了コードという 1 ビットに載せていた情報を、stderr という帯域の広い場所へ移したのです。0 と 1 では「途中まではできた」を表せません。
私は最初のうち、この変更を歓迎しきれませんでした。終了コードで全部を判定できたほうが、スクリプトは短く済むからです。いまは考えを改めております。短く書けていたのではなく、表現できないものを 0 に丸めていただけでした。
--print-timeout の超過を受け取り直す
判定を書き直します。要点は、成功と失敗の二値ではなく、部分(partial)という第三の状態を持たせることです。
#!/usr/bin/env bash
set -uo pipefail
OUT="$(mktemp)"; ERR="$(mktemp)"
trap 'rm -f "$OUT" "$ERR"' EXIT
agy -p "$PROMPT" --print-timeout 600 >"$OUT" 2>"$ERR" </dev/null
code=$?
if [ "$code" -ne 0 ]; then
status=failed # 割り込み・起動失敗はここに落ちます
elif grep -q '^error:' "$ERR"; then
status=failed # 終了コードが 0 でも、マーカーがあれば失敗です
elif [ -s "$ERR" ]; then
status=partial # 警告が出ている = 途中で区切られた可能性があります
else
status=ok
fi
printf '%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "$status" "$(wc -c <"$OUT")" >> run.tsvgrep -q '^error:' でマーカーを見ているのは、この文字列だけが安定したものとして定められているからです。タイムアウト時の警告文そのものは版で変わりえますので、文字列一致に頼らず「stderr に何か出ていること」を条件にしております。ここを凝った正規表現にすると、次の版で静かに外れます。
もう一つ添えておきたいことがあります。この判定でも、成果物が意味のあるかたちで完結しているかは分かりません。stderr が空でも、モデルが早めに話を畳んだだけかもしれません。
ですから私は、バイト数を毎回記録して、前日までの中央値から大きく外れた日を別途拾うようにしました。判定を賢くするより、外れ値を後から見つけられる形で残すほうが、結果として手戻りが少なくなります。
終了コードは走り切ったかを答えるだけです。完結したかどうかは、成果物に聞きます。 この順番だけは、急いでいる日でも崩さないようにしております。
なお、呼び出し側のパイプが終了コードそのものを握りつぶしてしまう問題は、これとは別の層の話になります。そちらはAntigravity CLI の失敗を握りつぶしていたのは呼び出し側のパイプでしたに書きました。set -o pipefail を入れていない環境では、まずそちらから直したほうが早いはずです。
外部 URL の取得が承認待ちになりました
4点のうち、いちばん気づかれないまま事故になるのがこれです。外部 URL の取得が、既定で「常に許可」から「先に尋ねる」へ変わりました。
対話で使っているぶんには、承認のプロンプトが出て終わりです。1.1.28 では表示も具体的になり、Run this command? Allow access to this URL? Allow calling this tool? と、何を承認するのかが明示されるようになりました。フックや別プロジェクトのファイルが理由のときは Reason: の行も添えられます。
問題は無人で走らせている側です。応答する人がいない場所で承認を求められると、処理はそこで待ちます。
私自身、無人で回す処理に「止まるべきところで止まる」仕組みを入れる意味は、よく分かっているつもりです。ただしそれは、止まる条件をこちらが把握している場合の話なのです。把握していなければ、ただ進まないだけになります。
対処は二つの方向があります。
一つめは、事前に許可ルールへ登録しておく方法です。許可ルールは CLI 側・共有・プロジェクトの各設定ファイルに書けます。ただし設定キーの名前は自分の版のドキュメントで確認してください。私は過去に、それらしい名前のキーを書いて、警告も出ないまま何も効いていなかったことがあります。誤ったキーに対する反応は一様ではなく、落ちるもの・警告だけのもの・完全に黙るものがあるのです。
ですから、書いたら必ず挙動で確かめます。
# 許可が効いているかを、待たせずに確かめます
timeout 90 agy -p "https://example.com の h1 を一行で返してください" \
--print-timeout 60 </dev/null 2>err.txt
echo "exit=$?"; cat err.txt標準入力を閉じ、--print-timeout を短く切り、外側にも timeout を重ねます。許可が効いていれば結果が返り、効いていなければ 60 秒で区切られて stderr に警告が残ります。どちらの場合も、待ち続けることはありません。
二つめは、そもそも URL を読ませない構成にしてしまう方法です。無人の処理で外部の内容を取りに行かせると、取得先が変わっただけで出力が変わります。私は定期実行の側では、取得は別工程で済ませ、CLI には手元のファイルだけを渡すようにしております。
終了のタイミングと、残されるデーモン
もう一つ、CI で引っかかりやすい変更があります。1.1.28 の -p は、最終回答を返したら速やかに終了するようになりました。実行中のバックグラウンドタスクとスケジュールされたタイマーは --print-timeout の範囲で待ちますが、dev server のようなデーモンは止めずに残します。
つまり、CLI は正常に終了したのに、ジョブそのものは終わらない、という並びが起こりえます。CLI が起動したプロセスが、まだポートを掴んだまま生きているからです。
エージェントに開発サーバーを立てさせる工程を挟んでいるなら、後始末は呼び出し側で持ってください。プロセスグループごと畳むか、確保させるポートを固定して最後に解放するか、どちらかです。同じ版で、実装計画の承認待ちで非対話実行が永久に止まる不具合も直っておりますので、1.1.27 以前で「たまに帰ってこない」に悩んでいた場合は、まず版を上げるのが先になります。
ついでに、地味ですが効く変更も書き残します。指定したモデル名が別のモデルへ解決されたとき——別名の解決、--effort による変種の選択、廃止されたモデルの置き換え——その事実が CLI のログに記録されるようになりました。「指定したはずのモデルで動いていない」を確かめる手段が、ようやく手元に来たわけです。
版を上げる前後で、確かめておく順序
最後に、私が実際に踏んだ順番をそのまま置いておきます。
| 順 | やること | 見るところ |
|---|---|---|
| 1 | 上げる前に、いまの判定が終了コードだけに依存していないか読み返す | ラッパーの if [ $? -ne 0 ] が唯一の分岐になっていないか |
| 2 | stderr を捨てていないか確かめる | 2>/dev/null や 2>&1 でマーカーが混ざっていないか |
| 3 | URL 取得の許可を、挙動で確かめる | 標準入力を閉じた短い実行が、待たずに返るか |
| 4 | 上げたあと、一度は人が見ている時間帯に流す | partial が出る頻度と、そのときの成果物 |
| 5 | 成果物の大きさを毎回記録し始める | 数日ぶんの中央値から外れた日 |
4 の「人が見ている時間帯に流す」は、面倒でも飛ばさないでいただきたいところです。無人実行の変更点は、無人のまま確かめようとすると、確かめたつもりだけが残ります。
まずは手元のラッパーを開いて、stderr をどこへ流しているかだけ見てみてください。私の場合、そこが 2>/dev/null になっていたせいで、error: マーカーが一度も届いていませんでした。
お読みいただきありがとうございました。同じ朝を迎える方が一人でも減りましたら幸いです。