# コードレビュー依頼：lab_start.sh で、同じ Log の Pod が残っていたら新しく作らずにつなぎ直す（2026-10-31 依頼 44）

前提: 依頼 37（lab_start.sh・lab_stop.sh・lab_lib.sh・コミット a043b60）と依頼 39（コミット 3fa44b5）の続き。

対象 (`git diff HEAD -- lab_start.sh lab_lib.sh` と新規の `test_lab_start.sh`。ほかのファイルの未コミット差分は対象外。lab_stop.sh は変えていない):

## 実機で起きたこと
`./lab_start.sh --log Log17` で推論を終え (Pod は残す)、同じ Log17 でもう一度打つと「Log17 の Pod (id …) がまだ残っています. 新しく作らずに止まります」で止まり、続けるには Pod を消して準備からやり直すしかなかった。

## 変えたこと
- lab_lib.sh
  - `lab_pod_kind ID`: GET /pods/ID で running (RUNNING) / stopped (EXITED など、TERMINATED 以外) / gone (API にない・TERMINATED・id のない 200) に分ける。2xx 以外は GET /pods の一覧に (TERMINATED 以外で) その id がないことを確かめてから gone。000・一覧が取れない・一覧にまだある・200 で desiredStatus が空・JSON が読めない → 戻り値 1 (呼ぶ側で止まる)。RUNPOD_HTTP を親に残すため `$(...)` に入れずに一時ファイルで受ける
  - `lab_probe_cmd COMMIT CKPT` / `lab_pod_probe`: 1 回の ssh で serve_policy.py のプロセス (pgrep -f '[s]cripts/serve_policy\.py')・8000 番の LISTEN (/proc/net/tcp・tcp6 の 1F40 で st=0A)・モデル (CKPT/assets/local があり *.incomplete がない)・openpi の HEAD が COMMIT・.venv/bin/python と $HOME/.local/bin/env を見て `PROBE serve=… listen=… ckpt=… openpi=… venv=…` を返し、PROBE_* に入れる。行が読めなければ戻り値 1
  - `lab_archive_state FILE`: lab_pod.json を lab_pod_<時刻>.json に mv (消さない)
  - `RUNPOD_HTTP=""` を初めに置く (`$(runpod_api …) || fail "… HTTP $RUNPOD_HTTP"` が set -u で落ちないように)
- lab_start.sh
  - 1: 記録があり running → POD_ID・料金・作った時刻を記録から取り「<Log> の Pod (id …) につなぎ直します」。stopped → `lab_stop.sh --log <Log> --skip-log` (serve.log は 1 回だけ試す・DELETE・消えたのを確かめる・費用) で消してから記録を時刻付きに変えて新しく作る。gone → 記録を時刻付きに変えて作る。記録に id がない → 時刻付きに変えて作る。lab_pod_kind が 1 → 止まる
  - 1 (作る側): 記録にない p2m-<Log> の Pod (TERMINATED 以外) が一覧にあれば止まる (二重に借りない)。ほかの Log の Pod は今までどおり WARN だけ
  - 2: 今までどおり lab_pod_addr で IP とポートを API から取る (つなぎ直しでも毎回取り直す)
  - 3 (つなぎ直しのときだけ): lab_pod_probe。serve と listen が両方 1 なら 3b〜4 を飛ばす。そうでなければ前の serve.log を <Log>/serve_before_restart_<時刻>.log に持ち帰り (だめでも続ける)、ckpt・openpi・venv のないものだけやり直す (openpi を取り直すなら uv sync も)。3f (install_addon.py・add_trim_config.py・assets の cp。どれも二度流しても同じ) と 4 (tmux kill-session してから起動) は流す
  - 3a (スクリプトを送る) はつなぎ直しでも毎回送る
  - かかった時間の一覧: 見出しに「(<Log> の Pod (id …) につなぎ直し)」、1 は「1. Pod につなぎ直す」、飛ばした段階は「… (飛ばした)  0 秒」
  - fail() の `「$CUR_STAGE」` を `「${CUR_STAGE}」` に (bash 3.2 + UTF-8 で「」のバイトが変数名に入り unbound variable で落ちていた。止まる道すべての文言が出ていなかった)
- test_lab_start.sh (新規): 一時フォルダに lab_start.sh・lab_stop.sh を写し、runpod_api・pod_ssh・pod_scp・ssh・lsof・python3 を偽物にして --dry-run で最後まで流す 14 の場合 + lab_probe_cmd を Mac の bash で実際に流す確かめ。47 件 OK

## 見てほしい点
1. 二重に借りる道が残っていないか (lab_pod_kind の分かれ道、stopped を消すのに失敗したとき、記録にない同名 Pod、API が一時的に落ちているとき)
2. Runpod REST API の答え方の想定 (GET /pods/ID の 404 などが本当に「ない」か、RUNNING 以外の desiredStatus を stopped として消してよいか。作ったばかりで起動中の Pod を stopped と取り違えて消す恐れはないか)
3. lab_probe_cmd の判定 (pgrep の自分自身への当たり、/proc/net/tcp の読み方、ckpt の判定が弱すぎないか、serve=1 listen=0 のとき起動し直すのでよいか)
4. つなぎ直しで 3f を毎回流すこと・3a を毎回送ることの是非
5. lab_stop.sh をサブプロセスで呼ぶことの副作用 (トンネルを閉じる、exec tee、終了コード)
6. テストの不足

ファイル変更・実機操作・Pod の操作はしないでください。読み取りだけでお願いします。日本語で、表（項目 / 判定 / 確認結果・修正案）と、必須 / 推奨 / 据え置きの分類で返してください。
