Skip to content

cross-review: 結果ファイルの書き出しを待たずに NO_RESULT と判定して中断する #584

Description

@takemi-ohama

何を見つけたか

cross-review で、レビュワーが結果ファイルを書き終える前に judge が NO_RESULT と読み、ラウンドを中断することがある。

monitor.py が戻った直後に read-resultjudge を流すと、<agent>-review-pr<N>-result.json が無いと読まれる。judge は「結果を残さなかった」として同じラウンドで 1 度だけ起動し直し、2 度目も同じ形で空振りすると final = error で中断する。

実際にはレビュワーはその後に結果ファイルを書き終えており、state の該当ラウンドには両者の APPROVE が後から入っていた。

どこで見つけたか

volareinc/carmo-cdk の PR #329(cross-review 1 ラウンド)。

  1. monitor.py 329 --agents agy,kiro が戻る
  2. read-result が agy・kiro とも「result 未生成」
  3. judgeRELAUNCH_AGENTS='agy kiro'(rc=7)→ 起動し直す
  4. 2 度目も 1〜3 と同じ → judge が rc=1「実行環境の側の問題として中断」
  5. しばらく後に state を読むと final: approved、agy・kiro とも intent=APPROVE

同じ変更の PR #330 では、monitor.py の後に「全担当の結果ファイルが揃うまで最大 4 分待つ」ループを足したところ、1 回目で正しく agy=REQUEST_CHANGES / kiro=APPROVE と判定できた。

#598 / #537 は v10.13.0(2026-09-16)で解決した。 監視の上限は工程ごとの表
plugins/ndf/scripts/lib/limits.py)が持ち、レビューの工程は 1200 秒になった。担当の CLI の
上限は、環境変数で解決した監視の上限 + 120 秒(既定 1320 秒)を導く。無進捗の許容は担当ごと
(codex 180 / agy 480 / kiro 480 / claude 900 秒)で、監視の上限とは別に働く。

そのため、agy と kiro に先に当たるのは無進捗の許容(480 秒)である。 レビューの工程の上限
1200 秒より短いため、打ち切りは STALLED として起きる。この事例はその打ち切りを経て起きた可能性が
最も高い。

なぜこの変更の範囲外なのか

cross-review の手順(scripts/monitor.pySKILL.md の骨組み)の課題で、開発対象のリポジトリでは直せない。

直さないと何が起きるか

収束しているラウンドが「環境の問題」として中断する。利用者は外部 AI が落ちたと読み、レビュー無しでマージするか、何度も回し直すかの判断を迫られる。

原因を切り分ける問い

monitor.py はプロセスの終了だけでは OK を返さない。 プロセスが終わった時点で結果ファイルの
存在(サイズが 0 より大きいこと)を見て、無ければ NO_RESULT(終了コード 3)を返す
plugins/ndf/scripts/lib/monitor.py:738-752_process_exit_outcome--no-require-result を渡さない限り有効)。
そのため「結果ファイルの存在を完了判定に含める」形は既にある。 それでも後から結果が入った
経路として、次のどれが起きたかを分ける。

問い 確かめる材料
monitor.py は OK / NO_RESULT / TIMEOUT のどれで戻ったか carmo-cdk PR #329monitor.py の標準出力(statusdetail
戻った後に誰が結果ファイルを書いたか 監視の上限で止めるとき、_kill_pidmonitor.py:444-469)は pidfile の 1 つの pid だけへ SIGTERM / SIGKILL を送り、プロセスグループは止めない。CLI が子プロセスに書かせていれば、止めた後に書き出しが起きうる。一般的な形では再現できた(下の実行結果。agy の実物では未確認)
起動し直した 2 度目も同じ経路か 2 度目の monitor.py の出力と、結果ファイルの更新時刻

止めた後の書き出しの再現。_kill_pid で、3 秒後に子プロセスが結果ファイルを書くシェルの親を止めた。

$ python3 - <<'E'
import sys, subprocess, time, os
sys.path.insert(0, 'plugins/ndf/scripts/lib')
import monitor
p = subprocess.Popen(['bash', '-c', '(sleep 3; echo {} > late-result.json) & wait'])
time.sleep(0.5)
monitor._kill_pid(p.pid)
time.sleep(0.3)
print('parent alive after kill:', p.poll() is None)
time.sleep(4)
print('result written after kill:', os.path.exists('late-result.json'))
E
parent alive after kill: False
result written after kill: True

切り分けの材料は残る。 #662 は v10.13.0(2026-09-16)で解決した。 監視の結果は担当ごとの <stem>-monitor.json
追記だけの monitor-outcomes.jsonl に残り(書き出すのは monitor.pymain:944)、理由の語彙は
plugins/ndf/scripts/lib/monitor_outcome.py が持つ(ok / timeout / stalled /
early_error / missing / pidfile_bad の 6 語。usage_limitcli_timeout は未実装)。
結果ファイルには status / reason / detail / result_exists / elapsed が入るため、監視が
打ち切ったのか、プロセスの終了後に結果が無かったのか
は後から分かる。

残るのは語彙 2 つである。 利用上限(usage_limit)と CLI 自身の上限(cli_timeout)は
未実装で、今は early_errormissing へ落ちる(#619)。

監視の上限(TIMEOUT)で戻った後に書き出された場合は、#583 と同じ順序の問題になる。
その場合は #583 と一緒に直す。

修正レイヤー

plugins/ndf/scripts/lib/launch-cli.sh の起動(担当の CLI を nohup だけで起動し、プロセスグループを分けない。
:95 / :105 / :112 / :120)と、plugins/ndf/scripts/lib/monitor.py_kill_pid:444。1 つの pid だけへ
シグナルを送る)。「監視が止めた担当は、止めた後に何も書かない」を起動と停止の組で保証する場所である。
現れている read-resultjudge の側で結果ファイルを待つと、止めた後に現れる結果ファイルや投稿の経路が
残り、#583 の投稿の後の打ち切りでも同じ食い違いが起きる。止めた担当の子プロセスまで止めれば、後から
結果ファイルが現れる経路そのものが消える。

止めた理由を読む側(結果なしの理由の語彙)は、担当 1 回の起動の結末を共通の語彙で読む #729 が持つ。

採る手

移動(move_responsibility)。停止の単位を pid からプロセスグループへ移す。launch-cli.sh は CLI を新しい
プロセスグループとして起動し、_kill_pid はそのグループへ SIGTERM / SIGKILL を送る。

直し方の候補

どちらも未実装である。 修正レイヤーに当たるのは 1 つ目で、2 つ目は現れている場所での受け皿になる。

  • 監視の上限で止めるときに、プロセスグループごと止める(止めた後に書き出させない)。
    plugins/ndf/scripts/lib/launch-cli.sh は CLI を nohup だけで起動しており、setsid
    os.killpg もリポジトリに無い(git grep -n 'setsid\|killpg\|getpgid'monitor.py
    launch-cli.sh のどちらにも一致しない)。setsid で新しいプロセスグループとして起動し、
    _kill_pidos.killpg でグループへ SIGTERM / SIGKILL を送る
  • SKILL.md の骨組みで、read-result の前に結果ファイルの出現を上限付きで待つ
    (PR cross-review の手順書 01-state-and-review.md が 508 行で、分割の基準を超えている #330 で足した「全担当の結果ファイルが揃うまで最大 4 分待つ」ループの形)。現行の骨組みに
    この待ちは無い(bg-wait.sh は Bash の 1 回 600 秒の制限を越えるためのもので、結果ファイルは
    待たない)

由来

volareinc/carmo-cdk issue #324 / PR #329

関連

#598#537 を直すと、この課題の頻度が下がる。 agy / kiro が 420 秒の監視の上限や 600 秒の CLI の上限に届くことが、止めた後の書き出しの入口になるためである。

次の課題は、同じ出口(結果なし → 同じラウンドで起動し直す → final=error)を持つ。
原因と直す箇所はそれぞれ別で、1 件を直しても他は残る。

進行

モード: standard / 作業ツリー: .worktrees/feat/issue-729-outcome-vocabulary / 計画: issues/issue-729-619-584-implementation-plan.md

  • 要求と受け入れ条件 — 2026-09-15 13:45
  • 作業場所の用意 — 2026-09-15 12:45
  • 設計 — 2026-09-15 13:45
  • 素材の収集と出典の確定
  • ドキュメント再構成 — 2026-09-15 13:46
  • ドキュメントレビュー — 2026-09-15 13:46
  • 計画 — 2026-09-19 10:54
  • 実装 — 2026-09-19 11:01
  • 構造改善 — 2026-09-19 11:45
  • 実装レビュー — 2026-09-19 19:48
  • 完了判定 — 2026-09-19 19:57
  • Pull Request — 2026-09-19 11:42
  • 確定仕様化 — 2026-09-21 20:53
  • 後片付け — 2026-09-21 20:51
  • 配布 — 2026-09-22 14:17
  • 体裁レビュー
  • リリース後テスト — 2026-09-22 15:06
  • 振り返り — 2026-09-22 15:38

閉じた理由

PR #791 で直り、ndf 10.16.0(2026-09-22、main / タグ ndf--v10.16.0、PR #810)で配布した。リリース後テスト(#810 (comment) )でこの課題の受け入れ条件はすべて合格した。

振り返り: #810 (comment)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ndf-skillNDF の Skill 本体bugSomething isn't workingpriority: high実害・安全機構の欠落など、優先して対応する

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions